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.
| Method | Targets | Behaviour |
|---|---|---|
get(key, target) | single | Returns the value on that one target, or undefined. |
getAll(key, targets) | array | Returns an array of each target’s value (no merging). |
getAllAndOverride(key, targets) | array | Returns the first defined value — handler wins over class. |
getAllAndMerge(key, targets) | array | Concatenates 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:
Reflectoronly reads metadata that was set with Nest’sSetMetadata(orReflect.defineMetadata). Plain object properties or static class fields are invisible to it — always go through a decorator.
Best Practices
- Wrap
SetMetadatain 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 ofany. - Read
[context.getHandler(), context.getClass()]together so handler-level metadata can override or extend controller-level metadata. - Pick
getAllAndOverridefor “closest declaration wins” flags andgetAllAndMergefor accumulating lists like roles or scopes. - Treat missing metadata as a deliberate default (e.g. open route or deny-all) rather than letting
undefinedslip through unchecked. - Keep guards stateless: derive everything from the
ExecutionContextand metadata, never from instance fields.