├── .github ├── FUNDING.yml ├── stale.yml └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── SECURITY.md ├── docs └── logo.svg ├── examples ├── define-types.ts ├── dev.to │ ├── api-key-plugin.ts │ ├── articles.ts │ ├── comments.ts │ ├── example.ts │ ├── followers.ts │ ├── follows.ts │ ├── params.ts │ └── users.ts ├── jsonplaceholder.ts └── types-from-definition.ts ├── jest.config.ts ├── package.json ├── renovate.json ├── src ├── api.test.ts ├── api.ts ├── index.ts ├── plugins │ ├── form-data.plugin.ts │ ├── form-data.utils.ts │ ├── form-url.plugin.ts │ ├── header.plugin.ts │ ├── index.ts │ ├── zod-validation.plugin.test.ts │ ├── zod-validation.plugin.ts │ ├── zodios-plugins.test.ts │ └── zodios-plugins.ts ├── utils.test.ts ├── utils.ts ├── utils.types.test.ts ├── utils.types.ts ├── zodios-error.ts ├── zodios-error.utils.ts ├── zodios.test.ts ├── zodios.ts └── zodios.types.ts ├── tsconfig.build.json ├── tsconfig.json ├── tsup.config.js ├── website ├── .gitignore ├── README.md ├── babel.config.js ├── docs │ ├── api │ │ ├── _category_.json │ │ ├── api-definition.md │ │ ├── examples.md │ │ ├── helpers.md │ │ ├── openapi.md │ │ └── typescript.md │ ├── client │ │ ├── _category_.json │ │ ├── client.md │ │ ├── error.md │ │ ├── plugins.md │ │ ├── react.md │ │ └── solid.md │ ├── ecosystem.md │ ├── installation.md │ ├── intro.md │ ├── server │ │ ├── _category_.json │ │ ├── express-app.md │ │ ├── express-context.md │ │ ├── express-router.md │ │ └── next.md │ └── sponsors.md ├── docusaurus.config.js ├── package.json ├── sidebars.js ├── src │ ├── components │ │ └── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ ├── index.tsx │ │ └── markdown-page.md ├── static │ ├── .nojekyll │ ├── CNAME │ ├── img │ │ ├── favicon.ico │ │ ├── logo.svg │ │ ├── openapi-to-zodios.png │ │ ├── openapi.png │ │ ├── undraw_docusaurus_mountain.svg │ │ ├── undraw_docusaurus_react.svg │ │ ├── undraw_docusaurus_tree.svg │ │ └── zodios-social.png │ └── video │ │ └── zodios.mp4 ├── tsconfig.json └── yarn.lock └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ecyrbe] 4 | custom: ["https://www.paypal.me/ecyrbe"] 5 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | daysUntilStale: 30 2 | # Number of days of inactivity before a stale issue is closed 3 | daysUntilClose: 7 4 | # Issues with these labels will never be considered stale 5 | exemptLabels: 6 | - pinned 7 | - security 8 | # Label to use when marking an issue as stale 9 | staleLabel: wontfix 10 | # Comment to post when marking an issue as stale. Set to `false` to disable 11 | markComment: > 12 | This issue has been automatically marked as stale because it has not had 13 | recent activity. It will be closed if no further activity occurs. Thank you 14 | for your contributions. 15 | # Comment to post when closing a stale issue. Set to `false` to disable 16 | closeComment: false 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | paths: 10 | - src/** 11 | - yarn.lock 12 | - package.json 13 | - tsconfig.json 14 | pull_request: 15 | branches: [main] 16 | paths: 17 | - src/** 18 | - yarn.lock 19 | - package.json 20 | - tsconfig.json 21 | 22 | jobs: 23 | build: 24 | name: Build 25 | runs-on: ubuntu-latest 26 | strategy: 27 | matrix: 28 | node-version: [14.x, 16.x, 18.x] 29 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 30 | 31 | steps: 32 | - uses: actions/checkout@v4 33 | - name: Use Node.js ${{ matrix.node-version }} 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: ${{ matrix.node-version }} 37 | cache: "yarn" 38 | - run: yarn install --ignore-engines 39 | - run: yarn build 40 | - run: yarn test 41 | analyze: 42 | name: Analyze 43 | runs-on: ubuntu-latest 44 | permissions: 45 | actions: read 46 | contents: read 47 | security-events: write 48 | strategy: 49 | fail-fast: false 50 | matrix: 51 | language: ["javascript"] 52 | steps: 53 | - name: Checkout repository 54 | uses: actions/checkout@v4 55 | - name: Initialize CodeQL 56 | uses: github/codeql-action/init@v2 57 | with: 58 | languages: ${{ matrix.language }} 59 | - name: Perform CodeQL Analysis 60 | uses: github/codeql-action/analyze@v2 61 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # publish on npm when there is a new version tag 2 | 3 | name: publish 4 | on: 5 | push: 6 | tags: [v*] 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [16.x] 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v4 18 | with: 19 | registry-url: "https://registry.npmjs.org" 20 | node-version: ${{ matrix.node-version }} 21 | cache: "yarn" 22 | - name: install dependencies 23 | run: yarn install 24 | - name: build 25 | run: yarn build 26 | - name: run test 27 | run: yarn test 28 | - name: publish to npm 29 | # only run when tag has no 'rc' in it 30 | if: ${{ !contains(github.ref, 'rc') }} 31 | run: yarn publish --access public 32 | env: 33 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 34 | - name: publish beta to npm 35 | # only run when tag has 'rc' in it 36 | if: ${{ contains(github.ref, 'rc') }} 37 | run: yarn publish --access public --tag beta 38 | env: 39 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | - name: generate changelog 41 | uses: orhun/git-cliff-action@v2 42 | id: cliff 43 | with: 44 | args: --latest --strip footer 45 | env: 46 | OUTPUT: CHANGES.md 47 | - name: save changelog 48 | id: changelog 49 | shell: bash 50 | run: | 51 | changelog=$(cat ${{ steps.cliff.outputs.changelog }}) 52 | changelog="${changelog//'%'/'%25'}" 53 | changelog="${changelog//$'\n'/'%0A'}" 54 | changelog="${changelog//$'\r'/'%0D'}" 55 | echo "changelog=$changelog" >> $GITHUB_OUTPUT 56 | - name: create release 57 | id: release 58 | uses: actions/create-release@v1 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | with: 62 | tag_name: ${{ github.ref }} 63 | release_name: Release ${{ github.ref }} 64 | body: ${{ steps.changelog.outputs.changelog }} 65 | draft: false 66 | prerelease: false 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | lib 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | # Developpement directory 108 | .devcontainer 109 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | !/lib 3 | /node_modules 4 | /src 5 | /examples 6 | /website 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | 16 | # OS 17 | .DS_Store 18 | 19 | # Tests 20 | /coverage 21 | /.nyc_output 22 | 23 | # IDEs and editors 24 | /.idea 25 | .project 26 | .classpath 27 | .c9/ 28 | *.launch 29 | .settings/ 30 | *.sublime-workspace 31 | 32 | # IDE - VSCode 33 | .vscode/* 34 | !.vscode/settings.json 35 | !.vscode/tasks.json 36 | !.vscode/launch.json 37 | !.vscode/extensions.json 38 | 39 | # ENV 40 | .env 41 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ecyrbe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Zodios

2 |

3 | 4 | Zodios logo 5 | 6 |

7 |

8 | Zodios is a typescript api client and an optional api server with auto-completion features backed by axios and zod and express 9 |
10 | Documentation 11 |

12 | 13 |

14 | 15 | langue typescript 16 | 17 | 18 | npm 19 | 20 | 21 | GitHub 22 | 23 | GitHub Workflow Status 24 |

25 |

26 | Bundle Size 27 | Bundle Size 28 | Bundle Size 29 | Bundle Size 30 | Bundle Size 31 | Bundle Size 32 | Bundle Size 33 |

