tRPC is type-safe but not REST. OpenAPI generates types but they're fragile. ts-rest gives you type-safe REST APIs — standard HTTP endpoints with full TypeScript inference.
What Is ts-rest?
ts-rest is a framework for building type-safe REST APIs with TypeScript. You define a contract (shared types) — the client and server both derive types from it. Standard REST endpoints, full type safety.
Quick Start
npm install @ts-rest/core
Define Contract (Shared)
// contract.ts — shared between client and server
import { initContract } from '@ts-rest/core';
import { z } from 'zod';
const c = initContract();
export const contract = c.router({
getUser: {
method: 'GET',
path: '/users/:id',
pathParams: z.object({ id: z.string() }),
responses: {
200: z.object({
id: z.string(),
name: z.string(),
email: z.string(),
}),
404: z.object({ message: z.string() }),
},
},
createUser: {
method: 'POST',
path: '/users',
body: z.object({
name: z.string().min(2),
email: z.string().email(),
}),
responses: {
201: z.object({
id: z.string(),
name: z.string(),
email: z.string(),
}),
},
},
listUsers: {
method: 'GET',
path: '/users',
query: z.object({
page: z.number().optional(),
limit: z.number().optional(),
}),
responses: {
200: z.array(z.object({
id: z.string(),
name: z.string(),
})),
},
},
});
Server (Express)
import { createExpressEndpoints } from '@ts-rest/express';
import { contract } from './contract';
const router = createExpressEndpoints(contract, {
getUser: async ({ params }) => {
const user = await db.findUser(params.id);
if (!user) return { status: 404, body: { message: 'Not found' } };
return { status: 200, body: user };
},
createUser: async ({ body }) => {
const user = await db.createUser(body);
return { status: 201, body: user };
},
listUsers: async ({ query }) => {
const users = await db.listUsers(query);
return { status: 200, body: users };
},
}, app);
Client
import { initClient } from '@ts-rest/core';
import { contract } from './contract';
const client = initClient(contract, {
baseUrl: 'http://localhost:3000',
});
// Fully typed — autocomplete works
const { body: user } = await client.getUser({ params: { id: '123' } });
console.log(user.name); // TypeScript knows this is a string
const { body: newUser } = await client.createUser({
body: { name: 'John', email: 'john@example.com' },
});
Why ts-rest
| Feature | ts-rest | tRPC | OpenAPI |
|---|---|---|---|
| Protocol | REST (HTTP) | Custom RPC | REST |
| Caching | HTTP cache | Manual | HTTP cache |
| CDN-friendly | Yes | No | Yes |
| Type safety | Full | Full | Generated |
| Standard endpoints | Yes | No | Yes |
| Zod validation | Built-in | Built-in | External |
| React Query | Integration | Built-in | Generated |
Framework Support
- Express, Fastify, Nest.js (server)
- React Query, SWR (client)
- Next.js (full-stack)
- OpenAPI generation (from contract)
Get Started
- Documentation
- GitHub — 3K+ stars
Building type-safe APIs? My Apify scrapers expose REST APIs for data. Custom solutions: spinov001@gmail.com
Top comments (0)