Skip to content
NestJS ns patterns 4 min read

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 data argument is the value you pass inside the decorator parentheses (@CurrentUser('email')). Type it as a union or keyof to 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

GoalBuild withReads / writesApplies to
Extract request datacreateParamDecoratorReads ExecutionContextHandler parameters
Attach metadataSetMetadataWrites metadataMethods or classes
Read metadata at runtimeReflectorReads metadataGuards / interceptors
Bundle multiple decoratorsapplyDecoratorsComposes existingMethods or classes

Best practices

  • Export a typed wrapper around SetMetadata instead 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 (or getAllAndMerge) so method-level metadata can override or extend class-level defaults.
  • Prefer applyDecorators for 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.
Last updated June 14, 2026
Was this helpful?