Custom parameter decorators
I've seen countless NestJS controllers where req.user appears in nearly every handler, cluttering the signature and making the code harder to read. Custom parameter decorators solve this elegantly by extracting request data into a clean, typed decorator that you apply once and reuse everywhere. In this article, I'll show you how to build decorators like @CurrentUser(), @CurrentTenant(), and @Locale() using NestJS's createParamDecorator API—and why this pattern scales beautifully as your codebase grows.
Understanding createParamDecorator
At its core, createParamDecorator is a factory function that lets you define how to extract a piece of data from the execution context and inject it as a parameter. The signature takes two arguments: an optional data value and the ExecutionContext.
Here's the fundamental pattern:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(_: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);When you apply @CurrentUser() to a handler parameter, NestJS invokes that callback, extracts the user from the request, and passes it directly to your handler. No middleware boilerplate, no controller-level request inspection—just clean, declarative code.
The first argument (_ in this case) is optional data you can pass to the decorator, like @CurrentUser('id') to extract just the user ID. We'll explore that pattern later.
Building a typed @CurrentUser decorator
In production, you'll want to ensure type safety. Let's build a real-world example that integrates with your authentication layer:
import { createParamDecorator, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Request } from 'express';
export interface AuthUser {
id: string;
email: string;
role: 'admin' | 'user';
tenantId: string;
}
export const CurrentUser = createParamDecorator(
(data: keyof AuthUser | undefined, ctx: ExecutionContext): AuthUser => {
const request = ctx.switchToHttp().getRequest<Request& { user?: AuthUser }>();
if (!request.user) {
throw new UnauthorizedException('User not found in request');
}
// If a specific field is requested (e.g., @CurrentUser('id')), return it
if (data) {
return request.user[data];
}
return request.user;
},
);Now your controller becomes dramatically cleaner:
import { Controller, Get } from '@nestjs/common';
import { CurrentUser, AuthUser } from './decorators/current-user.decorator';
@Controller('profile')
export class ProfileController {
@Get()
getProfile(@CurrentUser() user: AuthUser) {
return { id: user.id, email: user.email, role: user.role };
}
@Get('tenant')
getTenant(@CurrentUser('tenantId') tenantId: string) {
return { tenantId };
}
}Compare this to the old pattern where you'd need @Req() req: Request, then access req.user.id, then validate it exists. With the decorator, concerns are separated: authentication happens upstream (via your auth guard), and the controller just declares what it needs.
Creating reusable decorators for common scenarios
Custom decorators shine when you have multiple cross-cutting concerns. In a multi-tenant SaaS, I typically create three or four companion decorators:
// current-tenant.decorator.ts
export const CurrentTenant = createParamDecorator(
(_: unknown, ctx: ExecutionContext): string => {
const request = ctx.switchToHttp().getRequest<Request & { user?: AuthUser }>();
if (!request.user?.tenantId) {
throw new BadRequestException('Tenant ID not found');
}
return request.user.tenantId;
},
);
// locale.decorator.ts
export const Locale = createParamDecorator(
(_: unknown, ctx: ExecutionContext): string => {
const request = ctx.switchToHttp().getRequest();
return request.headers['accept-language']?.split(',')[0] || 'en-US';
},
);
// client-ip.decorator.ts
export const ClientIp = createParamDecorator(
(_: unknown, ctx: ExecutionContext): string => {
const request = ctx.switchToHttp().getRequest();
return request.ip || request.connection.remoteAddress || '';
},
);Using these in a real handler:
@Post('invoices')
createInvoice(
@CurrentUser() user: AuthUser,
@CurrentTenant() tenantId: string,
@Locale() locale: string,
@Body() dto: CreateInvoiceDto,
) {
// All dependencies injected cleanly
return this.invoiceService.create(tenantId, user.id, dto, locale);
}Notice how the handler signature reads like a contract: "I need the current user, the current tenant, the client locale, and this DTO." No digging through req, no null-coalescing chains, no guessing where data comes from.
Decorators with guards and interceptors
The real power emerges when you combine decorators with guards. A typical auth guard populates request.user, and then your decorator extracts it:
// jwt.guard.ts
@Injectable()
export class JwtGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const token = this.extractToken(request);
if (!token) {
throw new UnauthorizedException('No token');
}
const payload = this.jwtService.verify(token);
request.user = {
id: payload.sub,
email: payload.email,
role: payload.role,
tenantId: payload.tenant,
};
return true;
}
private extractToken(req: Request): string | null {
const auth = req.headers.authorization;
return auth?.replace('Bearer ', '') || null;
}
}
// controller.ts
@Controller('orders')
@UseGuards(JwtGuard)
export class OrderController {
@Get()
listOrders(@CurrentUser() user: AuthUser) {
// Guard ensures request.user exists before this runs
return this.orderService.find({ userId: user.id });
}
}This is a clean separation of concerns: the guard handles authentication, the decorator handles extraction, the handler handles business logic.
Testing with custom decorators
One major benefit of decorators is testability. Instead of mocking the entire Express request object, you can pass test data directly:
// profile.controller.spec.ts
describe('ProfileController', () => {
let controller: ProfileController;
let service: ProfileService;
beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [ProfileController],
providers: [ProfileService],
}).compile();
controller = module.get(ProfileController);
service = module.get(ProfileService);
});
it('should return user profile', async () => {
const mockUser: AuthUser = {
id: '123',
email: 'test@example.com',
role: 'user',
tenantId: 'tenant-1',
};
jest.spyOn(service, 'getProfile').mockResolvedValue({
id: mockUser.id,
email: mockUser.email,
});
// Call handler directly with mocked user
const result = await controller.getProfile(mockUser);
expect(result).toEqual({ id: '123', email: 'test@example.com' });
});
});You're testing the business logic in isolation. The decorator's extraction logic is tested separately via unit tests that mock the execution context.
Advanced: Optional decorators and defaults
Sometimes you want a decorator to be optional. Use the second argument to createParamDecorator` to specify a default:
export const OptionalUser = createParamDecorator(
(_: unknown, ctx: ExecutionContext): AuthUser | null => {
const request = ctx.switchToHttp().getRequest();
return request.user || null;
},
);
// In a public endpoint
@Get('featured-products')
getFeatured(@OptionalUser() user: AuthUser | null) {
// Recommend personalized products if user exists, else general trending
return user
? this.productService.getPersonalized(user.id)
: this.productService.getTrending();
}You can also stack decorators for composite extraction:
export const UserAndTenant = createParamDecorator(
(_: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return {
user: request.user,
tenant: request.tenant,
};
},
);
@Post('reports')
generateReport(
@UserAndTenant() { user, tenant }: { user: AuthUser; tenant: Tenant },
) {
return this.reportService.generate(tenant.id, user.id);
}Common mistakes
Mistake 1: Not validating data in the decorator. A decorator that assumes request.user exists will silently return undefined if the guard fails to run. Always throw an error if required data is missing:
// Bad: silently returns undefined
const CurrentUser = createParamDecorator(
(_, ctx) => ctx.switchToHttp().getRequest().user,
);
// Good: explicitly rejects
const CurrentUser = createParamDecorator(
(_, ctx) => {
const user = ctx.switchToHttp().getRequest().user;
if (!user) throw new UnauthorizedException('User not found');
return user;
},
);Mistake 2: Overloading decorators with too much logic. Decorators should extract and validate, not perform business transformations. Keep that in services.
Mistake 3: Mixing contexts without switchToHttp(). In GraphQL or WebSocket contexts, you'd use switchToGraphql() or switchToWs(). Always check the protocol before accessing request data.
Mistake 4: Not typing decorator data parameters. If you use the first argument for conditional extraction, declare its type explicitly to catch bugs early.
Key takeaways
- Custom parameter decorators extract request data cleanly via
createParamDecorator, replacing scatteredreq.userreferences throughout your codebase. - Type your decorators with interfaces like
AuthUserso handlers are self-documenting and type-safe. - Build reusable decorators for common concerns:
@CurrentUser(),@CurrentTenant(),@Locale(),@ClientIp(). - Pair decorators with guards to separate authentication (guard) from extraction (decorator) from business logic (handler).
- Always validate in decorators—throw meaningful errors if required data is absent rather than returning undefined.
- Test handlers by calling them directly with mock data, bypassing the decorator layer entirely.
- Keep decorator logic minimal; offload transformations to services or interceptors.






Comments
Loading comments…