Zod 4 (and all other Standard Schema support) is available as a release candidate `3.53.0-rc.1`
ts-rest
Server

NestJS

How to use ts-rest with NestJS

NestJS integration with ts-rest provides type-safe API endpoints while maintaining the familiar NestJS decorator-based architecture. We offer two approaches to fit different development styles and migration strategies.

Installation

pnpm add @ts-rest/nest
bun add @ts-rest/nest
npm install @ts-rest/nest

Approaches

NestJS has always been a tricky framework to integrate with ts-rest due to how Nest handles routing and controllers. Rather than requiring you to abandon Nest's decorator approach, we provide two flexible patterns that work alongside your existing Nest architecture.

Single Handler Approach - Easy Migration and Flexibility

The single handler approach provides a 1:1 migration strategy for existing controllers. You can replace individual Nest routes with ts-rest routes without affecting other routes in the same controller. This is perfect for gradual adoption or when you want different endpoints across multiple controllers.

single-handler.ts
@()
export class  {
  constructor(private readonly : ) {}

  @(.)
  async () {
    return (., async ({  }) => {
      const  = await this..(.);

      if (!) {
        return { : 404, : { : 'Post not found' } };
      }

      return { : 200, :  };
    });
  }

  @(.)
  async () {
    return (., async ({  }) => {
      const  = await this..();

      return { : 200, :  };
    });
  }
}

The method names don't matter in ts-rest controllers, just like regular Nest endpoints. Name them whatever makes sense for your codebase.

Why do we return tsRestHandler? This function simply returns its second argument, but provides full TypeScript intellisense and type safety for the implementation. This keeps the API consistent with other ts-rest server implementations like @ts-rest/express and @ts-rest/fastify.

Benefits

  • Easy migration: Replace one route at a time without touching others
  • Flexible architecture: Implement one contract across multiple controllers based on your domain
  • Gradual adoption: Perfect for existing codebases
  • Freedom of organization: No requirement for 1:1 contract-to-controller mapping

Multi Handler Approach - Ultimate Type Safety

The multi handler approach is ideal for those who prefer functional programming patterns or want compile-time guarantees that every route in a contract is implemented. TypeScript will error if you forget to implement any route.

multi-handler.ts
@()
export class  {
  constructor(private readonly : ) {}

  @()
  async () {
    return (, {
      : async ({  }) => {
        const  = await this..(.);

        if (!) {
          return { : 404, : { : 'Post not found' } };
        }

        return { : 200, :  };
      },
      : async ({  }) => {
        const  = await this..();

        return { : 200, :  };
      },
    });
  }
}

Pass the entire contract (or a subset) to both @TsRestHandler and tsRestHandler. TypeScript will enforce that you implement every route defined in the contract.

Benefits

  • Compile-time safety: TypeScript errors if you miss implementing any route
  • Less boilerplate: Single decorator and handler for multiple routes
  • Consistency: Easier to move code between @ts-rest/express, @ts-rest/fastify, and @ts-rest/next
  • Functional approach: Great for those who prefer this programming style

Using Nest Decorators

You can still use all existing Nest decorators alongside @TsRestHandler. This gives you access to the underlying request/response objects and Nest's dependency injection system.

nest-decorators.ts
@()
export class  {
  @(.)
  @()
  async (
    @() : AuthenticatedRequest,
    @('authorization') : string,
  ) {
    return (., async ({  }) => {
      // You can use both ts-rest typed data and Nest decorators
      .('Auth header from ts-rest:', .);
      .('Auth header from Nest:', );
      .('User from request:', .);

      return {
        : 200,
        : { : .., : .. },
      };
    });
  }
}

This isn't limited to parameter decorators - you can use guards, interceptors, pipes, and any other Nest decorators as you normally would.

Type-Safe Error Handling

While we recommend returning errors as responses to maintain full type safety in your contract, sometimes throwing exceptions is cleaner. TsRestException lets you throw type-safe exceptions that ts-rest will catch and convert to properly typed responses.

error-handling.ts

class  {
  async (: { : string; : string }) {
    const  = await .(.);

    if () {
      throw new (., {
        : 409,
        : {
          : 'UserAlreadyExists',
          : 'User with this email already exists',
        },
      });
    }

    return {
      : '1',
      : .,
      : .,
    };
  }
}

@()
export class  {
  constructor(private readonly : ) {}

  @(.)
  async () {
    return (., async ({  }) => {
      const  = await this..();

      return { : 201, :  };
    });
  }
}

TsRestException provides full autocomplete for valid status codes and response bodies defined in your contract.

Caution: Be careful when throwing exceptions from shared code used by multiple routes. You might throw the wrong exception type for a given route. For maximum safety, return responses directly from the handler.

Configuration

Configure ts-rest options using the @TsRest decorator on controllers or the @TsRestHandler decorator on methods. Controller options apply to all routes and override global options. Method options override controller options for that specific route.

configuration.ts
@()
@({ : true }) // Applied to all routes in this controller
export class  {
  @(.)
  async () {
    return (., async ({  }) => {
      // query.filters is properly parsed as an object due to jsonQuery: true
      .(..);
      return { : 200, : { : [] } };
    });
  }

  @(., { : false }) // Override for this route
  async () {
    return (., async ({ ,  }) => {
      // query.include is a string array due to normal query parsing
      return { : 200, : { : {} } };
    });
  }
}

For global configuration options and more details, check the configuration documentation.

Important Considerations

Path Prefixes

Known Limitation: Nest's global prefixes, versioning, and controller prefixes are currently ignored by ts-rest. See GitHub issue #70 for details.

Workaround: Use ts-rest's path prefix feature in your contract definition to achieve similar functionality.