Package Exports
- @zodios/core
- @zodios/core/lib/index.js
- @zodios/core/lib/utils.node.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@zodios/core) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
Zodios
Zodios is a typescript api client with auto-completion features backed by axios and zod
Documentation
https://user-images.githubusercontent.com/633115/185851987-554f5686-cb78-4096-8ff5-c8d61b645608.mp4
What is it ?
It's an axios compatible API client, with the following features:
- really simple centralized API declaration
- typescript autocompletion in your favorite IDE for URL and parameters
- typescript response types
- parameters and responses schema thanks to zod
- response schema validation
- bearer token injection and token renewal with simple token provider interface
- all axios features available
Table of contents:
- What is it ?
- Install
- How to use it ?
- Plugin system
- Migrate to v8
- Full end to end typesafety
- Ecosystem
- Roadmap
- Dependencies
Install
> npm install @zodios/coreor
> yarn add @zodios/coreHow to use it ?
For an almost complete example on how to use zodios and how to split your APIs declarations, take a look at dev.to example.
Declare your API with zodios
Here is an example of API declaration with Zodios.
import { Zodios } from "@zodios/core";
import { z } from "zod";
const apiClient = new Zodios(
"https://jsonplaceholder.typicode.com",
// API definition
[
{
method: "get",
path: "/users/:id", // auto detect :id and ask for it in apiClient get params
alias: "getUser", // optionnal alias to call this endpoint with it
description: "Get a user",
response: z.object({
id: z.number(),
name: z.string(),
}),
},
],
);Calling this API is now easy and has builtin autocomplete features :
// typed auto-complete path auto-complete params
// ▼ ▼ ▼
const user = await apiClient.get("/users/:id", { params: { id: 7 } });
console.log(user);It should output
{ id: 7, name: 'Kurtis Weissnat' }You can also use aliases :
// typed alias auto-complete params
// ▼ ▼ ▼
const user = await apiClient.getUser({ params: { id: 7 } });
console.log(user);API definition format
type ZodiosEndpointDescriptions = Array<{
method: 'get'|'post'|'put'|'patch'|'delete';
path: string; // example: /posts/:postId/comments/:commentId
alias?: string; // example: getPostComments
description?: string;
requestFormat?: 'json'|'form-data'|'form-url'|'binary'|'text'; // default to json if not set
parameters?: Array<{
name: string;
description?: string;
type: 'Query'|'Body'|'Header';
schema: ZodSchema; // you can use zod `transform` to transform the value of the parameter before sending it to the server
}>;
response: ZodSchema; // you can use zod `transform` to transform the value of the response before returning it
errors?: Array<{
status: number | 'default';
description?: string;
schema: ZodSchema; // transformations are not supported on error schemas
}>;
}>;API creation helpers
Defining your API is easy with zodios, as definitions are just plain objects.
But typescript error messages can be cryptic and hard to understand. So zodios provides some helpers to make your life easier.
asApi(): simple helper to create splitted API definition from an array of endpoint descriptions.asParameters(): simple helper to create splitted parameters definition to share between your endpoints.asCrudApi(): helper to create a CRUD API definition from a resource name and a resource schema.apiBuilder(): advanced helper to create splitted API definition endpoint by endpoint. It's the one with the better user experience.
Example (complete example) :
export const articlesApi = apiBuilder({
method: "get",
path: "/articles/latest",
alias: "getLatestArticle",
description: "Get latest articles",
parameters: paramPages,
response: devArticles,
})
.addEndpoint({
method: "get",
path: "/articles/:id",
alias: "getArticle",
description: "Get an article by id",
response: devArticle,
})
.build();Get underlying axios instance
you can get back the underlying axios instance to customize it.
const axiosInstance = apiClient.axios;Give your own axios instance to zodios
you can instanciate zodios with your own axios intance.
const apiClient = new Zodios(
"https://jsonplaceholder.typicode.com",
[ ... ],
// Optional Axios instance
{
axiosIntance: customAxiosInstance
}
);Disable zodios validation
You can disable zod validation for both parameters and responses. But be aware that this will also disable zod transformations. So if you want to disable zod validation, do not use transformations.
const apiClient = new Zodios(
"https://jsonplaceholder.typicode.com",
[ ... ],
{
validate: false
}
);Use zod transformations
Since Zodios is using zod, you can use zod transformations.
const apiClient = new Zodios(
"https://jsonplaceholder.typicode.com",
[
{
method: "get",
path: "/users/:id",
alias: "getUser",
description: "Get a user",
response: z.object({
id: z.number(),
name: z.string(),
}).transform(({ name,...rest }) => ({
...rest,
firstname: name.split(" ")[0],
lastname: name.split(" ")[1],
})),
},
]
);
const user = await apiClient.getUser({ params: { id: 7 } });
console.log(user);It should output
{ id: 7, firstname: 'Kurtis', lastname: 'Weissnat' }Send multipart/form-data requests
Zodios supports multipart/form-data requests with integrated requestFormat. Zodios is using formdata-node internally on NodeJs as it's the most up to date library for node.
const apiClient = new Zodios(
"https://mywebsite.com",
[{
method: "post",
path: "/upload",
alias: "upload",
description: "Upload a file",
requestFormat: "form-data",
parameters:[
{
name: "body",
type: "Body",
schema: z.object({
file: z.instanceof(File),
}),
}
],
response: z.object({
id: z.number(),
}),
}],
);
const id = await apiClient.upload({ file: document.querySelector('#file').files[0] });But you can also use your own multipart/form-data library, for example with form-data library on node.
import FormData from 'form-data';
const apiClient = new Zodios(
"https://mywebsite.com",
[{
method: "post",
path: "/upload",
alias: "upload",
description: "Upload a file",
parameters:[
{
name: "body",
type: "Body",
schema: z.instanceof(FormData),
}
],
response: z.object({
id: z.number(),
}),
}],
);
const form = new FormData();
form.append('file', document.querySelector('#file').files[0]);
const id = await apiClient.upload(form, { headers: form.getHeaders() });Send application/x-www-form-urlencoded requests
Zodios supports application/x-www-form-urlencoded requests with integrated requestFormat. Zodios is using URLSearchParams internally on both browser and node. (If you need IE support, see next example)
const apiClient = new Zodios(
"https://mywebsite.com",
[{
method: "post",
path: "/login",
alias: "login",
description: "Submit a form",
requestFormat: "form-url",
parameters:[
{
name: "body",
type: "Body",
schema: z.object({
userName: z.string(),
password: z.string(),
}),
}
],
response: z.object({
id: z.number(),
}),
}],
);
const id = await apiClient.login({ userName: "user", password: "password" }); But you can also use custom code to support for application/x-www-form-urlencoded requests.
For example with qs library on IE :
import qs from 'qs';
const apiClient = new Zodios(
"https://mywebsite.com",
[{
method: "post",
path: "/login",
alias: "login",
description: "Submit a form",
parameters:[
{
name: "body",
type: "Body",
schema: z.object({
userName: z.string(),
password: z.string(),
}).transform(data=> qs.stringify(data)),
}
],
response: z.object({
id: z.number(),
}),
}],
);
const id = await apiClient.login({ userName: "user", password: "password" },
{ headers:
{
'Content-Type': 'application/x-www-form-urlencoded'
}
});CRUD helper
Zodios has a helper to generate basic CRUD API. It will generate all the api definitions for you :
import { Zodios, asCrudApi } from '@zodios/core';
const apiClient = new Zodios(BASE_URL,
asCrudApi(
'user',
z.object({
id: z.number(),
name: z.string(),
})
));Is the same as :
const apiClient = new Zodios(BASE_URL, [
{
method: "get",
path: "/users",
alias: "getUsers",
description: "Get all users",
response: z.array(userSchema),
},
{
method: "get",
path: "/users/:id",
alias: "getUser",
description: "Get a user",
response: userSchema,
},
{
method: "post",
path: "/users",
alias: "createUser",
description: "Create a user",
parameters: [
{
name: "body",
type: "Body",
description: "The object to create",
schema: userSchema.partial(),
},
],
response: userSchema,
},
{
method: "put",
path: "/users/:id",
alias: "updateUser",
description: "Update a user",
parameters: [
{
name: "body",
type: "Body",
description: "The object to update",
schema: userSchema,
},
],
response: userSchema,
},
{
method: "patch",
path: "/users/:id",
alias: "patchUser",
description: "Patch a user",
parameters: [
{
name: "body",
type: "Body",
description: "The object to patch",
schema: userSchema.partial(),
},
],
response: userSchema,
},
{
method: "delete",
path: "/users/:id",
alias: "deleteUser",
description: "Delete a user",
response: userSchema,
},
]);React helpers
Zodios comes with a Query and Mutation hook helper.
It's a thin wrapper around React-Query but with zodios auto completion.
Zodios query hook also returns an invalidation helper to allow you to reset react query cache easily
import React from "react";
import { QueryClient, QueryClientProvider } from "react-query";
import { Zodios } from "@zodios/core";
import { ZodiosHooks } from "@zodios/react";
import { z } from "zod";
const baseUrl = "https://jsonplaceholder.typicode.com";
const zodios = new Zodios(baseUrl, [...]);
const zodiosHooks = new ZodiosHooks("jsonplaceholder", zodios);
const Users = () => {
const {
data: users,
isLoading,
error,
invalidate: invalidateUsers, // zodios also provides invalidation helpers
} = zodiosHooks.useQuery("/users"); // or useGetUsers();
const { mutate } = zodiosHooks.useMutation("post", "/users", undefined, {
onSuccess: () => invalidateUsers(),
}); // or .useCreateUser(...);
return (
<>
<h1>Users</h1>
<button onClick={() => mutate({ name: "john doe" })}>add user</button>
{isLoading && <div>Loading...</div>}
{error && <div>Error: {(error as Error).message}</div>}
{users && (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</>
);
};
// on another file
const queryClient = new QueryClient();
export const App = () => {
return (
<QueryClientProvider client={queryClient}>
<Users />
</QueryClientProvider>
);
};Plugin system
Zodios has a powefull plugin system that are middleware interceptors for requests and responses.
Use fetch on browser
since version 2.2.0 of @zodios/plugins
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.
For those use cases, you can use the fetch plugin that implements an axios adapter using the standard fetch.
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.
🚧 Warning 🚧 : 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.
import { pluginFetch } from "@zodios/plugins";
apiClient.use(pluginFetch({
// all fetch options are supported
keepAlive: true,
}));Use token provider plugin
@zodios/plugins comes with a plugin to inject and renew your tokens :
import { pluginToken } from '@zodios/plugins';
apiClient.use(pluginToken({
getToken: async () => "token"
}));Use a plugin only for some endpoints
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.
import { pluginToken } from '@zodios/plugins';
// apply a plugin by alias
apiClient.use("getUser", pluginToken({
getToken: async () => "token"
}));
// apply a plugin by endpoint
apiClient.use("get","/users/:id", pluginToken({
getToken: async () => "token"
}));Override plugin
Zodios plugins can be named and can be overridden. Here are the list of integrated plugins that are used by zodios by default :
- zodValidationPlugin : validation of response schema with zod library
- formDataPlugin : convert provided body object to
multipart/form-dataformat - formURLPlugin : convert provided body object to
application/x-www-form-urlencodedformat
For example, you can override internal 'zod-validation' plugin with your own validation plugin :
import { zodValidationPlugin } from '@zodios/core';
import { myValidationInterceptor } from './my-custom-validation';
apiClient.use({
name: zodValidationPlugin().name, // using the same name as an already existing plugin will override it
response: myValidationInterceptor,
});Plugin execution order
Zodios plugins that are not attached to an endpoint are executed first. Then plugins that match your endpoint are executed. In addition, plugins are executed in their declaration order for requests, and in reverse order for responses.
example, pluginLog logs the message it takes as parameter when it's called :
apiClient.use(pluginLog('1'));
apiClient.use("getUser", pluginLog('2'));
apiClient.use("get","/users/:id", pluginLog('3'));
apiClient.get("/users/:id", { params: { id: 7 } });
// output :
// request 1
// request 2
// request 3
// response 3
// response 2
// response 1Write your own plugin
Zodios plugins are middleware interceptors for requests and responses. If you want to create your own, they should have the following signature :
export type ZodiosPlugin = {
/**
* Optional name of the plugin
* naming a plugin allows to remove it or replace it later
*/
name?: string;
/**
* request interceptor to modify or inspect the request before it is sent
* @param api - the api description
* @param request - the request config
* @returns possibly a new request config
*/
request?: (
api: ZodiosEnpointDescriptions,
config: AnyZodiosRequestOptions
) => Promise<AnyZodiosRequestOptions>;
/**
* response interceptor to modify or inspect the response before it is returned
* @param api - the api description
* @param config - the request config
* @param response - the response
* @returns possibly a new response
*/
response?: (
api: ZodiosEnpointDescriptions,
config: AnyZodiosRequestOptions,
response: AxiosResponse
) => Promise<AxiosResponse>;
/**
* error interceptor for response errors
* there is no error interceptor for request errors
* @param api - the api description
* @param config - the config for the request
* @param error - the error that occured
* @returns possibly a new response or a new error
*/
error?: (
api: ZodiosEnpointDescriptions,
config: AnyZodiosRequestOptions,
error: Error
) => Promise<AxiosResponse>;
};Migrate to v8
BREAKING CHANGE Since version 8 of zodios, as const is no more needed nor supported to declare your apis.
Zodios can now infer your api definitions by using generic type narrowing.
To migrate to v8 :
- Remove the
as constfrom your api definitions. - Use the api creation helpers to declare your apis splitted api definitions, since
as constdoes not work anymore in V8.
Full end to end typesafety
If you have a fullstack control over your application, you can also use @zodios/express.
Here is an example of full end to end typesafety with zodios.
in a common directory (ex: src/common/api.ts) :
import { asApi } from "@zodios/core";
import { z } from "zod";
const userApi = asApi([
{
method: "get",
path: "/users/:id", // auto detect :id and ask for it in apiClient get params
alias: "getUser", // optionnal alias to call this endpoint with it
description: "Get a user",
response: z.object({
id: z.number(),
name: z.string(),
}),
},
]);in your frontend (ex: src/client/api.ts) :
import { Zodios } from "@zodios/core";
import { userApi } from "../../common/api";
const apiClient = new Zodios(
'http://localhost:3000', // base url
userApi
);
// typed alias auto-complete params
// ▼ ▼ ▼
const user = await apiClient.getUser({ params: { id: 1 } });in your backend (ex: src/server/router.ts) :
import { zodiosApp } from "@zodios/express";
import { userApi } from "../../common/api";
// just an express adapter that is aware of your api, app is just an express app with type annotations and validation middlewares
const app = zodiosApp(userApi);
// auto-complete path fully typed and validated input params (body, query, path, header)
// ▼ ▼ ▼
app.get("/users/:id", (req, res) => {
// res.json is typed thanks to zod
res.json({
// auto-complete req.params.id
// ▼
id: req.params.id,
name: "John Doe",
});
})
app.listen(3000);Ecosystem
- openapi-zod-client: generate a zodios client from an openapi specification
- @zodios/express: full end to end type safety like tRPC, but for REST APIs
- @zodios/plugins : some plugins for zodios
- @zodios/react : a react-query wrapper for zodios
Roadmap
The following will need investigation to check if it's doable :
- implement
@zodios/nestjsto define your API endpoints with nestjs and share it with your frontend (like tRPC) - generate openAPI json from your API endpoints
You have other ideas ? Let me know !
Dependencies
Zodios do not embed any dependency. It's your Job to install the peer dependencies you need.
Internally Zodios uses these libraries on all platforms :
- zod
- axios
In addition, it also uses on NodeJS :
- formdata-node
- form-data-encoder