npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

zodsei

v2.0.0

Published

Contract-first type-safe HTTP client with Zod validation

Readme

Zodsei

A contract-first, type-safe HTTP client with Zod validation for TypeScript.

Why Zodsei?

Zodsei was created to solve the limitations of existing HTTP client libraries:

The Problem

  • Zodios is unmaintained: The original Zodios library is no longer actively maintained, leaving users without updates and bug fixes
  • Poor API design: Many existing solutions have complex, unintuitive APIs that are hard to use and maintain
  • Limited flexibility: When you can't use tRPC or oRPC, or don't control the backend, you need a flexible contract-first solution
  • Type safety gaps: Most HTTP clients lack comprehensive compile-time type checking and runtime validation

The Solution

Zodsei provides:

  • Modern, clean API: Intuitive contract definition with {path, method, request, response} structure
  • True contract-first: Define your API contract once, get full type safety everywhere
  • Active maintenance: Built with modern tooling and actively maintained
  • Flexible architecture: Works with any backend, no server-side requirements
  • Complete type safety: From request to response, with runtime validation

When to Use Zodsei vs Other Solutions

For Full-Stack Projects (Recommended Alternatives)

If you're developing a full-stack project or have control over the backend, we recommend using these excellent alternatives:

  • ts-rest - Contract-first REST APIs with full-stack type safety
  • tRPC - End-to-end typesafe APIs made easy
  • oRPC - Modern RPC framework with excellent TypeScript support

These libraries provide superior developer experience when you control both frontend and backend.

When Zodsei is the Right Choice

Use Zodsei when:

  • 🔌 Consuming third-party APIs - You don't control the backend
  • 🏢 Working with existing REST APIs - Legacy systems or external services
  • 🔄 Migrating from unmaintained libraries - Moving away from Zodios or similar
  • 🎯 Need flexible HTTP client - Custom requirements not covered by full-stack solutions
  • 📱 Client-only applications - Mobile apps, browser extensions, or pure frontend projects

Features

  • 🔒 Type-safe: Full TypeScript support with automatic type inference
  • 📋 Contract-first: Define your API contract once, get type safety everywhere
  • ✅ Runtime validation: Request and response validation using Zod schemas
  • 🔌 Middleware support: Built-in retry, caching, and custom middleware
  • 🌐 Axios-based client: Bring-your-own Axios instance for requests
  • 🚀 Minimal dependencies: Zod + Axios
  • 📦 Modern: ESM-only package for modern Node.js and browsers

Installation

bun add zodsei zod axios

Quick Start

1. Define your API contract

import { z } from 'zod';
import { defineContract } from 'zodsei';

const UserSchema = z.object({
  id: z.uuid(),
  name: z.string(),
  email: z.email(),
});

const apiContract = defineContract({
  getUser: {
    path: '/users/:id',
    method: 'get' as const,
    request: z.object({
      id: z.uuid(),
    }),
    response: UserSchema,
  },

  createUser: {
    path: '/users',
    method: 'post' as const,
    request: z.object({
      name: z.string().min(1),
      email: z.email(),
    }),
    response: UserSchema,
  },
});

2. Create a client

import { createClient } from 'zodsei';
import axios from 'axios';

const axiosInstance = axios.create({ baseURL: 'https://api.example.com', timeout: 10000 });

const client = createClient(apiContract, {
  axios: axiosInstance,
  validateRequest: true,
  validateResponse: true,
});

3. Use the client

// Fully type-safe API calls
const user = await client.getUser({
  id: '123e4567-e89b-12d3-a456-426614174000',
});
// user is automatically typed as { id: string, name: string, email: string }

const newUser = await client.createUser({
  name: 'John Doe',
  email: '[email protected]',
});
// newUser is also automatically typed

Core Concepts

Type inference on endpoint methods

// Fully typed response inferred from the contract
const user = await client.getUser({ id: '123e4567-e89b-12d3-a456-426614174000' });
// `user` type is inferred from the endpoint response schema

Contract type helpers

import type { InferRequestType, InferResponseType } from 'zodsei';

type GetUserRequest = InferRequestType<typeof apiContract.getUser>;
type GetUserResponse = InferResponseType<typeof apiContract.getUser>;

Method-level schemas: .schema

// Runtime access to Zod schemas
const reqSchema = client.getUser.schema.request;
const resSchema = client.getUser.schema.response;

Contract-level schema explorer: $schema

// Explore the contract at runtime
const endpointPaths = client.$schema.getEndpointPaths();
const info = client.$schema.describeEndpoint('getUser');
// info: { path, method, requestSchema, responseSchema }

Nested contracts

type LoginRequest = InferRequestType<typeof contract.auth.login>;
const getByIdSchemas = client.users.getById.schema;

Re-exported z

import { z } from 'zodsei'; // re-exported for convenience

API Reference

Contract Definition

Each endpoint in your contract should have:

  • path: The API endpoint path (supports path parameters like :id)
  • method: HTTP method ('get' | 'post' | 'put' | 'delete' | 'patch')
  • request: Zod schema for request data
  • response: Zod schema for response data

Basic Contract

const contract = defineContract({
  endpointName: {
    path: '/api/path/:param',
    method: 'post',
    request: z.object({/* request schema */}),
    response: z.object({/* response schema */}),
  },
});

Nested Contracts

Contracts can be nested to organize your API endpoints by feature or module:

const contract = defineContract({
  auth: defineContract({
    login: {
      path: '/auth/login',
      method: 'post',
      request: z.object({ email: z.string(), password: z.string() }),
      response: z.object({ token: z.string() }),
    },
    logout: {
      path: '/auth/logout',
      method: 'post',
      request: z.object({}),
      response: z.object({ success: z.boolean() }),
    },
  }),

  users: defineContract({
    getById: {
      path: '/users/:id',
      method: 'get',
      request: z.object({ id: z.string() }),
      response: UserSchema,
    },
  }),
});

// Usage with nested structure
const loginResult = await client.auth.login({ email, password });
const user = await client.users.getById({ id: '123' });

Client Configuration

interface ClientConfig {
  axios: AxiosInstance; // Your Axios instance (required)
  validateRequest?: boolean; // Enable request validation (default: true)
  validateResponse?: boolean; // Enable response validation (default: true)
  middleware?: Middleware[]; // Custom middleware
}

Middleware

Zodsei supports middleware for cross-cutting concerns:

Retry Middleware

import { retryMiddleware } from 'zodsei';

const client = createClient(contract, {
  baseUrl: 'https://api.example.com',
  middleware: [
    retryMiddleware({
      retries: 3,
      delay: 1000,
      backoff: 'exponential',
      onRetry: (attempt, error) => {
        console.log(`Retry attempt ${attempt}:`, error.message);
      },
    }),
  ],
});

Cache Middleware

import { cacheMiddleware } from 'zodsei';

const client = createClient(contract, {
  baseUrl: 'https://api.example.com',
  middleware: [
    cacheMiddleware({
      ttl: 60000, // Cache for 1 minute
    }),
  ],
});

Custom Middleware

const loggingMiddleware = async (request, next) => {
  console.log('Request:', request);
  const response = await next(request);
  console.log('Response:', response);
  return response;
};

const client = createClient(contract, {
  baseUrl: 'https://api.example.com',
  middleware: [loggingMiddleware],
});

HTTP Client

Zodsei uses Axios under the hood. You must provide an AxiosInstance when creating the client. Interceptors configured on that instance continue to run normally; Zodsei does not register or manage them.

Error Handling

Zodsei provides specific error types for different scenarios:

import { ValidationError, HttpError, NetworkError, TimeoutError } from 'zodsei';

try {
  const user = await client.getUser({ id: 'invalid-uuid' });
} catch (error) {
  if (error instanceof ValidationError) {
    console.log('Validation failed:', error.issues);
  } else if (error instanceof HttpError) {
    console.log('HTTP error:', error.status, error.message);
  } else if (error instanceof NetworkError) {
    console.log('Network error:', error.message);
  } else if (error instanceof TimeoutError) {
    console.log('Request timeout');
  }
}

Advanced

Middleware vs Axios Interceptors

Middleware and Axios interceptors run at different layers:

| | Zodsei middleware | Axios interceptor | | ----------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------- | | Input | RequestContext / ResponseContext | AxiosRequestConfig / AxiosResponse | | Scope | One Zodsei client | Every consumer of the Axios instance | | Best for | Caching, retries, client-specific auth, logging, mocks | Axios configuration, shared headers, 401 token refresh, Axios-specific errors | | Can skip Axios entirely | Yes, for example on a cache hit | Not directly; it remains inside the Axios pipeline |

The execution order is:

request validation
→ Zodsei middleware
  → Axios request interceptor
    → HTTP request
  ← Axios response interceptor
← Zodsei middleware
← response validation

Use middleware when behavior belongs to one contract client or may short-circuit the request. Use an interceptor when behavior depends on Axios internals or must be shared by every consumer of the Axios instance. Simple authorization headers can use either layer; 401 refresh flows usually belong in an Axios interceptor.

Do not configure retries or caching in both layers. Doing so can duplicate requests, logs, and side effects.

Path Parameters

const contract = defineContract({
  getUserPosts: {
    path: '/users/:userId/posts/:postId',
    method: 'get' as const,
    request: z.object({
      userId: z.string().uuid(),
      postId: z.string().uuid(),
    }),
    response: PostSchema,
  },
});

// Usage
const post = await client.getUserPosts({
  userId: 'user-uuid',
  postId: 'post-uuid',
});

Query Parameters

For GET requests, non-path parameters are automatically converted to query parameters:

const contract = defineContract({
  searchUsers: {
    path: '/users',
    method: 'get' as const,
    request: z.object({
      q: z.string(),
      page: z.number().optional(),
      limit: z.number().optional(),
    }),
    response: z.object({
      users: z.array(UserSchema),
      total: z.number(),
    }),
  },
});

// Usage - generates: GET /users?q=john&page=1&limit=10
const results = await client.searchUsers({
  q: 'john',
  page: 1,
  limit: 10,
});

Request Body

For POST/PUT/PATCH requests, the request data is sent as JSON body:

const contract = defineContract({
  updateUser: {
    path: '/users/:id',
    method: 'put' as const,
    request: z.object({
      id: z.string().uuid(), // Path parameter
      name: z.string().optional(), // Body field
      email: z.string().email().optional(), // Body field
    }),
    response: UserSchema,
  },
});

// Usage
const updated = await client.updateUser({
  id: 'user-uuid',
  name: 'New Name',
  email: '[email protected]',
});

License

MIT

Contributing

Development uses Bun 1.3.14 or newer, with Oxlint and Oxfmt for linting and formatting:

bun install --frozen-lockfile
bun run check
bun run build

Contributions are welcome! Please read our contributing guide and submit pull requests to our repository.