34 | 35 | https://user-images.githubusercontent.com/633115/185851987-554f5686-cb78-4096-8ff5-c8d61b645608.mp4 36 | 37 | # What is it ? 38 | 39 | It's an axios compatible API client and an optional expressJS compatible API server with the following features: 40 | 41 | - really simple centralized API declaration 42 | - typescript autocompletion in your favorite IDE for URL and parameters 43 | - typescript response types 44 | - parameters and responses schema thanks to zod 45 | - response schema validation 46 | - powerfull plugins like `fetch` adapter or `auth` automatic injection 47 | - all axios features available 48 | - `@tanstack/query` wrappers for react and solid (vue, svelte, etc, soon) 49 | - all expressJS features available (middlewares, etc.) 50 | 51 | 52 | **Table of contents:** 53 | 54 | - [What is it ?](#what-is-it-) 55 | - [Install](#install) 56 | - [Client and api definitions :](#client-and-api-definitions-) 57 | - [Server :](#server-) 58 | - [How to use it on client side ?](#how-to-use-it-on-client-side-) 59 | - [Declare your API with zodios](#declare-your-api-with-zodios) 60 | - [API definition format](#api-definition-format) 61 | - [Full documentation](#full-documentation) 62 | - [Ecosystem](#ecosystem) 63 | - [Roadmap](#roadmap) 64 | - [Dependencies](#dependencies) 65 | 66 | # Install 67 | 68 | ## Client and api definitions : 69 | 70 | ```bash 71 | > npm install @zodios/core 72 | ``` 73 | 74 | or 75 | 76 | ```bash 77 | > yarn add @zodios/core 78 | ``` 79 | 80 | ## Server : 81 | 82 | ```bash 83 | > npm install @zodios/core @zodios/express 84 | ``` 85 | 86 | or 87 | 88 | ```bash 89 | > yarn add @zodios/core @zodios/express 90 | ``` 91 | 92 | # How to use it on client side ? 93 | 94 | For an almost complete example on how to use zodios and how to split your APIs declarations, take a look at [dev.to](examples/dev.to/) example. 95 | 96 | ## Declare your API with zodios 97 | 98 | Here is an example of API declaration with Zodios. 99 | 100 | ```typescript 101 | import { Zodios } from "@zodios/core"; 102 | import { z } from "zod"; 103 | 104 | const apiClient = new Zodios( 105 | "https://jsonplaceholder.typicode.com", 106 | // API definition 107 | [ 108 | { 109 | method: "get", 110 | path: "/users/:id", // auto detect :id and ask for it in apiClient get params 111 | alias: "getUser", // optional alias to call this endpoint with it 112 | description: "Get a user", 113 | response: z.object({ 114 | id: z.number(), 115 | name: z.string(), 116 | }), 117 | }, 118 | ], 119 | ); 120 | ``` 121 | 122 | Calling this API is now easy and has builtin autocomplete features : 123 | 124 | ```typescript 125 | // typed auto-complete path auto-complete params 126 | // ▼ ▼ ▼ 127 | const user = await apiClient.get("/users/:id", { params: { id: 7 } }); 128 | console.log(user); 129 | ``` 130 | 131 | It should output 132 | 133 | ```js 134 | { id: 7, name: 'Kurtis Weissnat' } 135 | ``` 136 | You can also use aliases : 137 | 138 | ```typescript 139 | // typed alias auto-complete params 140 | // ▼ ▼ ▼ 141 | const user = await apiClient.getUser({ params: { id: 7 } }); 142 | console.log(user); 143 | ``` 144 | ## API definition format 145 | 146 | ```typescript 147 | type ZodiosEndpointDescriptions = Array<{ 148 | method: 'get'|'post'|'put'|'patch'|'delete'; 149 | path: string; // example: /posts/:postId/comments/:commentId 150 | alias?: string; // example: getPostComments 151 | immutable?: boolean; // flag a post request as immutable to allow it to be cached with react-query 152 | description?: string; 153 | requestFormat?: 'json'|'form-data'|'form-url'|'binary'|'text'; // default to json if not set 154 | parameters?: Array<{ 155 | name: string; 156 | description?: string; 157 | type: 'Path'|'Query'|'Body'|'Header'; 158 | schema: ZodSchema; // you can use zod `transform` to transform the value of the parameter before sending it to the server 159 | }>; 160 | response: ZodSchema; // you can use zod `transform` to transform the value of the response before returning it 161 | status?: number; // default to 200, you can use this to override the sucess status code of the response (only usefull for openapi and express) 162 | responseDescription?: string; // optional response description of the endpoint 163 | errors?: Array<{ 164 | status: number | 'default'; 165 | description?: string; 166 | schema: ZodSchema; // transformations are not supported on error schemas 167 | }>; 168 | }>; 169 | ``` 170 | # Full documentation 171 | 172 | Check out the [full documentation](https://www.zodios.org) or following shortcuts. 173 | 174 | - [API definition](https://www.zodios.org/docs/category/zodios-api-definition) 175 | - [Http client](https://www.zodios.org/docs/category/zodios-client) 176 | - [React hooks](https://www.zodios.org/docs/client/react) 177 | - [Solid hooks](https://www.zodios.org/docs/client/solid) 178 | - [API server](http://www.zodios.org/docs/category/zodios-server) 179 | - [Nextjs integration](http://www.zodios.org/docs/server/next) 180 | 181 | # Ecosystem 182 | 183 | - [openapi-zod-client](https://github.com/astahmer/openapi-zod-client): generate a zodios client from an openapi specification 184 | - [@zodios/express](https://github.com/ecyrbe/zodios-express): full end to end type safety like tRPC, but for REST APIs 185 | - [@zodios/plugins](https://github.com/ecyrbe/zodios-plugins) : some plugins for zodios 186 | - [@zodios/react](https://github.com/ecyrbe/zodios-react) : a react-query wrapper for zodios 187 | - [@zodios/solid](https://github.com/ecyrbe/zodios-solid) : a solid-query wrapper for zodios 188 | 189 | # Roadmap for v11 190 | 191 | for Zod` / `Io-Ts` : 192 | 193 | - By using the TypeProvider pattern we can now make zodios validation agnostic. 194 | 195 | - Implement at least ZodTypeProvider and IoTsTypeProvider since they both support `input` and `output` type inferrence 196 | 197 | - openapi generation will only be compatible with zod though 198 | 199 | - Not a breaking change so no codemod needed 200 | 201 | - [x] MonoRepo: 202 | 203 | - Zodios will become a really large project so maybe migrate to turbo repo + pnpm 204 | 205 | - not a breaking change 206 | 207 | - [ ] Transform: 208 | 209 | - By default, activate transforms on backend and disable on frontend (today it's the opposite), would make server transform code simpler since with this option we could make any transforms activated not just zod defaults. 210 | 211 | - Rationale being that transformation can be viewed as business code that should be kept on backend 212 | 213 | - breaking change => codemod to keep current defaults by setting them explicitly 214 | 215 | - [x] Axios: 216 | 217 | - Move Axios client to it's own package `@zodios/axios` and keep `@zodios/core` with only common types and helpers 218 | 219 | - Move plugins to `@zodios/axios-plugins` 220 | 221 | - breaking change => easy to do a codemod for this 222 | 223 | - [x] Fetch: 224 | 225 | - Create a new Fetch client with almost the same features as axios, but without axios dependency `@zodios/fetch` 226 | 227 | - Today we have fetch support with a plugin for axios instance (zodios maintains it's own axios network adapter for fetch). But since axios interceptors are not used by zodios plugins, we can make fetch implementation lighter than axios instance. 228 | 229 | - Create plugins package `@zodios/fetch-plugins` 230 | 231 | - Not sure it's doable without a lot of effort to keep it in sync/compatible with axios client 232 | 233 | - new feature, so no codemod needed 234 | 235 | - [ ] React/Solid: 236 | 237 | - make ZodiosHooks independant of Zodios client instance (axios, fetch) 238 | 239 | - not a breaking change, so no codemod needed 240 | 241 | - [x] Client Request Config 242 | 243 | - uniform Query/Mutation with body sent on the config and not as a standalone object. This would allow to not do `client.deleteUser(undefined, { params: { id: 1 } })` but simply `client.deleteUser({ params: { id: 1 } })` 244 | 245 | - breaking change, so a codemod would be needed, but might be difficult to implement 246 | 247 | - [x] Mock/Tests: 248 | 249 | - if we implement an abstraction layer for client instance, relying on moxios to mock APIs response will likely not work for fetch implementation. 250 | 251 | - create a `@zodios/testing` package that work for both axios/fetch clients 252 | 253 | - new feature, so no breaking change (no codemod needed) 254 | 255 | You have other ideas ? [Let me know !](https://github.com/ecyrbe/zodios/discussions) 256 | # Dependencies 257 | 258 | Zodios even when working in pure Javascript is better suited to be working with Typescript Language Server to handle autocompletion. 259 | So you should at least use the one provided by your IDE (vscode integrates a typescript language server) 260 | However, we will only support fixing bugs related to typings for versions of Typescript Language v4.5 261 | Earlier versions should work, but do not have TS tail recusion optimisation that impact the size of the API you can declare. 262 | 263 | Also note that Zodios do not embed any dependency. It's your Job to install the peer dependencies you need. 264 | 265 | Internally Zodios uses these libraries on all platforms : 266 | - zod 267 | - axios 268 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | --------- | ------------------ | 10 | | 10.x.y | :white_check_mark: | 11 | | < 10.0.0 | :x: | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | If you identify a vulnerability on zodios, please create an issue with a reproductible setup. 16 | Do not open an issue if you can't show a reproductible setup of if the vulnerability is on third party dependency. 17 | 18 | Indeed, a vulnerability on a peer dependency should be reported on the appropriate project: 19 | - zod 20 | - axios 21 | -------------------------------------------------------------------------------- /docs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 34 | 39 | 40 | -------------------------------------------------------------------------------- /examples/define-types.ts: -------------------------------------------------------------------------------- 1 | import { Zodios, makeApi } from "../src/index"; 2 | import { z } from "zod"; 3 | 4 | // you can define schema before declaring the API to get back the type 5 | const userSchema = z 6 | .object({ 7 | id: z.number(), 8 | name: z.string(), 9 | }) 10 | .required(); 11 | 12 | const usersSchema = z.array(userSchema); 13 | 14 | // you can then get back the types 15 | type User = z.infer; 16 | type Users = z.infer; 17 | 18 | // you can also predefine your API 19 | const jsonplaceholderUrl = "https://jsonplaceholder.typicode.com"; 20 | const jsonplaceholderApi = makeApi([ 21 | { 22 | method: "get", 23 | path: "/users", 24 | description: "Get all users", 25 | parameters: [ 26 | { 27 | name: "q", 28 | description: "full text search", 29 | type: "Query", 30 | schema: z.string(), 31 | }, 32 | { 33 | name: "page", 34 | description: "page number", 35 | type: "Query", 36 | schema: z.number().optional(), 37 | }, 38 | ], 39 | response: usersSchema, 40 | }, 41 | { 42 | method: "get", 43 | path: "/users/:id", 44 | description: "Get a user", 45 | response: userSchema, 46 | }, 47 | ]); 48 | 49 | // and then use them in your API 50 | async function bootstrap() { 51 | const apiClient = new Zodios(jsonplaceholderUrl, jsonplaceholderApi); 52 | 53 | const users = await apiClient.get("/users", { queries: { q: "Nicholas" } }); 54 | console.log(users); 55 | const user = await apiClient.get("/users/:id", { params: { id: 7 } }); 56 | console.log(user); 57 | } 58 | 59 | bootstrap(); 60 | -------------------------------------------------------------------------------- /examples/dev.to/api-key-plugin.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from "axios"; 2 | import { config } from "process"; 3 | import { ZodiosPlugin } from "../../src/index"; 4 | 5 | export interface ApiKeyPluginConfig { 6 | getApiKey: () => Promise; 7 | } 8 | 9 | export function pluginApiKey(provider: ApiKeyPluginConfig): ZodiosPlugin { 10 | return { 11 | request: async (_, config) => { 12 | return { 13 | ...config, 14 | headers: { 15 | ...config.headers, 16 | "api-key": await provider.getApiKey(), 17 | }, 18 | }; 19 | }, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /examples/dev.to/articles.ts: -------------------------------------------------------------------------------- 1 | import { apiBuilder, makeApi } from "../../src/index"; 2 | import { z } from "zod"; 3 | import { devUser } from "./users"; 4 | import { paramPages } from "./params"; 5 | 6 | const devArticle = z.object({ 7 | id: z.number(), 8 | type_of: z.string(), 9 | title: z.string(), 10 | description: z.string(), 11 | cover_image: z.string().or(z.null()), 12 | readable_publish_date: z.string().optional(), 13 | social_image: z.string().optional(), 14 | tag_list: z.array(z.string()).or(z.string()), 15 | tags: z.array(z.string()).or(z.string()).optional(), 16 | slug: z.string(), 17 | path: z.string(), 18 | url: z.string(), 19 | canonical_url: z.string(), 20 | comments_count: z.number(), 21 | positive_reactions_count: z.number(), 22 | public_reactions_count: z.number(), 23 | collection_id: z.number().or(z.null()).optional(), 24 | created_at: z.string().optional(), 25 | edited_at: z.string().optional(), 26 | crossposted_at: z.string().or(z.null()).optional(), 27 | published_at: z.string().or(z.null()).optional(), 28 | last_comment_at: z.string().optional(), 29 | published_timestamp: z.string(), 30 | reading_time_minutes: z.number(), 31 | user: devUser.partial(), 32 | read_time_minutes: z.number().optional(), 33 | organization: z 34 | .object({ 35 | name: z.string(), 36 | username: z.string(), 37 | slug: z.string(), 38 | profile_image: z.string(), 39 | profile_image_90: z.string(), 40 | }) 41 | .optional(), 42 | flare_tag: z 43 | .object({ 44 | name: z.string(), 45 | bg_color_hex: z.string(), 46 | text_color_hex: z.string(), 47 | }) 48 | .optional(), 49 | }); 50 | 51 | const devArticles = z.array(devArticle); 52 | 53 | export type Article = z.infer; 54 | export type Articles = z.infer; 55 | 56 | export const articlesApi = apiBuilder({ 57 | method: "get", 58 | path: "/articles", 59 | alias: "getAllArticles", 60 | description: "Get all articles", 61 | parameters: [ 62 | ...paramPages, 63 | { 64 | name: "tag", 65 | description: "Filter by tag", 66 | type: "Query", 67 | schema: z.string().optional(), 68 | }, 69 | { 70 | name: "tags", 71 | description: "Filter by tags", 72 | type: "Query", 73 | schema: z.string().optional(), 74 | }, 75 | { 76 | name: "tags_exclude", 77 | description: "Exclude tags", 78 | type: "Query", 79 | schema: z.string().optional(), 80 | }, 81 | { 82 | name: "username", 83 | description: "Filter by username", 84 | type: "Query", 85 | schema: z.string().optional(), 86 | }, 87 | { 88 | name: "state", 89 | description: "Filter by state", 90 | type: "Query", 91 | schema: z.string().optional(), 92 | }, 93 | { 94 | name: "top", 95 | type: "Query", 96 | schema: z.number().optional(), 97 | }, 98 | { 99 | name: "collection_id", 100 | type: "Query", 101 | schema: z.number().optional(), 102 | }, 103 | ], 104 | response: devArticles, 105 | }) 106 | .addEndpoint({ 107 | method: "get", 108 | path: "/articles/latest", 109 | alias: "getLatestArticle", 110 | description: "Get latest articles", 111 | parameters: paramPages, 112 | response: devArticles, 113 | }) 114 | .addEndpoint({ 115 | method: "get", 116 | path: "/articles/:id", 117 | alias: "getArticle", 118 | description: "Get an article by id", 119 | response: devArticle, 120 | }) 121 | .addEndpoint({ 122 | method: "put", 123 | path: "/articles/:id", 124 | alias: "updateArticle", 125 | description: "Update an article", 126 | response: devArticle, 127 | }) 128 | .addEndpoint({ 129 | method: "get", 130 | path: "/articles/:username/:slug", 131 | alias: "getArticleByUsernameAndSlug", 132 | description: "Get an article by username and slug", 133 | response: devArticle, 134 | }) 135 | .addEndpoint({ 136 | method: "get", 137 | path: "/articles/me", 138 | alias: "getMyArticles", 139 | description: "Get current user's articles", 140 | parameters: paramPages, 141 | response: devArticles, 142 | }) 143 | .addEndpoint({ 144 | method: "get", 145 | path: "/articles/me/published", 146 | alias: "getMyPublishedArticles", 147 | description: "Get current user's published articles", 148 | parameters: paramPages, 149 | response: devArticles, 150 | }) 151 | .addEndpoint({ 152 | method: "get", 153 | path: "/articles/me/unpublished", 154 | alias: "getMyUnpublishedArticles", 155 | description: "Get current user's unpublished articles", 156 | parameters: paramPages, 157 | response: devArticles, 158 | }) 159 | .addEndpoint({ 160 | method: "get", 161 | path: "/articles/me/all", 162 | alias: "getAllMyArticles", 163 | description: "Get current user's all articles", 164 | parameters: paramPages, 165 | response: devArticles, 166 | }) 167 | .build(); 168 | -------------------------------------------------------------------------------- /examples/dev.to/comments.ts: -------------------------------------------------------------------------------- 1 | import { makeApi, makeEndpoint } from "../../src/index"; 2 | import { z } from "zod"; 3 | import { devUser, User } from "./users"; 4 | 5 | /** 6 | * zod does not handle recusive well, so we need to manually define the schema 7 | */ 8 | export type Comment = { 9 | type_of: string; 10 | id_code: string; 11 | created_at: string; 12 | body_html: string; 13 | user: User; 14 | children: Comment[]; 15 | }; 16 | 17 | export const devComment: z.ZodSchema = z.lazy(() => 18 | z.object({ 19 | type_of: z.string(), 20 | id_code: z.string(), 21 | created_at: z.string(), 22 | body_html: z.string(), 23 | user: devUser, 24 | children: z.array(devComment), 25 | }) 26 | ); 27 | export const devComments = z.array(devComment); 28 | 29 | export type Comments = z.infer; 30 | 31 | const getAllComments = makeEndpoint({ 32 | method: "get", 33 | path: "/comments", 34 | alias: "getAllComments", 35 | description: "Get all comments", 36 | parameters: [ 37 | { 38 | name: "a_id", 39 | description: "Article ID", 40 | type: "Query", 41 | schema: z.number().optional(), 42 | }, 43 | { 44 | name: "p_id", 45 | description: "Podcast comment ID", 46 | type: "Query", 47 | schema: z.number().optional(), 48 | }, 49 | ], 50 | response: devComments, 51 | }); 52 | 53 | const getComment = makeEndpoint({ 54 | method: "get", 55 | path: "/comments/:id", 56 | alias: "getComment", 57 | description: "Get a comment", 58 | parameters: [ 59 | { 60 | name: "id", 61 | description: "Comment ID", 62 | type: "Path", 63 | schema: z.number(), 64 | }, 65 | ], 66 | response: devComment, 67 | }); 68 | 69 | export const commentsApi = makeApi([getAllComments, getComment]); 70 | -------------------------------------------------------------------------------- /examples/dev.to/example.ts: -------------------------------------------------------------------------------- 1 | import { Zodios } from "../../src/index"; 2 | import { articlesApi } from "./articles"; 3 | import { commentsApi } from "./comments"; 4 | import { followsApi } from "./follows"; 5 | import { followersApi } from "./followers"; 6 | import { userApi } from "./users"; 7 | import { pluginApiKey } from "./api-key-plugin"; 8 | 9 | export const devTo = new Zodios("https://dev.to/api", [ 10 | ...articlesApi, 11 | ...commentsApi, 12 | ...followsApi, 13 | ...followersApi, 14 | ...userApi, 15 | ]); 16 | 17 | devTo.use( 18 | pluginApiKey({ 19 | getApiKey: async () => "", 20 | }) 21 | ); 22 | 23 | const result = devTo.get("/articles/:id", { 24 | params: { id: 123 }, 25 | }); 26 | -------------------------------------------------------------------------------- /examples/dev.to/followers.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { makeApi } from "../../src/index"; 3 | import { paramPages } from "./params"; 4 | 5 | const devFollower = z.object({ 6 | type_of: z.string(), 7 | created_at: z.string(), 8 | id: z.number(), 9 | name: z.string(), 10 | path: z.string(), 11 | username: z.string(), 12 | profile_image: z.string(), 13 | }); 14 | 15 | const devFollowers = z.array(devFollower); 16 | 17 | export const followersApi = makeApi([ 18 | { 19 | method: "get", 20 | path: "/followers/users", 21 | alias: "getAllFollowers", 22 | parameters: [ 23 | ...paramPages, 24 | { 25 | name: "sort", 26 | description: "Sort by. defaults to created_at", 27 | type: "Query", 28 | schema: z.string().optional(), 29 | }, 30 | ], 31 | response: devFollowers, 32 | }, 33 | ]); 34 | -------------------------------------------------------------------------------- /examples/dev.to/follows.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { makeApi } from "../../src/index"; 3 | 4 | export const devFollow = z.object({ 5 | id: z.number(), 6 | name: z.string(), 7 | points: z.number(), 8 | }); 9 | 10 | export const devFollows = z.array(devFollow); 11 | 12 | export type Follow = z.infer; 13 | export type Follows = z.infer; 14 | 15 | export const followsApi = makeApi([ 16 | { 17 | method: "get", 18 | path: "/follows/tags", 19 | alias: "getAllFollowedTags", 20 | description: "Get all followed tags", 21 | response: devFollows, 22 | }, 23 | ]); 24 | -------------------------------------------------------------------------------- /examples/dev.to/params.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { makeParameters } from "../../src/api"; 3 | 4 | export const paramPages = makeParameters([ 5 | { 6 | name: "page", 7 | type: "Query", 8 | description: "Page number", 9 | schema: z.number().optional(), 10 | }, 11 | { 12 | name: "per_page", 13 | type: "Query", 14 | schema: z.number().optional(), 15 | }, 16 | ]); 17 | -------------------------------------------------------------------------------- /examples/dev.to/users.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { makeApi } from "../../src/index"; 3 | 4 | export const devUser = z.object({ 5 | id: z.number(), 6 | type_of: z.string(), 7 | name: z.string(), 8 | username: z.string(), 9 | summary: z.string().or(z.null()), 10 | twitter_username: z.string().or(z.null()), 11 | github_username: z.string().or(z.null()), 12 | website_url: z.string().or(z.null()), 13 | location: z.string().or(z.null()), 14 | joined_at: z.string(), 15 | profile_image: z.string(), 16 | profile_image_90: z.string(), 17 | }); 18 | 19 | export type User = z.infer; 20 | 21 | export const devProfileImage = z.object({ 22 | type_of: z.string(), 23 | image_of: z.string(), 24 | profile_image: z.string(), 25 | profile_image_90: z.string(), 26 | }); 27 | 28 | export type ProfileImage = z.infer; 29 | 30 | export const userApi = makeApi([ 31 | { 32 | method: "get", 33 | path: "/users/:id", 34 | alias: "getUser", 35 | description: "Get a user", 36 | response: devUser, 37 | errors: [ 38 | { 39 | status: "default", 40 | description: "Default error", 41 | schema: z.object({ 42 | error: z.object({ 43 | code: z.string(), 44 | message: z.string(), 45 | }), 46 | }), 47 | }, 48 | ], 49 | }, 50 | { 51 | method: "get", 52 | path: "/users/me", 53 | alias: "getMe", 54 | description: "Get current user", 55 | response: devUser, 56 | }, 57 | { 58 | method: "get", 59 | path: "/profile_image/:username", 60 | alias: "getProfileImage", 61 | description: "Get a user's profile image", 62 | response: devProfileImage, 63 | }, 64 | ]); 65 | -------------------------------------------------------------------------------- /examples/jsonplaceholder.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApiOf, 3 | ZodiosResponseByAlias, 4 | ZodiosHeaderParamsByAlias, 5 | Zodios, 6 | } from "../src/index"; 7 | import { z } from "zod"; 8 | 9 | async function bootstrap() { 10 | const apiClient = new Zodios("https://jsonplaceholder.typicode.com", [ 11 | { 12 | method: "get", 13 | path: "/users", 14 | alias: "getUsers", 15 | description: "Get all users", 16 | parameters: [ 17 | { 18 | name: "q", 19 | type: "Query", 20 | schema: z.string(), 21 | }, 22 | { 23 | name: "page", 24 | type: "Query", 25 | schema: z.string().optional(), 26 | }, 27 | ], 28 | response: z.array(z.object({ id: z.number(), name: z.string() })), 29 | }, 30 | { 31 | method: "get", 32 | path: "/users/:id", 33 | alias: "getUser", 34 | description: "Get a user", 35 | response: z.object({ 36 | id: z.number(), 37 | name: z.string(), 38 | }), 39 | }, 40 | { 41 | method: "delete", 42 | path: "/users/:id", 43 | alias: "deleteUser", 44 | description: "Delete a user", 45 | response: z.object({ }), 46 | }, 47 | { 48 | method: "post", 49 | path: "/users", 50 | alias: "createUser", 51 | description: "Create a user", 52 | parameters: [ 53 | { 54 | name: "body", 55 | type: "Body", 56 | schema: z.object({ name: z.string() }), 57 | }, 58 | { 59 | name: "Content-Type", 60 | type: "Header", 61 | schema: z.string(), 62 | }, 63 | ], 64 | response: z.object({ id: z.number(), name: z.string() }), 65 | }, 66 | ]); 67 | 68 | type UserResponseAlias = ZodiosResponseByAlias< 69 | ApiOf, 70 | "getUsers" 71 | >; 72 | 73 | type Test = ZodiosHeaderParamsByAlias, "createUser">; 74 | 75 | const users: UserResponseAlias = await apiClient.getUsers({ 76 | queries: { q: "Nicholas" }, 77 | }); 78 | console.log(users); 79 | const user = await apiClient.getUser({ params: { id: 7 } }); 80 | console.log(user); 81 | const createdUser = await apiClient.createUser( 82 | { name: "john doe" }, 83 | { 84 | headers: { 85 | "Content-Type": "application/json", 86 | Accept: "application/json", 87 | }, 88 | } 89 | ); 90 | console.log(createdUser); 91 | const deletedUser = await apiClient.deleteUser(undefined, { 92 | params: { id: 7 }, 93 | }); 94 | console.log(deletedUser); 95 | } 96 | 97 | bootstrap(); 98 | -------------------------------------------------------------------------------- /examples/types-from-definition.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ZodiosResponseByPath, 3 | ZodiosBodyByPath, 4 | ZodiosPathParamsByPath, 5 | ZodiosQueryParamsByPath, 6 | makeApi, 7 | ZodiosPathParamByAlias, 8 | makeErrors, 9 | } from "../src/index"; 10 | import z from "zod"; 11 | 12 | const user = z.object({ 13 | id: z.number(), 14 | name: z.string(), 15 | email: z.string().email(), 16 | phone: z.string(), 17 | }); 18 | 19 | const errors = makeErrors([ 20 | { 21 | status: 404, 22 | schema: z.object({ 23 | message: z.string(), 24 | }), 25 | }, 26 | { 27 | status: 401, 28 | schema: z.object({ 29 | message: z.string(), 30 | }), 31 | }, 32 | { 33 | status: 500, 34 | schema: z.object({ 35 | message: z.string(), 36 | cause: z.record(z.string()), 37 | }), 38 | }, 39 | ]); 40 | 41 | const api = makeApi([ 42 | { 43 | path: "/users", 44 | method: "get", 45 | response: z.array(user), 46 | }, 47 | { 48 | path: "/users/:id", 49 | method: "get", 50 | alias: "getUser", 51 | parameters: [ 52 | { 53 | name: "id", 54 | type: "Path", 55 | schema: z.number().positive(), 56 | }, 57 | ], 58 | response: user, 59 | errors, 60 | }, 61 | { 62 | path: "/users", 63 | method: "post", 64 | parameters: [ 65 | { 66 | name: "body", 67 | type: "Body", 68 | schema: user, 69 | }, 70 | ], 71 | response: user, 72 | }, 73 | { 74 | path: "/users/:id", 75 | method: "put", 76 | parameters: [ 77 | { 78 | name: "body", 79 | type: "Body", 80 | schema: user, 81 | }, 82 | ], 83 | response: user, 84 | }, 85 | { 86 | path: "/users/:id", 87 | method: "patch", 88 | parameters: [ 89 | { 90 | name: "body", 91 | type: "Body", 92 | schema: user, 93 | }, 94 | ], 95 | response: user, 96 | }, 97 | { 98 | path: "/users/:id", 99 | method: "delete", 100 | response: z.object({}), 101 | }, 102 | ]); 103 | 104 | type User = z.infer; 105 | type Api = typeof api; 106 | 107 | type Users = ZodiosResponseByPath; 108 | // ^? 109 | type UserById = ZodiosResponseByPath; 110 | // ^? 111 | type GetUserParams = ZodiosPathParamsByPath; 112 | // ^? 113 | type GetUserParamsByAlias = ZodiosPathParamByAlias; 114 | // ^? 115 | type GetUsersParams = ZodiosPathParamsByPath; 116 | // ^? 117 | type GetUserQueries = ZodiosQueryParamsByPath; 118 | // ^? 119 | type CreateUserBody = ZodiosBodyByPath; 120 | // ^? 121 | type CreateUserResponse = ZodiosResponseByPath; 122 | // ^? 123 | type UpdateUserBody = ZodiosBodyByPath; 124 | // ^? 125 | type PatchUserBody = ZodiosBodyByPath; 126 | // ^? 127 | type DeleteUserResponse = ZodiosResponseByPath; 128 | // ^? 129 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "@jest/types"; 2 | 3 | // Objet synchrone 4 | const config: Config.InitialOptions = { 5 | verbose: true, 6 | moduleFileExtensions: ["ts", "js", "json", "node"], 7 | rootDir: "./", 8 | testRegex: ".(spec|test).tsx?$", 9 | transform: { 10 | "^.+\\.tsx?$": "ts-jest", 11 | }, 12 | coveragePathIgnorePatterns: ["/node_modules/", "index\\.ts"], 13 | coverageDirectory: "./coverage", 14 | coverageThreshold: { 15 | global: { 16 | branches: 80, 17 | functions: 85, 18 | lines: 85, 19 | statements: 85, 20 | }, 21 | }, 22 | testEnvironment: "node", 23 | }; 24 | export default config; 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@zodios/core", 3 | "description": "Typescript API client with autocompletion and zod validations", 4 | "version": "10.9.6", 5 | "main": "lib/index.js", 6 | "module": "lib/index.mjs", 7 | "typings": "lib/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": "./lib/index.mjs", 11 | "require": "./lib/index.js", 12 | "types": "./lib/index.d.ts" 13 | }, 14 | "./lib/*.types": { 15 | "import": "./lib/*.types.mjs", 16 | "require": "./lib/*.types.js", 17 | "types": "./lib/*.types.d.ts" 18 | }, 19 | "./package.json": "./package.json" 20 | }, 21 | "files": [ 22 | "lib" 23 | ], 24 | "author": { 25 | "name": "ecyrbe", 26 | "email": "ecyrbe@gmail.com" 27 | }, 28 | "homepage": "https://github.com/ecyrbe/zodios", 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/ecyrbe/zodios.git" 32 | }, 33 | "license": "MIT", 34 | "keywords": [ 35 | "axios", 36 | "openapi", 37 | "zod", 38 | "autocomplete", 39 | "validation" 40 | ], 41 | "scripts": { 42 | "prebuild": "rimraf lib", 43 | "example": "ts-node examples/jsonplaceholder.ts", 44 | "example:dev.to": "ts-node examples/dev.to/example.ts", 45 | "major-rc": "npm version premajor --preid=rc", 46 | "minor-rc": "npm version preminor --preid=rc", 47 | "patch-rc": "npm version prepatch --preid=rc", 48 | "rc": "npm version prerelease --preid=rc", 49 | "build": "tsup", 50 | "test": "jest --coverage" 51 | }, 52 | "peerDependencies": { 53 | "axios": "^0.x || ^1.0.0", 54 | "zod": "^3.x" 55 | }, 56 | "devDependencies": { 57 | "@jest/types": "29.6.3", 58 | "@types/express": "4.17.19", 59 | "@types/jest": "29.5.5", 60 | "@types/multer": "1.4.8", 61 | "@types/node": "20.8.9", 62 | "axios": "1.5.1", 63 | "express": "4.18.2", 64 | "form-data": "4.0.0", 65 | "jest": "29.7.0", 66 | "multer": "1.4.5-lts.1", 67 | "rimraf": "5.0.5", 68 | "ts-jest": "29.1.1", 69 | "ts-node": "10.9.1", 70 | "tsup": "6.3.0", 71 | "typescript": "5.2.2", 72 | "zod": "3.22.4" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "ignorePaths": ["website/package.json"], 4 | "ignoreDeps": ["tsup"] 5 | } 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { Zodios } from "./zodios"; 2 | export type { ApiOf } from "./zodios"; 3 | export type { ZodiosInstance, ZodiosClass, ZodiosConstructor } from "./zodios"; 4 | export { ZodiosError } from "./zodios-error"; 5 | export { isErrorFromPath, isErrorFromAlias } from "./zodios-error.utils"; 6 | export type { 7 | AnyZodiosMethodOptions, 8 | AnyZodiosRequestOptions, 9 | ZodiosBodyForEndpoint, 10 | ZodiosBodyByPath, 11 | ZodiosBodyByAlias, 12 | ZodiosHeaderParamsForEndpoint, 13 | ZodiosHeaderParamsByPath, 14 | ZodiosHeaderParamsByAlias, 15 | Method, 16 | ZodiosPathParams, 17 | ZodiosPathParamsForEndpoint, 18 | ZodiosPathParamsByPath, 19 | ZodiosPathParamByAlias, 20 | ZodiosPathsByMethod, 21 | ZodiosResponseForEndpoint, 22 | ZodiosResponseByPath, 23 | ZodiosResponseByAlias, 24 | ZodiosQueryParamsForEndpoint, 25 | ZodiosQueryParamsByPath, 26 | ZodiosQueryParamsByAlias, 27 | ZodiosEndpointDefinitionByPath, 28 | ZodiosEndpointDefinitionByAlias, 29 | ZodiosErrorForEndpoint, 30 | ZodiosErrorByPath, 31 | ZodiosErrorByAlias, 32 | ZodiosEndpointDefinition, 33 | ZodiosEndpointDefinitions, 34 | ZodiosEndpointParameter, 35 | ZodiosEndpointParameters, 36 | ZodiosEndpointError, 37 | ZodiosEndpointErrors, 38 | ZodiosOptions, 39 | ZodiosRequestOptions, 40 | ZodiosMethodOptions, 41 | ZodiosRequestOptionsByPath, 42 | ZodiosRequestOptionsByAlias, 43 | ZodiosPlugin, 44 | } from "./zodios.types"; 45 | export { 46 | PluginId, 47 | zodValidationPlugin, 48 | formDataPlugin, 49 | formURLPlugin, 50 | headerPlugin, 51 | } from "./plugins"; 52 | 53 | export { 54 | makeApi, 55 | makeCrudApi, 56 | apiBuilder, 57 | parametersBuilder, 58 | makeParameters, 59 | makeEndpoint, 60 | makeErrors, 61 | checkApi, 62 | prefixApi, 63 | mergeApis, 64 | } from "./api"; 65 | -------------------------------------------------------------------------------- /src/plugins/form-data.plugin.ts: -------------------------------------------------------------------------------- 1 | import { getFormDataStream } from "./form-data.utils"; 2 | import { ZodiosError } from "../zodios-error"; 3 | import type { ZodiosPlugin } from "../zodios.types"; 4 | 5 | const plugin: ZodiosPlugin = { 6 | name: "form-data", 7 | request: async (_, config) => { 8 | if (typeof config.data !== "object" || Array.isArray(config.data)) { 9 | throw new ZodiosError( 10 | "Zodios: multipart/form-data body must be an object", 11 | config 12 | ); 13 | } 14 | const result = getFormDataStream(config.data as any); 15 | return { 16 | ...config, 17 | data: result.data, 18 | headers: { 19 | ...config.headers, 20 | ...result.headers, 21 | }, 22 | }; 23 | }, 24 | }; 25 | 26 | /** 27 | * form-data plugin used internally by Zodios. 28 | * @example 29 | * ```typescript 30 | * const apiClient = new Zodios( 31 | * "https://mywebsite.com", 32 | * [{ 33 | * method: "post", 34 | * path: "/upload", 35 | * alias: "upload", 36 | * description: "Upload a file", 37 | * requestFormat: "form-data", 38 | * parameters:[ 39 | * { 40 | * name: "body", 41 | * type: "Body", 42 | * schema: z.object({ 43 | * file: z.instanceof(File), 44 | * }), 45 | * } 46 | * ], 47 | * response: z.object({ 48 | * id: z.number(), 49 | * }), 50 | * }], 51 | * ); 52 | * const id = await apiClient.upload({ file: document.querySelector('#file').files[0] }); 53 | * ``` 54 | * @returns form-data plugin 55 | */ 56 | export function formDataPlugin(): ZodiosPlugin { 57 | return plugin; 58 | } 59 | -------------------------------------------------------------------------------- /src/plugins/form-data.utils.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /** 4 | * getFormDataStream 5 | * @param data - the data to be encoded as form data stream 6 | * @returns a readable stream of the form data and optionnaly headers 7 | */ 8 | export function getFormDataStream(data: Record): { 9 | data: FormData; 10 | headers?: Record; 11 | } { 12 | const formData = new FormData(); 13 | for (const key in data) { 14 | formData.append(key, data[key]); 15 | } 16 | return { 17 | data: formData, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/plugins/form-url.plugin.ts: -------------------------------------------------------------------------------- 1 | import { ZodiosError } from "../zodios-error"; 2 | import type { ZodiosPlugin } from "../zodios.types"; 3 | 4 | const plugin: ZodiosPlugin = { 5 | name: "form-url", 6 | request: async (_, config) => { 7 | if (typeof config.data !== "object" || Array.isArray(config.data)) { 8 | throw new ZodiosError( 9 | "Zodios: application/x-www-form-urlencoded body must be an object", 10 | config 11 | ); 12 | } 13 | 14 | return { 15 | ...config, 16 | data: new URLSearchParams(config.data as any).toString(), 17 | headers: { 18 | ...config.headers, 19 | "Content-Type": "application/x-www-form-urlencoded", 20 | }, 21 | }; 22 | }, 23 | }; 24 | 25 | /** 26 | * form-url plugin used internally by Zodios. 27 | * @example 28 | * ```typescript 29 | * const apiClient = new Zodios( 30 | * "https://mywebsite.com", 31 | * [{ 32 | * method: "post", 33 | * path: "/login", 34 | * alias: "login", 35 | * description: "Submit a form", 36 | * requestFormat: "form-url", 37 | * parameters:[ 38 | * { 39 | * name: "body", 40 | * type: "Body", 41 | * schema: z.object({ 42 | * userName: z.string(), 43 | * password: z.string(), 44 | * }), 45 | * } 46 | * ], 47 | * response: z.object({ 48 | * id: z.number(), 49 | * }), 50 | * }], 51 | * ); 52 | * const id = await apiClient.login({ userName: "user", password: "password" }); 53 | * ``` 54 | * @returns form-url plugin 55 | */ 56 | export function formURLPlugin(): ZodiosPlugin { 57 | return plugin; 58 | } 59 | -------------------------------------------------------------------------------- /src/plugins/header.plugin.ts: -------------------------------------------------------------------------------- 1 | import type { ZodiosPlugin } from "../zodios.types"; 2 | 3 | export function headerPlugin(key: string, value: string): ZodiosPlugin { 4 | return { 5 | request: async (_, config) => { 6 | return { 7 | ...config, 8 | headers: { 9 | ...config.headers, 10 | [key]: value, 11 | }, 12 | }; 13 | }, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./form-data.plugin"; 2 | export * from "./form-url.plugin"; 3 | export * from "./header.plugin"; 4 | export * from "./zod-validation.plugin"; 5 | export * from "./zodios-plugins"; 6 | -------------------------------------------------------------------------------- /src/plugins/zod-validation.plugin.test.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosResponse } from "axios"; 2 | import { z } from "zod"; 3 | import { apiBuilder } from "../api"; 4 | import { ReadonlyDeep } from "../utils.types"; 5 | import { AnyZodiosRequestOptions } from "../zodios.types"; 6 | import { zodValidationPlugin } from "./zod-validation.plugin"; 7 | 8 | describe("zodValidationPlugin", () => { 9 | const plugin = zodValidationPlugin({ 10 | validate: true, 11 | transform: true, 12 | sendDefaults: false, 13 | }); 14 | const pluginWithDefaults = zodValidationPlugin({ 15 | validate: true, 16 | transform: true, 17 | sendDefaults: true, 18 | }); 19 | const pluginWithoutTransform = zodValidationPlugin({ 20 | validate: true, 21 | transform: false, 22 | sendDefaults: false, 23 | }); 24 | 25 | describe("request", () => { 26 | it("should be defined", () => { 27 | expect(plugin.request).toBeDefined(); 28 | }); 29 | 30 | it("should throw if endpoint is not found", async () => { 31 | await expect( 32 | plugin.request!(api, notExistingConfig) 33 | ).rejects.toThrowError("No endpoint found for get /notExisting"); 34 | }); 35 | 36 | it("should verify parameters", async () => { 37 | const transformed = await plugin.request!( 38 | api, 39 | createSampleConfig("/parse") 40 | ); 41 | 42 | expect(transformed.data).toBe("123"); 43 | expect(transformed.queries).toStrictEqual({ 44 | sampleQueryParam: "456", 45 | }); 46 | expect(transformed.headers).toStrictEqual({ 47 | sampleHeader: "789", 48 | }); 49 | }); 50 | 51 | it("should transform parameters", async () => { 52 | const transformed = await plugin.request!( 53 | api, 54 | createSampleConfig("/transform") 55 | ); 56 | 57 | expect(transformed.data).toBe("123_transformed"); 58 | expect(transformed.queries).toStrictEqual({ 59 | sampleQueryParam: "456_transformed", 60 | }); 61 | expect(transformed.headers).toStrictEqual({ 62 | sampleHeader: "789_transformed", 63 | }); 64 | }); 65 | 66 | it("should transform empty string parameters", async () => { 67 | const transformed = await plugin.request!( 68 | api, 69 | createEmptySampleConfig("/transform") 70 | ); 71 | 72 | expect(transformed.data).toBe("_transformed"); 73 | expect(transformed.queries).toStrictEqual({ 74 | sampleQueryParam: "_transformed", 75 | }); 76 | expect(transformed.headers).toStrictEqual({ 77 | sampleHeader: "_transformed", 78 | }); 79 | }); 80 | 81 | it("should generate default parameter when generateDefaults is activated", async () => { 82 | const defaulted = await pluginWithDefaults.request!( 83 | api, 84 | createUndefinedSampleConfig("/defaults") 85 | ); 86 | 87 | expect(defaulted.queries).toStrictEqual({ 88 | sampleQueryParam: "defaultQueryParam", 89 | }); 90 | expect(defaulted.headers).toStrictEqual({ 91 | sampleHeader: "defaultHeader", 92 | }); 93 | }); 94 | 95 | it("should not transform parameters when transform is disabled", async () => { 96 | const notTransformed = await pluginWithoutTransform.request!( 97 | api, 98 | createSampleConfig("/transform") 99 | ); 100 | 101 | expect(notTransformed.data).toBe("123"); 102 | expect(notTransformed.queries).toStrictEqual({ 103 | sampleQueryParam: "456", 104 | }); 105 | expect(notTransformed.headers).toStrictEqual({ 106 | sampleHeader: "789", 107 | }); 108 | }); 109 | 110 | it("should transform parameters (async)", async () => { 111 | const transformed = await plugin.request!( 112 | api, 113 | createSampleConfig("/transformAsync") 114 | ); 115 | 116 | expect(transformed.data).toBe("123_transformed"); 117 | expect(transformed.queries).toStrictEqual({ 118 | sampleQueryParam: "456_transformed", 119 | }); 120 | expect(transformed.headers).toStrictEqual({ 121 | sampleHeader: "789_transformed", 122 | }); 123 | }); 124 | 125 | it("should not transform parameters (async) when transform is disabled", async () => { 126 | const notTransformed = await pluginWithoutTransform.request!( 127 | api, 128 | createSampleConfig("/transformAsync") 129 | ); 130 | 131 | expect(notTransformed.data).toBe("123"); 132 | expect(notTransformed.queries).toStrictEqual({ 133 | sampleQueryParam: "456", 134 | }); 135 | expect(notTransformed.headers).toStrictEqual({ 136 | sampleHeader: "789", 137 | }); 138 | }); 139 | 140 | it("should throw on unsuccessful parse", async () => { 141 | const badConfig = createSampleConfig("/parse"); 142 | badConfig.queries = { 143 | sampleQueryParam: 123, 144 | }; 145 | 146 | await expect(plugin.request!(api, badConfig)).rejects.toThrowError( 147 | "Zodios: Invalid Query parameter 'sampleQueryParam'" 148 | ); 149 | }); 150 | }); 151 | 152 | describe("response", () => { 153 | it("should be defined", () => { 154 | expect(plugin.response).toBeDefined(); 155 | }); 156 | 157 | it("should throw if endpoint is not found", async () => { 158 | await expect( 159 | plugin.response!(api, notExistingConfig, createSampleResponse()) 160 | ).rejects.toThrowError("No endpoint found for get /notExisting"); 161 | }); 162 | 163 | it("should verify body", async () => { 164 | const transformed = await plugin.response!( 165 | api, 166 | createSampleConfig("/parse"), 167 | createSampleResponse() 168 | ); 169 | 170 | expect(transformed.data).toStrictEqual({ 171 | first: "123", 172 | second: 111, 173 | }); 174 | }); 175 | 176 | it("should transform body", async () => { 177 | const transformed = await plugin.response!( 178 | api, 179 | createSampleConfig("/transform"), 180 | createSampleResponse() 181 | ); 182 | 183 | expect(transformed.data).toStrictEqual({ 184 | first: "123_transformed", 185 | second: 234, 186 | }); 187 | }); 188 | 189 | it("should transform JSON:API body", async () => { 190 | const transformed = await plugin.response!( 191 | api, 192 | createSampleConfig("/transform"), 193 | createSampleResponse({ 194 | headers: { 195 | "content-type": "application/vnd.api+json; charset=utf-8", 196 | }, 197 | }) 198 | ); 199 | 200 | expect(transformed.data).toStrictEqual({ 201 | first: "123_transformed", 202 | second: 234, 203 | }); 204 | }); 205 | 206 | it("should not transform body when transform is disabled", async () => { 207 | const notTransformed = await pluginWithoutTransform.response!( 208 | api, 209 | createSampleConfig("/transform"), 210 | createSampleResponse() 211 | ); 212 | 213 | expect(notTransformed.data).toStrictEqual({ 214 | first: "123", 215 | second: 111, 216 | }); 217 | }); 218 | 219 | it("should transform body (async)", async () => { 220 | const transformed = await plugin.response!( 221 | api, 222 | createSampleConfig("/transformAsync"), 223 | createSampleResponse() 224 | ); 225 | 226 | expect(transformed.data).toStrictEqual({ 227 | first: "123_transformed", 228 | second: 234, 229 | }); 230 | }); 231 | 232 | it("should not transform body (async) when transform is disabled", async () => { 233 | const notTransformed = await pluginWithoutTransform.response!( 234 | api, 235 | createSampleConfig("/transformAsync"), 236 | createSampleResponse() 237 | ); 238 | 239 | expect(notTransformed.data).toStrictEqual({ 240 | first: "123", 241 | second: 111, 242 | }); 243 | }); 244 | 245 | it("should throw on unsuccessful parse", async () => { 246 | const badResponse = createSampleResponse(); 247 | badResponse.data.first = 123; 248 | 249 | await expect( 250 | plugin.response!(api, createSampleConfig("/parse"), badResponse) 251 | ).rejects 252 | .toThrowError(`Zodios: Invalid response from endpoint 'post /parse' 253 | status: 200 OK 254 | cause: 255 | [ 256 | { 257 | "code": "invalid_type", 258 | "expected": "string", 259 | "received": "number", 260 | "path": [ 261 | "first" 262 | ], 263 | "message": "Expected string, received number" 264 | } 265 | ] 266 | received: 267 | { 268 | "first": 123, 269 | "second": 111 270 | }`); 271 | }); 272 | }); 273 | 274 | const notExistingConfig: ReadonlyDeep = { 275 | method: "get", 276 | url: "/notExisting", 277 | }; 278 | 279 | const createSampleConfig = (url: string): AnyZodiosRequestOptions => ({ 280 | method: "post", 281 | url, 282 | data: "123", 283 | queries: { 284 | sampleQueryParam: "456", 285 | }, 286 | headers: { 287 | sampleHeader: "789", 288 | }, 289 | }); 290 | 291 | const createEmptySampleConfig = (url: string): AnyZodiosRequestOptions => ({ 292 | method: "post", 293 | url, 294 | data: "", 295 | queries: { 296 | sampleQueryParam: "", 297 | }, 298 | headers: { 299 | sampleHeader: "", 300 | }, 301 | }); 302 | 303 | const createUndefinedSampleConfig = ( 304 | url: string 305 | ): AnyZodiosRequestOptions => ({ 306 | method: "get", 307 | url, 308 | }); 309 | 310 | const createSampleResponse = ( 311 | { headers } = { headers: { "content-type": "application/json" } } 312 | ) => 313 | ({ 314 | data: { 315 | first: "123", 316 | second: 111, 317 | }, 318 | status: 200, 319 | headers: headers, 320 | config: {}, 321 | statusText: "OK", 322 | } as unknown as AxiosResponse); 323 | 324 | const api = apiBuilder({ 325 | path: "/parse", 326 | method: "post", 327 | response: z.object({ 328 | first: z.string(), 329 | second: z.number(), 330 | }), 331 | parameters: [ 332 | { 333 | type: "Body", 334 | schema: z.string(), 335 | name: "body", 336 | }, 337 | { 338 | type: "Query", 339 | schema: z.string(), 340 | name: "sampleQueryParam", 341 | }, 342 | { 343 | type: "Header", 344 | schema: z.string(), 345 | name: "sampleHeader", 346 | }, 347 | ], 348 | }) 349 | .addEndpoint({ 350 | path: "/defaults", 351 | method: "get", 352 | response: z.object({ 353 | first: z.string(), 354 | second: z.number(), 355 | }), 356 | parameters: [ 357 | { 358 | type: "Query", 359 | schema: z.string().default("defaultQueryParam"), 360 | name: "sampleQueryParam", 361 | }, 362 | { 363 | type: "Header", 364 | schema: z.string().default("defaultHeader"), 365 | name: "sampleHeader", 366 | }, 367 | ], 368 | }) 369 | .addEndpoint({ 370 | path: "/transform", 371 | method: "post", 372 | response: z.object({ 373 | first: z.string().transform((data) => data + "_transformed"), 374 | second: z.number().transform((data) => data + 123), 375 | }), 376 | parameters: [ 377 | { 378 | type: "Body", 379 | schema: z.string().transform((data) => data + "_transformed"), 380 | name: "body", 381 | }, 382 | { 383 | type: "Query", 384 | schema: z.string().transform((data) => data + "_transformed"), 385 | name: "sampleQueryParam", 386 | }, 387 | { 388 | type: "Header", 389 | schema: z.string().transform((data) => data + "_transformed"), 390 | name: "sampleHeader", 391 | }, 392 | ], 393 | }) 394 | // Even if nothing is awaited `transform` returns a `Promise` 395 | .addEndpoint({ 396 | path: "/transformAsync", 397 | method: "post", 398 | response: z.object({ 399 | first: z.string().transform(async (data) => data + "_transformed"), 400 | second: z.number().transform(async (data) => data + 123), 401 | }), 402 | parameters: [ 403 | { 404 | type: "Body", 405 | schema: z.string().transform(async (data) => data + "_transformed"), 406 | name: "body", 407 | }, 408 | { 409 | type: "Query", 410 | schema: z.string().transform(async (data) => data + "_transformed"), 411 | name: "sampleQueryParam", 412 | }, 413 | { 414 | type: "Header", 415 | schema: z.string().transform(async (data) => data + "_transformed"), 416 | name: "sampleHeader", 417 | }, 418 | ], 419 | }) 420 | .build(); 421 | }); 422 | -------------------------------------------------------------------------------- /src/plugins/zod-validation.plugin.ts: -------------------------------------------------------------------------------- 1 | import { ZodiosError } from "../zodios-error"; 2 | import type { ZodiosOptions, ZodiosPlugin } from "../zodios.types"; 3 | import { findEndpoint } from "../utils"; 4 | 5 | type Options = Required< 6 | Pick 7 | >; 8 | 9 | function shouldResponse(option: string | boolean) { 10 | return [true, "response", "all"].includes(option); 11 | } 12 | 13 | function shouldRequest(option: string | boolean) { 14 | return [true, "request", "all"].includes(option); 15 | } 16 | 17 | /** 18 | * Zod validation plugin used internally by Zodios. 19 | * By default zodios always validates the response. 20 | * @returns zod-validation plugin 21 | */ 22 | export function zodValidationPlugin({ 23 | validate, 24 | transform, 25 | sendDefaults, 26 | }: Options): ZodiosPlugin { 27 | return { 28 | name: "zod-validation", 29 | request: shouldRequest(validate) 30 | ? async (api, config) => { 31 | const endpoint = findEndpoint(api, config.method, config.url); 32 | if (!endpoint) { 33 | throw new Error( 34 | `No endpoint found for ${config.method} ${config.url}` 35 | ); 36 | } 37 | const { parameters } = endpoint; 38 | if (!parameters) { 39 | return config; 40 | } 41 | const conf = { 42 | ...config, 43 | queries: { 44 | ...config.queries, 45 | }, 46 | headers: { 47 | ...config.headers, 48 | }, 49 | params: { 50 | ...config.params, 51 | }, 52 | }; 53 | const paramsOf = { 54 | Query: (name: string) => conf.queries?.[name], 55 | Body: (_: string) => conf.data, 56 | Header: (name: string) => conf.headers?.[name], 57 | Path: (name: string) => conf.params?.[name], 58 | }; 59 | const setParamsOf = { 60 | Query: (name: string, value: any) => (conf.queries![name] = value), 61 | Body: (_: string, value: any) => (conf.data = value), 62 | Header: (name: string, value: any) => (conf.headers![name] = value), 63 | Path: (name: string, value: any) => (conf.params![name] = value), 64 | }; 65 | const transformRequest = shouldRequest(transform); 66 | for (const parameter of parameters) { 67 | const { name, schema, type } = parameter; 68 | const value = paramsOf[type](name); 69 | if (sendDefaults || value !== undefined) { 70 | const parsed = await schema.safeParseAsync(value); 71 | if (!parsed.success) { 72 | throw new ZodiosError( 73 | `Zodios: Invalid ${type} parameter '${name}'`, 74 | config, 75 | value, 76 | parsed.error 77 | ); 78 | } 79 | if (transformRequest) { 80 | setParamsOf[type](name, parsed.data); 81 | } 82 | } 83 | } 84 | return conf; 85 | } 86 | : undefined, 87 | response: shouldResponse(validate) 88 | ? async (api, config, response) => { 89 | const endpoint = findEndpoint(api, config.method, config.url); 90 | /* istanbul ignore next */ 91 | if (!endpoint) { 92 | throw new Error( 93 | `No endpoint found for ${config.method} ${config.url}` 94 | ); 95 | } 96 | if ( 97 | response.headers?.["content-type"]?.includes("application/json") || 98 | response.headers?.["content-type"]?.includes( 99 | "application/vnd.api+json" 100 | ) 101 | ) { 102 | const parsed = await endpoint.response.safeParseAsync( 103 | response.data 104 | ); 105 | if (!parsed.success) { 106 | throw new ZodiosError( 107 | `Zodios: Invalid response from endpoint '${endpoint.method} ${ 108 | endpoint.path 109 | }'\nstatus: ${response.status} ${ 110 | response.statusText 111 | }\ncause:\n${parsed.error.message}\nreceived:\n${JSON.stringify( 112 | response.data, 113 | null, 114 | 2 115 | )}`, 116 | config, 117 | response.data, 118 | parsed.error 119 | ); 120 | } 121 | if (shouldResponse(transform)) { 122 | response.data = parsed.data; 123 | } 124 | } 125 | return response; 126 | } 127 | : undefined, 128 | }; 129 | } 130 | -------------------------------------------------------------------------------- /src/plugins/zodios-plugins.test.ts: -------------------------------------------------------------------------------- 1 | import { ZodiosPlugin } from "../zodios.types"; 2 | import { ZodiosPlugins } from "./zodios-plugins"; 3 | 4 | describe("ZodiosPlugins", () => { 5 | it("should be defined", () => { 6 | expect(ZodiosPlugins).toBeDefined(); 7 | }); 8 | 9 | it("should register one plugin", () => { 10 | const plugins = new ZodiosPlugins("any", "any"); 11 | const plugin: ZodiosPlugin = { 12 | request: async (api, config) => config, 13 | response: async (api, config, response) => response, 14 | }; 15 | const id = plugins.use(plugin); 16 | expect(id.key).toBe("any-any"); 17 | expect(id.value).toBe(0); 18 | expect(plugins.count()).toBe(1); 19 | }); 20 | 21 | it("should unregister one plugin", () => { 22 | const plugins = new ZodiosPlugins("any", "any"); 23 | const plugin: ZodiosPlugin = { 24 | request: async (api, config) => config, 25 | response: async (api, config, response) => response, 26 | }; 27 | const id = plugins.use(plugin); 28 | plugins.eject(id); 29 | expect(plugins.count()).toBe(0); 30 | }); 31 | 32 | it("should replace named plugins", () => { 33 | const plugins = new ZodiosPlugins("any", "any"); 34 | const plugin: ZodiosPlugin = { 35 | name: "test", 36 | request: async (api, config) => config, 37 | response: async (api, config, response) => response, 38 | }; 39 | plugins.use(plugin); 40 | plugins.use(plugin); 41 | expect(plugins.count()).toBe(1); 42 | }); 43 | 44 | it("should throw if plugin is not registered", () => { 45 | const plugins = new ZodiosPlugins("any", "any"); 46 | const id = { key: "test-any", value: 5 }; 47 | expect(() => plugins.eject(id)).toThrowError( 48 | `Plugin with key 'test-any' is not registered for endpoint 'any-any'` 49 | ); 50 | }); 51 | 52 | it("should throw if named plugin is not registered", () => { 53 | const plugins = new ZodiosPlugins("any", "any"); 54 | expect(() => plugins.eject("test")).toThrowError( 55 | `Plugin with name 'test' not found` 56 | ); 57 | }); 58 | 59 | it("should execute response plugins consistently", async () => { 60 | const plugins = new ZodiosPlugins("any", "any"); 61 | const plugin1: ZodiosPlugin = { 62 | request: async (api, config) => config, 63 | response: async (api, config, response) => { 64 | response.data += "1"; 65 | return response; 66 | }, 67 | }; 68 | plugins.use(plugin1); 69 | const plugin2: ZodiosPlugin = { 70 | request: async (api, config) => config, 71 | response: async (api, config, response) => { 72 | response.data += "2"; 73 | return response; 74 | }, 75 | }; 76 | plugins.use(plugin2); 77 | const response1 = await plugins.interceptResponse( 78 | [], 79 | // @ts-ignore 80 | {}, 81 | Promise.resolve({ data: "test1:" }) 82 | ); 83 | expect(response1.data).toBe("test1:21"); 84 | const response2 = await plugins.interceptResponse( 85 | [], 86 | // @ts-ignore 87 | {}, 88 | Promise.resolve({ data: "test2:" }) 89 | ); 90 | expect(response2.data).toBe("test2:21"); 91 | }); 92 | 93 | it('should catch error if plugin "error" is defined', async () => { 94 | const plugins = new ZodiosPlugins("any", "any"); 95 | const plugin: ZodiosPlugin = { 96 | request: async (api, config) => config, 97 | // @ts-ignore 98 | error: async (api, config, error) => ({ test: true }), 99 | }; 100 | plugins.use(plugin); 101 | const response = await plugins.interceptResponse( 102 | [], 103 | // @ts-ignore 104 | { method: "any", url: "any" }, 105 | Promise.reject(new Error("test")) 106 | ); 107 | expect(response).toEqual({ test: true }); 108 | }); 109 | 110 | it("should count plugins", () => { 111 | const plugins = new ZodiosPlugins("any", "any"); 112 | const namedPlugin: (n: number) => ZodiosPlugin = (n) => ({ 113 | name: `test${n}`, 114 | request: async (api, config) => config, 115 | response: async (api, config, response) => response, 116 | }); 117 | const plugin: ZodiosPlugin = { 118 | request: async (api, config) => config, 119 | response: async (api, config, response) => response, 120 | }; 121 | plugins.use(namedPlugin(1)); 122 | plugins.use(plugin); 123 | plugins.use(namedPlugin(2)); 124 | expect(plugins.count()).toBe(3); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /src/plugins/zodios-plugins.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from "axios"; 2 | import { ReadonlyDeep } from "../utils.types"; 3 | import { 4 | AnyZodiosRequestOptions, 5 | Method, 6 | ZodiosEndpointDefinitions, 7 | ZodiosPlugin, 8 | } from "../zodios.types"; 9 | 10 | export type PluginId = { 11 | key: string; 12 | value: number; 13 | }; 14 | 15 | /** 16 | * A list of plugins that can be used by the Zodios client. 17 | */ 18 | export class ZodiosPlugins { 19 | public readonly key: string; 20 | private plugins: Array = []; 21 | 22 | /** 23 | * Constructor 24 | * @param method - http method of the endpoint where the plugins are registered 25 | * @param path - path of the endpoint where the plugins are registered 26 | */ 27 | constructor(method: Method | "any", path: string) { 28 | this.key = `${method}-${path}`; 29 | } 30 | 31 | /** 32 | * Get the index of a plugin by name 33 | * @param name - name of the plugin 34 | * @returns the index of the plugin if found, -1 otherwise 35 | */ 36 | indexOf(name: string) { 37 | return this.plugins.findIndex((p) => p?.name === name); 38 | } 39 | 40 | /** 41 | * register a plugin 42 | * if the plugin has a name it will be replaced if it already exists 43 | * @param plugin - plugin to register 44 | * @returns unique id of the plugin 45 | */ 46 | use(plugin: ZodiosPlugin): PluginId { 47 | if (plugin.name) { 48 | const id = this.indexOf(plugin.name); 49 | if (id !== -1) { 50 | this.plugins[id] = plugin; 51 | return { key: this.key, value: id }; 52 | } 53 | } 54 | this.plugins.push(plugin); 55 | return { key: this.key, value: this.plugins.length - 1 }; 56 | } 57 | 58 | /** 59 | * unregister a plugin 60 | * @param plugin - plugin to unregister 61 | */ 62 | eject(plugin: PluginId | string) { 63 | if (typeof plugin === "string") { 64 | const id = this.indexOf(plugin); 65 | if (id === -1) { 66 | throw new Error(`Plugin with name '${plugin}' not found`); 67 | } 68 | this.plugins[id] = undefined; 69 | } else { 70 | if (plugin.key !== this.key) { 71 | throw new Error( 72 | `Plugin with key '${plugin.key}' is not registered for endpoint '${this.key}'` 73 | ); 74 | } 75 | this.plugins[plugin.value] = undefined; 76 | } 77 | } 78 | 79 | /** 80 | * Intercept the request config by applying all plugins 81 | * before using it to send a request to the server 82 | * @param config - request config 83 | * @returns the modified config 84 | */ 85 | async interceptRequest( 86 | api: ZodiosEndpointDefinitions, 87 | config: ReadonlyDeep 88 | ) { 89 | let pluginConfig = config; 90 | for (const plugin of this.plugins) { 91 | if (plugin?.request) { 92 | pluginConfig = await plugin.request(api, pluginConfig); 93 | } 94 | } 95 | return pluginConfig; 96 | } 97 | 98 | /** 99 | * Intercept the response from server by applying all plugins 100 | * @param api - endpoint descriptions 101 | * @param config - request config 102 | * @param response - response from the server 103 | * @returns the modified response 104 | */ 105 | async interceptResponse( 106 | api: ZodiosEndpointDefinitions, 107 | config: ReadonlyDeep, 108 | response: Promise 109 | ) { 110 | let pluginResponse = response; 111 | for (let index = this.plugins.length - 1; index >= 0; index--) { 112 | const plugin = this.plugins[index]; 113 | if (plugin) { 114 | pluginResponse = pluginResponse.then( 115 | plugin?.response 116 | ? (res) => plugin.response!(api, config, res) 117 | : undefined, 118 | plugin?.error ? (err) => plugin.error!(api, config, err) : undefined 119 | ); 120 | } 121 | } 122 | return pluginResponse; 123 | } 124 | 125 | /** 126 | * Get the number of plugins registered 127 | * @returns the number of plugins registered 128 | */ 129 | count() { 130 | return this.plugins.reduce( 131 | (count, plugin) => (plugin ? count + 1 : count), 132 | 0 133 | ); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { omit, pick } from "./utils"; 2 | 3 | describe("omit", () => { 4 | it("should be defined", () => { 5 | expect(omit).toBeDefined(); 6 | }); 7 | 8 | it("should support undefined parameters", () => { 9 | const obj: any = undefined; 10 | expect(omit(obj, ["a"])).toEqual({}); 11 | }); 12 | 13 | it("should remove the given keys from the object", () => { 14 | const obj = { 15 | a: 1, 16 | b: 2, 17 | c: 3, 18 | }; 19 | 20 | expect(omit(obj, ["a"])).toEqual({ 21 | b: 2, 22 | c: 3, 23 | }); 24 | }); 25 | 26 | it("should accept to remove all keys from object", () => { 27 | const obj = { 28 | a: 1, 29 | b: 2, 30 | c: 3, 31 | }; 32 | 33 | expect(omit(obj, ["a", "b", "c"])).toEqual({}); 34 | }); 35 | }); 36 | 37 | describe("pick", () => { 38 | it("should be defined", () => { 39 | expect(pick).toBeDefined(); 40 | }); 41 | 42 | it("should support undefined parameters", () => { 43 | const obj: any = undefined; 44 | expect(pick(obj, ["a"])).toEqual({}); 45 | }); 46 | 47 | it("should pick the given keys from the object", () => { 48 | const obj = { 49 | a: 1, 50 | b: 2, 51 | c: 3, 52 | }; 53 | 54 | expect(pick(obj, ["a"])).toEqual({ 55 | a: 1, 56 | }); 57 | }); 58 | 59 | it("should accept to pick all keys from object", () => { 60 | const obj = { 61 | a: 1, 62 | b: 2, 63 | c: 3, 64 | }; 65 | 66 | expect(pick(obj, ["a", "b", "c"])).toEqual(obj); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from "axios"; 2 | import { ReadonlyDeep } from "./utils.types"; 3 | import { 4 | AnyZodiosRequestOptions, 5 | ZodiosEndpointDefinition, 6 | ZodiosEndpointDefinitions, 7 | } from "./zodios.types"; 8 | 9 | /** 10 | * omit properties from an object 11 | * @param obj - the object to omit properties from 12 | * @param keys - the keys to omit 13 | * @returns the object with the omitted properties 14 | */ 15 | export function omit( 16 | obj: T | undefined, 17 | keys: K[] 18 | ): Omit { 19 | const ret = { ...obj } as T; 20 | for (const key of keys) { 21 | delete ret[key]; 22 | } 23 | return ret; 24 | } 25 | 26 | /** 27 | * pick properties from an object 28 | * @param obj - the object to pick properties from 29 | * @param keys - the keys to pick 30 | * @returns the object with the picked properties 31 | */ 32 | export function pick( 33 | obj: T | undefined, 34 | keys: K[] 35 | ): Pick { 36 | const ret = {} as Pick; 37 | if (obj) { 38 | for (const key of keys) { 39 | ret[key] = obj[key]; 40 | } 41 | } 42 | return ret; 43 | } 44 | 45 | /** 46 | * set first letter of a string to uppercase 47 | * @param str - the string to capitalize 48 | * @returns - the string with the first letter uppercased 49 | */ 50 | export function capitalize(str: T): Capitalize { 51 | return (str.charAt(0).toUpperCase() + str.slice(1)) as Capitalize; 52 | } 53 | 54 | const paramsRegExp = /:([a-zA-Z_][a-zA-Z0-9_]*)/g; 55 | 56 | export function replacePathParams( 57 | config: ReadonlyDeep 58 | ) { 59 | let result: string = config.url; 60 | const params = config.params; 61 | if (params) { 62 | result = result.replace(paramsRegExp, (match, key) => 63 | key in params ? `${params[key]}` : match 64 | ); 65 | } 66 | return result; 67 | } 68 | 69 | export function findEndpoint( 70 | api: ZodiosEndpointDefinitions, 71 | method: string, 72 | path: string 73 | ) { 74 | return api.find((e) => e.method === method && e.path === path); 75 | } 76 | 77 | export function findEndpointByAlias( 78 | api: ZodiosEndpointDefinitions, 79 | alias: string 80 | ) { 81 | return api.find((e) => e.alias === alias); 82 | } 83 | 84 | export function findEndpointErrors( 85 | endpoint: ZodiosEndpointDefinition, 86 | err: AxiosError 87 | ) { 88 | const matchingErrors = endpoint.errors?.filter( 89 | (error) => error.status === err.response!.status 90 | ); 91 | if (matchingErrors && matchingErrors.length > 0) return matchingErrors; 92 | return endpoint.errors?.filter((error) => error.status === "default"); 93 | } 94 | 95 | export function findEndpointErrorsByPath( 96 | api: ZodiosEndpointDefinitions, 97 | method: string, 98 | path: string, 99 | err: AxiosError 100 | ) { 101 | const endpoint = findEndpoint(api, method, path); 102 | return endpoint && 103 | err.config && 104 | err.config.url && 105 | endpoint.method === err.config.method && 106 | pathMatchesUrl(endpoint.path, err.config.url) 107 | ? findEndpointErrors(endpoint, err) 108 | : undefined; 109 | } 110 | 111 | export function findEndpointErrorsByAlias( 112 | api: ZodiosEndpointDefinitions, 113 | alias: string, 114 | err: AxiosError 115 | ) { 116 | const endpoint = findEndpointByAlias(api, alias); 117 | 118 | return endpoint && 119 | err.config && 120 | err.config.url && 121 | endpoint.method === err.config.method && 122 | pathMatchesUrl(endpoint.path, err.config.url) 123 | ? findEndpointErrors(endpoint, err) 124 | : undefined; 125 | } 126 | 127 | export function pathMatchesUrl(path: string, url: string) { 128 | return new RegExp(`^${path.replace(paramsRegExp, () => "([^/]*)")}$`).test( 129 | url 130 | ); 131 | } 132 | -------------------------------------------------------------------------------- /src/utils.types.test.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Assert, 3 | FilterArrayByValue, 4 | FilterArrayByKey, 5 | PathParamNames, 6 | } from "./utils.types"; 7 | 8 | describe("utils.types", () => { 9 | describe("FilterArrayByValue", () => { 10 | it("should support empty array", () => { 11 | type Input = []; 12 | const test: Assert, []> = true; 13 | }); 14 | 15 | it("should filter typed Array by value in declared order", () => { 16 | type Input = [ 17 | { a: number; b: string }, 18 | { a: number; c: boolean }, 19 | { c: boolean }, 20 | { d: string }, 21 | { e: number } 22 | ]; 23 | const test1: Assert< 24 | FilterArrayByValue, 25 | [{ a: number; b: string }, { a: number; c: boolean }] 26 | > = true; 27 | const test2: Assert< 28 | FilterArrayByValue, 29 | [{ a: number; c: boolean }, { c: boolean }] 30 | > = true; 31 | const test3: Assert< 32 | FilterArrayByValue, 33 | [{ d: string }] 34 | > = true; 35 | const test4: Assert< 36 | FilterArrayByValue, 37 | [{ e: number }] 38 | > = true; 39 | }); 40 | }); 41 | 42 | describe("FilterArrayByKey", () => { 43 | it("should support empty array", () => { 44 | type Input = []; 45 | const test: Assert, []> = true; 46 | }); 47 | it("should filter typed Array by key in declared order", () => { 48 | type Input = [ 49 | { a: number; b: string }, 50 | { a: number; c: boolean }, 51 | { c: boolean }, 52 | { d: string }, 53 | { e: number } 54 | ]; 55 | const test1: Assert< 56 | FilterArrayByKey, 57 | [{ a: number; b: string }, { a: number; c: boolean }] 58 | > = true; 59 | const test2: Assert< 60 | FilterArrayByKey, 61 | [{ a: number; c: boolean }, { c: boolean }] 62 | > = true; 63 | const test3: Assert, [{ d: string }]> = true; 64 | const test4: Assert, [{ e: number }]> = true; 65 | }); 66 | }); 67 | 68 | describe("PathParamNames", () => { 69 | it("should support empty string", () => { 70 | type Input = PathParamNames<"">; 71 | // ^? 72 | const test: Assert, never> = true; 73 | }); 74 | it("should extract params from path", () => { 75 | type Input = PathParamNames<"/endpoint/:param1/:param2/rest">; 76 | // ^? 77 | const test: Assert = true; 78 | }); 79 | it("should extract multiple params one after another from path", () => { 80 | type Input = PathParamNames<"/endpoint/:param1:param2/rest">; 81 | // ^? 82 | const test: Assert = true; 83 | }); 84 | it("should extract param when encoded colon is in path", () => { 85 | type Input1 = PathParamNames<"/endpoint/:param1%3Aaction">; 86 | // ^? 87 | const test1: Assert = true; 88 | type Input2 = PathParamNames<"/endpoint/:param1%action:param2">; 89 | // ^? 90 | const test2: Assert = true; 91 | }); 92 | 93 | it("should allow path params in query string", () => { 94 | type Input = PathParamNames<"/endpoint/:param1?param=:param2">; 95 | // ^? 96 | const test: Assert = true; 97 | }); 98 | 99 | it("should allow path params in fragment string", () => { 100 | type Input = PathParamNames<"/endpoint/:param1#:param2">; 101 | // ^? 102 | const test: Assert = true; 103 | }); 104 | 105 | it("should allow params between parenthesis", () => { 106 | type Input = PathParamNames<"/endpoint/:param1/(:param2)">; 107 | // ^? 108 | const test: Assert = true; 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /src/utils.types.ts: -------------------------------------------------------------------------------- 1 | import { z, ZodType } from "zod"; 2 | 3 | /** 4 | * filter an array type by a predicate value 5 | * @param T - array type 6 | * @param C - predicate object to match 7 | * @details - this is using tail recursion type optimization from typescript 4.5 8 | */ 9 | export type FilterArrayByValue< 10 | T extends unknown[] | undefined, 11 | C, 12 | Acc extends unknown[] = [] 13 | > = T extends [infer Head, ...infer Tail] 14 | ? Head extends C 15 | ? FilterArrayByValue 16 | : FilterArrayByValue 17 | : Acc; 18 | 19 | /** 20 | * filter an array type by key 21 | * @param T - array type 22 | * @param K - key to match 23 | * @details - this is using tail recursion type optimization from typescript 4.5 24 | */ 25 | export type FilterArrayByKey< 26 | T extends unknown[], 27 | K extends string, 28 | Acc extends unknown[] = [] 29 | > = T extends [infer Head, ...infer Tail] 30 | ? Head extends { [Key in K]: unknown } 31 | ? FilterArrayByKey 32 | : FilterArrayByKey 33 | : Acc; 34 | 35 | /** 36 | * filter an array type by removing undefined values 37 | * @param T - array type 38 | * @details - this is using tail recursion type optimization from typescript 4.5 39 | */ 40 | export type DefinedArray< 41 | T extends unknown[], 42 | Acc extends unknown[] = [] 43 | > = T extends [infer Head, ...infer Tail] 44 | ? Head extends undefined 45 | ? DefinedArray 46 | : DefinedArray 47 | : Acc; 48 | 49 | type Try = A extends B ? A : C; 50 | 51 | type NarrowRaw = 52 | | (T extends Function ? T : never) 53 | | (T extends string | number | bigint | boolean ? T : never) 54 | | (T extends [] ? [] : never) 55 | | { 56 | [K in keyof T]: K extends "description" ? T[K] : NarrowNotZod; 57 | }; 58 | 59 | type NarrowNotZod = Try>; 60 | 61 | /** 62 | * Utility to infer the embedded primitive type of any type 63 | * Same as `as const` but without setting the object as readonly and without needing the user to use it 64 | * @param T - type to infer the embedded type of 65 | * @see - thank you tannerlinsley for this idea 66 | */ 67 | export type Narrow = Try>; 68 | 69 | /** 70 | * merge all union types into a single type 71 | * @param T - union type 72 | */ 73 | export type MergeUnion = ( 74 | T extends unknown ? (k: T) => void : never 75 | ) extends (k: infer I) => void 76 | ? { [K in keyof I]: I[K] } 77 | : never; 78 | 79 | /** 80 | * get all required properties from an object type 81 | * @param T - object type 82 | */ 83 | export type RequiredProps = Omit< 84 | T, 85 | { 86 | [P in keyof T]-?: undefined extends T[P] ? P : never; 87 | }[keyof T] 88 | >; 89 | 90 | /** 91 | * get all optional properties from an object type 92 | * @param T - object type 93 | */ 94 | export type OptionalProps = Pick< 95 | T, 96 | { 97 | [P in keyof T]-?: undefined extends T[P] ? P : never; 98 | }[keyof T] 99 | >; 100 | 101 | /** 102 | * get all properties from an object type that are not undefined or optional 103 | * @param T - object type 104 | * @returns - union type of all properties that are not undefined or optional 105 | */ 106 | export type RequiredKeys = { 107 | [P in keyof T]-?: undefined extends T[P] ? never : P; 108 | }[keyof T]; 109 | 110 | /** 111 | * Simplify a type by merging intersections if possible 112 | * @param T - type to simplify 113 | */ 114 | export type Simplify = T extends unknown ? { [K in keyof T]: T[K] } : T; 115 | 116 | /** 117 | * Merge two types into a single type 118 | * @param T - first type 119 | * @param U - second type 120 | */ 121 | export type Merge = Simplify; 122 | 123 | /** 124 | * transform possible undefined properties from a type into optional properties 125 | * @param T - object type 126 | */ 127 | export type UndefinedToOptional = Merge< 128 | RequiredProps, 129 | Partial> 130 | >; 131 | 132 | /** 133 | * remove all the never properties from a type object 134 | * @param T - object type 135 | */ 136 | export type PickDefined = Pick< 137 | T, 138 | { [K in keyof T]: T[K] extends never ? never : K }[keyof T] 139 | >; 140 | 141 | /** 142 | * check if two types are equal 143 | */ 144 | export type IfEquals = (() => G extends T 145 | ? 1 146 | : 2) extends () => G extends U ? 1 : 2 147 | ? Y 148 | : N; 149 | 150 | /** 151 | * get never if empty type 152 | * @param T - type 153 | * @example 154 | * ```ts 155 | * type A = {}; 156 | * type B = NotEmpty; // B = never 157 | */ 158 | export type NeverIfEmpty = IfEquals; 159 | 160 | /** 161 | * get undefined if empty type 162 | * @param T - type 163 | * @example 164 | * ```ts 165 | * type A = {}; 166 | * type B = NotEmpty; // B = never 167 | */ 168 | export type UndefinedIfEmpty = IfEquals; 169 | 170 | export type UndefinedIfNever = IfEquals; 171 | 172 | type RequiredChildProps = { 173 | [K in keyof T]: IfEquals, never, K>; 174 | }[keyof T]; 175 | 176 | export type OptionalChildProps = { 177 | [K in keyof T]: IfEquals, K, never>; 178 | }[keyof T]; 179 | 180 | /** 181 | * set properties to optional if their child properties are optional 182 | * @param T - object type 183 | */ 184 | export type SetPropsOptionalIfChildrenAreOptional = Merge< 185 | Pick, OptionalChildProps>, 186 | Pick> 187 | >; 188 | 189 | /** 190 | * transform an array type into a readonly array type 191 | * @param T - array type 192 | */ 193 | interface ReadonlyArrayDeep extends ReadonlyArray> {} 194 | 195 | /** 196 | * transform an object type into a readonly object type 197 | * @param T - object type 198 | */ 199 | export type DeepReadonlyObject = { 200 | readonly [P in keyof T]: ReadonlyDeep; 201 | }; 202 | 203 | /** 204 | * transform a type into a readonly type 205 | * @param T - type 206 | */ 207 | export type ReadonlyDeep = T extends (infer R)[] 208 | ? ReadonlyArrayDeep 209 | : T extends Function 210 | ? T 211 | : T extends object 212 | ? DeepReadonlyObject 213 | : T; 214 | 215 | export type MaybeReadonly = T | ReadonlyDeep; 216 | 217 | /** 218 | * Map a type an api description parameter to a zod infer type 219 | * @param T - array of api description parameters 220 | * @details - this is using tail recursion type optimization from typescript 4.5 221 | */ 222 | export type MapSchemaParameters< 223 | T, 224 | Frontend extends boolean = true, 225 | Acc = {} 226 | > = T extends [infer Head, ...infer Tail] 227 | ? Head extends { 228 | name: infer Name; 229 | schema: infer Schema; 230 | } 231 | ? Name extends string 232 | ? MapSchemaParameters< 233 | Tail, 234 | Frontend, 235 | Merge< 236 | { 237 | [Key in Name]: Schema extends z.ZodType 238 | ? Frontend extends true 239 | ? z.input 240 | : z.output 241 | : never; 242 | }, 243 | Acc 244 | > 245 | > 246 | : Acc 247 | : Acc 248 | : Acc; 249 | 250 | /** 251 | * Split string into a tuple, using a simple string literal separator 252 | * @description - This is a simple implementation of split, it does not support multiple separators 253 | * A more complete implementation is built on top of this one 254 | * @param Str - String to split 255 | * @param Sep - Separator, must be a string literal not a union of string literals 256 | * @returns Tuple of strings 257 | */ 258 | export type Split< 259 | Str, 260 | Sep extends string, 261 | Acc extends string[] = [] 262 | > = Str extends "" 263 | ? Acc 264 | : Str extends `${infer T}${Sep}${infer U}` 265 | ? Split 266 | : [...Acc, Str]; 267 | 268 | type ConcatSplits< 269 | Parts extends string[], 270 | Seps extends string[], 271 | Acc extends string[] = [] 272 | > = Parts extends [infer First extends string, ...infer Rest extends string[]] 273 | ? ConcatSplits]> 274 | : Acc; 275 | 276 | /** 277 | * Split a string into a tuple. 278 | * @param Str - The string to split. 279 | * @param Sep - The separators to split on, a tuple of strings with one or more characters. 280 | * @returns The tuple of each split. if sep is an empty string, returns a tuple of each character. 281 | */ 282 | export type SplitMany< 283 | Str extends string, 284 | Sep extends string[], 285 | Acc extends string[] = [] 286 | > = Sep extends [ 287 | infer FirstSep extends string, 288 | ...infer RestSep extends string[] 289 | ] 290 | ? ConcatSplits, RestSep> 291 | : [Str, ...Acc]; 292 | 293 | type PathSeparator = ["/", "?", "&", "#", "=", "(", ")", "[", "]", "%"]; 294 | 295 | type FilterParams = Params extends [ 296 | infer First, 297 | ...infer Rest 298 | ] 299 | ? First extends `${string}:${infer Param}` 300 | ? FilterParams]> 301 | : FilterParams 302 | : Acc; 303 | 304 | /** 305 | * Extract Path Params from a path 306 | * @param Path - Path to extract params from 307 | * @returns Path params 308 | * @example 309 | * ```ts 310 | * type Path = "/users/:id/posts/:postId" 311 | * type PathParams = ApiPathToParams 312 | * // output: ["id", "postId"] 313 | * ``` 314 | */ 315 | export type ApiPathToParams = FilterParams< 316 | SplitMany 317 | >; 318 | 319 | /** 320 | * get all parameters from an API path 321 | * @param Path - API path 322 | * @details - this is using tail recursion type optimization from typescript 4.5 323 | */ 324 | export type PathParamNames = Path extends string 325 | ? ApiPathToParams[number] 326 | : never; 327 | 328 | /** 329 | * Check if two type are equal else generate a compiler error 330 | * @param T - type to check 331 | * @param U - type to check against 332 | * @returns true if types are equal else a detailed compiler error 333 | */ 334 | export type Assert = IfEquals< 335 | T, 336 | U, 337 | true, 338 | { error: "Types are not equal"; type1: T; type2: U } 339 | >; 340 | 341 | export type PickRequired = Merge; 342 | 343 | /** 344 | * Flatten a tuple type one level 345 | * @param T - tuple type 346 | * @returns flattened tuple type 347 | * 348 | * @example 349 | * ```ts 350 | * type T0 = TupleFlat<[1, 2, [3, 4], 5]>; // T0 = [1, 2, 3, 4, 5] 351 | * ``` 352 | */ 353 | export type TupleFlat = T extends [ 354 | infer Head, 355 | ...infer Tail 356 | ] 357 | ? Head extends unknown[] 358 | ? TupleFlat 359 | : TupleFlat 360 | : Acc; 361 | 362 | /** 363 | * trick to combine multiple unions of objects into a single object 364 | * only works with objects not primitives 365 | * @param union - Union of objects 366 | * @returns Intersection of objects 367 | */ 368 | export type UnionToIntersection = ( 369 | union extends any ? (k: union) => void : never 370 | ) extends (k: infer intersection) => void 371 | ? intersection 372 | : never; 373 | /** 374 | * get last element of union 375 | * @param Union - Union of any types 376 | * @returns Last element of union 377 | */ 378 | type GetUnionLast = UnionToIntersection< 379 | Union extends any ? () => Union : never 380 | > extends () => infer Last 381 | ? Last 382 | : never; 383 | 384 | /** 385 | * Convert union to tuple 386 | * @param Union - Union of any types, can be union of complex, composed or primitive types 387 | * @returns Tuple of each elements in the union 388 | */ 389 | export type UnionToTuple = [ 390 | Union 391 | ] extends [never] 392 | ? Tuple 393 | : UnionToTuple< 394 | Exclude>, 395 | [GetUnionLast, ...Tuple] 396 | >; 397 | -------------------------------------------------------------------------------- /src/zodios-error.ts: -------------------------------------------------------------------------------- 1 | import { ReadonlyDeep } from "./utils.types"; 2 | import { AnyZodiosRequestOptions } from "./zodios.types"; 3 | 4 | /** 5 | * Custom Zodios Error with additional information 6 | * @param message - the error message 7 | * @param data - the parameter or response object that caused the error 8 | * @param config - the config object from zodios 9 | * @param cause - the error cause 10 | */ 11 | export class ZodiosError extends Error { 12 | constructor( 13 | message: string, 14 | public readonly config?: ReadonlyDeep, 15 | public readonly data?: unknown, 16 | public readonly cause?: Error 17 | ) { 18 | super(message); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/zodios-error.utils.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from "axios"; 2 | import { findEndpointErrorsByAlias, findEndpointErrorsByPath } from "./utils"; 3 | import { 4 | Aliases, 5 | Method, 6 | ZodiosEndpointDefinitions, 7 | ZodiosEndpointError, 8 | ZodiosMatchingErrorsByAlias, 9 | ZodiosMatchingErrorsByPath, 10 | ZodiosPathsByMethod, 11 | } from "./zodios.types"; 12 | 13 | function isDefinedError( 14 | error: unknown, 15 | findEndpointErrors: (error: AxiosError) => ZodiosEndpointError[] | undefined 16 | ): boolean { 17 | if ( 18 | error instanceof AxiosError || 19 | (error && typeof error === "object" && "isAxiosError" in error) 20 | ) { 21 | const err = error as AxiosError; 22 | if (err.response) { 23 | const endpointErrors = findEndpointErrors(err); 24 | if (endpointErrors) { 25 | return endpointErrors.some( 26 | (desc) => desc.schema.safeParse(err.response!.data).success 27 | ); 28 | } 29 | } 30 | } 31 | return false; 32 | } 33 | 34 | /** 35 | * check if the error is matching the endpoint errors definitions 36 | * @param api - the api definition 37 | * @param method - http method of the endpoint 38 | * @param path - path of the endpoint 39 | * @param error - the error to check 40 | * @returns - if true, the error type is narrowed to the matching endpoint errors 41 | */ 42 | export function isErrorFromPath< 43 | Api extends ZodiosEndpointDefinitions, 44 | M extends Method, 45 | Path extends string 46 | >( 47 | api: Api, 48 | method: M, 49 | path: Path extends ZodiosPathsByMethod 50 | ? Path 51 | : ZodiosPathsByMethod, 52 | error: unknown 53 | ): error is ZodiosMatchingErrorsByPath< 54 | Api, 55 | M, 56 | Path extends ZodiosPathsByMethod ? Path : never 57 | > { 58 | return isDefinedError(error, (err) => 59 | findEndpointErrorsByPath(api, method, path, err) 60 | ); 61 | } 62 | 63 | /** 64 | * check if the error is matching the endpoint errors definitions 65 | * @param api - the api definition 66 | * @param alias - alias of the endpoint 67 | * @param error - the error to check 68 | * @returns - if true, the error type is narrowed to the matching endpoint errors 69 | */ 70 | export function isErrorFromAlias< 71 | Api extends ZodiosEndpointDefinitions, 72 | Alias extends string 73 | >( 74 | api: Api, 75 | alias: Alias extends Aliases ? Alias : Aliases, 76 | error: unknown 77 | ): error is ZodiosMatchingErrorsByAlias { 78 | return isDefinedError(error, (err) => 79 | findEndpointErrorsByAlias(api, alias, err) 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES6", 5 | "sourceMap": false 6 | }, 7 | "exclude": [ 8 | "node_modules", 9 | "lib", 10 | "examples", 11 | "website", 12 | "**/*.config.ts", 13 | "**/*.spec.ts", 14 | "**/*.test.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // ts config for es 2021 3 | "compilerOptions": { 4 | "target": "ES2019", 5 | "module": "CommonJS", 6 | "moduleResolution": "Node", 7 | "lib": ["ES2019"], 8 | "outDir": "./lib", 9 | "alwaysStrict": true, 10 | "strict": true, 11 | "esModuleInterop": true, 12 | "sourceMap": true, 13 | "declaration": true, 14 | "noErrorTruncation": true, 15 | "jsx": "react-jsx" 16 | }, 17 | "exclude": ["node_modules", "lib"] 18 | } 19 | -------------------------------------------------------------------------------- /tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | clean: true, 5 | dts: true, 6 | entry: ["src/index.ts", "src/utils.types.ts", "src/zodios.types.ts"], 7 | outDir: "lib", 8 | format: ["cjs", "esm"], 9 | minify: true, 10 | treeshake: true, 11 | tsconfig: "tsconfig.build.json", 12 | splitting: true, 13 | sourcemap: false, 14 | }); 15 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /website/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /website/docs/api/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Zodios API definition", 3 | "position": 5, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Api definitions and client" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /website/docs/api/api-definition.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # API definition 6 | 7 | Zodios uses a centralised definition to declare your REST API endpoints. 8 | 9 | The API definition is the only object that you need to share between your server and client code. 10 | If you don't have the control over your API server or if it's developped in another language than javascript, you can still use Zodios API definition only for your client. 11 | 12 | ## API Definition Structure 13 | 14 | The API definition is a javascript array of endpoint descriptions. Each endpoint description is an object with the following properties: 15 | 16 | | Property | Type | Description | 17 | | ------------------- | --------------------------------------------- | -------------------------------------------------------------- | 18 | | method | string | The HTTP method of the endpoint. | 19 | | path | string | The path of the endpoint. | 20 | | response | ZodSchema | The response Schema of the endpoint using Zod. | 21 | | status | number | The status code of the response. default: 200 | 22 | | alias | string | Optional alias of the endpoint. | 23 | | responseDescription | string | Optional description of the response. | 24 | | immutable | boolean | Optional flag to indicate if the 'post' endpoint is immutable. | 25 | | description | string | Optional description of the endpoint. Used for openapi. | 26 | | requestFormat | `json`,`form-data`,`form-url`,`binary`,`text` | Optional request format of the endpoint. Default is `json`. | 27 | | parameters | array | Optional parameters of the endpoint. | 28 | | errors | array | Optional errors of the endpoint. | 29 | 30 | ### Parameters 31 | 32 | The parameters of an endpoint are an array of parameter descriptions. Each parameter description is an object with the following properties: 33 | 34 | | Property | Type | Description | 35 | | ----------- | ----------------------------------- | ------------------------------------------------------- | 36 | | name | string | The name of the parameter. | 37 | | type | `Path`, `Query`, `Body` or `Header` | The type of the parameter. | 38 | | description | string | Optional description of the endpoint. Used for openapi. | 39 | | schema | ZodSchema | The schema of the parameter using Zod. | 40 | 41 | :::note Path parameters do not need to be defined in the API definition `parameters` array. 42 | Indeed, they are automatically deduced from the path and added to the request parameters implicitly. 43 | Only declare path parameters in the `parameters` array if you want to add a description or a schema to validate them 44 | ::: 45 | 46 | 47 | ### Errors 48 | 49 | The errors of an endpoint are an array of error descriptions. Each error description is an object with the following properties: 50 | 51 | | Property | Type | Description | 52 | | ----------- | --------- | ---------------------------------------------------- | 53 | | status | number | The status code of the error. | 54 | | description | string | Optional description of the error. Used for openapi. | 55 | | schema | ZodSchema | The schema of the error using Zod. | 56 | -------------------------------------------------------------------------------- /website/docs/api/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Examples 6 | 7 | ## Complete example 8 | 9 | There is a complete example of full dev.to API in github [examples](https://github.com/ecyrbe/zodios/blob/main/examples/dev.to/) 10 | 11 | ## dev.to User API 12 | 13 | Here is an example of API definition for [dev.to](https://dev.to) user API. 14 | 15 | ```ts 16 | import { z } from "zod"; 17 | import { makeApi, makeErrors } from "@zodios/core"; 18 | 19 | export const devUser = z.object({ 20 | id: z.number(), 21 | type_of: z.string(), 22 | name: z.string(), 23 | username: z.string(), 24 | summary: z.string().or(z.null()), 25 | twitter_username: z.string().or(z.null()), 26 | github_username: z.string().or(z.null()), 27 | website_url: z.string().or(z.null()), 28 | location: z.string().or(z.null()), 29 | joined_at: z.string(), 30 | profile_image: z.string(), 31 | profile_image_90: z.string(), 32 | }); 33 | 34 | export type User = z.infer; 35 | 36 | export const devProfileImage = z.object({ 37 | type_of: z.string(), 38 | image_of: z.string(), 39 | profile_image: z.string(), 40 | profile_image_90: z.string(), 41 | }); 42 | 43 | export const userErrors = makeErrors([ 44 | { 45 | status: 404, 46 | description: "User not found", 47 | schema: z.object({ 48 | error: z.object({ 49 | code: z.string(), 50 | message: z.string(), 51 | }), 52 | }), 53 | }, 54 | { 55 | status: "default", 56 | description: "Default error", 57 | schema: z.object({ 58 | error: z.object({ 59 | code: z.string(), 60 | message: z.string(), 61 | }), 62 | }), 63 | }, 64 | ]); 65 | 66 | 67 | export type ProfileImage = z.infer; 68 | 69 | export const userApi = makeApi([ 70 | { 71 | method: "get", 72 | path: "/users/:id", 73 | alias: "getUser", 74 | description: "Get a user", 75 | response: devUser, 76 | errors: userErrors, 77 | }, 78 | { 79 | method: "get", 80 | path: "/users/me", 81 | alias: "getMe", 82 | description: "Get current user", 83 | response: devUser, 84 | errors: userErrors, 85 | }, 86 | { 87 | method: "get", 88 | path: "/profile_image/:username", 89 | alias: "getProfileImage", 90 | description: "Get a user's profile image", 91 | response: devProfileImage, 92 | errors: userErrors, 93 | }, 94 | ]); 95 | ``` 96 | 97 | -------------------------------------------------------------------------------- /website/docs/api/helpers.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # API definition helpers 6 | 7 | Usually, you'll want to define your API definition in a separate file and import it in your server and client code. 8 | For this use case, Zodios provides some helpers to make your life easier and still keep your API definition correctly inferred without needing to use Typescript `as const`. 9 | 10 | :::caution 11 | These helpers, are mandatory to be used when declaring your definitions outside of `Zodios` constructor to allow your API definitions to be correctly inferred in both pure Javascript and Typescript. 12 | ::: 13 | 14 | ## makeApi 15 | 16 | `makeApi` is a helper to narrow your api definitions and make some runtime checks. 17 | 18 | ```ts 19 | function makeApi(api: ZodiosEndpointDescriptions): ZodiosEndpointDescriptions; 20 | ``` 21 | 22 | **Example** 23 | ```ts 24 | import { makeApi } from "@zodios/core"; 25 | 26 | const api = makeApi([ 27 | { 28 | method: "get", 29 | path: "/users/:id", 30 | response: user, 31 | alias: "getUser", 32 | description: "Get user", 33 | }, 34 | { 35 | method: "get", 36 | path: "/users", 37 | response: z.array(user), 38 | alias: "getUsers", 39 | description: "Get users", 40 | }, 41 | ]); 42 | ``` 43 | 44 | ## makeEndpoint 45 | 46 | `makeEndpoint` is a helper to narrow a single endpoint definition and make some runtime checks. 47 | 48 | ```ts 49 | function makeEndpoint(endpoint: ZodiosEndpointDescription): ZodiosEndpointDescription; 50 | ``` 51 | 52 | **Example** 53 | ```ts 54 | import { makeEndpoint } from "@zodios/core"; 55 | 56 | const getUser = makeEndpoint({ 57 | method: "get", 58 | path: "/users/:id", 59 | response: user, 60 | alias: "getUser", 61 | description: "Get user", 62 | }); 63 | ``` 64 | 65 | It can then be combined with `makeApi` to compose a full api description. 66 | 67 | ```ts 68 | import { makeApi } from "@zodios/core"; 69 | import { getUser,getUsers } from "./endpoints"; 70 | 71 | const api = makeApi([getUser, getUsers]); 72 | ``` 73 | 74 | ## makeParameters 75 | 76 | `makeParameters` is a helper to narrow your parameter definitions. 77 | 78 | ```ts 79 | function makeParameters(params: ZodiosEndpointParameters): ZodiosEndpointParameters; 80 | ``` 81 | 82 | **Example** 83 | ```ts 84 | import { makeParameters } from "@zodios/core"; 85 | 86 | const params = makeParameters([ 87 | { 88 | name: "limit", 89 | description: "Limit", 90 | type: "Query", 91 | schema: z.number().positive(), 92 | }, 93 | { 94 | name: "offset", 95 | description: "Offset", 96 | type: "Query", 97 | schema: z.number().positive(), 98 | }, 99 | ]); 100 | ``` 101 | 102 | It can then be combined with `makeApi` to compose a full api description. 103 | ```ts 104 | const api = makeApi([ 105 | { 106 | method: "get", 107 | path: "/users", 108 | response: z.array(user), 109 | alias: "getUsers", 110 | description: "Get users", 111 | parameters: params, 112 | }, 113 | ]); 114 | ``` 115 | is equivalent to 116 | ```ts 117 | import { makeApi } from "@zodios/core"; 118 | 119 | const api = makeApi([ 120 | { 121 | method: "get", 122 | path: "/users", 123 | response: z.array(user), 124 | alias: "getUsers", 125 | description: "Get users", 126 | parameters: [ 127 | { 128 | name: "limit", 129 | description: "Limit", 130 | type: "Query", 131 | schema: z.number().positive(), 132 | }, 133 | { 134 | name: "offset", 135 | description: "Offset", 136 | type: "Query", 137 | schema: z.number().positive(), 138 | }, 139 | ], 140 | }, 141 | ]); 142 | ``` 143 | 144 | ## makeErrors 145 | 146 | `makeErrors` is a helper to narrow your error definitions. 147 | 148 | ```ts 149 | function makeErrors(errors: ZodiosEndpointErrors): ZodiosEndpointErrors; 150 | ``` 151 | 152 | **Example** 153 | ```ts 154 | import { makeErrors } from "@zodios/core"; 155 | 156 | const errors = makeErrors([ 157 | { 158 | status: 404, 159 | description: "User not found", 160 | schema: z.object({ 161 | error: z.object({ 162 | userId: z.number(), 163 | code: z.string(), 164 | message: z.string(), 165 | }), 166 | }), 167 | }, 168 | { 169 | status: "default", 170 | description: "Default error", 171 | schema: z.object({ 172 | error: z.object({ 173 | code: z.string(), 174 | message: z.string(), 175 | }), 176 | }), 177 | }, 178 | ]); 179 | ``` 180 | 181 | It can then be combined with `makeApi` to compose a full api description. 182 | ```ts 183 | const api = makeApi([ 184 | { 185 | method: "get", 186 | path: "/users/:id", 187 | response: user, 188 | alias: "getUser", 189 | description: "Get user", 190 | errors, 191 | }, 192 | ]); 193 | ``` 194 | is equivalent to 195 | ```ts 196 | import { makeApi } from "@zodios/core"; 197 | 198 | const api = makeApi([ 199 | { 200 | method: "get", 201 | path: "/users/:id", 202 | response: user, 203 | alias: "getUser", 204 | description: "Get user", 205 | errors: [ 206 | { 207 | status: 404, 208 | description: "User not found", 209 | schema: z.object({ 210 | error: z.object({ 211 | userId: z.number(), 212 | code: z.string(), 213 | message: z.string(), 214 | }), 215 | }), 216 | }, 217 | { 218 | status: "default", 219 | description: "Default error", 220 | schema: z.object({ 221 | error: z.object({ 222 | code: z.string(), 223 | message: z.string(), 224 | }), 225 | }), 226 | }, 227 | ], 228 | }, 229 | ]); 230 | ``` 231 | 232 | ## parametersBuilder 233 | 234 | `parametersBuilder` is a helper to build parameter definitions with better type autocompletion. 235 | 236 | ```ts 237 | function parametersBuilder(): ParametersBuilder; 238 | ``` 239 | 240 | ### ParametersBuilder methods 241 | 242 | ParametersBuilder is a helper to build parameter definitions with better type autocompletion. 243 | 244 | | methods | parameters | return | Description | 245 | | ------------- | ------------------------------------------- | ------------------------ | ---------------------------------- | 246 | | addParameter | name: Name, type: Type, schema: Schema | ParametersBuilder | Add a parameter to the API | 247 | | addParameters | type: Type, schemas: Record | ParametersBuilder | Add multiple parameters to the API | 248 | | addBody | schema: Schema | ParametersBuilder | Add a body to the API | 249 | | addHeader | name: Name, schema: Schema | ParametersBuilder | Add a header to the API | 250 | | addHeaders | schemas: Record | ParametersBuilder | Add multiple headers to the API | 251 | | addQuery | name: Name, schema: Schema | ParametersBuilder | Add a query to the API | 252 | | addQueries | schemas: Record | ParametersBuilder | Add multiple queries to the API | 253 | | addPath | name: Name, schema: Schema | ParametersBuilder | Add a path to the API | 254 | | addPaths | schemas: Record | ParametersBuilder | Add multiple paths to the API | 255 | | build | none | ZodiosEndpointParameters | Build the parameters | 256 | 257 | **Example** 258 | ```ts 259 | import { parametersBuilder } from "@zodios/core"; 260 | 261 | const params = parametersBuilder() 262 | .addParameters("Query", { 263 | limit: z.number().positive(), 264 | offset: z.number().positive(), 265 | }) 266 | .build(); 267 | ``` 268 | is equivalent to 269 | ```ts 270 | import { parametersBuilder } from "@zodios/core"; 271 | 272 | const params = parametersBuilder() 273 | .addQuery("limit", z.number().positive()) 274 | .addQuery("offset", z.number().positive()) 275 | .build(); 276 | ``` 277 | 278 | is equivalent to 279 | ```ts 280 | import { parametersBuilder } from "@zodios/core"; 281 | 282 | const params = parametersBuilder() 283 | .addQueries({ 284 | limit: z.number().positive(), 285 | offset: z.number().positive(), 286 | }) 287 | .build(); 288 | ``` 289 | 290 | is equivalent to 291 | ```ts 292 | import { parametersBuilder } from "@zodios/core"; 293 | 294 | const params = parametersBuilder() 295 | .addParameter("limit", "Query", z.number().positive()) 296 | .addParameter("offset", "Query", z.number().positive()) 297 | .build(); 298 | ``` 299 | is equivalent to 300 | ```ts 301 | import { makeParameters } from "@zodios/core"; 302 | 303 | const params = makeParameters([ 304 | { 305 | name: "limit", 306 | type: "Query", 307 | schema: z.number().positive(), 308 | }, 309 | { 310 | name: "offset", 311 | type: "Query", 312 | schema: z.number().positive(), 313 | }, 314 | ]); 315 | ``` 316 | 317 | It can then be combined with `makeApi` to compose a full api description. 318 | ```ts 319 | const api = makeApi([ 320 | { 321 | method: "get", 322 | path: "/users", 323 | response: z.array(user), 324 | alias: "getUsers", 325 | description: "Get users", 326 | parameters: params, 327 | }, 328 | ]); 329 | ``` 330 | 331 | is equivalent to 332 | ```ts 333 | import { makeApi } from "@zodios/core"; 334 | 335 | const api = makeApi([ 336 | { 337 | method: "get", 338 | path: "/users", 339 | response: z.array(user), 340 | alias: "getUsers", 341 | description: "Get users", 342 | parameters: [ 343 | { 344 | name: "limit", 345 | type: "Query", 346 | schema: z.number().positive(), 347 | }, 348 | { 349 | name: "offset", 350 | type: "Query", 351 | schema: z.number().positive(), 352 | }, 353 | ], 354 | }, 355 | ]); 356 | ``` 357 | 358 | ## apiBuilder 359 | 360 | `apiBuilder` is a helper to build API definitions with better type autocompletion. 361 | 362 | ```ts 363 | function apiBuilder(endpoint: ZodiosEndpointDescription): ApiBuilder; 364 | ``` 365 | 366 | ### ApiBuilder methods 367 | 368 | ApiBuilder is a helper to build API definitions with better type autocompletion. 369 | 370 | | methods | parameters | return | Description | 371 | | ----------- | ------------------------- | -------------------------- | -------------------------- | 372 | | addEndpoint | ZodiosEndpointDescription | ApiBuilder | Add an endpoint to the API | 373 | | build | none | ZodiosEndpointDescriptions | Build the API | 374 | 375 | **Example** 376 | ```ts 377 | import { apiBuilder } from "@zodios/core"; 378 | 379 | const api = apiBuilder({ 380 | method: "get", 381 | path: "/users", 382 | response: z.array(user), 383 | alias: "getUsers", 384 | description: "Get users", 385 | }) 386 | .addEndpoint({ 387 | method: "get", 388 | path: "/users/:id", 389 | response: user, 390 | alias: "getUser", 391 | description: "Get user", 392 | }) 393 | .build(); 394 | ``` 395 | 396 | ## mergeApis 397 | 398 | `mergeApis` is a helper to merge multiple API definitions in a router friendly way. 399 | 400 | ```ts 401 | function mergeApis(apis: Record): ZodiosEndpointDescriptions; 402 | ``` 403 | 404 | **Example** 405 | ```ts 406 | import { mergeApis } from "@zodios/core"; 407 | import { usersApi } from "./users"; 408 | import { postsApi } from "./posts"; 409 | 410 | const api = mergeApis({ 411 | '/users': usersApi, 412 | '/posts': postsApi, 413 | }); 414 | ``` 415 | -------------------------------------------------------------------------------- /website/docs/api/openapi.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # OpenAPI 6 | 7 | 8 | Zodios api definition format while simple can contain sufficient information to generate an OpenAPI documentation. 9 | 10 | ## `openApiBuilder` 11 | 12 | `openApiBuilder` is a builder that can be used to generate an OpenAPI documentation from a Zodios api definition. 13 | 14 | ```ts 15 | function openApiBuilder(info: OpenAPIV3.InfoObject): OpenApiBuilder 16 | ``` 17 | 18 | ### Methods of the builder 19 | 20 | | Method | Description | 21 | | --------------------------------------------------------------------------------------------------------- | ------------------------------------------ | 22 | | `addSecurityScheme(name: string, securityScheme: OpenAPIV3.SecuritySchemeObject)` | add a security scheme to proctect the apis | 23 | | `addPublicApi(definitions: ZodiosEndpointDefinitions)` | add an api with public endpoints | 24 | | `addProtectedApi(scheme: string, definitions: ZodiosEndpointDefinitions, securityRequirement?: string[])` | add an api protected by a security scheme | 25 | | `addServer(server: OpenAPIV3.ServerObject)` | add a server to the openapi document | 26 | | `setCustomTagsFn(tagsFromPathFn: (path: string) => string[])` | override the default tagsFromPathFn | 27 | | `build() => OpenAPIV3.Document` | build the openapi document | 28 | 29 | 30 | ### Security scheme 31 | 32 | The `securityScheme` allows you to specify an OpenAPI security scheme object. Zodios has 3 helper functions to generate the security scheme object. 33 | 34 | #### Basic auth 35 | 36 | ```ts 37 | import { basicAuthScheme } from "@zodios/openapi"; 38 | 39 | const doc = openApiBuilder(info) 40 | .addSecurityScheme('auth',basicAuthScheme()) 41 | .addProtectedApi('auth', api) 42 | .build(); 43 | ``` 44 | 45 | #### Bearer auth 46 | 47 | ```ts 48 | import { bearerAuthScheme } from "@zodios/openapi"; 49 | 50 | const doc = openApiBuilder(info) 51 | .addSecurityScheme('bearer',bearerAuthScheme()) 52 | .addProtectedApi('bearer', api) 53 | .build(); 54 | ``` 55 | 56 | #### OAuth2 57 | 58 | ```ts 59 | 60 | import { oauth2Scheme } from "@zodios/openapi"; 61 | 62 | const doc = openApiBuilder(info) 63 | .addSecurityScheme('oauth2',oauth2Scheme({ 64 | implicit: { 65 | authorizationUrl: "https://example.com/oauth2/authorize", 66 | scopes: { 67 | "read:users": "read users", 68 | "write:users": "write users", 69 | }, 70 | }, 71 | })) 72 | .addProtectedApi('oauth2', api) 73 | .build(); 74 | ``` 75 | 76 | ### custom tags 77 | 78 | The `tagsFromPathFn` option allows you to specify a function that returns an array of tags for a given path. This is useful if you want to group your endpoints by tags. 79 | 80 | :::note 81 | Zodios will by default deduce tags from the last named resource in the path. So for example, the path `/users/:id` will have the tag `users`, and the path `/users/:id/comments` will have the tag `comments`. 82 | Only pass this option if you want to override this behavior. 83 | ::: 84 | 85 | ```ts 86 | const doc = openApiBuilder(info) 87 | .addPublicApi(api) 88 | .setCustomTagsFn((path) => ({ 89 | '/users': ['users'], 90 | '/users/:id': ['users'], 91 | '/users/:id/comments': ['users'], 92 | }[path]) 93 | .build(); 94 | ``` 95 | ## `toOpenApi` - deprecated 96 | 97 | 98 | To generate an OpenAPI documentation from your api definition, you can use the `toOpenApi` method of `@zodios/openapi` package. 99 | 100 | ```ts 101 | function toOpenApi( 102 | api: ZodiosApiDefinition, 103 | options?: { 104 | info?: OpenAPIV3.InfoObject 105 | servers?: OpenAPIV3.ServerObject[]; 106 | securityScheme?: OpenAPIV3.SecuritySchemeObject; 107 | tagsFromPathFn?: (path: string) => string[]; 108 | } 109 | ): OpenAPIV3.Document; 110 | ``` 111 | 112 | ## Examples 113 | 114 | ### swagger-ui-express 115 | 116 | You can expose your OpenAPI documentation with the `@zodios/express` package. 117 | 118 | ```ts 119 | import { serve, setup } from "swagger-ui-express"; 120 | import { zodiosApp } from "@zodios/express"; 121 | import { openApiBuilder } from "@zodios/openapi"; 122 | import { userApi, adminApi } from "./api"; 123 | import { userRouter } from './userRouter'; 124 | import { adminRouter } from './adminRouter'; 125 | 126 | const app = zodiosApp(); 127 | 128 | // expose user api endpoints 129 | app.use('/api/v1', userRouter); 130 | app.use('/api/v1', adminRouter); 131 | 132 | // expose openapi documentation 133 | const document = openApiBuilder({ 134 | title: "User API", 135 | version: "1.0.0", 136 | description: "A simple user API", 137 | }) 138 | // you can declare as many security servers as you want 139 | .addServer({ url: "/api/v1" }) 140 | // you can declare as many security schemes as you want 141 | .addSecurityScheme("admin", bearerAuthScheme()) 142 | // you can declare as many apis as you want 143 | .addPublicApi(userApi) 144 | // you can declare as many protected apis as you want 145 | .addProtectedApi("admin", adminApi) 146 | .build(); 147 | 148 | app.use(`/docs/swagger.json`, (_, res) => res.json(document)); 149 | app.use("/docs", serve); 150 | app.use("/docs", setup(undefined, { swaggerUrl: "/docs/swagger.json" })); 151 | 152 | app.listen(3000); 153 | ``` 154 | 155 | Result: 156 | ![openapi](/img/openapi.png) 157 | 158 | ## OpenAPI to Zodios API definition 159 | 160 | If you want to use an existing OpenAPI documentation to generate your Zodios API definition, you can use the [openapi-zod-client](https://github.com/astahmer/openapi-zod-client) package. 161 | 162 | ```bash 163 | npx openapi-zod-client "swagger.json" -o "zodios-client.ts" 164 | ``` 165 | 166 | You can also use the [online openapi to zodios api definition](https://openapi-zod-client.vercel.app/) tool. 167 | 168 | ![openapi-to-zodios](/img/openapi-to-zodios.png) -------------------------------------------------------------------------------- /website/docs/api/typescript.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Typescript 6 | 7 | Even though zodios is written in typescript, you can use it with javascript. However, if you are using typescript, you can benefit from the typescript type helpers. 8 | 9 | ## `ApiOf` 10 | 11 | `ApiOf` is a type helper that extracts the api definition type from your zodios client instance. 12 | 13 | ```ts 14 | import { ApiOf, Zodios } from '@zodios/core';; 15 | import { myApiDefinition } from './api-definition'; 16 | 17 | const client = new Zodios(myApiDefinition); 18 | 19 | type MyApi = ApiOf; 20 | ``` 21 | ## `ZodiosBodyByPath` 22 | 23 | `ZodiosBodyByPath` is a type helper that extracts the body type of a request from your api definition. 24 | 25 | ```ts 26 | import { ZodiosBodyByPath } from '@zodios/core'; 27 | import { MyApi } from './my-api'; 28 | 29 | type User = ZodiosBodyByPath; 30 | ``` 31 | 32 | ## `ZodiosBodyByAlias` 33 | 34 | `ZodiosBodyByAlias` is a type helper that extracts the body type of a request from your api definition. 35 | 36 | ```ts 37 | import { ZodiosBodyByAlias } from '@zodios/core'; 38 | import { MyApi } from './my-api'; 39 | 40 | type User = ZodiosBodyByAlias; 41 | ``` 42 | ## `ZodiosHeaderParamsByPath` 43 | 44 | `ZodiosHeaderParamsByPath` is a type helper that extracts the header params type of a request from your api definition. 45 | 46 | ```ts 47 | import { ZodiosHeaderParamsByPath } from '@zodios/core'; 48 | import { MyApi } from './my-api'; 49 | 50 | type CreateUsersHeaderParams = ZodiosHeaderParamsByPath; 51 | ``` 52 | ## `ZodiosHeaderParamsByAlias` 53 | 54 | `ZodiosHeaderParamsByAlias` is a type helper that extracts the header params type of a request from your api definition. 55 | 56 | ```ts 57 | import { ZodiosHeaderParamsByAlias } from '@zodios/core'; 58 | import { MyApi } from './my-api'; 59 | 60 | type CreateUsersHeaderParams = ZodiosHeaderParamsByAlias; 61 | ``` 62 | 63 | ## `ZodiosPathParamsByPath` 64 | 65 | `ZodiosPathParamsPath` is a type helper that extracts the path params type of a request from your api definition. 66 | 67 | ```ts 68 | import { ZodiosPathParamsByPath } from '@zodios/core'; 69 | import { MyApi } from './my-api'; 70 | 71 | type GetUserPathParams = ZodiosPathParamsByPath; 72 | ``` 73 | ## `ZodiosPathParamByAlias` 74 | 75 | `ZodiosPathParamByAlias` is a type helper that extracts the path params type of a request from your api definition. 76 | 77 | ```ts 78 | import { ZodiosPathParamByAlias } from '@zodios/core'; 79 | import { MyApi } from './my-api'; 80 | 81 | type GetUserPathParams = ZodiosPathParamByAlias; 82 | ``` 83 | ## `ZodiosResponseByPath` 84 | 85 | `ZodiosResponseByPath` is a type helper that extracts the response type of a request from your api definition. 86 | 87 | ```ts 88 | import { ZodiosResponseByPath } from '@zodios/core'; 89 | import { MyApi } from './my-api'; 90 | 91 | type Users = ZodiosResponseByPath; 92 | ``` 93 | 94 | ## `ZodiosResponseByAlias` 95 | 96 | `ZodiosResponseByAlias` is a type helper that extracts the response type of a request from your api definition. 97 | 98 | ```ts 99 | import { ZodiosResponseByAlias } from '@zodios/core'; 100 | import { MyApi } from './my-api'; 101 | 102 | type Users = ZodiosResponseByAlias; 103 | ``` 104 | ## `ZodiosQueryParamsByPath` 105 | 106 | `ZodiosQueryParamsByPath` is a type helper that extracts the query params type of a request from your api definition. 107 | 108 | ```ts 109 | import { ZodiosQueryParamsByPath } from '@zodios/core'; 110 | import { MyApi } from './my-api'; 111 | 112 | type GetUsersQueryParams = ZodiosQueryParamsByPath; 113 | ``` 114 | ## `ZodiosQueryParamsByAlias` 115 | 116 | `ZodiosQueryParamsByAlias` is a type helper that extracts the query params type of a request from your api definition. 117 | 118 | ```ts 119 | import { ZodiosQueryParamsByAlias } from '@zodios/core'; 120 | import { MyApi } from './my-api'; 121 | 122 | type GetUsersQueryParams = ZodiosQueryParamsByAlias; 123 | ``` 124 | ## `ZodiosErrorByPath` 125 | 126 | `ZodiosErrorByPath` is a type helper that extracts the error type of a request from your api definition given a status code. 127 | 128 | ```ts 129 | import { ZodiosErrorByPath } from '@zodios/core'; 130 | import { MyApi } from './my-api'; 131 | 132 | type NotFoundUsersError = ZodiosErrorByPath; 133 | ``` 134 | ## `ZodiosErrorByAlias` 135 | 136 | `ZodiosErrorByAlias` is a type helper that extracts the error type of a request from your api definition given a status code. 137 | 138 | ```ts 139 | import { ZodiosErrorByAlias } from '@zodios/core'; 140 | import { MyApi } from './my-api'; 141 | 142 | type NotFoundUsersError = ZodiosErrorByAlias; 143 | ``` 144 | ## Example 145 | 146 | ```ts 147 | import { 148 | makeCrudApi, 149 | ZodiosBodyByPath, 150 | ZodiosResponseByPath, 151 | ZodiosPathParamsByPath, 152 | ZodiosQueryParamsByPath, 153 | } from "@zodios/code"; 154 | import z from "zod"; 155 | 156 | const user = z.object({ 157 | id: z.number(), 158 | name: z.string(), 159 | email: z.string().email(), 160 | phone: z.string(), 161 | }); 162 | 163 | const api = makeCrudApi("user", user); 164 | 165 | type User = z.infer; 166 | type Api = typeof api; 167 | 168 | type Users = ZodiosResponseByPath; 169 | // ^? type Users = { id: number; name: string; email: string; phone: string; }[] 170 | type UserById = ZodiosResponseByPath; 171 | // ^? type UserById = { id: number; name: string; email: string; phone: string; } 172 | type GetUserParams = ZodiosPathParamsByPath; 173 | // ^? type GetUserParams = { id: number; } 174 | type GetUserQueries = ZodiosQueryParamsByPath; 175 | // ^? type GetUserQueries = never 176 | type CreateUserBody = ZodiosBodyByPath; 177 | // ^? type CreateUserBody = { name: string; email: string; phone: string; } 178 | type CreateUserResponse = ZodiosResponseByPath; 179 | // ^? type CreateUserResponse = { id: number; name: string; email: string; phone: string; } 180 | type UpdateUserBody = ZodiosBodyByPath; 181 | // ^? type UpdateUserBody = { name: string; email: string; phone: string; } 182 | type PatchUserBody = ZodiosBodyByPath; 183 | // ^? type PatchUserBody = { name?: string | undefined; email?: string | undefined; phone?: string | undefined; } 184 | ``` 185 | -------------------------------------------------------------------------------- /website/docs/client/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Zodios client", 3 | "position": 6, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Api definitions and client" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /website/docs/client/error.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Client Error Handling 6 | 7 | Error handling is a very important part of any API client. Zodios provides helpers to handle errors in a typesafe way. 8 | Indeed, many things can go wrong when making a request to an API. The server can be down, the request can be malformed, the response can be malformed, the response can be a 404, etc. 9 | 10 | ## `isErrorFromPath` 11 | 12 | `isErrorFromPath` is a type guard that allows you to check if an error is an expected error by its path. Allowing to have typesafe error handling. 13 | 14 | ```ts 15 | function isErrorFromPath(api: ZodiosEndpointDefinitions, method: string, path: string, error: unknown): error is AxiosError 16 | ``` 17 | 18 | ## `isErrorFromAlias` 19 | 20 | `isErrorFromAlias` is a type guard that allows you to check if an error is an expected error by its alias. Allowing to have typesafe error handling. 21 | 22 | ```ts 23 | function isErrorFromAlias(api: ZodiosEndpointDefinitions, alias: string, error: unknown): error is AxiosError 24 | ``` 25 | 26 | ## Example 27 | 28 | ```typescript 29 | import { isErrorFromPath, makeApi, Zodios } from "@zodios/core"; 30 | 31 | const api = makeApi([ 32 | { 33 | path: "/users/:id", 34 | method: "get", 35 | alias: "getUser", 36 | response: z.object({ 37 | id: z.number(), 38 | name: z.string(), 39 | }), 40 | errors: [ 41 | { 42 | status: 404, 43 | schema: z.object({ 44 | message: z.string(), 45 | specificTo404: z.string(), 46 | }), 47 | }, 48 | { 49 | status: 'default', 50 | schema: z.object({ 51 | message: z.string(), 52 | }), 53 | } 54 | ], 55 | }, 56 | ]); 57 | 58 | const apiClient = new Zodios(api); 59 | 60 | try { 61 | const response = await apiClient.getUser({ params: { id: 1 } }); 62 | } catch (error) { 63 | // you can also do: 64 | // - isErrorFromPath(zodios.api, "get", "/users/:id", error) 65 | // - isErrorFromAlias(api, "getUser", error) 66 | // - isErrorFromAlias(zodios.api, "getUser", error) 67 | if(isErrorFromPath(api, "get", "/users/:id", error)){ 68 | // error type is now narrowed to an axios error with a response from the ones defined in the api 69 | if(error.response.status === 404) { 70 | // error.response.data is guaranteed to be of type { message: string, specificTo404: string } 71 | } else { 72 | // error.response.data is guaranteed to be of type { message: string } 73 | } 74 | } 75 | } 76 | ``` -------------------------------------------------------------------------------- /website/docs/client/plugins.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Client API plugins 6 | 7 | Zodios client has a powerfull plugin system. You can attach a plugin to all your API calls or to a specific API call. 8 | ## Fetch plugin 9 | 10 | Axios is using XHR on the browser. This might be a showstopper for your application, because XHR lacks some options of `fetch` you might rely on. 11 | For those use cases, you can use the `fetch` plugin that implements an axios adapter using the standard fetch. 12 | 13 | :::caution 🚧 Warning 🚧** 14 | It's worth noting, that you should not use the `fetch` plugin on nodejs. Indeed, fetch lacks a lot of features on backend side and you should use axios default http adapter for node (default). If you still want to use fetch on your backend, you should use a polyfill, zodios does not provide one. 15 | 16 | Do not open an issue for `fetch` support on `nodejs` unless you are willing to add support for it with a PR at the same time. I might reconsider this position in the future when fetch becomes feature complete on nodejs. 17 | ::: 18 | 19 | ```typescript 20 | import { pluginFetch } from "@zodios/plugins"; 21 | 22 | apiClient.use(pluginFetch({ 23 | // all fetch options are supported 24 | keepAlive: true, 25 | })); 26 | ``` 27 | 28 | ## Authorization with Token plugin 29 | 30 | `@zodios/plugins` comes with a plugin to inject and renew your tokens : 31 | ```typescript 32 | import { pluginToken } from '@zodios/plugins'; 33 | 34 | apiClient.use(pluginToken({ 35 | getToken: async () => "token" 36 | })); 37 | ``` 38 | ## Use a plugin only for some endpoints 39 | 40 | Zodios plugin system is much like the middleware system of `express`. This means you can apply a plugin to a specific endpoint or to all endpoints. 41 | 42 | ```typescript 43 | import { pluginToken } from '@zodios/plugins'; 44 | 45 | // apply a plugin by alias 46 | apiClient.use("getUser", pluginToken({ 47 | getToken: async () => "token" 48 | })); 49 | // apply a plugin by endpoint 50 | apiClient.use("get","/users/:id", pluginToken({ 51 | getToken: async () => "token" 52 | })); 53 | ``` 54 | ## Override plugin 55 | 56 | Zodios plugins can be named and can be overridden. 57 | Here are the list of integrated plugins that are used by zodios by default : 58 | - zodValidationPlugin : validation of response schema with zod library 59 | - formDataPlugin : convert provided body object to `multipart/form-data` format 60 | - formURLPlugin : convert provided body object to `application/x-www-form-urlencoded` format 61 | 62 | For example, you can override internal 'zod-validation' plugin with your own validation plugin : 63 | 64 | ```typescript 65 | import { zodValidationPlugin } from '@zodios/core'; 66 | import { myValidationInterceptor } from './my-custom-validation'; 67 | 68 | apiClient.use({ 69 | name: zodValidationPlugin().name, // using the same name as an already existing plugin will override it 70 | response: myValidationInterceptor, 71 | }); 72 | ``` 73 | 74 | Some Zodios plugins are registered per-endpoint. For example, you cannot override `formDataPlugin` globally. Instead, you should do: 75 | 76 | ```typescript 77 | import { formDataPlugin } from '@zodios/core'; 78 | import { myFormDataPlugin } from './my-custom-formdata'; 79 | 80 | for(const endpoint of apiClient.api) { 81 | if(endpoint.requestFormat === 'form-data') { 82 | apiClient.use(endpoint.alias, { 83 | name: formDataPlugin().name, // using the same name as an already existing plugin will override it 84 | request: myFormDataPlugin 85 | }) 86 | } 87 | } 88 | ``` 89 | 90 | ## Plugin execution order 91 | 92 | Zodios plugins that are not attached to an endpoint are executed first. 93 | Then plugins that match your endpoint are executed. 94 | In addition, plugins are executed in their declaration order for requests, and in reverse order for responses. 95 | 96 | example, `pluginLog` logs the message it takes as parameter when it's called : 97 | ```typescript 98 | apiClient.use("getUser", pluginLog('2')); 99 | apiClient.use(pluginLog('1')); 100 | apiClient.use("get","/users/:id", pluginLog('3')); 101 | 102 | apiClient.get("/users/:id", { params: { id: 7 } }); 103 | 104 | // output : 105 | // request 1 106 | // request 2 107 | // request 3 108 | // response 3 109 | // response 2 110 | // response 1 111 | ``` 112 | 113 | ## Write your own plugin 114 | 115 | Zodios plugins are middleware interceptors for requests and responses. 116 | If you want to create your own, they should have the following signature : 117 | 118 | ```typescript 119 | export type ZodiosPlugin = { 120 | /** 121 | * Optional name of the plugin 122 | * naming a plugin allows to remove it or replace it later 123 | */ 124 | name?: string; 125 | /** 126 | * request interceptor to modify or inspect the request before it is sent 127 | * @param api - the api description 128 | * @param request - the request config 129 | * @returns possibly a new request config 130 | */ 131 | request?: ( 132 | api: ZodiosEnpointDescriptions, 133 | config: AnyZodiosRequestOptions 134 | ) => Promise; 135 | /** 136 | * response interceptor to modify or inspect the response before it is returned 137 | * @param api - the api description 138 | * @param config - the request config 139 | * @param response - the response 140 | * @returns possibly a new response 141 | */ 142 | response?: ( 143 | api: ZodiosEnpointDescriptions, 144 | config: AnyZodiosRequestOptions, 145 | response: AxiosResponse 146 | ) => Promise; 147 | /** 148 | * error interceptor for response errors 149 | * there is no error interceptor for request errors 150 | * @param api - the api description 151 | * @param config - the config for the request 152 | * @param error - the error that occured 153 | * @returns possibly a new response or throw a new error 154 | */ 155 | error?: ( 156 | api: ZodiosEnpointDescriptions, 157 | config: AnyZodiosRequestOptions, 158 | error: Error 159 | ) => Promise; 160 | }; 161 | ``` 162 | -------------------------------------------------------------------------------- /website/docs/client/react.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # React hooks 6 | 7 | Zodios comes with a Query and Mutation hook helper. 8 | It's a thin wrapper around React-Query but with zodios auto completion and automatic key management. 9 | No need to remember your keys anymore. 10 | 11 | Zodios query hook also returns an invalidation helper to allow you to reset react query cache easily. 12 | 13 | ## Zodios Hooks instance 14 | 15 | When creating an instance or zodios hooks, you need to provide a name that will be used as `react-query` key prefix and your instance of Zodios Api Client. 16 | 17 | ```ts 18 | new ZodiosHooks(name: string, client: Zodios) 19 | ``` 20 | 21 | **Example** 22 | ```ts 23 | const apiClient = new Zodios(baseUrl, [...]); 24 | const apiHooks = new ZodiosHooks("myAPI", apiClient); 25 | ``` 26 | ## Zodios hooks methods 27 | 28 | ### `hooks.use[Alias]` 29 | 30 | You will usually want to use aliases to call your endpoints. You can define them in the `alias` option in your API definition endpoint. 31 | 32 | #### query alias: 33 | 34 | Query alias hooks will return a `QueryResult` object from `react-query` with: 35 | - the response data and all react-query result properties 36 | - the generated `key` 37 | - the `invalidate` helper. 38 | 39 | ```ts 40 | function use[Alias](config?: ZodiosRequestOptions, queryOptions: QueryOptions): QueryResult; 41 | ``` 42 | 43 | **example**: 44 | ```ts 45 | // identical to hooks.useQuery("/users") 46 | const { data: users, isLoading, isError, invalidate, key } = hooks.useGetUsers(); 47 | ``` 48 | 49 | #### immutable query alias: 50 | ```ts 51 | function use[Alias](body: Body, config?: ZodiosRequestOptions, queryOptions: QueryOptions): QueryResult; 52 | ``` 53 | 54 | **example**: 55 | ```ts 56 | // identical to hooks.useImmutableQuery("/users/search") 57 | const { data: users, isLoading, isError } = hooks.useSearchUsers({ name: "John" }); 58 | ``` 59 | 60 | :::note 61 | Immutable query aliases are only available for `post` endpoints. 62 | you also need to set the `immutable` option to `true` in your API definition endpoint if you want alias to use `useImmutableQuery` hook. 63 | ::: 64 | #### mutation alias 65 | 66 | Alias for `post`, `put`, `patch`, `delete` endpoints: 67 | ```ts 68 | function use[Alias](config?: ZodiosRequestOptions, queryOptions?: QueryOptions): MutationResult; 69 | ``` 70 | 71 | **example**: 72 | ```ts 73 | // identical to usePost("/users") or useMutation("post","/users") 74 | const { mutate } = hooks.useCreateUser(); 75 | 76 | ``` 77 | 78 | ### `zodios.useQuery` 79 | 80 | Generic request method that allows to do queries (same as useGet). 81 | Query hooks will return a `QueryResult` object from `react-query` with: 82 | - the response data and all react-query result properties 83 | - the generated `key` 84 | - the `invalidate` helper. 85 | 86 | ```ts 87 | useQuery(path: string, config?: ZodiosRequestOptions, queryOptions?: QueryOptions): QueryResult; 88 | ``` 89 | 90 | **Example**: 91 | ```ts 92 | const { data: users, isLoading, isError } = hooks.useQuery('/users'); 93 | ``` 94 | 95 | :::note 96 | check [react-query documentation](https://react-query.tanstack.com/reference/useQuery) for more informations on `QueryResult` and `QueryOptions`. 97 | ::: 98 | 99 | ### `zodios.useImmutableQuery` 100 | 101 | Generic request method that allows to do queries on post requests. 102 | 103 | ```ts 104 | useImmutableQuery(path: string, body: Body ,config?: ZodiosRequestOptions, queryOptions?: QueryOptions): QueryResult; 105 | ``` 106 | 107 | **Example**: 108 | ```ts 109 | const { data: users, isLoading, isError } = hooks.useImmutableQuery('/users/search', { name: "John" }); 110 | ``` 111 | 112 | :::note 113 | check [react-query documentation](https://react-query.tanstack.com/reference/useQuery) for more informations on `QueryResult` and `QueryOptions`. 114 | ::: 115 | 116 | 117 | ### `zodios.useInfiniteQuery` 118 | 119 | Generic request method that allows to load pages indefinitly. 120 | 121 | ```ts 122 | useInfiniteQuery(path: string, config?: ZodiosRequestOptions, infiniteQueryOptions?: InfiniteQueryOptions): InfiniteQueryResult; 123 | ``` 124 | 125 | Compared to native react-query infinite query, you also need to provide a function named `getPageParamList` to tell zodios which parameters will be used to paginate. Indeed, zodios needs to know it to be able to generate the correct query key automatically for you. 126 | 127 | **Example**: 128 | ```ts 129 | const { data: userPages, isFectching, fetchNextPage } = apiHooks.useInfiniteQuery( 130 | "/users", 131 | { 132 | // request 10 users per page 133 | queries: { limit: 10 }, 134 | }, 135 | { 136 | // tell zodios to not use page as query key to allow infinite loading 137 | getPageParamList: () => ["page"], 138 | // get next page param has to return the next page as a query or path param 139 | getNextPageParam: (lastPage, pages) => lastPage.nextPage ? { 140 | queries: { 141 | page: lastPage.nextPage, 142 | }, 143 | }: undefined; 144 | } 145 | ); 146 | ``` 147 | 148 | :::note 149 | check [react-query infinite query documentation](https://react-query.tanstack.com/reference/useInfiniteQuery) for more informations on `InfiniteQueryResult` and `InfiniteQueryOptions`. 150 | ::: 151 | 152 | ### `zodios.useImmutableInfiniteQuery` 153 | 154 | Generic request method that allows to search pages indefinitly with post requests. 155 | 156 | ```ts 157 | useImmutableInfiniteQuery(path: string, body: Body ,config?: ZodiosRequestOptions, infiniteQueryOptions?: InfiniteQueryOptions): InfiniteQueryResult; 158 | ``` 159 | 160 | Compared to native react-query infinite query, you also need to provide a function named `getPageParamList` to tell zodios which parameters will be used to paginate. Indeed, zodios needs to know it to be able to generate the correct query key automatically for you. 161 | 162 | **Example**: 163 | ```ts 164 | const { data: userPages, isFectching, fetchNextPage } = apiHooks.useImmutableInfiniteQuery( 165 | "/users/search", 166 | { 167 | // search for users named John 168 | name: "John", 169 | // request 10 users per page 170 | limit: 10, 171 | }, 172 | undefined, 173 | { 174 | // tell zodios to not use page as query key to allow infinite loading 175 | getPageParamList: () => ["page"], 176 | // get next page param has to return the next page as a query or path param 177 | getNextPageParam: (lastPage, pages) => lastPage.nextPage ? { 178 | body: { 179 | page: lastPage.nextPage, 180 | }, 181 | }: undefined; 182 | } 183 | ); 184 | ``` 185 | 186 | :::note 187 | check [react-query infinite query documentation](https://react-query.tanstack.com/reference/useInfiniteQuery) for more informations on `InfiniteQueryResult` and `InfiniteQueryOptions`. 188 | ::: 189 | 190 | ### `zodios.useMutation` 191 | 192 | Generic request method that allows to do mutations. 193 | 194 | ```ts 195 | useMutation(method: string, path: string, config: ZodiosRequestOptions, reactQueryOptions?: ReactQueryOptions): ReactMutationResult; 196 | ``` 197 | 198 | **Example**: 199 | ```ts 200 | const { mutate } = hooks.useMutation('post','/users'); 201 | ``` 202 | 203 | :::note 204 | check [react-query documentation](https://react-query.tanstack.com/reference/useMutation) for more informations on `MutationResult` and `MutationOptions`. 205 | ::: 206 | 207 | ### `zodios.useGet` 208 | 209 | Query hooks will return a `QueryResult` object from `react-query` with: 210 | - the response data and all react-query result properties 211 | - the generated `key` 212 | - the `invalidate` helper. 213 | 214 | ```ts 215 | useGet(path: string, config?: ZodiosRequestOptions, reactQueryOptions?: ReactQueryOptions): ReactQueryResult; 216 | ``` 217 | 218 | **Example**: 219 | ```ts 220 | const { data: user, isLoading, isError, invalidate, key } = hooks.useGet("/users/:id", { params: { id: 1 } }); 221 | ``` 222 | 223 | ### `zodios.usePost` 224 | 225 | ```ts 226 | usePost(path: string, config?: ZodiosRequestOptions, reactQueryOptions?: ReactQueryOptions): ReactMutationResult; 227 | ``` 228 | 229 | **Example**: 230 | ```ts 231 | const { mutate } = hooks.usePost("/users"); 232 | ``` 233 | 234 | ### `zodios.usePut` 235 | 236 | ```ts 237 | usePut(path: string, config?: ZodiosRequestOptions, reactQueryOptions?: ReactQueryOptions): ReactMutationResult; 238 | ``` 239 | 240 | **Example**: 241 | ```ts 242 | const { mutate } = hooks.usePut("/users/:id", { params: { id: 1 } }); 243 | ``` 244 | 245 | ### `zodios.usePatch` 246 | 247 | ```ts 248 | usePatch(path: string, config?: ZodiosRequestOptions, reactQueryOptions?: ReactQueryOptions): ReactMutationResult; 249 | ``` 250 | 251 | **Example**: 252 | ```ts 253 | const { mutate } = hooks.usePatch("/users/:id", {params: {id: 1}}); 254 | ``` 255 | 256 | ### `zodios.useDelete` 257 | 258 | ```ts 259 | useDelete(path: string, config?: ZodiosRequestOptions, reactQueryOptions?: ReactQueryOptions): ReactMutationResult; 260 | ``` 261 | 262 | **Example**: 263 | ```ts 264 | const { mutate } = hooks.useDelete("/users/:id", { params: {id: 1 }}); 265 | ``` 266 | ## Zodios key helpers 267 | 268 | Zodios provides some helpers to generate query keys to be used to invalidate cache or to get it directly from cache with 'QueryClient.getQueryData(key)'. 269 | 270 | ### `zodios.getKeyByPath` 271 | 272 | ```ts 273 | getKeyByPath(method: string, path: string, config?: ZodiosRequestOptions): QueryKey; 274 | ``` 275 | 276 | **Examples**: 277 | 278 | To get a key for a path endpoint with parameters: 279 | ```ts 280 | const key = zodios.getKeyByPath('get', '/users/:id', { params: { id: 1 } }); 281 | const user = queryClient.getQueryData(key); 282 | ``` 283 | 284 | To get a key to invalidate a path endpoint for all possible parameters: 285 | ```ts 286 | const key = zodios.getKeyByPath('get', '/users/:id'); 287 | queryClient.invalidateQueries(key); 288 | ``` 289 | 290 | ### `zodios.getKeyByAlias` 291 | 292 | ```ts 293 | getKeyByAlias(alias: string, config?: ZodiosRequestOptions): QueryKey; 294 | ``` 295 | 296 | **Examples**: 297 | 298 | To get a key for an alias endpoint with parameters: 299 | ```ts 300 | const key = zodios.getKeyByAlias('getUser', { params: { id: 1 } }); 301 | const user = queryClient.getQueryData(key); 302 | ``` 303 | To get a key to invalidate an alias endpoint for all possible parameters: 304 | ```ts 305 | const key = zodios.getKeyByAlias('getUser'); 306 | queryClient.invalidateQueries(key); 307 | ``` 308 | 309 | ## Bundling 310 | 311 | If you are bundling your App with Webpack and with a custom library that embeds `@zodios/react` or `@tanstack/react-query`, you need to bundle you library with both `esm` and `cjs` support. 312 | We recommend using [tsup](https://tsup.egoist.dev/) to bundle your library and declare your `package.json` like [this](https://github.com/ecyrbe/zodios/blob/main/package.json) 313 | Else, you'll get the following error: 314 | 315 | ![error](https://user-images.githubusercontent.com/38932402/196659212-5bdb675f-d019-4d8b-8681-5f00ed24db4d.png) 316 | ## Example 317 | 318 | ```tsx title="users.tsx" 319 | import React from "react"; 320 | import { Zodios } from "@zodios/core"; 321 | import { ZodiosHooks } from "@zodios/react"; 322 | import { z } from "zod"; 323 | 324 | const baseUrl = "https://jsonplaceholder.typicode.com"; 325 | const zodios = new Zodios(baseUrl, [...]); 326 | const zodiosHooks = new ZodiosHooks("jsonplaceholder", zodios); 327 | 328 | const Users = () => { 329 | const { 330 | data: users, 331 | isLoading, 332 | error, 333 | invalidate: invalidateUsers, // zodios also provides invalidation helpers 334 | key // zodios also returns the generated key 335 | } = zodiosHooks.useQuery("/users"); // or useGetUsers(); 336 | const { mutate } = zodiosHooks.useMutation("post", "/users", undefined, { 337 | onSuccess: () => invalidateUsers(), 338 | }); // or .useCreateUser(...); 339 | 340 | return ( 341 | <> 342 |

