Skip to main content

Nest 🆕

info

As part of @ts-rest/nest 3.26.0 we introduced the single-handler and multi-handler approach to Nest with ts-rest, for the legacy approach please see here.

The old API will likely be deprecated in 4.0.0, so we recommend using the new API although there is no rush to migrate.

Installation

pnpm add @ts-rest/nest

Approaches

Nest has always been a tricky framework to integrate with ts-rest, due to the way Nest handles routing and controllers. Nest normally uses large amounts of untyped decorators for each route, with ts-rest we introduce two new approaches to handle most of the heavy lifting for you.

Single Handler Approach - Easy Migration and Flexibility ⚡️

We recommend the single handler approach as it provides a 1-1 migration strategy for legacy controllers. You can swap one Nest route for one ts-rest route without affecting other routes in a Controller.

import { TsRestHandler, tsRestHandler } from '@ts-rest/nest';
import { c } from './contract';

@Controller()
export class MyController {
constructor(private readonly service: Service) {}

@TsRestHandler(c.getPost)
async getPost() {
return tsRestHandler(c.getPost, async ({ params }) => {
const post = await this.service.getPost(params.id);

if (!post) {
return { status: 404, body: null };
}

return { status: 200, body: post };
});
}

@TsRestHandler(c.getPosts)
async getPosts() {
return tsRestHandler(c.getPosts, async () => {
const posts = await this.service.getPosts();

return { status: 200, body: posts };
});
}
}
note

Just like in regular Nest endpoints the names of the methods do not matter, you can name them whatever you like.

You may be wondering why do we return tsRestHandler from this method, all this function does is return the second argument. The only reason this exists is to provide you a fully typed implementation object which matches 1-1 with the way the @ts-rest/next, @ts-rest/express and @ts-rest/fastify packages work.

Benefits

  • 1-1 migration strategy
  • Able to not have a one to one relationship between your contract and controllers
    • You are able to implement one ts-rest contract across multiple controllers based on your domain.

Multi Handler Approach - Ultimate Type Safety 🛡️

The multi handler approach is fantastic for those of you who tend towards a more functional approach to your code, or if you want to ensure that your controller methods are always returning the correct response types.

import { TsRestHandler, tsRestHandler } from '@ts-rest/nest';
import { c } from './contract';

@Controller()
export class MyController {
constructor(private readonly service: Service) {}

@TsRestHandler(c)
async handler() {
return tsRestHandler(c, {
getPost: async ({ params }) => {
const post = await this.service.getPost(params.id);

if (!post) {
return { status: 404, body: null };
}

return { status: 200, body: post };
},
getPosts: async () => {
const posts = await this.service.getPosts();

return { status: 200, body: posts };
},
});
}
}

The key difference here is you pass the entire contract (or a subset of a contract by defining a new contract or spreading multiple contracts) to the @TsRestHandler decorator and the tsRestHandler function.

caution

As of right now, for each route in a multi handler approach we instantiate the @All decorator for each route, this is potentially problematic if you have the same route in multiple contracts (with different methods), as it will cause a conflict.

We're looking into ways to solve this, but right now this may be a Nest limitation.

Benefits

  • Less room for error, you are forced to implement all routes in a contract
  • Less boilerplate
  • Easier to move code between @ts-rest/next, @ts-rest/express and @ts-rest/fastify servers

Using Nest Decorators

Despite using @TsRestHandler rather than @Get or @Post (etc.) you can still use all of the existing Nest decorators on your controller methods.

import { TsRestHandler, tsRestHandler } from '@ts-rest/nest';

@Controller()
export class MyController {
constructor(private readonly service: Service) {}

@TsRestHandler(c.test)
async myMethod(@Req() req: Request) {
return tsRestHandler(c.test, async ({ params }) => {
// ...

console.log(req.headers);
// ^ You can still use Nest decorators
});
}
}

In this example above we're using the @Req decorator to get access to the Request object, this is a great example of how you can use Nest decorators to get access to the underlying request/response objects.

This isn't limited to Param decorators, you can use any Nest decorator on your controllers as you would normally.

Throwing Type-Safe Errors 🍀

Generally speaking you should try to return all errors as a response, so that you get the benefits of ts-rest typing in the contract (to let your consumers know what errors to expect).

