Skip to content
/ zodios Public
forked from ecyrbe/zodios

typescript http client with zod validation

License

Notifications You must be signed in to change notification settings

dpschen/zodios

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Zodios

Zodios logo

Zodios is a typescript api client with auto-completion features backed by axios and zod
Documentation

langue typescript npm GitHub GitHub Workflow Status

zodios.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:

Install

> npm install @zodios/core

or

> yarn add @zodios/core

How 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-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,
  });

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 1

Write 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 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.

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

Roadmap

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 !

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

About

typescript http client with zod validation

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • TypeScript 93.9%
  • JavaScript 4.1%
  • CSS 2.0%