NestJS course Β· Module 2: Routing and Request Lifecycle
Custom Decorators - seals and insignia of the empire
In this lesson6
Senator Cicero looked through the empire's controllers and frowned. Every method had the same line, const user = req.user, every endpoint carried the same four decorators, and the list of roles was copied by hand into ten places. One typo and the treasury stands open. Rome solved this with insignia: a senator's ring or a praetor's toga told everyone at a glance who the wearer was and what he was allowed to do. In NestJS that job belongs to custom decorators - your own seals that you attach to a method, a class or a parameter.
The senator's ring: a parameter decorator
The decorator you will write most often is a parameter decorator. It pulls data out of the request and hands it straight to a method argument. You build it with createParamDecorator, which takes a factory with two arguments: data, the value written inside the decorator's parentheses, and ExecutionContext, which you already know from the guards lesson:
1// decorators/current-user.decorator.ts
2import { createParamDecorator, ExecutionContext } from '@nestjs/common';
3
4// @CurrentUser() decorator - extracts user data from the request
5export const CurrentUser = createParamDecorator(
6 (data: string, ctx: ExecutionContext) => {
7 const request = ctx.switchToHttp().getRequest();
8 const user = request.user; // Set earlier by AuthGuard
9
10 // If a specific field is provided, return only that field
11 return data ? user?.[data] : user;
12 },
13);The factory runs on every request, and NestJS puts its result in place of the parameter. The decorator fetches nothing from the database and does not change the request - it only reads request.user, which an authentication guard set earlier. The ?. operator prevents an error when there is no user.
This is how the legion controller uses it:
1// Usage in controller
2@Controller('legiones')
3export class LegionController {
4 @Get('profile')
5 @UseGuards(JwtAuthGuard)
6 getProfile(@CurrentUser() user: any) {
7 // user = { id: 1, name: 'Marcus', rank: 'Centurio', legion: 'Legio X' }
8 return { message: `Ave, ${user.name}!`, profile: user };
9 }
10
11 @Get('rank')
12 @UseGuards(JwtAuthGuard)
13 getRank(@CurrentUser('rank') rank: string) {
14 // rank = 'Centurio' (only the specific field)
15 return { rank };
16 }
17}Writing @CurrentUser('rank') means data === 'rank', so the factory returns just user.rank. One practical note: ValidationPipe does not validate parameters coming from custom decorators unless you set validateCustomDecorators: true.
An imperial edict: SetMetadata
The second tool is metadata, labels attached to a method or a class. SetMetadata(key, value) stores such a label, and we wrap it in our own decorator so the key is not repeated in every controller:
1// decorators/roles.decorator.ts
2import { SetMetadata } from '@nestjs/common';
3
4// Simple @Roles() decorator setting metadata
5export const Roles = (...roles: string[]) => SetMetadata('roles', roles);The rest parameter ...roles collects all the given roles into an array. Now we attach the edict to methods:
1// Usage
2@Controller('tributa')
3export class TributeController {
4 @Post()
5 @Roles('consul', 'praetor') // Only consul and praetor can create
6 createTribute(@Body() dto: any) {
7 return { message: 'Tribute created!' };
8 }
9
10 @Delete(':id')
11 @Roles('consul') // Only consul can delete
12 removeTribute(@Param('id') id: string) {
13 return { message: `Tribute ${id} removed` };
14 }
15}Watch out, because this is the most common misunderstanding: metadata alone blocks nothing. It is an edict posted in the forum - it only takes effect when someone reads it and enforces it.
The praetorian reads the edict: Reflector in a guard
The reader is a guard. Reflector from @nestjs/core reads metadata, and context.getHandler() points at the method that is about to handle the request:
1// guards/roles.guard.ts
2import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
3import { Reflector } from '@nestjs/core';
4
5@Injectable()
6export class RolesGuard implements CanActivate {
7 constructor(private reflector: Reflector) {}
8
9 canActivate(context: ExecutionContext): boolean {
10 // Read 'roles' metadata from the handler
11 const requiredRoles = this.reflector.get<string[]>(
12 'roles',
13 context.getHandler(),
14 );
15
16 // No required roles (or an empty list) = access for every signed-in user
17 if (!requiredRoles?.length) return true;
18
19 const request = context.switchToHttp().getRequest();
20 const user = request.user;
21
22 // Check if user has at least one required role
23 return requiredRoles.some(role => user?.roles?.includes(role));
24 }
25}No label means no requirements, so the guard lets the request through. The !requiredRoles?.length check also covers an empty list - without it, a decorator with no roles would block everyone, because [].some() returns false. Remember too that get() reads metadata only from the target you give it - here, the method (context.getHandler()). If you also put @Roles() on the class, use this.reflector.getAllAndOverride('roles', [context.getHandler(), context.getClass()]) - otherwise the class-level role is silently ignored.
The NestJS documentation calls SetMetadata the low-level approach and shows a typed variant:
1// decorators/roles.decorator.ts - variant with Reflector.createDecorator
2import { Reflector } from '@nestjs/core';
3
4export const Roles = Reflector.createDecorator<string[]>();
5// usage: @Roles(['consul', 'praetor'])
6// reading: this.reflector.get(Roles, context.getHandler())Here the key is the decorator itself, so a typo in a key name becomes impossible and TypeScript checks the type of the value. In new code this is the variant I pick. SetMetadata still works, and you will meet it in plenty of projects.
A public marker: @Public()
Not every decorator has to combine several others. This one sets a single label and exports the key as a constant, so a guard can read it without retyping the string:
1// decorators/public.decorator.ts
2import { SetMetadata } from '@nestjs/common';
3
4// Decorator marking an endpoint as public (no authorization)
5export const IS_PUBLIC_KEY = 'isPublic';
6export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
7
8// Usage
9@Controller('forum')
10export class ForumController {
11 @Get()
12 @Public() // This endpoint is public
13 getPublicPosts() {
14 return { posts: ['Message from Forum Romanum'] };
15 }
16}The marker alone opens nothing. It works together with a global authentication guard that first checks getAllAndOverride(IS_PUBLIC_KEY, ...) and, when the value is true, lets the request through without a token.
The great seal: applyDecorators
When you repeat the same set of decorators above your endpoints, applyDecorators merges them into one:
1// decorators/auth.decorator.ts
2import { applyDecorators, UseGuards, SetMetadata } from '@nestjs/common';
3import { ApiBearerAuth, ApiUnauthorizedResponse } from '@nestjs/swagger';
4import { JwtAuthGuard } from '../guards/jwt-auth.guard';
5import { RolesGuard } from '../guards/roles.guard';
6
7// Composite @Auth() decorator combining guard, roles, and documentation
8export function Auth(...roles: string[]) {
9 return applyDecorators(
10 SetMetadata('roles', roles),
11 UseGuards(JwtAuthGuard, RolesGuard),
12 ApiBearerAuth(),
13 ApiUnauthorizedResponse({ description: 'Unauthorized' }),
14 );
15}Inside are four ordinary decorators: role metadata, two guards and descriptions for Swagger. applyDecorators adds no new logic - it simply applies all of them at once.
1// Usage - one decorator instead of four!
2@Controller('imperium')
3export class ImperiumController {
4 @Post('decree')
5 @Auth('consul', 'praetor') // Authorization + roles + Swagger
6 issueDecree(@Body() decree: any) {
7 return { message: 'Decree issued!', decree };
8 }
9
10 @Get('treasury')
11 @Auth('consul', 'quaestor') // Only consul and quaestor
12 getTreasury() {
13 return { treasury: 'State of the empire treasury' };
14 }
15}One line replaces four, and the guards work exactly as before. Changing the set inside the Auth function updates the whole project at once.
Practical uses
Finally, two more metadata decorators which, together with @Auth() and @CurrentUser(), describe recruiting a legionary:
1// decorators/log-action.decorator.ts
2import { SetMetadata } from '@nestjs/common';
3
4// Decorator for logging actions in the empire chronicles
5export const LOG_ACTION_KEY = 'logAction';
6export const LogAction = (actionName: string) =>
7 SetMetadata(LOG_ACTION_KEY, actionName);
8
9// decorators/rate-limit.decorator.ts
10export const RATE_LIMIT_KEY = 'rateLimit';
11export const RateLimit = (maxRequests: number, windowMs: number) =>
12 SetMetadata(RATE_LIMIT_KEY, { maxRequests, windowMs });
13
14// Usage in controller
15@Controller('legiones')
16export class LegionController {
17 @Post('recruit')
18 @Auth('consul')
19 @LogAction('RECRUIT_LEGIONARY')
20 @RateLimit(10, 60000) // Max 10 recruitments per minute
21 recruitLegionary(@Body() data: any, @CurrentUser() user: any) {
22 return {
23 message: `${user.name} recruited a new legionary`,
24 legionary: data,
25 };
26 }
27}Remember the rule of the edict: @LogAction() only works with an interceptor that reads the key and writes an entry to the chronicle, and @RateLimit() only with a guard that counts requests. For rate limiting I recommend the ready-made @nestjs/throttler package and its @Throttle({ default: { limit: 10, ttl: 60000 } }) decorator instead of building a counter from scratch.
In the next lesson you will build the tribunals, that is exception filters, and in the authentication module @CurrentUser() will receive the user prepared by the JWT strategy.
Remember: a parameter decorator runs its factory on every request, while a metadata decorator is only a seal that works once a guard or an interceptor reads it.
Code for this lesson: src/custom-decorators.ts
1// Custom Decorators - Seals and Insignia of the Empire
2import { createParamDecorator, ExecutionContext, SetMetadata, applyDecorators, UseGuards } from '@nestjs/common';
3
4console.log("Custom Decorators - creating your own empire seals!");
5
6// ===========================================
7// 1. Custom Parameter Decorator
8// ===========================================
9
10const CurrentUser = createParamDecorator(
11 (data: string, ctx: ExecutionContext) => {
12 const request = ctx.switchToHttp().getRequest();
13 const user = request.user;
14 return data ? user?.[data] : user;
15 },
16);
17
18console.log("@CurrentUser() - extracts user data from request");
19console.log("@CurrentUser('rank') - extracts specific field");
20
21// ===========================================
22// 2. SetMetadata i Roles
23// ===========================================
24
25const Roles = (...roles: string[]) => SetMetadata('roles', roles);
26
27console.log("\n@Roles('consul', 'praetor') - sets required roles");
28console.log("Reflector.get('roles', handler) - reads roles in guard");
29
30// ===========================================
31// 3. applyDecorators - combining decorators
32// ===========================================
33
34function Auth(...roles: string[]) {
35 return applyDecorators(
36 SetMetadata('roles', roles),
37 // UseGuards(JwtAuthGuard, RolesGuard),
38 );
39}
40
41console.log("\napplyDecorators() - combines multiple decorators into one");
42console.log("@Auth('consul') = SetMetadata('roles', ...) (in the lesson also UseGuards and ApiBearerAuth)");
43
44// ===========================================
45// 4. Practical example
46// ===========================================
47
48const IS_PUBLIC_KEY = 'isPublic';
49const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
50
51const LOG_ACTION_KEY = 'logAction';
52const LogAction = (actionName: string) => SetMetadata(LOG_ACTION_KEY, actionName);
53
54console.log("\n=== CUSTOM DECORATORS SUMMARY ===");
55console.log("createParamDecorator - parameter decorator");
56console.log("SetMetadata - metadata assignment");
57console.log("Reflector - reading metadata in guard/interceptor");
58console.log("applyDecorators - combining multiple decorators");
59Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Which NestJS function do we use to create a custom parameter decorator (e.g., @CurrentUser())?
2. What does the 'data' argument represent in createParamDecorator((data, ctx) => ...)? E.g., when using @CurrentUser('rank').
3. What is the SetMetadata() function used for in NestJS?
4. What does the applyDecorators() function do in NestJS?
Hands-on tasks in the game
- Code editor
Write a @CurrentUser() decorator using createParamDecorator that returns request.user or a specific field user[data]
- Click in order
Arrange the elements of creating a custom parameter decorator in the correct order:
- Code editor
Write a @Roles() decorator using SetMetadata('roles', roles) and a RolesGuard with Reflector that checks user roles
- Horizontal ordering
Arrange the elements of a SetMetadata call to set required roles:
- Vertical ordering
Order the lines of the @Auth() decorator from the lesson from top to bottom: