NestJS Custom Decorators: Metadata, Reflector & Guards

Photo by Robosprout via Openverse (CC BY 2.0)
createParamDecorator builds a parameter decorator whose factory function runs on every matching request to extract and return a value for a handler argument, like pulling the user off the request object. SetMetadata does not run per request at all — it stamps a key/value pair onto a class or method's metadata once, when Nest scans your controllers at startup. Something else, usually a guard or interceptor using Reflector, has to explicitly read that metadata back later for it to have any effect.
This almost always means the guard is calling reflector.get or getAllAndOverride with the wrong metadata key, or checking context.getClass() instead of context.getHandler() (or vice versa) for where the decorator was actually applied. It can also happen if the guard is registered but never actually consulted because it's missing from @UseGuards() on the route, or a different guard earlier in the chain already returned true and short-circuited the check.
get() reads metadata from a single target, so it only checks the method or only the class, not both. getAllAndOverride() checks a list of targets in priority order and returns the first non-undefined value it finds, which is exactly the behavior you want for something like roles: a method-level @Roles() should override a controller-level default, and getAllAndOverride encodes that precedence in one call instead of manual fallback logic.
Yes — createParamDecorator's factory function receives the full ExecutionContext, so after calling context.switchToHttp().getRequest() you have the same request object available to any other Nest decorator, including params, query, and headers. It's common to build several small decorators this way (a Roles-style tag decorator is different — it uses SetMetadata rather than createParamDecorator — but request-derived decorators like CurrentUser, ClientIp, or RequestId all follow this same pattern).
The underlying ExecutionContext is transport-agnostic, but switchToHttp() specifically only makes sense for HTTP requests. For GraphQL resolvers you'd use GqlExecutionContext.create(context) instead to get at the resolver context, and for microservices you'd use switchToRpc(). The decorator declaration itself (createParamDecorator) doesn't change — only which switchTo method you call inside the factory function changes based on the transport.

Photo by Robosprout via Openverse (CC BY 2.0)
Every NestJS codebase past a certain size grows its own vocabulary of decorators — @CurrentUser, @Roles, @Public, @ApiVersion. They read like framework features, but they are almost always thin wrappers a team wrote in an afternoon. The part that trips people up is not the decorator syntax itself, it is the two-step relationship between attaching metadata at compile time and reading it back at request time. This post builds two of the most common custom decorators from scratch — a parameter decorator that pulls the authenticated user off the request, and a method decorator that tags a route with required roles — then wires a guard that reads that tag with Reflector to enforce it.
A NestJS decorator almost never runs business logic itself. createParamDecorator wraps a function that runs once per request to extract a value and hand it to your handler as an argument. SetMetadata does something different and, for beginners, more confusing: it does not run anything at request time at all. It stamps a key and value onto the class or method's metadata using Reflect.defineMetadata under the hood, at the moment the module is loaded. Nothing reads that metadata unless something goes looking for it. That something is almost always a guard, interceptor, or pipe holding an instance of Reflector, which is NestJS's built-in wrapper around the Reflect Metadata API. Keep those two facts separate in your head — decorators write metadata, Reflector reads metadata — and the rest of this pattern stops feeling like magic.
If you are ever confused about whether a decorator runs per-request or once at startup, ask what it wraps. createParamDecorator and interceptors run per-request. SetMetadata runs once, when Nest scans your controllers during bootstrap — it just leaves a note for something else to find later.
createParamDecorator takes a factory function with two arguments: an arbitrary data payload you pass when applying the decorator, and an ExecutionContext, which is NestJS's abstraction over whatever transport triggered the request — HTTP, RPC, or WebSocket. Inside an HTTP context you call context.switchToHttp().getRequest() to get the underlying Express or Fastify request object, which by this point already has a user property attached by an upstream auth guard (typically a Passport strategy). The decorator's job is just to reach into that request and hand back exactly the slice the handler asked for.
The factory below accepts an optional key. Call it as @CurrentUser() and you get the whole user object; call it as @CurrentUser('email') and you get just that property, typed correctly because the key is constrained to keyof AuthedUser. This narrows what a handler can accidentally destructure and keeps controller signatures self-documenting.
// current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from "@nestjs/common"
export interface AuthedUser {
id: string
email: string
roles: string[]
}
export const CurrentUser = createParamDecorator(
(data: keyof AuthedUser | undefined, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest()
const user: AuthedUser | undefined = request.user
// @CurrentUser() returns the whole user
// @CurrentUser("email") returns just that property
return data ? user?.[data] : user
},
)Applied to a controller, the decorator collapses what would otherwise be request.user reads scattered across every handler into a single reusable, typed argument.
// profile.controller.ts
import { Controller, Get, UseGuards } from "@nestjs/common"
import { AuthGuard } from "@nestjs/passport"
import { CurrentUser, AuthedUser } from "./current-user.decorator"
@Controller("profile")
export class ProfileController {
@Get()
@UseGuards(AuthGuard("jwt"))
getProfile(@CurrentUser() user: AuthedUser) {
return { id: user.id, email: user.email }
}
@Get("email")
@UseGuards(AuthGuard("jwt"))
getEmail(@CurrentUser("email") email: string) {
return { email }
}
}Reaching into @Req() request and pulling request.user manually works, but it couples every handler to Express's request shape and to wherever your auth guard happens to stash the user. Centralizing that lookup in one decorator means if you ever swap Passport strategies, rename the property, or move to a different auth library, there is exactly one place to update. It also plays nicely with automated testing — a param decorator's underlying factory function is just a plain function you can unit test in isolation, without spinning up a full HTTP request.
SetMetadata takes a key and a value and returns a decorator that attaches that pair to whatever class or method it's applied to. Calling SetMetadata directly on every route would work but reads poorly and invites typos in the key string, so the idiomatic NestJS pattern wraps it in a small factory function — conventionally named after what it represents, like Roles — that forwards a rest-parameter list of roles as the metadata value. Exporting the key as its own constant, ROLES_KEY, avoids ever having to retype the raw string 'roles' when a guard later needs to look the value back up.
// roles.decorator.ts
import { SetMetadata } from "@nestjs/common"
export const ROLES_KEY = "roles"
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles)Applied above a handler, @Roles('admin', 'finance-manager') reads like a plain-English access rule, but at this point it is purely descriptive — nothing enforces it yet. That enforcement is the guard's job.
// invoices.controller.ts
import { Controller, Delete, Param, UseGuards } from "@nestjs/common"
import { AuthGuard } from "@nestjs/passport"
import { Roles } from "./roles.decorator"
import { RolesGuard } from "./roles.guard"
@Controller("invoices")
export class InvoicesController {
@Delete(":id")
@Roles("admin", "finance-manager")
@UseGuards(AuthGuard("jwt"), RolesGuard)
remove(@Param("id") id: string) {
return { removed: id }
}
}A guard implements CanActivate, and Nest calls its canActivate method for every incoming request matched to a route the guard protects. Inject Reflector through the constructor — it is a built-in provider, so no module registration is needed — and call getAllAndOverride with the metadata key and an array of targets to check, ordered from most specific to least: the handler first, then the controller class. This lets a controller-level @Roles() act as a default for every route in that controller, while a method-level @Roles() on one specific handler overrides it. If no route in the call chain ever applied @Roles(), getAllAndOverride returns undefined, and the guard should treat that as 'no restriction' and allow the request through rather than silently rejecting it.
// roles.guard.ts
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 {
// getAllAndOverride checks the handler (method) first, then falls back
// to the class — so a controller-level @Roles() acts as a default that
// a method-level @Roles() can override.
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
])
// No @Roles() on this route at all — nothing to enforce, let it through.
if (!requiredRoles || requiredRoles.length === 0) {
return true
}
const { user } = context.switchToHttp().getRequest()
return requiredRoles.some((role) => user?.roles?.includes(role))
}
}A guard that treats a missing @Roles() decorator as automatic denial will lock every unannotated route in your app the moment the guard is registered globally. Always check for the undefined/empty case first and return true — the decorator's job is to opt routes into a restriction, not for the guard to assume one.
The plain-string version above works but tolerates typos like @Roles('admni') that TypeScript cannot catch. A small upgrade backs the Roles decorator and the guard with a shared enum or union type instead of bare strings, so the compiler rejects an unknown role at the call site rather than silently failing at runtime. Nothing about the Reflector call changes — getAllAndOverride is generic, so swapping the type parameter from string[] to Role[] is the entire migration.
Apply the guard globally with APP_GUARD in your root module once your Roles pattern stabilizes, rather than repeating @UseGuards(RolesGuard) on every controller. Routes that need no restriction simply never apply @Roles(), and the guard's undefined check lets them through automatically.
The same attach-then-reflect shape powers most of NestJS's own building blocks that feel like framework magic: the @Public() decorator many teams add to bypass a global auth guard, cache-control decorators read by an interceptor, or API-versioning decorators read by a custom route resolver. Once the two-step model is internalized — a decorator to write a marker, a Reflector call inside a guard or interceptor to read it back — building a new one is mostly picking the key name and deciding whether it belongs on a parameter, a method, or a class.
Reflector also exposes a plain get() for reading metadata off a single target and getAll() for reading off multiple targets without the override behavior. Reach for getAllAndOverride specifically when you want method-level metadata to take priority over class-level metadata, which is the common case for route guards.