JSPM

  • Created
  • Published
  • Downloads 244681
  • Score
    100M100P100Q172234F
  • License MIT

Typescript API client with autocompletion and zod validations

Package Exports

  • @zodios/core
  • @zodios/core/lib/index.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 logo

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

langue typescript npm GitHub GitHub Workflow Status

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(),
      }),
    },
  ] as const,
);

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);

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",
  [ ... ] as const,
  // Optional Axios instance
  {
    axiosIntance: customAxiosInstance
  }
);

Disable zodios response validation

const apiClient = new Zodios(
  "https://jsonplaceholder.typicode.com",
  [ ... ] as const,
  // Disable validation
  {
    validateResponse: false
  }
);

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(),
    }),
  }] as const,
);
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(),
    }),
  }] as const,
);
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(),
    }),
  }] as const,
);
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.string()
      }
    ],
    response: z.object({
      id: z.number(),
    }),
  }] as const,
);
const id = await apiClient.login(qs.stringify({ 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,
  },
] as const);

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";

// you can define schema before declaring the API to get back the type
const userSchema = z
  .object({
    id: z.number(),
    name: z.string(),
  })
  .required();

const createUserSchema = z
  .object({
    name: z.string(),
  })
  .required();

const usersSchema = z.array(userSchema);

// you can then get back the types
type User = z.infer<typeof userSchema>;
type Users = z.infer<typeof usersSchema>;

const api = [
  {
    method: "get",
    path: "/users",
    description: "Get all users",
    parameters: [
      {
        name: "q",
        type: "Query",
        schema: z.string(),
      },
      {
        name: "page",
        type: "Query",
        schema: z.string().optional(),
      },
    ],
    response: usersSchema,
  },
  {
    method: "get",
    path: "/users/:id",
    description: "Get a user",
    response: userSchema,
  },
  {
    method: "post",
    path: "/users",
    description: "Create a user",
    parameters: [
      {
        name: "body",
        type: "Body",
        schema: createUserSchema,
      },
    ],
    response: userSchema,
  },
] as const;
const baseUrl = "https://jsonplaceholder.typicode.com";

const zodios = new Zodios(baseUrl, api);
const zodiosHooks = new ZodiosHooks("jsonplaceholder", zodios);

const Users = () => {
  const {
    data: users,
    isLoading,
    error,
    invalidate: invalidateUsers, // zodios also provides invalidation helpers
  } = zodiosHooks.useQuery("/users");
  const { mutate } = zodiosHooks.useMutation("post", "/users", undefined, {
    onSuccess: () => invalidateUsers(),
  });

  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

Use token provider plugin

Zodios has a powefull plugin system that are middleware interceptors for requests and responses.
For example, zodios 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>;
};

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