Skip to main content

Quickstart

Embarking on your journey with ts-rest is a breeze! You have the liberty to utilize just the front-end, the backend, or merely the contract (should you choose to do so).

Once you've mastered the fundamentals, you'll find the developer experience to be smooth as silk. ✨

Installation

Install the core package and zod - Some of our generics rely on zod being installed, so make sure to install it (even as a dev dependency if you don't plan to use Zod)

If you don't install zod, some confusing errors may appear but it should mostly work #303

pnpm add @ts-rest/core zod

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.

Make sure to use strict with Zod

Enable strict in your tsconfig.json! This is required as part of Zod

  "compilerOptions": {
...
"strict": true
}

If you don't enable strict mode, ts-rest will still work, but you may face performance issues #162

// contract.ts

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

const c = initContract();

const PostSchema = z.object({
id: z.string(),
title: z.string(),
body: z.string(),
});

export const contract = c.router({
createPost: {
method: 'POST',
path: '/posts',
responses: {
201: PostSchema,
},
body: z.object({
title: z.string(),
body: z.string(),
}),
summary: 'Create a post',
},
getPost: {
method: 'GET',
path: `/posts/:id`,
responses: {
200: PostSchema.nullable(),
},
summary: 'Get a post by id',
},
});

Zod also has some nice features, like enabling body parsing and OpenAPI type generation.

Server Implementation

pnpm add @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:

// post.controller.ts

const c = nestControllerContract(contract);
type RequestShapes = NestRequestShapes<typeof c>;

@Controller()
export class PostController implements NestControllerInterface<typeof c> {
constructor(private readonly postService: PostService) {}

@TsRest(c.getPost)
async getPost(@TsRestRequest() { params: { id } }: RequestShapes['getPost']) {
const post = await this.postService.getPost(id);

return { status: 200 as const, body: post };
}

@TsRest(c.createPost)
async createPost(@TsRestRequest() { body }: RequestShapes['createPost']) {
const post = await this.postService.createPost({
title: body.title,
body: body.body,
});

return { status: 201 as const, body: post };
}
}

You can see that we're using the runtime object c in the TsRest decorator to automatically declare your path from the contract's getPost route. We're also using the RequestShapes TypeScript Types (which comes from the runtime object c) to ensure type safety of your contract on the Nest controller.

If you were to change the body return type to { body: true } for example, this will give you a typescript error: Your body is defined as an object in the contract above, not boolean!

Client Implementation

This is the basic client, using fetch under the hood which is exported from @ts-rest/core.

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

// `contract` is the AppRouter returned by `c.router`
const client = initClient(contract, {
baseUrl: 'http://localhost:3000',
baseHeaders: {},
});

const { body, status } = await client.createPost({
body: {
title: 'Post Title',
body: 'Post Body',
},
});

if (status === 201) {
// body is Post
console.log(body);
} else {
// body is unknown
console.log(body);
}