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.
The mental model: decorators attach, guards read
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.
Building a CurrentUser parameter decorator
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 decorator
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 }
}
}Why this beats reading request.user directly
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.
Building a Roles method decorator with SetMetadata
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 }
}
}Reading the metadata back with Reflector.getAllAndOverride
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.
Putting it together: enum-backed roles for type safety
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.
Where this pattern extends
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.
Sources
- NestJS official docs — Custom decorators (docs.nestjs.com/custom-decorators)
- NestJS official docs — Guards (docs.nestjs.com/guards)
- NestJS official docs — Execution context and Reflector (docs.nestjs.com/fundamentals/execution-context)
- NestJS official docs — Authorization with Roles and RolesGuard (docs.nestjs.com/security/authorization)

