SetMetadata와 Reflector로 메타데이터 연결하기
경로 수준 메타데이터를 정의하고 Reflector 서비스를 통해 가드와 인터셉터 내부에서 다시 읽습니다
SetMetadata와 Reflector로 메타데이터 연결하기은(는) CoddyKit의 무료 NestJS Enterprise Backend APIs 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 NestJS Enterprise Backend APIs 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. NestJS Enterprise Backend APIs 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
Why Route Metadata?
In an enterprise NestJS API you often need to tag a route with extra information that guards, interceptors, or pipes can read later. Examples include required roles, permission flags, cache TTLs, or a flag that marks a route as public.
NestJS solves this with metadata reflection: you attach key/value data to a handler or controller class, then read it back at request time using the Reflector service. This keeps cross-cutting concerns out of your business logic.
SetMetadata— writes the metadata onto the route.Reflector— reads it back inside guards/interceptors.
Attaching Metadata with SetMetadata
SetMetadata(key, value) is a decorator factory from @nestjs/common. You give it a string key and any value, and apply it to a controller method (or whole class).
Here we tag the findAll handler with a list of roles allowed to call it. The metadata is stored against the route handler but does nothing on its own until something reads it.
import { Controller, Get, SetMetadata } from '@nestjs/common';
@Controller('reports')
export class ReportsController {
@Get()
@SetMetadata('roles', ['admin', 'manager'])
findAll() {
return ['Q1 report', 'Q2 report'];
}
}Custom Decorators Wrap SetMetadata
Calling @SetMetadata('roles', [...]) inline everywhere is repetitive and easy to typo. The idiomatic pattern is to wrap it in a named custom decorator so the key lives in exactly one place.
Now controllers read clearly with @Roles('admin'), and the magic string 'roles' is encapsulated.
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) =>
SetMetadata(ROLES_KEY, roles);Using the Custom Decorator
With the Roles decorator defined, controllers become declarative. The handler simply states who may access it; the enforcement logic lives elsewhere in a guard.
Exporting a shared ROLES_KEY constant is important: the decorator and the guard must agree on the exact key string, otherwise the read returns undefined.
import { Controller, Delete, Param } from '@nestjs/common';
import { Roles } from './roles.decorator';
@Controller('users')
export class UsersController {
@Delete(':id')
@Roles('admin')
remove(@Param('id') id: string) {
return `Deleted user ${id}`;
}
}Reading Metadata with Reflector
The Reflector service (from @nestjs/core) reads metadata back at runtime. Inject it into a guard, then call reflector.get(key, target) where target is usually the route handler returned by context.getHandler().
If no metadata was set, get returns undefined, so guards typically treat that as 'no restriction' and allow the request.
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>(
ROLES_KEY,
context.getHandler(),
);
if (!roles) return true; // no @Roles => open route
const req = context.switchToHttp().getRequest();
return roles.includes(req.user?.role);
}
}getAllAndOverride for Handler + Class
Metadata can sit on the handler or the whole controller class. To merge both correctly, use getAllAndOverride: it scans an ordered list of targets and returns the first defined value. Put the handler first so a method-level decorator overrides a class-level one.
Pass [context.getHandler(), context.getClass()] as the targets array.
const roles = this.reflector.getAllAndOverride<string[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
// Handler-level @Roles wins over class-level @RolesgetAllAndMerge for Combining Values
Sometimes you don't want override semantics — you want to combine metadata from both handler and class. getAllAndMerge concatenates arrays (or merges objects) from every target.
getAllAndOverride→ first defined value wins.getAllAndMerge→ all values combined into one array/object.
Use merge when permissions accumulate; use override when the most specific level should fully replace broader ones.
// Class: @Roles('staff') Handler: @Roles('admin')
const merged = this.reflector.getAllAndMerge<string[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
// merged => ['admin', 'staff']A Public Route Flag
A very common enterprise pattern is a global JwtAuthGuard that protects every route, plus an @Public() escape hatch for login or health-check endpoints. The decorator just sets a boolean flag.
The global guard reads that flag first and skips authentication when it is true.
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);Honoring the Public Flag in a Guard
Inside the global auth guard, read IS_PUBLIC_KEY with getAllAndOverride across handler and class. If it is true, short-circuit and allow the request before any JWT validation runs.
This is why metadata reflection is powerful: one decorator declaratively changes how an unrelated guard behaves.
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(
IS_PUBLIC_KEY,
[context.getHandler(), context.getClass()],
);
if (isPublic) return true;
return validateJwt(context); // your real check
}
}Reflector in Interceptors Too
Guards aren't the only consumers. Interceptors also receive an ExecutionContext, so they can read metadata the same way. A classic example is a per-route cache TTL.
Define a @CacheTtl(60) decorator with SetMetadata('cacheTtl', 60), then read it inside an interceptor to decide caching behavior dynamically.
@Injectable()
export class TtlInterceptor implements NestInterceptor {
constructor(private reflector: Reflector) {}
intercept(context: ExecutionContext, next: CallHandler) {
const ttl = this.reflector.get<number>(
'cacheTtl',
context.getHandler(),
) ?? 30;
console.log(`Caching for ${ttl}s`);
return next.handle();
}
}Plain TypeScript: The Core Idea
Under the hood, Nest's reflection is just storing values in a map keyed by the function and a metadata key. Here is the same idea in plain, runnable TypeScript with no framework: a tiny store, a 'decorator' that writes, and a 'reflector' that reads with override semantics.
type Target = Function;
const store = new Map<Target, Map<string, unknown>>();
function setMeta(key: string, value: unknown, t: Target) {
if (!store.has(t)) store.set(t, new Map());
store.get(t)!.set(key, value);
}
function getAllAndOverride<T>(key: string, targets: Target[]): T | undefined {
for (const t of targets) {
const v = store.get(t)?.get(key);
if (v !== undefined) return v as T;
}
return undefined;
}
function handler() {}
function controller() {}
setMeta('roles', ['staff'], controller);
setMeta('roles', ['admin'], handler);
const roles = getAllAndOverride<string[]>('roles', [handler, controller]);
console.log('Effective roles:', roles); // ['admin'] — handler winsQuick Check
You apply @Roles('staff') on the controller class and @Roles('admin') on a specific handler. Inside the guard you want the handler-level value to fully replace the class-level value. Which Reflector call should you use?
Recap
You learned how to attach and read route-level metadata in NestJS:
- SetMetadata(key, value) attaches data to a handler or class; wrap it in a named custom decorator (e.g.
@Roles(),@Public()) and share the key via a constant. - Reflector.get reads metadata from a single target like
context.getHandler(). - getAllAndOverride scans
[getHandler(), getClass()]and returns the first defined value — handler overrides class. - getAllAndMerge combines values from all targets instead of overriding.
- Both guards and interceptors consume metadata the same way, enabling clean cross-cutting concerns like roles, public routes, and cache TTLs.
AI 튜터와 함께 TypeScript을(를) 배우세요 — 무료
브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.
- 코스
- 20
- 레슨
- 76
자주 묻는 질문
“SetMetadata와 Reflector로 메타데이터 연결하기” 강의는 무료인가요?
네 — “SetMetadata와 Reflector로 메타데이터 연결하기” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 NestJS Enterprise Backend APIs 강의 전체를 잠금 해제할 수 있습니다. NestJS Enterprise Backend APIs 강의에는 총 4개의 강의가 포함되어 있습니다.
“SetMetadata와 Reflector로 메타데이터 연결하기”에서 뭘 배우나요?
경로 수준 메타데이터를 정의하고 Reflector 서비스를 통해 가드와 인터셉터 내부에서 다시 읽습니다 브라우저에서 직접 실행하는 실습 코드로 NestJS Enterprise Backend APIs을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
NestJS Enterprise Backend APIs을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 NestJS Enterprise Backend APIs은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.
“SetMetadata와 Reflector로 메타데이터 연결하기” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 NestJS Enterprise Backend APIs 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 NestJS Enterprise Backend APIs 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 매개변수 데코레이터로 요청 컨텍스트 읽기
- SetMetadata와 Reflector로 메타데이터 연결하기
- applyDecorators로 데코레이터 조합하기
- 공통 설정을 위한 클래스 수준 데코레이터