Zod 4 (and all other Standard Schema support) is available as a release candidate `3.53.0-rc.1`
ts-rest
GitHub Repo starsNPM VersionNPM Version

Simple cross-stack type-safety for your API, with just a sprinkle of TypeScript magic ✨

  • 🛟 Contract-First API
  • 🌈 It's just HTTP/REST
  • 🔒 Supports all Standard Schema validation libraries
  • 📦 OpenAPI generation

Define your API contract

Define an API contact, allowing you to share your API contract between client and server, or even publish it for a third party to consume.

We support any Typescript validation library that supports Standard Schema, or if you want you can use plain TS types and get no runtime validation.

contract.ts
    import { initContract } from '@ts-rest/core';
    import { z } from 'zod'; 

    const c = initContract();

    export const contract = c.router({
      getPokemon: {
        method: 'GET',
        path: '/pokemon/:id',
        responses: {
          200: z.object({ 
            name: z.string(), 
          }), 
        },
      },
    });
contract.ts
import { initContract } from '@ts-rest/core';
import * as v from 'valibot'; 

const c = initContract();

export const contract = c.router({
  getPokemon: {
    method: 'GET',
    path: '/pokemon/:id',
    responses: {
      200: v.object({ 
        name: v.string(), 
      }), 
    },
  },
});
contract.ts
import { initContract } from '@ts-rest/core';
import { type } from 'arktype'; 

const c = initContract();

export const contract = c.router({
  getPokemon: {
    method: 'GET',
    path: '/pokemon/:id',
    responses: {
      200: type({ 
        name: 'string', 
      }), 
    },
  },
});
contract.ts
import { initContract } from '@ts-rest/core';

const c = initContract();

export const contract = c.router({
  getPokemon: {
    method: 'GET',
    path: '/pokemon/:id',
    responses: {
      200: c.type<{ name: string }>(), 
    },
  },
});

Fulfill the contract on your server

We support Nest, Next.js, Express, Fastify and more - Our server implementation allows you to guarentee your backend stays in sync with your API contract.

server.ts
import {  } from '@ts-rest/express';

const  = ();

const  = .(, {
  : async ({ params: {  } }) => {
params: {
    id: string;
}
const = await .(); return { : 200, : , }; }, });

Use the API on the client

Our @ts-rest/core package provides a super light-weight fetch based client - We remain close to HTTP fundamentals, so we always return a status code, and body.

client.ts
import {  } from '@ts-rest/core';

const  = (, {
  : 'http://localhost:3000',
});

const  = await .({
  params: { : '1' },
params: {
    id: string;
}
}); if (. === 200) { .body;
body: {
    name: string;
}
}

Meet the team

We've got many, many contributors, but the core team consists of:

We all work full time, so we tend to have non overlapping schedules, which normally means at some point one of us will be available to help with issues - We respond more easily on GitHub than on Discord, although reach out to us on Discord if you'd like to chat.