Custom Decorators

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against NestJS 12.1 (npm 12.1.2), TypeScript 6 (6.0.3), Node.js 20.19+ or 22.12+, Express 5 and Fastify 5, and was checked on 2026-10-02. The examples use the Nest 12 CLI project layout (ESM, Vitest and supertest) and run on Express and, where it matters, on Fastify.

A decorator in Nest is how you attach information or behaviour to a class, method or parameter without touching its body. Nest ships many (@Controller(), @Get(), @Body(), @UseGuards()), and it gives you four small tools to write your own without dealing with the raw decorator signatures described in TypeScript for NestJS and Decorators and Metadata:

Tool What it produces Typical use

createParamDecorator()

A parameter decorator whose value is computed from the execution context, per request.

@CurrentUser(), @TenantId(), @ClientIp()

applyDecorators()

One decorator that applies several others.

@Auth('admin') = metadata + guard + Swagger annotations

Reflector.createDecorator()

A typed metadata decorator that a guard or interceptor reads back through Reflector.

@Roles(['admin']), @Public()

DiscoveryService.createDecorator()

A typed metadata decorator whose decorated classes can be found at runtime.

Plugin and feature-registration systems

Parameter decorators with createParamDecorator()

Nest already provides parameter decorators for the framework objects: @Req(), @Res(), @Body(), @Query(), @Param(), @Headers(), @Ip(), @Session() and @HostParam(). A common Node.js habit is to read something a previous layer attached to the request (usually req.user) in every handler. A custom parameter decorator replaces that line with a named, reusable declaration.

The decorator is built from a factory that receives two arguments: data (whatever you pass to the decorator when you apply it) and an ExecutionContext (see the Guards page for what it offers). The factory runs on every request, when Nest resolves the handler’s arguments; the decorator itself runs once, when the class is defined.

current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export interface AuthenticatedUser {
  id: number;
  email: string;
  roles: string[];
}

// Generic arguments: the type of `data` and the type of the produced value.
export const CurrentUser = createParamDecorator<
  keyof AuthenticatedUser | undefined,
  unknown
>((data, ctx: ExecutionContext) => {
  const request = ctx.switchToHttp().getRequest<{ user?: AuthenticatedUser }>();
  const user = request.user;
  return data ? user?.[data] : user;
});

The decorator reads request.user, which the Bookshelf authentication layer sets before the handler runs. Authentication itself is covered in the security pages; for this page a stand-in guard fills request.user from an x-user header, so the examples are runnable without a token library.

fake-auth.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';

// Stand-in for a real authentication layer: it only turns the x-user header
// into request.user so that the decorators below have something to read.
@Injectable()
export class FakeAuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const header = request.headers['x-user'];
    if (typeof header === 'string') {
      const [id, email, roles] = header.split(';');
      request.user = { id: Number(id), email, roles: roles ? roles.split(',') : [] };
    }
    return true;
  }
}

Use the decorator in a controller:

books.controller.ts (excerpt)
@Controller('books')
@UseGuards(FakeAuthGuard)
export class BooksController {
  @Get('me')
  whoAmI(@CurrentUser() user: AuthenticatedUser) {
    return user ?? null;
  }
}
curl -H 'x-user: 1;ada@example.com;reader' http://localhost:3000/books/me
{ "id": 1, "email": "ada@example.com", "roles": ["reader"] }

Things worth knowing about the factory:

  • Types. createParamDecorator<FactoryData, FactoryOutput>() is generic; if you give neither type argument nor annotate data, data is any. The generic output type is not checked against the parameter you decorate, so annotate the parameter yourself (user: AuthenticatedUser).

  • Transport. ctx.switchToHttp() is correct only for HTTP handlers. The same decorator on a microservice, WebSocket or GraphQL handler gets a different context; check ctx.getType() ('http', 'rpc', 'ws', or 'graphql' for GraphQL) or write one decorator per transport.

  • Platform. The request object is Express’s or Fastify’s; reading headers and assigning request.user in a guard work the same on both. The example’s endpoints returned identical results on the Express and Fastify adapters.

  • Lifecycle position. Custom parameter decorators are resolved after guards and interceptors have run and before the handler, together with the built-in ones, so a guard can populate request.user for the decorator to read. See Request Lifecycle.

  • Where the book is outdated: books and tutorials often write @User() against req.user without ever saying where it is populated, or read the whole request with @Req() inside every handler. The decorator keeps controllers free of the HTTP object, which also makes them easier to unit test.

Passing data

The argument you pass in the parentheses becomes data. The @CurrentUser() decorator above already supports it: with a property name it returns just that property (or undefined if there is no user).

books.controller.ts (excerpt)
@Get('me/email')
myEmail(@CurrentUser('email') email: string) {
  return { email };
}
curl -H 'x-user: 1;ada@example.com;reader' http://localhost:3000/books/me/email
{ "email": "ada@example.com" }

Without the x-user header the handler receives undefined and the response body is {}, with status 200. A custom decorator that reads from the request does not make a route protected; a guard does. Use the decorator to read, and a guard to decide.

Pipes and validation on custom decorators

Custom parameter decorators go through the same pipe machinery as @Body(), @Param() and @Query(), so you can pass a pipe where you would pass data. There is one catch: ValidationPipe skips arguments produced by custom decorators unless you opt in with validateCustomDecorators: true.

user.dto.ts and books.controller.ts (excerpt)
import { IsEmail, IsInt, IsString } from 'class-validator';

export class UserDto {
  @IsInt()
  id!: number;

  @IsEmail()
  email!: string;

  @IsString({ each: true })
  roles!: string[];
}
@Get('me/validated')
validated(
  @CurrentUser(new ValidationPipe({ validateCustomDecorators: true }))
  user: UserDto,
) {
  return user;
}
curl -i -H 'x-user: 1;not-an-email;reader' http://localhost:3000/books/me/validated
{ "message": ["email must be an email"], "error": "Bad Request", "statusCode": 400 }

A valid user is returned unchanged. Pipes are covered in Pipes & Validation.

Validating with a Standard Schema

Nest 12 also lets parameter decorators take a schema option, anything that implements Standard Schema (Zod, Valibot, ArkType). Custom decorators accept the same option as the built-in ones. A StandardSchemaValidationPipe with validateCustomDecorators enabled applies it. The example uses Zod’s z.uuid() to validate a tenant header.

tenant-id.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const TenantId = createParamDecorator(
  (_data: unknown, ctx: ExecutionContext): string | undefined =>
    ctx.switchToHttp().getRequest().headers['x-tenant-id'],
);
Registering the pipe and using the decorator
app.useGlobalPipes(
  new StandardSchemaValidationPipe({ validateCustomDecorators: true }),
);
@Get('tenant')
tenant(@TenantId({ schema: z.uuid() }) tenantId: string) {
  return { tenantId };
}
curl -H 'x-tenant-id: nope' http://localhost:3000/books/tenant
{ "message": ["Invalid UUID"], "error": "Bad Request", "statusCode": 400 }

A valid UUID is returned as { "tenantId": "…​" }, and a missing header is rejected as well (Invalid input: expected string, received undefined, status 400). Without validateCustomDecorators: true the schema is ignored for custom decorators.

Composing decorators with applyDecorators()

applyDecorators() takes any number of decorators and returns one that applies them all. Use it when the same group of decorators keeps appearing together: here, the role metadata, the guard that enforces it, and the Swagger annotations that document it. The @Roles() decorator is the metadata decorator introduced in the next section.

auth.decorator.ts
import { applyDecorators, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiForbiddenResponse } from '@nestjs/swagger';
import { Roles } from './roles.decorator.js';
import { RolesGuard } from './roles.guard.js';

export function Auth(...roles: string[]) {
  return applyDecorators(
    Roles(roles),
    UseGuards(RolesGuard),
    ApiBearerAuth(),
    ApiForbiddenResponse({ description: 'Missing role' }),
  );
}
books.controller.ts (excerpt)
@Post()
@Auth('librarian')
create(@CurrentUser('id') id: number) {
  return { createdBy: id };
}
curl -i -X POST -H 'x-user: 1;ada@example.com;reader' http://localhost:3000/books
curl -i -X POST -H 'x-user: 2;grace@example.com;librarian' http://localhost:3000/books

The first request is rejected with 403 Forbidden; the second returns 201 with { "createdBy": 2 }.

One limit to remember: the official docs note that @ApiHideProperty() from @nestjs/swagger is not composable and does not work correctly with applyDecorators().

Metadata decorators with Reflector.createDecorator()

A metadata decorator does nothing at runtime by itself; it records a value that a guard, interceptor or other component reads back later. Reflector.createDecorator<T>() creates such a decorator with a typed argument and no key to manage:

roles.decorator.ts and roles.guard.ts
import { Reflector } from '@nestjs/core';

export const Roles = Reflector.createDecorator<string[]>();
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from './roles.decorator.js';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const required = this.reflector.getAllAndOverride(Roles, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!required) {
      return true;
    }
    const { user } = context.switchToHttp().getRequest();
    return required.some((role) => user?.roles?.includes(role));
  }
}

The decorator is a function, so @Roles(['admin']) is type-checked, and reflector.get(Roles, target) returns string[] | undefined without a cast. Reflector is available for injection without importing a module. getAllAndOverride() returns the first defined value among the targets (handler first, then class), getAllAndMerge() concatenates them. The createDecorator() options, checked against the installed typings, are:

Option Meaning

key

Use an explicit metadata key instead of a generated one. The decorator exposes the key as .KEY.

transform

A function that converts the decorator’s argument before it is stored; the second type argument is the stored type.

transform and key
import { Reflector } from '@nestjs/core';

// transform runs on the argument before it is stored as metadata.
export const Scopes = Reflector.createDecorator<string, string[]>({
  transform: (value) => value.split(' '),
});

// key sets an explicit metadata key instead of a generated one.
export const Audited = Reflector.createDecorator<boolean>({ key: 'audited' });

With @Scopes('read write') on a handler, reflector.get(Scopes, handler) returned ['read', 'write'], and reflector.get<boolean>('audited', handler) read the Audited value through its explicit key.

The low-level alternative: SetMetadata()

SetMetadata(key, value) is the primitive underneath. A decorator written with it takes arguments of your choosing (such as rest parameters) and uses a string key you control:

import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

It is read back with reflector.get<string[]>('roles', context.getHandler()). The cost is that the key is a bare string and nothing ties the reader’s type to the writer’s. Reflector.createDecorator() is the form the Nest 12 CLI generates for nest g decorator, and the one the docs use now; prefer it for new code. Guards that consume these decorators, including a global guard with a @Public() opt-out, are covered in the Guards page.

Discoverable decorators with DiscoveryService.createDecorator()

Reflector answers "what metadata does this handler have?" when a guard or interceptor is already running for a request. DiscoveryService answers the opposite question at startup: "which providers or controllers carry this decorator?" DiscoveryService.createDecorator<T>() creates a decorator that both stores metadata and registers the decorated class so it can be found.

feature-flag.decorator.ts and experimental.service.ts
import { DiscoveryService } from '@nestjs/core';

export const FeatureFlag = DiscoveryService.createDecorator<string>();
@Injectable()
@FeatureFlag('experimental')
export class ExperimentalService {}
feature-flags.service.ts
@Injectable()
export class FeatureFlagsService implements OnModuleInit {
  readonly flags = new Map<string, string>();

  constructor(private readonly discovery: DiscoveryService) {}

  onModuleInit() {
    const wrappers = this.discovery.getProviders({ metadataKey: FeatureFlag.KEY });
    for (const wrapper of wrappers) {
      const flag = this.discovery.getMetadataByDecorator(FeatureFlag, wrapper);
      this.flags.set(wrapper.name as string, flag as string);
    }
  }
}

The module must import DiscoveryModule (from @nestjs/core) for DiscoveryService to be injectable. At startup the map contained ExperimentalService mapped to experimental. Passing metadataKey: FeatureFlag.KEY returns only the decorated classes instead of every provider.

The two creators differ in purpose:

Reflector.createDecorator() DiscoveryService.createDecorator()

Read with

Reflector#get(), getAllAndOverride(), getAllAndMerge()

DiscoveryService#getMetadataByDecorator(), getProviders({ metadataKey })

Question answered

What does the handler or class I am currently executing declare?

Which classes in the whole application declare this?

Typical reader

Guards, interceptors, filters, at request time

Providers that register things at startup (plugins, schedulers, event handlers)

Choosing the right tool

  • Computing a value for a handler argument from the request: createParamDecorator().

  • Declaring data a guard or interceptor needs: Reflector.createDecorator().

  • Finding classes at startup: DiscoveryService.createDecorator().

  • Bundling several decorators under one name: applyDecorators().

  • Needing behaviour (not only data) that varies per handler, such as rejecting or transforming a request: write a guard, pipe or interceptor and combine it with a metadata decorator via applyDecorators().

Do not hide expensive work or side effects in a parameter decorator’s factory: it runs on every request, before the handler, and exceptions it throws surface as the response, so it is a poor place for business logic.

What changed recently

  • Nest 12 keeps createParamDecorator() and applyDecorators() unchanged and documents Reflector.createDecorator() as the standard way to define metadata decorators; the CLI’s decorator schematic now generates it.

  • Standard Schema support (the schema option on parameter decorators and StandardSchemaValidationPipe) is new in the Nest 12 docs and covers custom decorators through validateCustomDecorators. See Pipes & Validation.

  • DiscoveryService.createDecorator() and the metadataKey filter let you find decorated classes without scanning every provider.

  • Decorators here are still the legacy (experimentalDecorators) kind; Nest does not support Stage 3 decorators.

Source: Migration guide.

Where the book is outdated

The reference book (see the section bibliography) builds its @Roles decorator with SetMetadata('roles', roles) and reads it with a string key. That still works, but Reflector.createDecorator() is now the documented form: typed, with no key to mistype. The book’s parameter-decorator examples also never mention validateCustomDecorators; without it, a ValidationPipe silently skips custom-decorator values.