Users

343 | 344 | {isLoading &&
Loading...
} 345 | {error &&
Error: {(error as Error).message}
} 346 | {users && ( 347 |
    348 | {users.map((user) => ( 349 |
  • {user.name}
  • 350 | ))} 351 |
352 | )} 353 | 354 | ); 355 | }; 356 | ``` 357 | 358 | ```tsx title="root.tsx" 359 | import { QueryClient, QueryClientProvider } from "react-query"; 360 | import { Users } from "./users"; 361 | 362 | const queryClient = new QueryClient(); 363 | 364 | export const App = () => { 365 | return ( 366 | 367 | 368 | 369 | ); 370 | }; 371 | ``` 372 | -------------------------------------------------------------------------------- /website/docs/ecosystem.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Ecosystem 6 | 7 | Outside of zodios own [packages](/docs/intro), you can find the following ones from zodios community: 8 | 9 | | Package | Description | 10 | | -------------------------------------------------------------------------------- | ------------------------------------------------------ | 11 | | [openapi-zod-client](https://github.com/astahmer/openapi-zod-client) | generate a zodios client from an openapi specification | 12 | | [api-definition-shorthand](https://github.com/thelinuxlich/zodios-api-shorthand) | shorthand for zodios api definitions | 13 | -------------------------------------------------------------------------------- /website/docs/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Installation 6 | 7 | :::info 8 | Zodios packages can be installed using your preferred package manager. 9 | We only show npm and yarn as they are the most popular. 10 | ::: 11 | 12 | ## Frontend installation 13 | 14 | ```bash npm2yarn 15 | npm install @zodios/core axios zod 16 | ``` 17 | ### With React 18 | 19 | if you want to use the react hooks, you need to install the following packages: 20 | 21 | ```bash npm2yarn 22 | npm install @tanstack/react-query @zodios/core @zodios/react axios react react-dom zod 23 | ``` 24 | 25 | ### With Solid 26 | 27 | if you want to use the solid hooks, you need to install the following packages: 28 | 29 | ```bash npm2yarn 30 | npm install @tanstack/solid-query @zodios/core @zodios/solid axios solid-js zod 31 | ``` 32 | ### Install type definitions 33 | 34 | install those even in javascript projects. 35 | 36 | ```bash npm2yarn 37 | // if you use react 38 | npm install --dev @types/react @types/react-dom 39 | ``` 40 | 41 | ## Backend installation 42 | 43 | ### With Express 44 | 45 | ```bash npm2yarn 46 | npm install @zodios/core @zodios/express express zod axios 47 | ``` 48 | 49 | optional packages for openapi generation: 50 | 51 | ```bash npm2yarn 52 | npm install @zodios/openapi swagger-ui-express 53 | ``` 54 | 55 | ### With NextJS 56 | 57 | ```bash npm2yarn 58 | npm install @zodios/core @zodios/express next zod axios react react-dom 59 | ``` 60 | 61 | ### Install type definitions 62 | 63 | install those even in javascript projects 64 | 65 | ```bash npm2yarn 66 | // if you use express 67 | npm install --dev @types/express 68 | // or with nextjs 69 | npm install --dev @types/express @types/react @types/react-dom 70 | ``` 71 | -------------------------------------------------------------------------------- /website/docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Introduction 6 | 7 | Zodios is a REST API toolbox with end-to-end typesafety. 8 | It allows you to create a REST API with a clean, intuitive and declarative syntax. 9 | 10 | It's best used with [TypeScript](https://www.typescriptlang.org/), but it's also usable with pure [JavaScript](https://www.javascript.com/). 11 | 12 | It's composed of multiple packages : 13 | 14 | | Package | Type | Description | 15 | | ----------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | 16 | | `@zodios/core` | Frontend Backend | The core library that contains an API client with full typesafety and autocompletion.
You can use it as a standalone API client without using other modules. | 17 | | `@zodios/plugins` | Frontend Backend | A set of plugins for the API client. | 18 | | `@zodios/react` | Frontend | React hooks for the client based on [tanstack-query](https://tanstack.com/query). | 19 | | `@zodios/solid` | Frontend | Solid hooks for the client based on [tanstack-query](https://tanstack.com/query). | 20 | | `@zodios/express` | Backend | A simple adapter for [Express](https://expressjs.com/) but with full typesafety and autocompletion. | 21 | | `@zodios/openapi` | Backend | Helper that generates OpenAPI specs from Zodios [API definitions](api/api-definition.md) and allows you to easily generate swagger ui. | 22 | 23 | :::tip It's worth noting that frontend and backend packages can be used as standalone packages. 24 | Meaning that you don't need to use Zodios Backend to use Zodios Frontend packages and vice-versa. Allowing you to scale the development of your API between frontend and backend teams. 25 | 26 | You only need to share the API definition between the two teams. 27 | ::: 28 | -------------------------------------------------------------------------------- /website/docs/server/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Zodios server", 3 | "position": 7, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Api definitions and server" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /website/docs/server/express-app.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Zodios Application 6 | 7 | A Zodios application is a simple adapter for [Express](https://expressjs.com/). It's an express instance but with full typesafety and autocompletion. 8 | 9 | :::info 10 | For more information on how to use express, check out the [Express documentation](https://expressjs.com/) 11 | ::: 12 | 13 | ## `zodiosApp` 14 | 15 | To upgrade an existing express application with typesafety, replace your `express()` calls to `zodiosApp(api)` 16 | 17 | ```ts 18 | function zodiosApp(api?: ZodiosEndpointDescriptions, options?: ZodiosAppOptions): ZodiosApp 19 | ``` 20 | ## `ctx.app` 21 | 22 | You can also create a context aware express application with `ctx.app`: 23 | 24 | ```ts 25 | Context.app(api?: ZodiosEndpointDescriptions, options?: ZodiosAppOptions): ZodiosApp 26 | ``` 27 | 28 | ## Options 29 | 30 | | Property | Type | Description | 31 | | ---------------------- | ---------------------------- | --------------------------------------------------------------------- | 32 | | express | Express | optional express instance - default to express() | 33 | | enableJsonBodyParser | boolean | enable json body parser - default to true | 34 | | validate | boolean | enable zod input validation - default to true | 35 | | transform | boolean | enable zod input transformation - default to false | 36 | | validationErrorHandler | RouterValidationErrorHandler | error handler for validation errors - default to `defaulErrorHandler` | 37 | 38 | ```ts 39 | type RouterValidationErrorHandler = ( 40 | err: { 41 | context: string; 42 | error: z.ZodIssue[]; 43 | }, 44 | req: Request, 45 | res: Response, 46 | next: NextFunction 47 | ): void; 48 | ``` 49 | 50 | ## Examples 51 | 52 | ### Express Application from context 53 | 54 | ```ts 55 | import { zodiosContext } from "@zodios/express"; 56 | import z from "zod"; 57 | import { userApi } from "../../common/api"; 58 | import { userMiddleware } from "./userMiddleware"; 59 | 60 | const ctx = zodiosContext(z.object({ 61 | user: z.object({ 62 | id: z.number(), 63 | name: z.string(), 64 | isAdmin: z.boolean(), 65 | }), 66 | })); 67 | 68 | const app = ctx.app(userApi); 69 | // middleware that adds the user to the context 70 | app.use(userMiddleware); 71 | 72 | // auto-complete path fully typed and validated input params (body, query, path, header) 73 | // ▼ ▼ ▼ 74 | app.get("/users/:id", (req, res) => { 75 | // auto-complete user fully typed 76 | // ▼ 77 | if(req.user.isAdmin) { 78 | // res.json is typed thanks to zod 79 | return res.json({ 80 | // auto-complete req.params.id 81 | // ▼ 82 | id: req.params.id, 83 | name: "John Doe", 84 | }); 85 | } 86 | return res.status(403).end(); 87 | }) 88 | 89 | app.listen(3000); 90 | ``` 91 | 92 | ### Express Application 93 | 94 | 95 | ```ts title="/src/server/app.ts" 96 | import { zodiosApp } from "@zodios/express"; 97 | import { userApi } from "../../common/api"; 98 | 99 | // just an express adapter that is aware of your api, app is just an express app with type annotations and validation middlewares 100 | const app = zodiosApp(userApi); 101 | 102 | // auto-complete path fully typed and validated input params (body, query, path, header) 103 | // ▼ ▼ ▼ 104 | app.get("/users/:id", (req, res) => { 105 | // res.json is typed thanks to zod 106 | res.json({ 107 | // auto-complete req.params.id 108 | // ▼ 109 | id: req.params.id, 110 | name: "John Doe", 111 | }); 112 | }) 113 | 114 | app.listen(3000); 115 | ``` 116 | 117 | ### Error Handling 118 | 119 | Zodios express can infer the status code to match your API error response and also have your errors correctly typed. 120 | 121 | ```typescript title="/src/server/app.ts" 122 | import { makeApi } from "@zodios/core"; 123 | import { zodiosApp } from "@zodios/express"; 124 | import { z } from "zod"; 125 | 126 | const userApi = makeApi([ 127 | { 128 | method: "get", 129 | path: "/users/:id", 130 | alias: "getUser", 131 | description: "Get a user", 132 | response: z.object({ 133 | id: z.number(), 134 | name: z.string(), 135 | }), 136 | errors: [ 137 | { 138 | status: 404, 139 | response: z.object({ 140 | code: z.string(), 141 | message: z.string(), 142 | id: z.number(), 143 | }), 144 | }, { 145 | status: 'default', // default status code will be used if error is not 404 146 | response: z.object({ 147 | code: z.string(), 148 | message: z.string(), 149 | }), 150 | }, 151 | ], 152 | }, 153 | ]); 154 | 155 | const app = zodiosApp(userApi); 156 | app.get("/users/:id", (req, res) => { 157 | try { 158 | const id = +req.params.id; 159 | const user = service.findUser(id); 160 | if(!user) { 161 | // match error 404 schema with auto-completion 162 | res.status(404).json({ 163 | code: "USER_NOT_FOUND", 164 | message: "User not found", 165 | id, // compile time error if you forget to add id 166 | }); 167 | } else { 168 | // match response schema with auto-completion 169 | res.json(user); 170 | } 171 | } catch(err) { 172 | // match default error schema with auto-completion 173 | res.status(500).json({ 174 | code: "INTERNAL_ERROR", 175 | message: "Internal error", 176 | }); 177 | } 178 | }) 179 | 180 | app.listen(3000); 181 | ``` 182 | -------------------------------------------------------------------------------- /website/docs/server/express-context.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Zodios Context 6 | 7 | Zodios context allows you to declare a context object that will be available in all your zodios handlers. 8 | Usually in express apps, you would use `req` to store some data, like `req.user`, that you want to access in all your handlers. 9 | Zodios context allows you to do the same it by declaring a context schema , and this way have your `req.user` properly typed. 10 | 11 | ## Creating a context 12 | 13 | To create a context, you need to use the `zodiosContext` function. 14 | 15 | ```ts 16 | import { zodiosContext } from "@zodios/express"; 17 | import z from "zod"; 18 | 19 | const ctx = zodiosContext(z.object({ 20 | user: z.object({ 21 | id: z.number(), 22 | name: z.string(), 23 | }), 24 | })); 25 | ``` 26 | 27 | ## Creating a context-aware app 28 | 29 | To create a context-aware app, you need to use the `app()` method of the context. 30 | 31 | ```ts 32 | const app = ctx.app(); 33 | ``` 34 | 35 | ## Creating a context-aware router 36 | 37 | To create a context-aware router, you need to use the `router()` method of the context. 38 | 39 | ```ts 40 | const router = ctx.router(); 41 | ``` 42 | -------------------------------------------------------------------------------- /website/docs/server/express-router.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Zodios Router 6 | 7 | Zodios Router allows you to split your API endpoints into different files. You need attach an API definition for each router for them to be typesafe and give you autocompletion. 8 | 9 | :::info 10 | For more information on how to use express Router, check out the [Express documentation](https://expressjs.com/en/guide/routing.html) 11 | ::: 12 | 13 | ## `zodiosRouter` 14 | 15 | To upgrade an existing express router with typesafety, replace your `express.Router()` calls to `zodiosRouter(api)`. 16 | 17 | ```ts 18 | function zodiosRouter(api?: ZodiosEndpointDescriptions, options?: ZodiosRouterOptions): ZodiosRouter 19 | ``` 20 | 21 | ## `ctx.router` 22 | 23 | You can also create a context aware express router with `ctx.router`: 24 | 25 | ```ts 26 | Context.router(api?: ZodiosEndpointDescriptions, options?: ZodiosRouterOptions): ZodiosRouter 27 | ``` 28 | 29 | ## Options 30 | 31 | | Property | Type | Description | 32 | | ---------------------- | ---------------------------- | --------------------------------------------------------------------- | 33 | | router | express.Router | optional express router - default to express.Router() | 34 | | enableJsonBodyParser | boolean | enable json body parser - default to true | 35 | | validate | boolean | enable zod input validation - default to true | 36 | | transform | boolean | enable zod input transformation - default to false | 37 | | validationErrorHandler | RouterValidationErrorHandler | error handler for validation errors - default to `defaulErrorHandler` | 38 | | caseSensitive | boolean | enable case sensitive path matching - default to false | 39 | | strict | boolean | enable strict path matching - default to false | 40 | 41 | ```ts 42 | type RouterValidationErrorHandler = ( 43 | err: { 44 | context: string; 45 | error: z.ZodIssue[]; 46 | }, 47 | req: Request, 48 | res: Response, 49 | next: NextFunction 50 | ): void; 51 | ``` 52 | 53 | ## Examples 54 | 55 | 56 | ### Express Router from context 57 | 58 | ```ts 59 | 60 | import { zodiosContext } from "@zodios/express"; 61 | import z from "zod"; 62 | import { userApi } from "../../common/api"; 63 | import { userMiddleware } from "./userMiddleware"; 64 | 65 | const ctx = zodiosContext(z.object({ 66 | user: z.object({ 67 | id: z.number(), 68 | name: z.string(), 69 | isAdmin: z.boolean(), 70 | }), 71 | })); 72 | 73 | const router = ctx.router(); 74 | 75 | // middleware that adds the user to the context 76 | router.use(userMiddleware); 77 | ``` 78 | 79 | ### Merge multiple routers 80 | 81 | ```ts 82 | import { zodiosApp, zodiosRouter } from "@zodios/express"; 83 | 84 | const app = zodiosApp(); // just an axpess app with type annotations 85 | const userRouter = zodiosRouter(userApi); // just an express router with type annotations and validation middlewares 86 | const adminRouter = zodiosRouter(adminApi); // just an express router with type annotations and validation middlewares 87 | 88 | const app.use(userRouter,adminRouter); 89 | 90 | app.listen(3000); 91 | ``` 92 | 93 | or context aware 94 | 95 | ```ts 96 | import { zodiosContext } from "@zodios/express"; 97 | 98 | const ctx = zodiosContext(z.object({ 99 | user: z.object({ 100 | id: z.number(), 101 | name: z.string(), 102 | isAdmin: z.boolean(), 103 | }), 104 | })); 105 | 106 | const app = ctx.app(); 107 | const userRouter = ctx.router(userApi); 108 | const adminRouter = ctx.router(adminApi); 109 | 110 | app.use(userRouter,adminRouter); 111 | 112 | app.listen(3000); 113 | ``` 114 | -------------------------------------------------------------------------------- /website/docs/server/next.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Next integration 6 | 7 | Next integration is easy, with end-to-end typesafe developer experience, where you can combine the power of all the Zodios packages into one single codebase. 8 | 9 | :::info 10 | For more information about the Next framework, check out the [Next documentation](https://nextjs.org/docs) 11 | ::: 12 | 13 | ## File structure 14 | 15 | To integrate zodios to NextJS, you need to create a `slug` file named `[...zodios].ts` in your api folder. 16 | `@zodios/express` works out of the box with [NextJS](https://nextjs.org/) if you use the following structure: 17 | 18 | ```bash 19 | │ 20 | ├── src 21 | │ ├── common 22 | │ │ └── api.ts # API definition 23 | │ ├── pages 24 | │ │ ├── _app.tsx 25 | │ │ ├── api 26 | │ │ │ └── [...zodios].ts # import and re-export your main server app router here 27 | │ │ └── [..] 28 | │ ├── server 29 | │ │ ├── routers 30 | │ │ │ ├── app.ts # import your API definition and export your main app router here 31 | │ │ │ ├── users.ts # sub routers 32 | │ │ │ └── [..] 33 | │ │ ├── context.ts # export your main app context here 34 | └── [..] 35 | ``` 36 | :::tip It's recommended to use the example below to bootstrap your NextJS application. 37 | It has correct setup for webpack configuration with `@zodios/react` 38 | [Example project](https://github.com/ecyrbe/zodios-express/tree/main/examples/next) 39 | ::: 40 | 41 | ## Bundling 42 | 43 | If you are bundling your NextJS App with a custom library that embeds `@zodios/react` or `@tanstack/react-query`, you need to bundle you library with both `esm` and `cjs` support. 44 | We recommend using [tsup](https://esbuild.github.io/) to bundle your library and declare your `package.json` like [this](https://github.com/ecyrbe/zodios/blob/main/package.json) 45 | Else, you'll get the following error: 46 | 47 | ![error](https://user-images.githubusercontent.com/38932402/196659212-5bdb675f-d019-4d8b-8681-5f00ed24db4d.png) 48 | 49 | ## example 50 | 51 | ### Export your main router 52 | 53 | ```typescript title="/src/pages/api/[...zodios].ts" 54 | import { app } from "../../server/routers/app"; 55 | 56 | export default app; 57 | ``` 58 | 59 | ### Declare your main router 60 | 61 | Use `zodiosNextApp` or `ctx.nextApp` to create your main app router. 62 | 63 | ```typescript title="/src/server/routers/app.ts" 64 | import { zodiosNextApp } from "@zodios/express"; 65 | import { userRouter } from "./users"; 66 | 67 | export const app = zodiosNextApp(); 68 | app.use("/api", userRouter); 69 | ``` 70 | 71 | ### Declare your sub routers 72 | 73 | Use `zodiosRouter` or `ctx.router` to create your sub routers. 74 | 75 | ```typescript title="/src/server/routers/users.ts" 76 | import { zodiosRouter } from "@zodios/express"; 77 | import { userService } from "../services/users"; 78 | import { userApi } from "../../common/api"; 79 | 80 | export const userRouter = zodiosRouter(userApi); 81 | 82 | userRouter.get("/users", async (req, res) => { 83 | const users = await userService.getUsers(); 84 | res.status(200).json(users); 85 | }); 86 | 87 | userRouter.get("/users/:id", async (req, res, next) => { 88 | const user = await userService.getUser(req.params.id); 89 | if (!user) { 90 | return res.status(404).json({ 91 | error: { 92 | code: 404, 93 | message: "User not found", 94 | }, 95 | }); 96 | } 97 | return res.status(200).json(user); 98 | }); 99 | 100 | userRouter.post("/users", async (req, res) => { 101 | const createdUser = await userService.createUser(req.body); 102 | res.status(201).json(createdUser); 103 | }); 104 | ``` 105 | -------------------------------------------------------------------------------- /website/docs/sponsors.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Sponsors 6 | 7 | If you would like to sponsor Zodios, go on github or donate directly via 8 | - [paypal](https://www.paypal.me/ecyrbe) 9 | - [stripe](https://github.com/sponsors/ecyrbe) 10 | 11 | ## List of sponsors 12 | 13 | - D. Noland 14 | - S. Osterkil 15 | 16 | Thank you for your support! -------------------------------------------------------------------------------- /website/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Note: type annotations allow type checking and IDEs autocompletion 3 | 4 | const lightCodeTheme = require("prism-react-renderer/themes/github"); 5 | const darkCodeTheme = require("prism-react-renderer/themes/dracula"); 6 | 7 | /** @type {import('@docusaurus/types').Config} */ 8 | const config = { 9 | title: "Zodios", 10 | tagline: "End-to-end typesafe REST API toolbox", 11 | url: "https://www.zodios.org", 12 | baseUrl: "/", 13 | onBrokenLinks: "throw", 14 | onBrokenMarkdownLinks: "warn", 15 | favicon: "img/favicon.ico", 16 | 17 | // GitHub pages deployment config. 18 | // If you aren't using GitHub pages, you don't need these. 19 | organizationName: "ecyrbe", // Usually your GitHub org/user name. 20 | projectName: "zodios", // Usually your repo name. 21 | trailingSlash: false, 22 | 23 | // Even if you don't use internalization, you can use this field to set useful 24 | // metadata like html lang. For example, if your site is Chinese, you may want 25 | // to replace "en" with "zh-Hans". 26 | i18n: { 27 | defaultLocale: "en", 28 | locales: ["en"], 29 | }, 30 | 31 | presets: [ 32 | [ 33 | "classic", 34 | /** @type {import('@docusaurus/preset-classic').Options} */ 35 | ({ 36 | docs: { 37 | sidebarPath: require.resolve("./sidebars.js"), 38 | remarkPlugins: [ 39 | [require("@docusaurus/remark-plugin-npm2yarn"), { sync: true }], 40 | ], 41 | }, 42 | blog: { 43 | showReadingTime: true, 44 | }, 45 | theme: { 46 | customCss: require.resolve("./src/css/custom.css"), 47 | }, 48 | gtag: { 49 | trackingID: "G-J0FDBSMYKD", 50 | anonymizeIP: true, 51 | }, 52 | }), 53 | ], 54 | ], 55 | 56 | themeConfig: 57 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 58 | ({ 59 | image: "/img/zodios-social.png", 60 | navbar: { 61 | title: "Zodios", 62 | logo: { 63 | alt: "Zodios logo", 64 | src: "img/logo.svg", 65 | }, 66 | items: [ 67 | { 68 | type: "doc", 69 | docId: "intro", 70 | position: "left", 71 | label: "Documentation", 72 | }, 73 | { 74 | type: "doc", 75 | docId: "server/next", 76 | position: "left", 77 | label: "NextJS Integration", 78 | }, 79 | { 80 | href: "https://github.com/ecyrbe/zodios", 81 | className: "header-social-link header-github-link", 82 | "aria-label": "GitHub Repository", 83 | position: "right", 84 | }, 85 | { 86 | href: "https://twitter.com/ecyrbedev", 87 | className: "header-social-link header-twitter-link", 88 | "aria-label": "Twitter", 89 | position: "right", 90 | }, 91 | { 92 | href: "https://discord.gg/pfSZ2sVAcs", 93 | className: "header-social-link header-discord-link", 94 | "aria-label": "Discord", 95 | position: "right", 96 | }, 97 | ], 98 | }, 99 | footer: { 100 | style: "dark", 101 | links: [ 102 | { 103 | title: "Docs", 104 | items: [ 105 | { 106 | label: "Documentation", 107 | to: "/docs/intro", 108 | }, 109 | ], 110 | }, 111 | { 112 | title: "Community", 113 | items: [ 114 | { 115 | label: "Twitter", 116 | href: "https://twitter.com/ecyrbedev", 117 | }, 118 | { 119 | label: "Discord", 120 | href: "https://discord.gg/pfSZ2sVAcs", 121 | }, 122 | ], 123 | }, 124 | { 125 | title: "More", 126 | items: [ 127 | { 128 | label: "GitHub", 129 | href: "https://github.com/ecyrbe/zodios", 130 | }, 131 | ], 132 | }, 133 | ], 134 | copyright: `Copyright © ${new Date().getFullYear()} ecyrbe`, 135 | }, 136 | prism: { 137 | theme: lightCodeTheme, 138 | darkTheme: darkCodeTheme, 139 | }, 140 | }), 141 | }; 142 | 143 | module.exports = config; 144 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "^2.3.1", 19 | "@docusaurus/preset-classic": "^2.3.1", 20 | "@docusaurus/remark-plugin-npm2yarn": "^2.3.1", 21 | "@mdx-js/react": "1.6.22", 22 | "clsx": "1.2.1", 23 | "prism-react-renderer": "1.3.5", 24 | "react": "17.0.2", 25 | "react-dom": "17.0.2", 26 | "react-icons": "4.4.0" 27 | }, 28 | "devDependencies": { 29 | "@docusaurus/module-type-aliases": "^2.3.1", 30 | "@tsconfig/docusaurus": "1.0.6", 31 | "typescript": "4.8.2" 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.5%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | }, 45 | "engines": { 46 | "node": ">=16.14" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /website/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | { 23 | type: 'category', 24 | label: 'Tutorial', 25 | items: ['hello'], 26 | }, 27 | ], 28 | */ 29 | }; 30 | 31 | module.exports = sidebars; 32 | -------------------------------------------------------------------------------- /website/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clsx from "clsx"; 3 | import styles from "./styles.module.css"; 4 | import { IoBrowsersOutline, IoServer } from "react-icons/io5"; 5 | import { TbApi } from "react-icons/tb"; 6 | 7 | type FeatureItem = { 8 | title: string; 9 | Icon: JSX.Element; 10 | descriptions: string[]; 11 | }; 12 | 13 | const FeatureList: FeatureItem[] = [ 14 | { 15 | title: "Client", 16 | Icon: , 17 | descriptions: [ 18 | "autocompletion even in pure javascript", 19 | "typed parameters and response", 20 | "parameters and response validation", 21 | "powerfull plugin system", 22 | "react and solid hooks based on tanstack-query", 23 | ], 24 | }, 25 | { 26 | title: "API definition", 27 | Icon: , 28 | descriptions: [ 29 | "shared api definition", 30 | "schema declaration with zod", 31 | "openapi generator and swagger ui", 32 | ], 33 | }, 34 | { 35 | title: "Server", 36 | Icon: , 37 | descriptions: [ 38 | "autocompletion even in pure javascript", 39 | "network inputs validation", 40 | "100% compatibility with express", 41 | "easy integration with NextJS", 42 | ], 43 | }, 44 | ]; 45 | 46 | function Feature({ title, Icon, descriptions }: FeatureItem) { 47 | return ( 48 |
49 |
50 |
{Icon}
51 |
52 |
53 |

{title}

54 |
    55 | {descriptions.map((description) => ( 56 |
  • {description}
  • 57 | ))} 58 |
59 |
60 |
61 | ); 62 | } 63 | 64 | export default function HomepageFeatures(): JSX.Element { 65 | return ( 66 |
67 |
68 |
69 | {FeatureList.map((props, idx) => ( 70 | 71 | ))} 72 |
73 |
74 |
75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /website/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureIconCenter { 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | margin-bottom: 5px; 14 | } 15 | 16 | .featureIcon { 17 | display: flex; 18 | flex-direction: column; 19 | justify-content: center; 20 | align-items: center; 21 | background-color: var(--ifm-color-primary-lightest); 22 | border-radius: 20%; 23 | width: 70px; 24 | height: 70px; 25 | } 26 | 27 | .featureDescriptions { 28 | text-align: left; 29 | } -------------------------------------------------------------------------------- /website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #613583; 10 | --ifm-color-primary-dark: #522774; 11 | --ifm-color-primary-darker: #441a66; 12 | --ifm-color-primary-darkest: #441a66; 13 | --ifm-color-primary-light: #6f4291; 14 | --ifm-color-primary-lighter: #754898; 15 | --ifm-color-primary-lightest: #d6a2fa; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme="dark"] { 22 | --ifm-color-primary: hsl(250 95% 76.8%); 23 | --ifm-color-primary-dark: #522774; 24 | --ifm-color-primary-darker: #441a66; 25 | --ifm-color-primary-darkest: #441a66; 26 | --ifm-color-primary-light: #6f4291; 27 | --ifm-color-primary-lighter: #754898; 28 | --ifm-color-primary-lightest: #8455a7; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | 32 | .header-social-link:hover { 33 | transition-property: background-color, border-color, color, fill, stroke; 34 | opacity: 0.6; 35 | } 36 | 37 | .header-github-link { 38 | display: flex; 39 | background-position: center; 40 | background-repeat: no-repeat; 41 | width: 1.5rem; 42 | height: 1.5rem; 43 | margin: 0 0.5rem; 44 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 496 512'%3E%3C!--! Font Awesome Pro 6.2.0 by %40fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons Inc. --%3E%3Cpath d='M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z'/%3E%3C/svg%3E"); 45 | } 46 | 47 | [data-theme="dark"] .header-github-link { 48 | background-position: center; 49 | background-repeat: no-repeat; 50 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='white' viewBox='0 0 496 512'%3E%3C!--! Font Awesome Pro 6.2.0 by %40fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons Inc. --%3E%3Cpath d='M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z'/%3E%3C/svg%3E"); 51 | } 52 | 53 | .header-twitter-link { 54 | display: flex; 55 | background-position: center; 56 | background-repeat: no-repeat; 57 | width: 1.5rem; 58 | height: 1.5rem; 59 | margin: 0 0.5rem; 60 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3C!--! Font Awesome Pro 6.2.0 by %40fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons Inc. --%3E%3Cpath d='M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z'/%3E%3C/svg%3E"); 61 | } 62 | 63 | [data-theme="dark"] .header-twitter-link { 64 | background-position: center; 65 | background-repeat: no-repeat; 66 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='white' viewBox='0 0 512 512'%3E%3C!--! Font Awesome Pro 6.2.0 by %40fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons Inc. --%3E%3Cpath d='M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z'/%3E%3C/svg%3E"); 67 | } 68 | 69 | .header-discord-link { 70 | display: flex; 71 | background-position: center; 72 | background-repeat: no-repeat; 73 | width: 1.5rem; 74 | height: 1.5rem; 75 | margin: 0 0.5rem; 76 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 512'%3E%3C!--! Font Awesome Pro 6.2.0 by %40fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons Inc. --%3E%3Cpath d='M524.531 69.836a1.5 1.5 0 0 0-.764-.7A485.065 485.065 0 0 0 404.081 32.03a1.816 1.816 0 0 0-1.923.91 337.461 337.461 0 0 0-14.9 30.6 447.848 447.848 0 0 0-134.426 0 309.541 309.541 0 0 0-15.135-30.6 1.89 1.89 0 0 0-1.924-.91A483.689 483.689 0 0 0 116.085 69.137a1.712 1.712 0 0 0-.788.676C39.068 183.651 18.186 294.69 28.43 404.354a2.016 2.016 0 0 0 .765 1.375A487.666 487.666 0 0 0 176.02 479.918a1.9 1.9 0 0 0 2.063-.676A348.2 348.2 0 0 0 208.12 430.4a1.86 1.86 0 0 0-1.019-2.588 321.173 321.173 0 0 1-45.868-21.853 1.885 1.885 0 0 1-.185-3.126c3.082-2.309 6.166-4.711 9.109-7.137a1.819 1.819 0 0 1 1.9-.256c96.229 43.917 200.41 43.917 295.5 0a1.812 1.812 0 0 1 1.924.233c2.944 2.426 6.027 4.851 9.132 7.16a1.884 1.884 0 0 1-.162 3.126 301.407 301.407 0 0 1-45.89 21.83 1.875 1.875 0 0 0-1 2.611 391.055 391.055 0 0 0 30.014 48.815 1.864 1.864 0 0 0 2.063.7A486.048 486.048 0 0 0 610.7 405.729a1.882 1.882 0 0 0 .765-1.352C623.729 277.594 590.933 167.465 524.531 69.836ZM222.491 337.58c-28.972 0-52.844-26.587-52.844-59.239S193.056 219.1 222.491 219.1c29.665 0 53.306 26.82 52.843 59.239C275.334 310.993 251.924 337.58 222.491 337.58Zm195.38 0c-28.971 0-52.843-26.587-52.843-59.239S388.437 219.1 417.871 219.1c29.667 0 53.307 26.82 52.844 59.239C470.715 310.993 447.538 337.58 417.871 337.58Z'/%3E%3C/svg%3E"); 77 | } 78 | 79 | [data-theme="dark"] .header-discord-link { 80 | background-position: center; 81 | background-repeat: no-repeat; 82 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='white' viewBox='0 0 640 512'%3E%3C!--! Font Awesome Pro 6.2.0 by %40fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons Inc. --%3E%3Cpath d='M524.531 69.836a1.5 1.5 0 0 0-.764-.7A485.065 485.065 0 0 0 404.081 32.03a1.816 1.816 0 0 0-1.923.91 337.461 337.461 0 0 0-14.9 30.6 447.848 447.848 0 0 0-134.426 0 309.541 309.541 0 0 0-15.135-30.6 1.89 1.89 0 0 0-1.924-.91A483.689 483.689 0 0 0 116.085 69.137a1.712 1.712 0 0 0-.788.676C39.068 183.651 18.186 294.69 28.43 404.354a2.016 2.016 0 0 0 .765 1.375A487.666 487.666 0 0 0 176.02 479.918a1.9 1.9 0 0 0 2.063-.676A348.2 348.2 0 0 0 208.12 430.4a1.86 1.86 0 0 0-1.019-2.588 321.173 321.173 0 0 1-45.868-21.853 1.885 1.885 0 0 1-.185-3.126c3.082-2.309 6.166-4.711 9.109-7.137a1.819 1.819 0 0 1 1.9-.256c96.229 43.917 200.41 43.917 295.5 0a1.812 1.812 0 0 1 1.924.233c2.944 2.426 6.027 4.851 9.132 7.16a1.884 1.884 0 0 1-.162 3.126 301.407 301.407 0 0 1-45.89 21.83 1.875 1.875 0 0 0-1 2.611 391.055 391.055 0 0 0 30.014 48.815 1.864 1.864 0 0 0 2.063.7A486.048 486.048 0 0 0 610.7 405.729a1.882 1.882 0 0 0 .765-1.352C623.729 277.594 590.933 167.465 524.531 69.836ZM222.491 337.58c-28.972 0-52.844-26.587-52.844-59.239S193.056 219.1 222.491 219.1c29.665 0 53.306 26.82 52.843 59.239C275.334 310.993 251.924 337.58 222.491 337.58Zm195.38 0c-28.971 0-52.843-26.587-52.843-59.239S388.437 219.1 417.871 219.1c29.667 0 53.307 26.82 52.844 59.239C470.715 310.993 447.538 337.58 417.871 337.58Z'/%3E%3C/svg%3E"); 83 | } 84 | -------------------------------------------------------------------------------- /website/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /website/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clsx from "clsx"; 3 | import Link from "@docusaurus/Link"; 4 | import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; 5 | import Layout from "@theme/Layout"; 6 | import HomepageFeatures from "@site/src/components/HomepageFeatures"; 7 | 8 | import styles from "./index.module.css"; 9 | 10 | function HomepageHeader() { 11 | const { siteConfig } = useDocusaurusContext(); 12 | return ( 13 |
14 |
15 |

{siteConfig.title}

16 |

{siteConfig.tagline}

17 | 20 |

21 |

52 |

53 |
54 |
55 | ); 56 | } 57 | 58 | export default function Home(): JSX.Element { 59 | const { siteConfig } = useDocusaurusContext(); 60 | return ( 61 | 62 | 63 |
64 | 65 |
66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /website/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /website/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecyrbe/zodios/6e6f3b3dbc3fdd62bc2c043efbdcd0254823fcb4/website/static/.nojekyll -------------------------------------------------------------------------------- /website/static/CNAME: -------------------------------------------------------------------------------- 1 | www.zodios.org -------------------------------------------------------------------------------- /website/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecyrbe/zodios/6e6f3b3dbc3fdd62bc2c043efbdcd0254823fcb4/website/static/img/favicon.ico -------------------------------------------------------------------------------- /website/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 34 | 39 | 40 | -------------------------------------------------------------------------------- /website/static/img/openapi-to-zodios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecyrbe/zodios/6e6f3b3dbc3fdd62bc2c043efbdcd0254823fcb4/website/static/img/openapi-to-zodios.png -------------------------------------------------------------------------------- /website/static/img/openapi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecyrbe/zodios/6e6f3b3dbc3fdd62bc2c043efbdcd0254823fcb4/website/static/img/openapi.png -------------------------------------------------------------------------------- /website/static/img/undraw_docusaurus_tree.svg: -------------------------------------------------------------------------------- 1 | 2 | Focus on What Matters 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /website/static/img/zodios-social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecyrbe/zodios/6e6f3b3dbc3fdd62bc2c043efbdcd0254823fcb4/website/static/img/zodios-social.png -------------------------------------------------------------------------------- /website/static/video/zodios.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ecyrbe/zodios/6e6f3b3dbc3fdd62bc2c043efbdcd0254823fcb4/website/static/video/zodios.mp4 -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@tsconfig/docusaurus/tsconfig.json", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | --------------------------------------------------------------------------------