Guards, Pipes, and Interceptors
Architecting the Gatekeeper
In Express, we wrote a middleware function to protect routes. It worked, but it was not tightly integrated with our application's architecture.
function authMiddleware(req, res, next) {
// Manually check header
// Manually verify token
// Manually attach user to `req`
// Manually handle errors
// Call `next()`
}
app.get('/profile', authMiddleware, getProfile);
A Guard is a class with a single responsibility: it determines whether a given request will be handled by the route handler or not. It makes a "yes" or "no" decision.
They are the perfect tool for authentication and authorization.
A Guard implements the `CanActivate` interface, which requires a single method: `canActivate()`.
This guard will implement our JWT verification logic.
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; // A NestJS provided service
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, { secret: 'YOUR_SECRET' });
// We're assigning the payload to the request object here
// so that we can access it in our route handlers
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true; // If no error was thrown, access is granted
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
Guards are applied using the `@UseGuards()` decorator. This is far cleaner than passing middleware functions manually.
import { UseGuards, Get, Req } from '@nestjs/common';
import { AuthGuard } from '../auth/auth.guard';
@Controller('profile')
export class ProfileController {
@UseGuards(AuthGuard) // Apply the guard to this route
@Get()
getProfile(@Req() req) {
// Because the guard ran, we know `req.user` exists
return req.user;
}
}
You can apply `@UseGuards` at the method level (for a single route) or at the class level (for all routes in a controller).
Transformation and Validation
A Pipe is a class that operates on the arguments being passed to a route handler. It has two primary use cases:
NestJS comes with a powerful set of built-in pipes that handle common tasks.
This pipe automatically handles the validation and transformation of route parameters.
Before (Manual):
@Get(':id')
findOne(@Param('id') id: string) {
const numericId = parseInt(id, 10);
if (isNaN(numericId)) {
throw new BadRequestException('ID must be a number.');
}
return this.usersService.findOne(numericId);
}
After (Using the Pipe):
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
// `id` is now guaranteed to be a number.
// The pipe handles the error if it's not.
return this.usersService.findOne(id);
}
Binding Cross-Cutting Concerns
An Interceptor is a class that allows you to run logic before and after a route handler executes. It can:
They are perfect for cross-cutting concerns like logging, caching, or standardizing your API response format.
This interceptor will log the time it takes for a request to be processed.
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable {
console.log('Before...'); // Logic before the handler runs
const now = Date.now();
return next
.handle() // This calls the route handler
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)), // Logic after the handler runs
);
}
}
Interceptors use RxJS `Observables`, which is a powerful library for handling asynchronous streams.
A professional API should always have a consistent response structure. We can enforce this with an interceptor.
**Goal:** Transform all successful responses from `` into `{ "statusCode": 200, "data": }`.
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<{ statusCode: number, data: T }> {
const statusCode = context.switchToHttp().getResponse().statusCode;
return next.handle().pipe(
map(data => ({ statusCode, data }))
);
}
}
Like Guards, Interceptors are applied with a decorator: `@UseInterceptors()`.
If you apply this globally in `main.ts`, every single response from your API will be automatically wrapped in the standard format!
// main.ts
app.useGlobalInterceptors(new TransformInterceptor());
// In your controller
@Get()
findAll() {
return this.usersService.findAll(); // This just returns an array of users
}
// The client receives:
// {
// "statusCode": 200,
// "data": [ { "id": 1, "name": "Alex" }, ... ]
// }
You will be given a NestJS project with a working JWT `AuthGuard`. Your task is to create a new `RolesGuard` to implement simple authorization.
@UseGuards(AuthGuard, RolesGuard)
@SetMetadata('roles', ['admin'])
@Delete(':id')
remove(...) { /* ... */ }