Quickstart
Let's get started, making a basic contract, server, and client.
Create a contract with @ts-rest/core
Create a server implementation with one of our supported frameworks
Create a client implementation with one of our supported libraries
Installation
Install the @ts-rest/core package, this is the core package that provides the type-safe contract, and a basic fetch based client.
pnpm add @ts-rest/corebun add @ts-rest/corenpm install @ts-rest/coreEnable strict in your tsconfig.json, this is required to work with some ts-rest functionality, and often downstream libraries like Zod
"compilerOptions": {
"strict": false
"strict": true
}Create a contract
This should ideally be shared between your consumers and producers, e.g. in a shared library in a monorepo, or a shared npm package. Think of this as your HTTP Schema that both your client and backend can use.
Shared contract
We strongly reccomend using a validation library like Zod, Valibot, or Arktype to define your contract.
This provides runtime validation of your contract, as opposed to just type safety.
import { initContract } from '@ts-rest/core';
import { z } from 'zod';
const c = initContract();
const Pokemon = z.object({
name: z.string(),
});
export const pokemonContract = c.router({
getPokemon: {
method: 'GET',
path: '/pokemon/:id',
responses: {
200: Pokemon,
},
summary: 'Get a pokemon by id',
},
});import { initContract } from '@ts-rest/core';
import * as v from 'valibot';
const c = initContract();
const Pokemon = v.object({
name: v.string(),
});
export const pokemonContract = c.router({
getPokemon: {
method: 'GET',
path: '/pokemon/:id',
responses: {
200: Pokemon,
},
summary: 'Get a pokemon by id',
},
});import { initContract } from '@ts-rest/core';
import { type } from 'arktype';
const c = initContract();
const Pokemon = type({
name: 'string',
});
export const pokemonContract = c.router({
getPokemon: {
method: 'GET',
path: '/pokemon/:id',
responses: {
200: Pokemon,
},
summary: 'Get a pokemon by id',
},
});import { initContract } from '@ts-rest/core';
const c = initContract();
type Pokemon = {
name: string;
};
export const pokemonContract = c.router({
getPokemon: {
method: 'GET',
path: '/pokemon/:id',
responses: {
200: c.type<Pokemon>(),
},
summary: 'Get a pokemon by id',
},
});Server Implementation
pnpm add @ts-rest/nestbun add @ts-rest/nestnpm install @ts-rest/nestts-rest offers a unique way to create a fully type safe REST API server, normally Nest APIs are extremely powerful, but hard to make type safe.
Let's add @ts-rest/nest to a basic Nest controller:
import { TsRestHandler, tsRestHandler } from '@ts-rest/nest';
import { Controller } from '@nestjs/common';
import { pokemonContract } from './contract';
@Controller()
export class PokemonController {
constructor(private readonly pokemonService: PokemonService) {}
@TsRestHandler(pokemonContract.getPokemon)
async getPokemon() {
return tsRestHandler(pokemonContract.getPokemon, async ({ params }) => {
const pokemon = await this.pokemonService.getPokemon(params.id);
if (!pokemon) {
return { status: 404, body: null };
}
return { status: 200, body: pokemon };
});
}
}You can see that we're using the modern @TsRestHandler decorator with the tsRestHandler function to get full type safety. The params are automatically typed based on your contract, and the return type is enforced to match your contract's response schema.
pnpm add @ts-rest/expressbun add @ts-rest/expressnpm install @ts-rest/expressThe express implementaton allows full type safety, offering; body parsing, query parsing, param parsing and full error handling
import express from 'express';
import cors from 'cors';
import bodyParser from 'body-parser';
import { initServer } from '@ts-rest/express';
import { createExpressEndpoints } from '@ts-rest/express';
import { pokemonContract } from './contract';
const app = express();
app.use(cors());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
const s = initServer();
const router = s.router(pokemonContract, {
getPokemon: async ({ params: { id } }) => {
// Mock pokemon data
const pokemon = { name: 'Pikachu' };
if (id !== '1') {
return {
status: 404,
body: null,
};
}
return {
status: 200,
body: pokemon,
};
},
});
createExpressEndpoints(pokemonContract, router, app);
const port = process.env.port || 3333;
const server = app.listen(port, () => {
console.log(`Listening at http://localhost:${port}`);
});pnpm add @ts-rest/fastifybun add @ts-rest/fastifynpm install @ts-rest/fastifyThe fastify implementaton allows full type safety, offering; body parsing, query parsing, param parsing and full error handling
import fastify from 'fastify';
import { initServer } from '@ts-rest/fastify';
import { pokemonContract } from './contract';
const app = fastify();
const s = initServer();
const router = s.router(pokemonContract, {
getPokemon: async ({ params: { id } }) => {
// Mock pokemon data
const pokemon = { name: 'Pikachu' };
if (id !== '1') {
return {
status: 404,
body: null,
};
}
return {
status: 200,
body: pokemon,
};
},
});
app.register(s.plugin(router));
const start = async () => {
try {
await app.listen({ port: 3000 });
} catch (err) {
app.log.error(err);
process.exit(1);
}
};
start();pnpm add @ts-rest/nextbun add @ts-rest/nextnpm install @ts-rest/nextimport { createNextRoute, createNextRouter } from '@ts-rest/next';
import { pokemonContract } from './contract';
const router = createNextRoute(pokemonContract, {
getPokemon: async ({ params: { id } }) => {
// Mock pokemon data
const pokemon = { name: 'Pikachu' };
if (id !== '1') {
return {
status: 404,
body: null,
};
}
return {
status: 200,
body: pokemon,
};
},
});
// Actually initiate the collective endpoints
export default createNextRouter(pokemonContract, router);Client Implementation
This is the basic client, using fetch under the hood which is exported from @ts-rest/core.
import { initClient } from '@ts-rest/core';
const client = initClient(pokemonContract, {
baseUrl: 'http://localhost:3000',
baseHeaders: {},
});
const res = await client.getPokemon({
params: { id: '1' },
});
if (res.status === 200) {
res.body.name;} else {
res;}pnpm add @ts-rest/react-querybun add @ts-rest/react-querynpm install @ts-rest/react-queryThe @ts-rest/react-query integration follows the same underlying pattern as the core @tanstack/react-query package so it should feel super familiar.
import { initQueryClient } from '@ts-rest/react-query';
export const client = initQueryClient(pokemonContract, {
baseUrl: 'http://localhost:3333',
baseHeaders: {},
});
export const PokemonComponent = () => {
const { data, isLoading, error } = client.getPokemon.useQuery(
['pokemon', '1'],
{
params: { id: '1' },
},
);
if (isLoading) {
return <div>Loading...</div>;
}
if (data?.status !== 200 || error) { return <div>Pokemon not found</div>;
}
return <div>{data.body.name}</div>;
};