However, sometimes it's much cleaner and simpler to throw an error rather than passing a response back to your handler - the problem is that you normally lose all type safety when you do this.

With TsRestException you can throw a Nest exception and it will be handled by the tsRestHandler function and returned as a fully typed response.

throw new TsRestException(contract.test, {
status: 400,
body: { code: 'UserAlreadySignedUp', message: 'User has already signed up' },
});

This example above will provide you autocomplete for the given status within the contract.test responses contract.

caution

The only risk of doing it this way is that you throw the wrong exception for a given route, which can happen if you throw from a piece of code used by multiple routes. If you want to be 100% sure you are throwing the correct exception you should use the tsRestHandler function to return the response.

This can be somewhat mitigated if you share the error code schema between multiple routes, so that you can't throw the wrong error code.

Configuration

To configure certain ts-rest options you can use the @TsRest decorator on either your Controller or use the existing @Api decorator.

JSON Query Parameters

To handle JSON query parameters, pass the jsonQuery option to the @TsRest decorator on your entire controller.

import { TsRest } from '@ts-rest/nest';

@Controller()
@TsRest({ jsonQuery: true })
export class MyController {}

You can also use the method decorator to override or configure options for a specific route.

import { TsRestHandler, tsRestHandler } from '@ts-rest/nest';

@Controller()
export class MyController {
constructor(private readonly service: Service) {}

@TsRestHandler(c.getPost, { jsonQuery: true })
async getPost() {
return tsRestHandler(c.getPost, async () => {
// ...
});
}
}

Request Validation

By default, ts-rest validates all request components - body, headers and query parameters. In case of validation errors, the server responds with a ZodError object in the response body and a status code of 400. You can disable the validation of these components if you wish to perform the validation manually or handle the error differently.

Using the @TsRest() decorator, you can configure request validation parameters for the entire controller:

import { TsRest } from '@ts-rest/nest';

@Controller()
@TsRest({
validateRequestBody: false,
validateRequestQuery: false,
validateRequestHeaders: false
})
class MyController {}

You can also override controller decorator values for a specific route:

import { TsRest } from '@ts-rest/nest';

@Controller()
@TsRest({
validateRequestBody: false,
})
class MyController {
@TsRest(contract.create, {
validateRequestBody: true
})
async create(@TsRestRequest() { body }) {
// body validated
}
}

Or finally, using @TsRestHandler() for specific route:

import { TsRestHandler, tsRestHandler } from '@ts-rest/nest';

@Controller()
export class MyController {
constructor() {}

@TsRestHandler(c.getPost, {
validateRequestBody: false,
validateRequestQuery: false,
validateRequestHeaders: false
})
async getPost() {
return tsRestHandler(c.getPost, async ({ query, body }) => {
const isQueryValid = querySchema.safeParse(query);
console.log(isQueryValid) // => { success: false; error: ZodError }

const isBodyValid = bodySchema.safeParse(body);
console.log(isBodyValid) // => { success: true; data: {...} }
});
}
}

Response Validation

You can enable response validation by passing the validateResponses option to the @TsRest() decorator. This will enable response parsing and validation for a returned status code, if there is a corresponding response Zod schema defined in the contract. This is useful for ensuring absolute safety that your controller is returning the correct response types as well as stripping any extra properties.

import { TsRest } from '@ts-rest/nest';

@Controller()
@TsRest({ validateResponses: true })
export class MyController {}

You can also use the method decorator to override or configure options for a specific route.

import { TsRestHandler, tsRestHandler } from '@ts-rest/nest';

@Controller()
export class MyController {
constructor(private readonly service: Service) {}

@TsRestHandler(c.getPost, { validateResponses: true })
async getPost() {
return tsRestHandler(c.getPost, async () => {
// ...
});
}
}

If validation fails a ResponseValidationError will be thrown causing a 500 response to be returned. You can catch this error and handle it as you wish by using a NestJS exception filter.

Gotchas

caution

Currently any existing Nest global prefix, versioning, or controller prefixes will be ignored, please see https://github.com/ts-rest/ts-rest/issues/70 for more details.

If this feature is highly requested, we can investigate a solution.

We have added the ability to prefix paths, allowing more flexibility in defining your API endpoints. This can be used as a workaround for this functionality.