Zodios is a typescript api client with auto-completion features backed by axios and zod
Documentation
zodios.mp4
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
> npm install @zodios/core
or
> yarn add @zodios/core
For an almost complete example on how to use zodios and how to split your APIs declarations, take a look at dev.to example.
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);
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
}>;
}>;
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();
you can get back the underlying axios instance to customize it.
const axiosInstance = apiClient.axios;
you can instanciate zodios with your own axios intance.
const apiClient = new Zodios(
"https://jsonplaceholder.typicode.com",
[ ... ],
// Optional Axios instance
{
axiosIntance: customAxiosInstance
}
);
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
}
);
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' }
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() });
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'
}
});
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,
},
]);
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>
);
};
Zodios has a powefull plugin system that are middleware interceptors for requests and responses.
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,
}));
@zodios/plugins
comes with a plugin to inject and renew your tokens :
import { pluginToken } from '@zodios/plugins';
apiClient.use(pluginToken({
getToken: async () => "token"
}));
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"
}));
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-data
format - formURLPlugin : convert provided body object to
application/x-www-form-urlencoded
format
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,
});
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 1
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>;
};
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 const
from your api definitions. - Use the api creation helpers to declare your apis splitted api definitions, since
as const
does not work anymore in V8.
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);
- 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
The following will need investigation to check if it's doable :
- implement
@zodios/nestjs
to 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 !
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