| name | decorator-patterns |
| description | NestJS decorator usage: built-in route/param/class decorators, custom decorators via `createParamDecorator` and `SetMetadata`, metadata reflection via `Reflector`. Apply when designing controllers, guards, interceptors, or custom decorators.
Use this skill to:
- Pick the right built-in decorator for routes, params, and DI.
- Compose decorators (`@UseGuards(A, B) @UseInterceptors(C)`).
- Build custom param decorators (e.g., `@CurrentUser()`).
- Use metadata for role-based logic (Reflector + SetMetadata).
- Avoid common decorator mistakes.
Do NOT use this skill for:
- General module/DI patterns (see nest-conventions).
- GraphQL-specific decorators (see nest-advanced).
- ORM entity decorators (see nest-data-layer).
|
NestJS Decorator Patterns
NestJS is decorator-driven. Knowing which decorator does what — and how they compose — prevents most "weird DI / weird routing" bugs.
Required tsconfig
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
emitDecoratorMetadata is what makes runtime introspection (and class-validator/class-transformer) work. Never disable.
reflect-metadata polyfill must be imported once at the top of main.ts:
import 'reflect-metadata';
(NestJS does this implicitly via @nestjs/core, but if you write a script that uses decorators outside the Nest app, import it manually.)
Class-level decorators
@Controller(prefix)
@Controller('users')
@Controller({ path: 'users', version: '1' })
export class UsersController {}
@Injectable(options?)
@Injectable()
@Injectable({ scope: Scope.REQUEST })
export class MyService {}
@Module(...)
@Module({ imports, controllers, providers, exports })
export class UsersModule {}
@Global()
@Global()
@Module({ providers: [...], exports: [...] })
export class LoggerModule {}
Use sparingly — see nest-conventions.
Method-level: HTTP routing
@Get()
@Get(':id')
@Post()
@Put(':id')
@Patch(':id')
@Delete(':id')
@All('debug')
@Head()
@Options()
Route options
@HttpCode(204)
@Header('Cache-Control', 'no-store')
@Redirect('https://example.com', 301)
@Render('user-profile')
Versioning
@Controller({ path: 'users', version: ['1', '2'] })
@Version('2')
@Get()
findV2() {}
Param decorators
@Get(':id')
async findOne(
@Param('id') id: string,
@Query('include') include: string,
@Headers('authorization') authHeader: string,
@Headers() allHeaders: Record<string, string>,
@Body() dto: UpdateUserDto,
@Body('email') email: string,
@Req() req: Request,
@Res() res: Response,
@Ip() ip: string,
@HostParam() hosts: Record<string, string>,
@Session() session: Record<string, unknown>,
) {}
@Res() warning
When you inject @Res() and call res.send() directly, NestJS skips its response-handling pipeline (interceptors, exception filters that produce a response). Either:
- Don't inject
@Res() — return data and let Nest handle the response.
- Or inject
@Res({ passthrough: true }) and still return data.
@Get()
async getStream(@Res({ passthrough: true }) res: Response): Promise<StreamableFile> {
res.set({ 'Content-Type': 'application/octet-stream' });
return new StreamableFile(buffer);
}
Pipe binding
@Get(':id')
findOne(
@Param('id', ParseIntPipe) id: number,
@Query('limit', new DefaultValuePipe(20), ParseIntPipe) limit: number,
@Body(new ValidationPipe({ whitelist: true })) dto: CreateUserDto,
) {}
Pipes run in order, left-to-right. Combine for type-safe + validated input.
Guard / Interceptor / Filter binding
@UseGuards(JwtAuthGuard, RolesGuard)
@UseInterceptors(LoggingInterceptor, CacheInterceptor)
@UseFilters(AllExceptionsFilter)
@UsePipes(new ValidationPipe({ transform: true }))
@Controller('users')
export class UsersController {}
Apply at route level OR globally:
@Module({
providers: [
{ provide: APP_GUARD, useClass: JwtAuthGuard },
{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
{ provide: APP_FILTER, useClass: AllExceptionsFilter },
{ provide: APP_PIPE, useClass: ValidationPipe },
],
})
app.useGlobalGuards(new JwtAuthGuard());
APP_GUARD etc. are imported from @nestjs/core constants.
Metadata via SetMetadata + Reflector
For role-based or feature-flag logic, attach metadata to a route, then read in a guard.
Define a metadata decorator
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
Apply on route
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@Get('users')
@Roles('admin', 'super-admin')
listUsers() { ... }
}
Read in guard
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const required = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!required) return true;
const { user } = context.switchToHttp().getRequest();
return required.some(r => user.roles?.includes(r));
}
}
getAllAndOverride walks handler → class metadata; method-level wins. getAllAndMerge combines both.
Custom param decorator
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: keyof User | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user as User;
return data ? user?.[data] : user;
},
);
Usage:
@Get('me')
me(@CurrentUser() user: User) {}
@Get('email')
myEmail(@CurrentUser('email') email: string) {}
Works in HTTP, GraphQL, WebSocket contexts via switchToHttp / switchToWs / GraphQL execution context wrapper.
Composing decorators
For a frequently-used combo, build a meta-decorator with applyDecorators:
import { applyDecorators, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiUnauthorizedResponse } from '@nestjs/swagger';
export function Auth(...roles: string[]) {
return applyDecorators(
UseGuards(JwtAuthGuard, RolesGuard),
Roles(...roles),
ApiBearerAuth(),
ApiUnauthorizedResponse({ description: 'unauthorized' }),
);
}
Usage:
@Auth('admin')
@Get('users')
listUsers() { ... }
Reduces decorator stack noise when 4+ decorators repeat.
Anti-patterns
- ❌ Using
Reflect.getMetadata(...) directly — use Reflector.get() (NestJS API). It handles inheritance correctly.
- ❌
@Body('field') instead of a typed DTO — bypasses class-validator, no type safety.
- ❌ Injecting
@Res() then forgetting passthrough: true — breaks interceptors.
- ❌ Multiple class-level
@UseGuards(A) @UseGuards(B) — last one wins; combine into one: @UseGuards(A, B).
- ❌ Custom decorator without preserving type info — declare return type explicitly.
- ❌
SetMetadata('key', val) inline at the route — make a named decorator (Roles(...)) so it's discoverable.
- ❌
@Controller('/users/') — no leading or trailing slash; Nest adds them.