| |

TypeScript 80 🔷 Decorators in NestJS

NestJS is the framework that brought Angular’s architectural patterns to the server side. Its author, Kamil Myśliwiec, drew directly on Angular’s decorator-based dependency injection and modular structure, and the result is a Node.js framework where @Controller, @Injectable, and @Module look and feel like their Angular counterparts . But NestJS did not simply copy Angular. It adapted the decorator system for backend requirements: it requires emitDecoratorMetadata to power its dependency injection, it uses parameter decorators extensively for request extraction, and it added a rich set of custom decorator factories for everything from validation to Swagger documentation .

Key point: NestJS is built entirely on legacy experimental decorators. It hard-requires "experimentalDecorators": true and "emitDecoratorMetadata": true in tsconfig.json, and its DI container resolves constructor parameter types from the metadata that the compiler emits . The framework provides a large set of built-in parameter decorators — @Body(), @Param(), @Query(), @Headers(), @Req(), @Res() — that extract values from the request and inject them into the route handler . Custom parameter decorators are created with createParamDecorator(), and custom method decorators can combine multiple built-ins with applyDecorators() .

The NestJS decorator ecosystem

NestJS uses decorators at every level of the application. The categories are distinct and each serves a different purpose.

CategoryDecoratorsPurpose
Class@Controller(), @Injectable(), @Module()Declare the class’s role
Method@Get(), @Post(), @Put(), @Delete(), @Patch()Map HTTP methods to handlers
Parameter@Body(), @Param(), @Query(), @Headers(), @Req(), @Res()Extract request data
CustomcreateParamDecorator(), applyDecorators()User-defined extraction and composition
Validation@IsEmail(), @MinLength(), @IsNumber() (from class-validator)Validate DTO fields
Swagger@ApiProperty(), @ApiResponse(), @ApiTags()Generate OpenAPI documentation

Why the class decorators are Angular-like. The @Controller('users') decorator marks a class as a controller and defines the route prefix, exactly as @Component marks an Angular class as a component and defines the selector . The @Injectable() decorator marks a class as a provider that can be injected, exactly as in Angular . The @Module() decorator defines a feature module, exactly as in Angular’s @NgModule() .

Why the method decorators are HTTP-specific. The @Get(), @Post(), and their siblings map handler methods to HTTP routes. This is a NestJS-specific addition — Angular has no equivalent because Angular is not a server framework .

Why the parameter decorators are the workhorse. Every route handler needs access to the request. Instead of injecting the whole request and extracting values manually, NestJS provides decorators that extract exactly what the handler needs .

The built-in parameter decorators

NestJS ships with a complete set of parameter decorators that map to the underlying Express (or Fastify) request object. Each decorator extracts a specific part of the request and passes it as the method argument .

@Get(':id')
async findOne(
  @Param('id') id: string,
  @Query('filter') filter: string,
  @Headers('authorization') auth: string,
) {
  return this.userService.findOne(id, filter);
}

The @Param('id') extracts req.params.id, the @Query('filter') extracts req.query.filter, and the @Headers('authorization') extracts req.headers.authorization .

The full list of built-in parameter decorators:

DecoratorExpress/Fastify Equivalent
@Request(), @Req()req
@Response(), @Res()res
@Next()next
@Session()req.session
@Param(param?)req.params / req.params[param]
@Body(param?)req.body / req.body[param]
@Query(param?)req.query / req.query[param]
@Headers(param?)req.headers / req.headers[param]
@Ip()req.ip
@HostParam(param?)req.hosts / req.hosts[param]

Why the optional parameter matters. Each decorator accepts an optional string argument. Without it, the whole object is passed; with it, only the named property is extracted. This keeps handler signatures short and readable .

Custom parameter decorators

The most common custom decorator is one that extracts the authenticated user from the request. The authentication layer attaches the user to req.user, and a @User() decorator makes it available in every handler without repeating the extraction code .

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);

