Open-source project
ecyrbe/zodios avatar
ecyrbe/zodios

Zodios declares your REST API as data, then validates both directions

typescript http client and server with zod validation

1,917 stars57 forksTypeScriptMIT

At a glance

What is it?
Zodios is a TypeScript API client and optional Express server built around a single array of endpoint descriptions, where zod schemas do double duty as types and as runtime validation. Its peer dependency floor, zod 3.x, is the constraint most likely to affect adoption today.
Who is it for?
Adopt Zodios if your team already writes TypeScript and axios and wants one declaration of each endpoint to drive both the client call signature and runtime response validation, because the definition array is the contract and everything else derives from it. Check that your zod version satisfies the declared `zod: ^3.x` peer range, and whether `@zodios/react` fits your data layer, before committing.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • ecyrbe/zodios1,917 ★ · -1
Star history
Repository2026-10-162026-11-07
ecyrbe/zodios1.9k1.9k

-1 Stars in the last 22 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

One array is the whole API contract

Zodios describes itself as an axios compatible API client and an optional ExpressJS compatible API server. The design centre is a single array of endpoint descriptions passed to a `Zodios` constructor, and everything else in the library falls out of that.

typescript
import { Zodios } from "@zodios/core";
import { z } from "zod";

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

The path is a string, so `:id` is detected and the client asks for it as a parameter. The `alias` is optional and gives you a named method instead. The call site then carries three kinds of help from the editor:

typescript
const user = await apiClient.get("/users/:id", { params: { id: 7 } });

Because `response` is a zod schema rather than a TypeScript interface, the same declaration produces the return type and the runtime parse. That is the part that matters: with an interface you get a compile-time promise from a server you do not control, and with a zod schema you get a value that was actually checked against the shape you wrote.

The type of the definition is called `ZodiosEndpointDescriptions`, and it is an array of objects whose fields are worth reading closely because each one is a decision you make per endpoint.

Reading the endpoint type: requestFormat, status and errors

The definition type carries more than the happy path. Here is the shape as documented:

typescript
type ZodiosEndpointDescriptions = Array<{
  method: 'get'|'post'|'put'|'patch'|'delete';
  path: string; // example: /posts/:postId/comments/:commentId
  alias?: string;
  immutable?: boolean;
  description?: string;
  requestFormat?: 'json'|'form-data'|'form-url'|'binary'|'text';
  parameters?: Array<{
    name: string;
    description?: string;
    type: 'Path'|'Query'|'Body'|'Header';
    schema: ZodSchema;
  }>;
  response: ZodSchema;
  status?: number;
  responseDescription?: string;
  errors?: Array<{
    status: number | 'default';
    description?: string;
    schema: ZodSchema;
  }>;
}>;

Four fields carry decisions you would otherwise scatter across a codebase.

`requestFormat` defaults to `json` when unset, and the alternatives are `form-data`, `form-url`, `binary` and `text`. That is the switch between sending a JSON body, a multipart upload, a URL-encoded form, a binary payload or raw text, declared once per endpoint.

`parameters` are typed by location as `Path`, `Query`, `Body` or `Header`, and each carries its own zod schema. The comment on the schema field notes you can use a zod `transform` to change the value before it is sent, which means the wire format and the value your code handles do not have to be the same type.

`response` supports the same trick in the other direction, with a `transform` applied before the value is returned.

`errors` takes a list of status codes, or the string `default`, with a schema each. The comment attached to it is the catch: transformations are not supported on error schemas. So the convenient `transform` escape hatch you have on success responses is closed on failures, which means an error body with an unusual shape has to be described literally rather than reshaped.

Two smaller fields: `immutable` is a flag marking a post request as immutable so react-query is allowed to cache it, which is wrong for most posts and right for a few. And `status` overrides the success status code, defaulting to 200, with the comment that it is only useful for openapi and express.

Installation is two npm commands and two peer dependencies

The client package installs on its own:

bash
npm install @zodios/core

The server side needs the core plus the Express integration:

bash
npm install @zodios/core @zodios/express

Both are also available through yarn. The README links the documentation at zodios.org and describes the project as a TypeScript API client with auto-completion and zod validations.

What decides whether the install works is peerDependencies. package.json declares `axios: ^0.x || ^1.0.0` and `zod: ^3.x`, with axios pinned at 1.5.1 in devDependencies. The axios range is permissive, covering both the old 0.x line and 1.x. The zod range is not: it is `^3.x` only. Check that your zod version satisfies `^3.x`, because that is the peer range declared by this package.

The package itself is built with tsup, tested with `jest --coverage`, and published from a `lib` directory with separate `import`, `require` and `types` entries in the exports map, plus a `./lib/*.types` subpath so consumers can import generated definition types directly. The `prebuild` script runs `rimraf lib`, and the version in package.json is 10.9.6.

The package split is the plugin story

The README tracks bundle size for seven packages with Bundlephobia badges: `@zodios/core`, `@zodios/fetch`, `@zodios/axios`, `@zodios/react`, `@zodios/express`, `@zodios/openapi` and `@zodios/testing`. Listing them as separate badges rather than one is the point: the client is not one monolith with an HTTP layer baked in.

Two of those exist because axios is a peer dependency rather than a hard one. `@zodios/axios` and `@zodios/fetch` are the two transports, so the same definition array can drive either. The README also names plugins such as the `fetch` adapter and automatic `auth` injection as features, and lists `@zodios/plugins` in the ecosystem as a separate repository for plugins.

