We use cookies to enhance your experience on the site
CodeWorlds

Custom Decorators - seals and insignia of the empire

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!

What are Custom Decorators in the Roman world?

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.

Custom Parameter Decorators - senator's rings

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'
.

SetMetadata and Reflector - imperial edicts

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}

Custom Method Decorators - seals on documents

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}

applyDecorators - combining seals

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()
.

Practical applications

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!

Go to CodeWorlds