Skip to main content

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)
  • createParamDecorator for request extraction, SetMetadata for guard metadata, applyDecorators to bundle both
  • Decision rule: if you copy req.user or SetMetadata('roles', ...) in 2+ places, create a decorator; for one-offs, use guards or pipes directly

Quick example

typescript
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.user extraction 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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/passport uses a @User() param decorator for JWT user extraction
  • NestJS GraphQL examples use @CurrentUser() inside resolvers, switching to GqlExecutionContext instead of HTTP
  • RBAC in e-commerce backends (orders, inventory) uses @Roles() with RolesGuard for access control
  • API platforms combine everything into @Auth() so @UseGuards, @ApiBearerAuth(), and @SetMetadata stay 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

typescript
// 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

typescript
// 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

typescript
// 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?