The decorator is used like any built-in one:

@Get('profile')
async getProfile(@User() user: UserEntity) {
  return user;
}

Why the data parameter exists. The first argument to the factory function is the data passed to the decorator at the usage site. If the decorator is used as @User('firstName'), then data is 'firstName'. This lets the same decorator extract different properties .

export const User = createParamDecorator(
  (data: string | undefined, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const user = request.user;
    return data ? user?.[data] : user;
  },
);

@Get()
async findOne(@User('firstName') firstName: string) {
  console.log(`Hello ${firstName}`);
}

Why the generic exists. The createParamDecorator<T>() signature lets the developer enforce the type of the data parameter. Without it, data is typed as any, which loses type safety at the decorator boundary .

Why pipes work with custom decorators. NestJS treats custom parameter decorators the same way as built-in ones. Pipes run for custom-decorated parameters by default, but the ValidationPipe requires the validateCustomDecorators: true option to validate them .

Decorator composition

NestJS provides applyDecorators() to combine multiple decorators into one. This is used for grouping authentication-related decorators, or combining a route decorator with Swagger documentation .

import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common';

export function Auth(...roles: string[]) {
  return applyDecorators(
    SetMetadata('roles', roles),
    UseGuards(AuthGuard),
  );
}

The composed @Auth() decorator applies both the metadata and the guard in one line .

Why composition matters for readability. A route that requires authentication and has specific roles would otherwise need multiple decorators stacked:

@SetMetadata('roles', ['admin'])
@UseGuards(AuthGuard)
@Get('admin')

The composed version is one decorator that expresses the intent .

Decorators and metadata reflection

NestJS’s dependency injection relies on the emitDecoratorMetadata flag. When the compiler sees a constructor with parameters, it emits metadata that records the runtime type of each parameter. The DI container reads that metadata to resolve the dependency .

@Injectable()
export class UserService {
  constructor(private readonly db: DatabaseService) {}
}

The compiler emits design:paramtypes metadata recording that the first parameter is DatabaseService. The NestJS DI container reads this metadata and injects the correct instance .

Why the metadata is fragile with generics. TypeScript generics are erased at runtime. The emitDecoratorMetadata can only record the runtime type, which is the class name without its generic parameters. If a DTO is typed PaginationAbstract<OrderFilter, OrderSearch>, the metadata records PaginationAbstract, and the two generic parameters are lost. The workaround is to use mixins — functions that generate concrete classes with the specific DTOs baked in .

Why the metadata requirement limits tooling. Only tools that route through tsc or swc support emitDecoratorMetadata. Tools that use esbuild — tsx, esbuild-register — do not emit the metadata, and NestJS applications fail at runtime with those tools. The canonical NestJS development setup uses the SWC builder with legacyDecorator: true and decoratorMetadata: true in .swcrc .

Validation with class-validator

NestJS integrates with class-validator for DTO validation. The validation decorators are applied to the DTO class properties, and the ValidationPipe reads them at runtime .

import { IsEmail, IsString, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  password: string;
}

The @IsEmail() and @MinLength() decorators store validation metadata on the class. The ValidationPipe reads the metadata and validates the incoming request body .

Why the validation decorators are legacy. The class-validator library uses Reflect.defineMetadata to store the constraints. This requires the reflect-metadata import and the emitDecoratorMetadata flag. The Stage 3 decorators do not support this metadata mechanism .

Swagger documentation decorators

NestJS provides a separate set of decorators for OpenAPI documentation. They are prefixed with Api to distinguish them from the core decorators .

@ApiTags('users')
@Controller('users')
export class UsersController {
  @ApiOperation({ summary: 'Get a user by ID' })
  @ApiResponse({ status: 200, description: 'The user record' })
  @Get(':id')
  async findOne(@Param('id') id: string) {
    return this.usersService.findOne(id);
  }
}

