Chapter 35 — Guards, Middleware, Interceptors, Pipes, Filters
📖 Overview
NestJS provides five composable building blocks for cross-cutting concerns:
| Construct | Purpose | Where it runs |
|---|---|---|
| Middleware | Pre-route work (logging, parsing) | Before guards |
| Guards | Authorization (true/false to allow/deny) | After middleware, before interceptors |
| Interceptors | Wrap handler — transform, cache, log timings | Around handler |
| Pipes | Transform/validate inputs | Before handler arguments are passed |
| Exception filters | Catch thrown errors | At the end (on error) |
Execution Order
Request
↓
[ Middleware ]
↓
[ Guards ] ← may reject 403
↓
[ Interceptors (before) ]
↓
[ Pipes ] ← validates/transforms params
↓
[ Controller method ]
↓
[ Interceptors (after) ] ← transform response
↓
[ Exception filters ] ← if anything threw
↓
Response💻 Code Example — Middleware
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`[${req.method}] ${req.url}`);
next();
}
}
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes("*");
}
}💻 Code Example — Auth Guard
@Injectable()
export class JwtAuthGuard implements CanActivate {
canActivate(ctx: ExecutionContext): boolean {
const req = ctx.switchToHttp().getRequest();
const token = req.headers.authorization?.split(" ")[1];
if (!token) throw new UnauthorizedException();
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
return true;
} catch {
throw new UnauthorizedException("Invalid token");
}
}
}
@Controller("orders")
@UseGuards(JwtAuthGuard)
export class OrdersController { /* ... */ }💻 Code Example — Roles Guard with Custom Decorator
// roles.decorator.ts
export const Roles = (...roles: string[]) => SetMetadata("roles", roles);
// roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(ctx: ExecutionContext): boolean {
const required = this.reflector.get<string[]>("roles", ctx.getHandler());
if (!required) return true;
const { user } = ctx.switchToHttp().getRequest();
return required.some(r => user?.roles?.includes(r));
}
}
// Usage
@Controller("admin")
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@Get("stats")
@Roles("admin")
getStats() { /* ... */ }
}💻 Code Example — Logging Interceptor (Timing)
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(ctx: ExecutionContext, next: CallHandler): Observable<any> {
const start = Date.now();
const req = ctx.switchToHttp().getRequest();
return next.handle().pipe(
tap(() => console.log(`${req.method} ${req.url} - ${Date.now() - start}ms`)),
);
}
}
@UseInterceptors(LoggingInterceptor)
@Controller("orders")
export class OrdersController { /* ... */ }💻 Code Example — Response Transform Interceptor
@Injectable()
export class WrapResponseInterceptor<T> implements NestInterceptor<T, { data: T }> {
intercept(ctx: ExecutionContext, next: CallHandler) {
return next.handle().pipe(map(data => ({ data, ts: Date.now() })));
}
}
// Apply globally
app.useGlobalInterceptors(new WrapResponseInterceptor());
// Now every response becomes: { data: ..., ts: ... }💻 Code Example — Cache Interceptor
@Injectable()
export class CacheInterceptor implements NestInterceptor {
private cache = new Map<string, any>();
intercept(ctx: ExecutionContext, next: CallHandler): Observable<any> {
const req = ctx.switchToHttp().getRequest();
const key = req.url;
if (this.cache.has(key)) {
return of(this.cache.get(key));
}
return next.handle().pipe(tap(data => this.cache.set(key, data)));
}
}💻 Code Example — Custom Pipe (ParseIntPipe is built-in)
@Injectable()
export class TrimPipe implements PipeTransform {
transform(value: any) {
if (typeof value === "string") return value.trim();
if (value && typeof value === "object") {
Object.keys(value).forEach(k => {
if (typeof value[k] === "string") value[k] = value[k].trim();
});
}
return value;
}
}
@Post()
create(@Body(new TrimPipe()) dto: CreateUserDto) { /* ... */ }
// Built-in pipes
@Get(":id")
get(@Param("id", ParseIntPipe) id: number) {}
@Get()
list(@Query("active", new DefaultValuePipe(true), ParseBoolPipe) active: boolean) {}💻 Code Example — Exception Filter
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const res = ctx.getResponse<Response>();
const req = ctx.getRequest<Request>();
const status = exception.getStatus();
const message = exception.getResponse();
res.status(status).json({
timestamp: new Date().toISOString(),
path: req.url,
error: message,
});
}
}
// Apply
app.useGlobalFilters(new HttpExceptionFilter());💻 Code Example — All-Errors Filter (Catch Anything)
@Catch() // no argument = catch all
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const res = host.switchToHttp().getResponse();
const status = exception instanceof HttpException ? exception.getStatus() : 500;
const body = exception instanceof HttpException ? exception.getResponse()
: { error: "Internal server error" };
res.status(status).json(body);
}
}💻 Code Example — Composing Everything
@Controller("users")
@UseGuards(JwtAuthGuard)
@UseInterceptors(LoggingInterceptor)
@UseFilters(HttpExceptionFilter)
export class UserController {
@Get(":id")
@Roles("admin")
get(@Param("id", ParseUUIDPipe) id: string) {
return this.service.findById(id);
}
}🌍 Real-World Impact
- Guards centralize auth — controllers stay clean.
- Interceptors wrap every endpoint with logging/metrics in one line.
- Pipes ensure inputs are validated and transformed before reaching business logic.
- Filters standardize error responses across the API.
🎯 Likely Interview Questions
- What's the order of execution: middleware, guard, interceptor, pipe, filter? (See diagram above.)
- Difference between a guard and middleware? — Guards have access to
ExecutionContext(controller class, handler metadata viaReflector); middleware does not. Use guards for auth. - What is an interceptor?
- How do you implement role-based access?
- How do you customize error responses globally?