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/core
bun add @ts-rest/core
npm install @ts-rest/core
Enable 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/nest
bun add @ts-rest/nest
npm install @ts-rest/nest
ts-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/express
bun add @ts-rest/express
npm install @ts-rest/express
The 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/fastify
bun add @ts-rest/fastify
npm install @ts-rest/fastify
The 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/next
bun add @ts-rest/next
npm install @ts-rest/next
import { 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-query
bun add @ts-rest/react-query
npm install @ts-rest/react-query
The @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>;
};