Guards, Interceptors & Pipes
Apply authentication, validation, logging, and response transformation with NestJS 11 guards, interceptors, and pipes.
Recipe
Quick-reference recipe card - copy-paste ready.
import { Controller, Get, Param, UseGuards, UseInterceptors, UsePipes, ValidationPipe } from "@nestjs/common";
import { AuthGuard } from "./auth.guard.js";
import { LoggingInterceptor } from "./logging.interceptor.js";
@Controller("users")
@UseGuards(AuthGuard)
@UseInterceptors(LoggingInterceptor)
export class UsersController {
@Get(":id")
@UsePipes(ValidationPipe)
findOne(@Param("id") id: string) {
return { id };
}
}Execution order: Middleware -> Guards -> Interceptors (before) -> Pipes -> Handler -> Interceptors (after) -> Exception Filters
Working Example
// auth.guard.ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) throw new UnauthorizedException();
req.userId = "user-42";
return true;
}
}
// logging.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from "@nestjs/common";
import { Observable, tap } from "rxjs";
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const start = Date.now();
const req = context.switchToHttp().getRequest();
return next.handle().pipe(
tap(() => console.log(`${req.method} ${req.url} ${Date.now() - start}ms`))
);
}
}
// roles.guard.ts
import { SetMetadata, Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
export const ROLES_KEY = "roles";
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
@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;
const req = context.switchToHttp().getRequest();
return roles.includes(req.userRole);
}
}What this demonstrates:
- Guard returns
booleanor throws to allow/deny access - Interceptor wraps handler with RxJS
pipefor before/after logic SetMetadata+Reflectorfor declarative role checks- Guards and interceptors are injectable (DI works)
Deep Dive
How It Works
| Component | Runs when | Purpose | Returns |
|---|---|---|---|
| Pipe | Before handler | Transform/validate input | Transformed value |
| Guard | Before handler | Auth/authorization | true or throw |
| Interceptor | Around handler | Logging, caching, mapping | Observable stream |
| Filter | On exception | Error response shaping | HTTP response |
Global Registration
// main.ts
app.useGlobalGuards(new AuthGuard());
app.useGlobalInterceptors(new LoggingInterceptor());
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));Built-in Pipes
| Pipe | Purpose |
|---|---|
ValidationPipe | class-validator DTO validation |
ParseIntPipe | String param to integer |
ParseUUIDPipe | Validate UUID format |
DefaultValuePipe | Default for optional params |
Gotchas
- Guard after interceptor in mind, not code - guards run before interceptors. Fix: put auth in guards, not interceptors.
- ValidationPipe without DTO decorators - validation does nothing. Fix: add
class-validatordecorators to DTO classes. - Interceptor errors not caught by filter - RxJS errors need
catchError. Fix: handle in interceptor or let exception filter catch. - Global guard blocks health check - K8s probes fail. Fix:
@Public()decorator with guard that skips marked routes. - Request-scoped guard in singleton - scope mismatch. Fix: match scopes or use Reflector for metadata.
- Logging PII in interceptor - log method/path/duration only, not bodies.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Express middleware | NestJS on Express, simple auth | Want declarative per-route guards |
| Fastify hooks | NestJS on Fastify adapter | Need Nest decorator DX |
| Middleware in NestJS | Raw request/response access (cors) | Auth (guards are better) |
| Manual checks in controller | Quick prototype | Production auth |
FAQs
Guard vs middleware for auth?
Guards have access to execution context (handler, class metadata) and integrate with Reflector. Middleware is lower-level. Prefer guards for auth.
Can interceptors modify the response?
Yes. Use map() operator to transform the return value. Or use tap() for side effects only.
How do I skip auth for specific routes?
Create a @Public() metadata decorator and check it in the AuthGuard with Reflector.
What is the difference between pipe and guard?
Pipes transform/validate data going into the handler. Guards decide if the handler should run at all.
How do I add request timing globally?
Global LoggingInterceptor with tap() measuring elapsed time. Or use OpenTelemetry interceptor.
Do pipes work on WebSocket gateways?
Yes. NestJS supports guards, pipes, and interceptors on WebSocket and RPC contexts too.
How does ValidationPipe compare to Fastify JSON Schema?
ValidationPipe uses class-validator decorators. Fastify uses JSON Schema. Different syntax, same goal.
Can I use multiple guards on one route?
Yes. @UseGuards(AuthGuard, RolesGuard) runs all guards in order. All must return true.
Related
- NestJS Basics - module structure
- Dependency Injection - injectable guards
- Security Middleware - Express equivalent
- Middleware Pattern - underlying concept
- NestJS Best Practices - section checklist
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.