The @ApiTags() decorator groups endpoints in the Swagger UI, the @ApiOperation() adds a summary, and the @ApiResponse() documents the response shape. All are optional and all are processed by the Swagger module at build time .

Complete Example Session

// ============================================
// PART 1: THE CONTROLLER
// ============================================

import { Controller, Get, Post, Body, Param, Query } from '@nestjs/common';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  async findAll(@Query('page') page: string) {
    return this.usersService.findAll(Number(page));
  }

  @Get(':id')
  async findOne(@Param('id') id: string) {
    return this.usersService.findOne(id);
  }

  @Post()
  async create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }
}

// ============================================
// PART 2: THE SERVICE
// ============================================

import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
  constructor(private readonly db: DatabaseService) {}

  async findAll(page: number) {
    return this.db.users.findMany({ skip: page * 10, take: 10 });
  }

  async findOne(id: string) {
    return this.db.users.findUnique({ where: { id } });
  }

  async create(dto: CreateUserDto) {
    return this.db.users.create({ data: dto });
  }
}

// ============================================
// PART 3: THE CUSTOM @User() DECORATOR
// ============================================

import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const User = createParamDecorator(
  (data: string | undefined, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const user = request.user;
    return data ? user?.[data] : user;
  },
);

// Usage:
@Get('profile')
async getProfile(@User('email') email: string) {
  return this.usersService.findByEmail(email);
}

// ============================================
// PART 4: DECORATOR COMPOSITION
// ============================================

import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common';

export function Roles(...roles: string[]) {
  return applyDecorators(
    SetMetadata('roles', roles),
    UseGuards(RolesGuard),
  );
}

// Usage:
@Roles('admin')
@Get('admin')
async adminOnly() {
  return { message: 'Admin access granted' };
}

// ============================================
// PART 5: THE DTO WITH VALIDATION
// ============================================

import { IsEmail, IsString, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  password: string;
}

// ============================================
// PART 6: THE MODULE
// ============================================

import { Module } from '@nestjs/common';

@Module({
  controllers: [UsersController],
  providers: [UsersService, DatabaseService],
  exports: [UsersService],
})
export class UsersModule {}

// ============================================
// PART 7: THE SWAGGER DECORATORS
// ============================================

import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';

@ApiTags('users')
@Controller('users')
export class UsersController {
  @ApiOperation({ summary: 'Find all users' })
  @ApiResponse({ status: 200, description: 'List of users' })
  @Get()
  async findAll() { ... }
}

// ============================================
// PART 8: THE TSCONFIG
// ============================================

// tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "target": "ES2022"
  }
}

// ============================================
// PART 9: THE SWC CONFIGURATION
// ============================================

// .swcrc
{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "decorators": true
    },
    "transform": {
      "legacyDecorator": true,
      "decoratorMetadata": true
    }
  }
}

// ============================================
// PART 10: WHAT NOT TO DO
// ============================================

// Don't forget the emitDecoratorMetadata flag
// The DI will fail at runtime.                                  // ⚠️

// Don't use tsx or esbuild-register for NestJS
// They do not emit the metadata.                                // ⚠️

// Don't use Stage 3 decorators with NestJS
// Parameter decorators are not supported.                      // ⚠️

// Don't rely on generics for validation
// The metadata records only the base class.                     // ⚠️

// Don't forget reflect-metadata
// The validation and metadata calls will fail.                  // ⚠️

Quick Reference

The Core NestJS Decorators

DecoratorPurpose
@Controller()Declare a controller
@Injectable()Declare a provider
@Module()Declare a module
@Get(), @Post(), etc.Map HTTP methods

The Parameter Decorators

DecoratorExtracts
@Body(param?)req.body
@Param(param?)req.params
@Query(param?)req.query
@Headers(param?)req.headers
@Req(), @Res()Full request/response
@Ip()req.ip

Custom Decorator Creation

FunctionPurpose
createParamDecorator()Create a parameter decorator
applyDecorators()Combine multiple decorators
SetMetadata()Store metadata for guards/interceptors

The Compiler Options

OptionPurpose
experimentalDecoratorsEnable legacy decorators
emitDecoratorMetadataEnable DI metadata
legacyDecorator (SWC)Enable legacy decorators
decoratorMetadata (SWC)Enable DI metadata

Why NestJS Cannot Migrate

ReasonDetail
Parameter decoratorsNot in Stage 3
Metadata reflectionNot in Stage 3
Ecosystem dependencyTypeORM, class-validator

Best Practices

✅ Do This:

// Use the built-in parameter decorators
async findOne(@Param('id') id: string) { ... }                 // ✅
// Create custom decorators for repeated extraction
export const User = createParamDecorator(...);                 // ✅
// Use applyDecorators for composition
export function Auth(...roles) { return applyDecorators(...); }// ✅
// Use class-validator for DTO validation
@IsEmail() email: string;                                      // ✅
// Enable both flags
{
  "experimentalDecorators": true,
  "emitDecoratorMetadata": true
}                                                              // ✅
// Use SWC for the development
{
  "legacyDecorator": true,
  "decoratorMetadata": true
}                                                              // ✅

❌ Don’t Do This:

// Don't use tsx or esbuild-register
tsx script.ts  // ❌ the metadata is not emitted                  // ⚠️
// Don't use Stage 3 decorators with NestJS
// The parameter decorators are not supported.                 // ⚠️
// Don't rely on generics for the validation
@Body() data: PaginationAbstract<Filter, Search>;  // ❌         // ⚠️
// Don't forget reflect-metadata
// The metadata calls will fail.                               // ⚠️
// Don't forget the flags
{
  "experimentalDecorators": false  // ❌ the DI fails
}

Common Pitfalls

PitfallProblemSolution
emitDecoratorMetadata offDI failsEnable the flag
tsx or esbuildNo metadataUse SWC
Generics in DTOMetadata erasedUse mixins
Missing reflect-metadataRuntime errorImport it
Stage 3 decoratorsParameter decorators failUse legacy
Missing @Injectable()DI cannot resolveAdd the decorator

Real-World Examples

1. The controller

@Controller('users')
export class UsersController {}

2. The service

@Injectable()
export class UsersService {}

3. The module

@Module({ controllers: [...], providers: [...] })
export class UsersModule {}

4. The @Get() route

@Get(':id')
async findOne(@Param('id') id: string) {}

5. The @Body() extraction

@Post()
async create(@Body() dto: CreateUserDto) {}

6. The custom @User()

export const User = createParamDecorator(...);

7. The @Roles() composition

export function Roles(...roles) { return applyDecorators(...); }

8. The DTO validation

@IsEmail() email: string;

9. The Swagger decorator

@ApiTags('users')

10. The tsconfig

{
  "experimentalDecorators": true,
  "emitDecoratorMetadata": true
}

Visual: The NestJS Decorator Stack

┌──────────────────────────────────────────────┐
│  @Controller('users')                        │
│    The class's declaration                   │
│                                              │
│  @Get(':id')                                 │
│    The method's mapping                      │
│                                              │
│  @Param('id') id: string                     │
│    The parameter's extraction                │
│                                              │
│  @IsEmail() email: string                    │
│    The DTO's validation                      │
│                                              │
│  The four are the stack's.                   │
│                                              │
└──────────────────────────────────────────────┘

Visual: The Metadata Flow

┌──────────────────────────────────────────────┐
│  @Injectable()                               │
│  class UserService {                         │
│    constructor(private db: DatabaseService) {}│
│  }                                           │
│                                              │
│         │  The compiler                      │
│         ▼                                    │
│                                              │
│  __metadata("design:paramtypes", [DatabaseService])│
│                                              │
│         │  The runtime                       │
│         ▼                                    │
│                                              │
│  THE DI CONTAINER:                           │
│    reads the metadata                        │
│    resolves DatabaseService                  │
│    injects it                                │
│                                              │
│  The emitDecoratorMetadata's is the bridge's.│
│                                              │
└──────────────────────────────────────────────┘

Visual: The Custom Decorator

┌──────────────────────────────────────────────┐
│  createParamDecorator(                       │
│    (data, ctx) => {                          │
│      const request = ctx.switchToHttp()...   │
│      return request.user;                    │
│    }                                         │
│  )                                           │
│                                              │
│  THE USAGE:                                  │
│    @User() user: UserEntity                  │
│    @User('email') email: string              │
│                                              │
│  The decorator's is the reusable's.          │
│                                              │
└──────────────────────────────────────────────┘

Visual: The Framework’s Constraint

┌──────────────────────────────────────────────┐
│  NestJS REQUIRES:                            │
│    experimentalDecorators: true              │
│    emitDecoratorMetadata: true               │
│                                              │
│  WHICH MEANS:                                │
│    The legacy decorators only                │
│    The parameter decorators only             │
│    The metadata reflection only              │
│                                              │
│  THE STAGE 3 DECORATORS:                     │
│    No parameter decorators                   │
│    No metadata reflection                    │
│    → NestJS cannot migrate.                  │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
The flagsexperimentalDecorators, emitDecoratorMetadata
The class decorators@Controller, @Injectable, @Module
The method decorators@Get, @Post, @Put, @Delete
The parameter decorators@Body, @Param, @Query, @Headers
The custom creationcreateParamDecorator, applyDecorators
The validationclass-validator
The Swagger@ApiTags, @ApiOperation
The metadataemitDecoratorMetadata
The toolingSWC with legacyDecorator
The migrationBlocked by parameter decorators

Key takeaways:

  • NestJS is built entirely on legacy experimental decorators — it hard-requires experimentalDecorators: true and emitDecoratorMetadata: true, and there is no Stage 3 migration path because parameter decorators and metadata reflection are not in the standard
  • The class decorators mirror Angular’s — @Controller, @Injectable, and @Module serve the same architectural roles as Angular’s @Component, @Injectable, and @NgModule
  • The parameter decorators are the workhorse — @Body(), @Param(), @Query(), @Headers(), and the others extract exactly what the handler needs from the request
  • createParamDecorator() builds custom extractors — the @User() decorator is the canonical example, and the data parameter lets one decorator extract different properties
  • applyDecorators() composes multiple decorators — the @Auth() or @Roles() decorator combines metadata and guards into one expression
  • The DI relies on emitDecoratorMetadata — the compiler emits design:paramtypes metadata that the container reads to resolve constructor dependencies
  • Generics are erased in the metadata — a DTO typed PaginationAbstract<Filter, Search> records only PaginationAbstract, and the validation cannot see the generic parameters. Mixins are the workaround
  • The tooling must route through tsc or swc — tsx and esbuild-register do not emit the metadata, and NestJS applications fail at runtime with those tools
  • class-validator provides the DTO validation — the @IsEmail(), @MinLength(), and similar decorators store metadata that the ValidationPipe reads
  • The Swagger decorators document the API — @ApiTags(), @ApiOperation(), and @ApiResponse() generate the OpenAPI specification

Remember: NestJS is the framework that brought Angular’s decorator-based architecture to the backend, and it did so on the legacy experimental decorators. The @Controller, @Injectable, and @Module class decorators mirror Angular’s; the @Get, @Post, and their siblings map HTTP methods; the @Body, @Param, and @Query parameter decorators extract request data; and createParamDecorator and applyDecorators let you build your own. The DI and the validation depend on emitDecoratorMetadata, which is why the Stage 3 standard cannot replace the legacy system here. The framework is a deliberate port of Angular’s mental model to the server, and the decorators are the language it speaks.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!