Skip to content
ExponaBLOG
NESTJS · DAY 025 / 100

Custom parameter decorators: what developers should know

Clean controllers with @CurrentUser() instead of req.user everywhere.

Expona Admin8 min read

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:

typescript
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:

typescript
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:

typescript
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:

typescript
// 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:

typescript
@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:

typescript
// 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:

typescript
// 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:

typescript
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:

typescript
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:

typescript
// 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 scattered req.user references throughout your codebase.
  • Type your decorators with interfaces like AuthUser so 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.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles