How to create and use custom decorators in NestJS?
Custom decorators in NestJS are TypeScript functions that either extract values from the request context or store metadata for guards and pipes to read, using Reflect.metadata for runtime reflection.
Theory
TL;DR
- Custom decorators work like labels on assembly line parts: attach one to a method or parameter, and NestJS reads it at runtime to adjust behavior automatically
- Two main types: param decorators (pull data from the request) and metadata decorators (store data for guards to read later)
createParamDecoratorfor request extraction,SetMetadatafor guard metadata,applyDecoratorsto bundle both- Decision rule: if you copy
req.userorSetMetadata('roles', ...)in 2+ places, create a decorator; for one-offs, use guards or pipes directly
Quick example
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user; // populated by JwtAuthGuard
return data ? user?.[data] ?? null : user ?? null;
},
);
// Controller usage
@Get('me')
getMe(@CurrentUser() user: User) {
return user; // full user object from request.user
}
@Get('email')
getEmail(@CurrentUser('email') email: string) {
return { email }; // just the email field
}data carries the argument you pass inside the decorator call. @CurrentUser('email') sets data to 'email'. No argument returns the full object. Returning null instead of undefined keeps TypeScript types honest in the controller.
Key difference: param vs metadata decorators
Param decorators run per request inside createParamDecorator and extract a value from ExecutionContext. Metadata decorators run once at class definition time via SetMetadata, storing key-value pairs that guards read later with Reflector. If you mix these up, the metadata is undefined at runtime and the guard silently allows or blocks everything, which is hard to trace.
When to use
- Repeated
request.userextraction across 2+ controllers: param decorator - Role or permission checks on multiple routes: metadata decorator paired with a guard
- Custom field validation: property decorator with
registerDecorator - Four decorators stacked on every protected endpoint: composed decorator with
applyDecorators - Single-use logic: skip the decorator, use guards or pipes directly
Decorator types and their APIs
Parameter decorator runs per request and receives ExecutionContext:
// current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] ?? null : user ?? null;
},
);Metadata decorator stores static data that guards read:
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);The guard reads this with Reflector.getAllAndOverride:
// roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(), // method-level first
context.getClass(), // then class-level
]);
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user?.roles?.includes(role));
}
}Composed decorator bundles multiple decorators into one call:
// auth.decorator.ts
import { applyDecorators, UseGuards, SetMetadata } from '@nestjs/common';
import { ApiBearerAuth, ApiUnauthorizedResponse } from '@nestjs/swagger';
export function Auth(...roles: string[]) {
return applyDecorators(
SetMetadata(ROLES_KEY, roles),
UseGuards(JwtAuthGuard, RolesGuard),
ApiBearerAuth(),
ApiUnauthorizedResponse({ description: 'Unauthorized' }),
);
}Instead of stacking four decorators on every protected route, you write @Auth('admin') once.
Property decorator for custom validation rules:
import { registerDecorator, ValidationOptions } from 'class-validator';
export function IsStrongPassword(validationOptions?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'isStrongPassword',
target: object.constructor,
propertyName,
options: validationOptions,
validator: {
validate(value: string) {
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).{8,}$/.test(value);
},
defaultMessage() {
return 'Password must include uppercase, lowercase, number, and special character';
},
},
});
};
}How it works internally
TypeScript compiles decorators as static function calls that run when the class is defined, not when a request arrives. They store key-value pairs via Reflect.defineMetadata in a WeakMap tied to the class or method. NestJS scans these during app bootstrap and wires the metadata into the DI system. When a request comes in, guards and pipes call Reflect.getMetadata to read what was stored.
Reflector.getAllAndOverride checks method-level metadata first, then class-level. Method wins when both are set. That is why you can put @Roles('admin') on a controller and override a specific route with @Roles('user'). The closest definition wins.
Common mistakes
Assuming HTTP context in all param decorators:
// Wrong: crashes in WebSocket or microservice transport
export const UserId = createParamDecorator(
(_, ctx) => ctx.switchToHttp().getRequest().user.id
);
// Right: check context type first
export const UserId = createParamDecorator(
(_, ctx) => {
if (ctx.getType() === 'http') {
return ctx.switchToHttp().getRequest().user?.id;
}
return ctx.switchToRpc().getData().user?.id;
}
);Returning undefined when the user is missing:
// Wrong: types say User, runtime gives undefined -> null ref errors
export const CurrentUser = createParamDecorator(
(_, ctx) => ctx.switchToHttp().getRequest().user
);
// Right: return null explicitly so callers handle it
export const CurrentUser = createParamDecorator(
(_, ctx) => ctx.switchToHttp().getRequest().user ?? null
);Missing reflect-metadata polyfill:
NestJS auto-imports reflect-metadata in main.ts. But if you move a decorator into a shared library without that import, Reflector.getMetadata returns undefined everywhere and guards stop working. Always confirm import 'reflect-metadata' runs before any decorator code.
Naming conflicts with NestJS built-ins:
// Wrong: shadows @Body() from @nestjs/common, breaks validation pipes
export const Body = createParamDecorator(...);
// Right: use a unique prefix
export const ParsedBody = createParamDecorator(...);Inline SetMetadata instead of a named constant:
// Wrong: typo-prone, duplicated string across guard and decorator
@SetMetadata('roles', ['admin'])
// Right: export the key, share it between decorator and guard
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);Real-world usage
@nestjs/passportuses a@User()param decorator for JWT user extraction- NestJS GraphQL examples use
@CurrentUser()inside resolvers, switching toGqlExecutionContextinstead of HTTP - RBAC in e-commerce backends (orders, inventory) uses
@Roles()withRolesGuardfor access control - API platforms combine everything into
@Auth()so@UseGuards,@ApiBearerAuth(), and@SetMetadatastay in sync on every route
Follow-up questions
Q: What is the difference between createParamDecorator and a method decorator?
A: createParamDecorator runs per request and extracts a value from ExecutionContext. A method decorator runs once at class definition time, typically to set static metadata via SetMetadata.
Q: Can the factory function inside createParamDecorator be async?
A: Yes. The function can await inside, for example to fetch a user from a database. It runs after guards complete, so request.user is already set if your auth guard ran first.
Q: How do you unit test a custom param decorator?
A: Create a mock ExecutionContext with the request data you need, then call the decorator factory directly. No full NestJS app is required for this.
Q: Why does Reflector.getAllAndOverride check both handler and class?
A: It merges metadata across the prototype chain, and the closest definition wins. This lets you set a default @Roles('admin') on a controller and override specific routes with a different value.
Q (senior): If @Roles('admin') is on the controller class and @Roles('user') is on a method, what does getAllAndOverride return?
A: It returns ['user']. getAllAndOverride starts from the handler (method) and picks the first defined value, so method-level metadata always beats class-level when both exist.
Examples
Basic: CurrentUser param decorator
// current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user; // set by JwtAuthGuard before this runs
return data ? user?.[data] ?? null : user ?? null;
},
);
// profile.controller.ts
@Controller('profile')
@UseGuards(JwtAuthGuard)
export class ProfileController {
@Get()
getMe(@CurrentUser() user: User) {
return user;
}
@Get('email')
getEmail(@CurrentUser('email') email: string) {
return { email };
}
}The data argument is whatever you pass inside the decorator call. Without it you get the full user object. Returning null instead of undefined means TypeScript callers see an honest type instead of a runtime surprise.
Intermediate: Roles decorator with RolesGuard
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
// roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user?.roles?.includes(role));
}
}
// orders.controller.ts
@Controller('orders')
@UseGuards(JwtAuthGuard, RolesGuard)
export class OrdersController {
@Post()
@Roles('customer', 'admin') // 403 if user.roles has neither
create(@Body() dto: CreateOrderDto) {
return this.ordersService.create(dto);
}
@Delete(':id')
@Roles('admin') // overrides class-level if one was set
remove(@Param('id') id: string) {
return this.ordersService.remove(id);
}
}getAllAndOverride checks getHandler() first (the method), then getClass() (the controller). Whichever is defined closest to the route wins.
Advanced: Composed Auth decorator
// auth.decorator.ts
import { applyDecorators, UseGuards, SetMetadata } from '@nestjs/common';
import { ApiBearerAuth, ApiUnauthorizedResponse } from '@nestjs/swagger';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { RolesGuard } from './guards/roles.guard';
export const ROLES_KEY = 'roles';
export function Auth(...roles: string[]) {
return applyDecorators(
SetMetadata(ROLES_KEY, roles),
UseGuards(JwtAuthGuard, RolesGuard),
ApiBearerAuth(),
ApiUnauthorizedResponse({ description: 'Unauthorized' }),
);
}
// users.controller.ts
@Controller('users')
export class UsersController {
@Get()
@Auth('admin')
findAll() {
return this.usersService.findAll();
}
@Get('me')
@Auth('customer', 'admin')
getProfile(@CurrentUser() user: User) {
return user;
}
}Without @Auth, each route needs four separate decorators. With it, the controller stays readable and Swagger docs stay accurate on every route. I have seen projects where forgetting @ApiBearerAuth() on a new route broke OpenAPI client generation - this pattern makes that impossible.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.