Architect of the empire! Senator Cicero noticed that many operations in controllers are repetitive - extracting user data from requests, checking roles, adding metadata. It's time to learn Custom Decorators - the art of creating your own seals and insignia that automate repetitive patterns in NestJS code!
In the Roman Empire, every official wore the insignia of their rank - a senator's ring, a praetor's toga, a consul's staff. These insignia automatically informed others about the person's permissions and role. Custom Decorators in NestJS work exactly the same way - they are special markers that you add to methods, classes, or parameters to automatically perform certain operations.
The most commonly created type of decorator are parameter decorators - they extract data from the request and inject it directly into controller method parameters. NestJS provides the
createParamDecorator function for this purpose.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);
14
15// Usage in controller
16@Controller('legiones')
17export class LegionController {
18 @Get('profile')
19 @UseGuards(JwtAuthGuard)
20 getProfile(@CurrentUser() user: any) {
21 // user = { id: 1, name: 'Marcus', rank: 'Centurio', legion: 'Legio X' }
22 return { message: `Ave, ${user.name}!`, profile: user };
23 }
24
25 @Get('rank')
26 @UseGuards(JwtAuthGuard)
27 getRank(@CurrentUser('rank') rank: string) {
28 // rank = 'Centurio' (only the specific field)
29 return { rank };
30 }
31}Notice that the
data argument in createParamDecorator is the value passed to the decorator - @CurrentUser('rank') means data === 'rank'.Another key tool are metadata.
SetMetadata allows you to attach additional information to a method or class, and Reflector allows you to read it later - for example, in a guard.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);
6
7// Usage
8@Controller('tributa')
9export class TributeController {
10 @Post()
11 @Roles('consul', 'praetor') // Only consul and praetor can create
12 createTribute(@Body() dto: any) {
13 return { message: 'Tribute created!' };
14 }
15
16 @Delete(':id')
17 @Roles('consul') // Only consul can delete
18 removeTribute(@Param('id') id: string) {
19 return { message: `Tribute ${id} removed` };
20 }
21}Metadata alone don't do anything - we need a guard that reads them using
Reflector: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 = access for everyone
17 if (!requiredRoles) 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}You can create your own method decorators that combine several decorators into one - like a seal on a document that simultaneously confirms authenticity, rank, and permissions.
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
applyDecorators function allows you to combine multiple decorators into one - like a master seal that replaces several smaller seals.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}
16
17// Usage - one decorator instead of four!
18@Controller('imperium')
19export class ImperiumController {
20 @Post('decree')
21 @Auth('consul', 'praetor') // Authorization + roles + Swagger
22 issueDecree(@Body() decree: any) {
23 return { message: 'Decree issued!', decree };
24 }
25
26 @Get('treasury')
27 @Auth('consul', 'quaestor') // Only consul and quaestor
28 getTreasury() {
29 return { treasury: 'State of the empire treasury' };
30 }
31}Thanks to
applyDecorators, your code is cleaner and easier to maintain. Instead of repeating the same four decorators on every endpoint, you use a single @Auth().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}Custom Decorators are a powerful NestJS tool - they allow you to create readable, reusable code. Every decorator is like a senator's seal - it carries a specific meaning and automatically performs certain operations!