Skip to content
NestJS ns guards 4 min read

Reflector & Custom Metadata

Guards, interceptors, and pipes often need to make decisions based on intent declared at the route level — “this handler is public”, “this one requires the admin role”, “cache this response for 30 seconds”. NestJS expresses that intent as metadata attached to controllers and route handlers, and reads it back at runtime through the Reflector helper. Mastering this attach-and-read pattern is the foundation of clean, declarative authorization in Nest.

Attaching metadata with SetMetadata

The built-in @SetMetadata(key, value) decorator stamps an arbitrary key/value pair onto a handler or a whole controller. The key is what you later look up; the value can be anything serializable.

import { Controller, Get, SetMetadata } from '@nestjs/common';

@Controller('reports')
export class ReportsController {
  @Get()
  @SetMetadata('roles', ['admin', 'auditor'])
  findAll() {
    return ['Q1', 'Q2'];
  }
}

Using a raw string key everywhere is brittle and untyped, so the idiomatic approach is to wrap SetMetadata in a custom decorator that owns the key.

Building a typed custom decorator

A decorator factory keeps the key in one place and gives callers a clean, discoverable API. Export the key alongside the decorator so guards can reuse it.

// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export type Role = 'admin' | 'auditor' | 'user';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
// public.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

Now handlers read declaratively:

@Controller('reports')
export class ReportsController {
  @Public()
  @Get('status')
  status() {
    return { ok: true };
  }

  @Roles('admin', 'auditor')
  @Get()
  findAll() {
    return ['Q1', 'Q2'];
  }
}

Reading metadata with Reflector

Reflector is an injectable provider available everywhere. Inject it into a guard and call one of its lookup methods with your key and the relevant execution-context target.

// roles.guard.ts
import {
  CanActivate,
  ExecutionContext,
  Injectable,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY, Role } from './roles.decorator';

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

  canActivate(context: ExecutionContext): boolean {
    const required = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    // No @Roles() anywhere → route is open.
    if (!required?.length) return true;

    const { user } = context.switchToHttp().getRequest();
    return required.some((role) => user?.roles?.includes(role));
  }
}

Choosing the right lookup method

Reflector exposes four methods. The plain get reads a single target; the getAllAnd* variants accept an array of targets (typically [handler, class]) and combine the results, which is what you almost always want so that handler-level metadata can refine or override controller-level metadata.

MethodTargetsBehaviour
get(key, target)singleReturns the value on that one target, or undefined.
getAll(key, targets)arrayReturns an array of each target’s value (no merging).
getAllAndOverride(key, targets)arrayReturns the first defined value — handler wins over class.
getAllAndMerge(key, targets)arrayConcatenates arrays / merges objects from all targets.

Use getAllAndOverride for flags like @Public() where the closest declaration should win. Use getAllAndMerge when roles or scopes should accumulate from controller and handler together.

// Class declares ['user']; handler adds ['admin'].
const roles = this.reflector.getAllAndMerge<string[]>(ROLES_KEY, [
  context.getHandler(),
  context.getClass(),
]);
// → ['admin', 'user']

Tip: Order matters. getAllAndOverride([handler, class]) returns the handler’s value first; flip the array and the class would override the handler. Keep handler-first as the convention.

Putting it together in a global guard

Register the guard globally so every route is protected, then let @Public() opt specific routes out.

// auth.guard.ts (excerpt)
@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) return true;

    const req = context.switchToHttp().getRequest();
    return Boolean(req.headers['authorization']);
  }
}
// app.module.ts
import { APP_GUARD } from '@nestjs/core';

@Module({
  providers: [{ provide: APP_GUARD, useClass: AuthGuard }],
})
export class AppModule {}

A request to a non-public route without a token is rejected:

Output:

$ curl -i http://localhost:3000/reports
HTTP/1.1 403 Forbidden
{"statusCode":403,"message":"Forbidden resource","error":"Forbidden"}

Gotcha: Reflector only reads metadata that was set with Nest’s SetMetadata (or Reflect.defineMetadata). Plain object properties or static class fields are invisible to it — always go through a decorator.

Best Practices

  • Wrap SetMetadata in a named decorator (@Roles, @Public) and export the key constant so guards never hardcode strings.
  • Always pass type parameters to reflector.get*<T>() so the returned value is typed instead of any.
  • Read [context.getHandler(), context.getClass()] together so handler-level metadata can override or extend controller-level metadata.
  • Pick getAllAndOverride for “closest declaration wins” flags and getAllAndMerge for accumulating lists like roles or scopes.
  • Treat missing metadata as a deliberate default (e.g. open route or deny-all) rather than letting undefined slip through unchecked.
  • Keep guards stateless: derive everything from the ExecutionContext and metadata, never from instance fields.
Last updated June 14, 2026
Was this helpful?