Custom Decorators
Decorators are the syntactic backbone of NestJS — controllers, providers, and route handlers are all configured through them. Beyond the built-ins, NestJS lets you author your own decorators to extract request data, attach metadata for guards and interceptors to read, and bundle several decorators into one expressive annotation. This keeps controllers thin, removes boilerplate, and turns cross-cutting concerns into declarative, reusable building blocks.
Custom param decorators
A param decorator pulls a value out of the request context and injects it into a route handler argument. You build one with createParamDecorator, whose factory receives the data passed at the call site and the ExecutionContext. A classic use case is extracting the authenticated user that an auth guard placed on the request.
// src/auth/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export interface AuthUser {
id: string;
email: string;
roles: string[];
}
export const CurrentUser = createParamDecorator(
(data: keyof AuthUser | undefined, ctx: ExecutionContext): AuthUser | unknown => {
const request = ctx.switchToHttp().getRequest();
const user: AuthUser = request.user;
// When called as @CurrentUser('email') return just that property.
return data ? user?.[data] : user;
},
);
Now the controller reads cleanly, with no manual digging into the request object:
// src/users/users.controller.ts
import { Controller, Get } from '@nestjs/common';
import { CurrentUser, AuthUser } from '../auth/current-user.decorator';
@Controller('profile')
export class ProfileController {
@Get()
getProfile(@CurrentUser() user: AuthUser) {
return { message: `Hello ${user.email}`, roles: user.roles };
}
@Get('email')
getEmail(@CurrentUser('email') email: string) {
return { email };
}
}
Output:
GET /profile
{ "message": "Hello [email protected]", "roles": ["admin"] }
GET /profile/email
{ "email": "[email protected]" }
The
dataargument is the value you pass inside the decorator parentheses (@CurrentUser('email')). Type it as a union orkeyofto keep call sites type-safe and self-documenting.
Metadata decorators with SetMetadata
Some decorators don’t read the request — they tag a handler or class with metadata that a guard, interceptor, or pipe inspects later via Reflector. The primitive for this is SetMetadata(key, value). Rather than scatter raw string keys, wrap it in a named, typed decorator.
// src/auth/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
A guard then reads the metadata. getAllAndOverride checks the handler first, then falls back to the controller class, so method-level roles win over class-level defaults.
// src/auth/roles.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
@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?.length) return true; // no @Roles → public
const { user } = context.switchToHttp().getRequest();
return required.some((role) => user?.roles?.includes(role));
}
}
// src/admin/admin.controller.ts
import { Controller, Delete, UseGuards } from '@nestjs/common';
import { Roles } from '../auth/roles.decorator';
import { RolesGuard } from '../auth/roles.guard';
@Controller('admin')
@UseGuards(RolesGuard)
export class AdminController {
@Delete('cache')
@Roles('admin')
clearCache() {
return { cleared: true };
}
}
Composing decorators with applyDecorators
When several decorators always travel together, applyDecorators merges them into a single decorator. This is ideal for an “auth” annotation that combines metadata, guards, and Swagger documentation in one line.
// src/auth/auth.decorator.ts
import { applyDecorators, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiUnauthorizedResponse } from '@nestjs/swagger';
import { Roles } from './roles.decorator';
import { RolesGuard } from './roles.guard';
import { JwtAuthGuard } from './jwt-auth.guard';
export function Auth(...roles: string[]) {
return applyDecorators(
Roles(...roles),
UseGuards(JwtAuthGuard, RolesGuard),
ApiBearerAuth(),
ApiUnauthorizedResponse({ description: 'Authentication required' }),
);
}
// usage
@Delete('cache')
@Auth('admin')
clearCache() {
return { cleared: true };
}
One @Auth('admin') now applies the role metadata, both guards, and two OpenAPI annotations — keeping handlers declarative and consistent across the codebase.
Decorator types at a glance
| Goal | Build with | Reads / writes | Applies to |
|---|---|---|---|
| Extract request data | createParamDecorator | Reads ExecutionContext | Handler parameters |
| Attach metadata | SetMetadata | Writes metadata | Methods or classes |
| Read metadata at runtime | Reflector | Reads metadata | Guards / interceptors |
| Bundle multiple decorators | applyDecorators | Composes existing | Methods or classes |
Best practices
- Export a typed wrapper around
SetMetadatainstead of using raw string keys at call sites — it prevents typos and centralizes the key. - Share the metadata key constant (e.g.
ROLES_KEY) between the decorator and the consumer so they never drift. - Keep param decorator factories pure and fast — they run on every matching request; avoid I/O or heavy computation inside them.
- Use
reflector.getAllAndOverride(orgetAllAndMerge) so method-level metadata can override or extend class-level defaults. - Prefer
applyDecoratorsfor any combination you repeat more than twice; it documents intent and guarantees consistent ordering. - Co-locate decorators with the feature they serve (auth decorators in the auth module) and type their parameters strictly for editor autocompletion.