DEV Community

Alex Spinov
Alex Spinov

Posted on

ts-rest Has a Free Type-Safe REST API — Here's How to Use It

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
Enter fullscreen mode Exit fullscreen mode

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(),
      })),
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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' },
});
Enter fullscreen mode Exit fullscreen mode

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


Building type-safe APIs? My Apify scrapers expose REST APIs for data. Custom solutions: spinov001@gmail.com

Top comments (0)