Skip to main content

Quickstart

Setting up ts-rest is easy! You can opt to use either just the front-end, just the backend, or just the contract (if you really wanted)

Once you've got to grips with the basics, the developer experience is like butter. 🧈🪄✨

Installation

Install the core package

pnpm add @ts-rest/core

Create a contract

Install Zod

pnpm add zod
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 do this, ts-rest will still work, but you may face performance issues #162

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.

// 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(apiBlog);
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

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);
}