The React layer is `@zodios/react`, described as a react-query wrapper for Zodios, and the README states there are `@tanstack/query` wrappers for react and solid, with vue, svelte and others marked as coming soon. Solid hooks and React hooks have their own documentation sections. The devDependencies of the core package include `@types/express` and `@types/multer`, which reflects that the server integration, including multipart handling, is exercised from the same repository.

The `openapi` package is the bridge to the outside world, and the ecosystem names the tool that uses it: `openapi-zod-client`, which generates a Zodios client from an OpenAPI specification. That is the adoption path for teams with an existing spec and no desire to hand-write definitions.

The examples directory makes the intended workflow concrete. It holds `jsonplaceholder.ts`, `types-from-definition.ts`, `define-types.ts` and a full `dev.to/` directory, and the README points at the dev.to example as an almost complete demonstration of how to use Zodios and how to split API declarations across files.

A maintenance signal that needs reading carefully

The repository is not archived and the last push was on 2026-10-15, which is recent. The published releases tell a different story at a glance: v10.9.4 on 2023-08-11, v10.9.5 on 2023-08-20 and v10.9.6 on 2023-08-22, and package.json still reads 10.9.6.

So the last npm version and the last commit are more than three years apart, while commits continue to land on main. The reading that fits is that work has continued on the branch without new published versions, or that the release tagging has lapsed. Either way, the practical consequence is the same and it is worth stating plainly: `npm install @zodios/core` gives you the code as of 2023-08-22, not as of the newest commit.

That is not automatically disqualifying for a library whose core is a type definition and a thin axios wrapper. But if you are evaluating it as an actively released dependency, the version history is the fact to weigh, not the commit dates.

The peer dependency on `zod: ^3.x` is the second signal, and it is consistent with that history. Whatever has changed on main since, the published package still constrains you to zod 3.

The licence is MIT, the author is ecyrbe, and dependency updates are handled by renovate.json with a SECURITY.md at the root. The build tooling is tsup with tsconfig.json and tsconfig.build.json, jest.config.ts for tests, and a docs/ plus website/ pair for the documentation site.

Where the boundary sits against plain axios and against tRPC

The alternative you already have is plain axios with TypeScript interfaces, which Zodios is described as compatible with. All axios features remain available, so this is an additive change rather than a replacement: you keep the same HTTP client and add a declaration plus runtime validation on responses.

What you give up is per-call flexibility. In plain axios, a request you did not anticipate can be made with any URL and any params. Under Zodios, the client is constructed from an array, and the set of callable endpoints is what is in that array. That is the point of the design and it is also its cost: an endpoint nobody declared is a call you have to cast around or add later.

The other comparison worth making is with tRPC, which the ecosystem section uses as a benchmark when it describes `@zodios/express` as full end to end type safety like tRPC but for REST APIs. The difference in approach is real. tRPC derives its types from a shared TypeScript procedure definition over its own transport, so the client and server import the same file. Zodios derives types from a zod schema over ordinary HTTP verbs and URLs, so the definition array is something you could also generate from an OpenAPI document, and it works against an API you did not write.

That is the trade in one line. tRPC gives tighter coupling between your own client and your own server. Zodios gives validation against a third-party API, plus the ability to start from an existing spec, at the cost of running everything through HTTP status codes and a declaration you maintain.

Editorial conclusion

Adopt Zodios if your team already writes TypeScript and axios and wants one declaration of each endpoint to drive both the client call signature and runtime response validation, because the definition array is the contract and everything else derives from it. Check that your zod version satisfies the declared `zod: ^3.x` peer range, and whether `@zodios/react` fits your data layer, before committing. Verify first that `npm install @zodios/core` resolves against your axios version, that the `openapi` and `express` extras exist for the version you install, and that the response schema you write matches the server's actual error behaviour, since transformations are supported on responses but not on error schemas.

Frequently asked questions

How do I install Zodios?

For the client, `npm install @zodios/core` or `yarn add @zodios/core`. For the server, install both packages: `npm install @zodios/core @zodios/express`. The core package declares axios and zod as peer dependencies.

How do I call an endpoint in Zodios?

You construct a Zodios instance with a base URL and an array of endpoint descriptions, then call by path with params, for example `apiClient.get("/users/:id", { params: { id: 7 } })`. If the definition carries an `alias`, you can call `apiClient.getUser({ params: { id: 7 } })` instead.

Does Zodios validate responses at runtime?

Yes, the `response` field of an endpoint definition is a zod schema, so the parsed value is checked against the shape you declared, and a zod `transform` can reshape it before it is returned. Error schemas declared under `errors` do not support transformations.

What zod and axios versions does @zodios/core support?

package.json declares peerDependencies of `axios: ^0.x || ^1.0.0` and `zod: ^3.x`. The zod range is limited to the 3.x line, so check this before combining it with another zod version.

Can I generate a Zodios client from an OpenAPI spec?

The ecosystem section names openapi-zod-client, a tool for generating a zodios client from an OpenAPI specification. Zodios also ships a `@zodios/openapi` package with its own documentation.

Which request formats can a Zodios endpoint send?

The `requestFormat` field takes `json`, `form-data`, `form-url`, `binary` or `text`, and defaults to `json` when it is not set. Parameters are typed separately as Path, Query, Body or Header, each with its own zod schema.

Official sources

  1. ecyrbe/zodios on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/ecyrbe-zodios.svg)](https://hysenlabs.com/projects/ecyrbe-zodios)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/ecyrbe-zodios.svg)](https://hysenlabs.com/projects/ecyrbe-zodios)

Was this useful?