When people start with NestJS, they often learn pieces in isolation—guards, pipes, interceptors, middleware—but don’t see how everything fits together.
This guide connects all the building blocks into one clear mental model, with simple examples you can actually use in your project.
Every request in NestJS goes through a structured pipeline:
Request→ Middleware→ Guards→ Interceptors (before)→ Pipes→ Controller→ Service→ Interceptors (after)→ Exception Filters→ Response
Each block has one responsibility. That’s why NestJS scales well.
@Injectable()export class LoggerMiddleware implements NestMiddleware {use(req: Request, res: Response, next: () => void) {console.log(`${req.method} ${req.url}`);next();}}
👉 Runs before everything
@Injectable()export class AuthGuard implements CanActivate {canActivate(context: ExecutionContext): boolean {const req = context.switchToHttp().getRequest<Request>();return !!req.headers.authorization;}}
👉 Decides: Can user access this route?
@Injectable()export class ParseIntPipe implements PipeTransform<string, number> {transform(value: string): number {const val = parseInt(value, 10);if (isNaN(val)) throw new BadRequestException('Invalid number');return val;}}
👉 Keeps controllers clean
@Injectable()export class TransformInterceptor<T>implements NestInterceptor<T, { success: boolean; data: T }>{intercept(context: ExecutionContext, next: CallHandler<T>) {return next.handle().pipe(map((data: T) => ({success: true,data,})),);}}
👉 Runs before and after controller
@Catch(HttpException)export class HttpExceptionFilter implements ExceptionFilter {catch(exception: HttpException, host: ArgumentsHost) {const res = host.switchToHttp().getResponse<Response>();res.status(exception.getStatus()).json({statusCode: exception.getStatus(),message: exception.message,});}}
👉 Replaces messy try/catch
export const User = createParamDecorator((_, ctx: ExecutionContext) => {const req = ctx.switchToHttp().getRequest();return req.user;},);
@Get()getProfile(@User() user: any) {return user;}
👉 Cleaner than req.user
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
@Roles('admin')@Get()findAll() {}
👉 Used with Reflector in guards
| Level | Scope |
|---|---|
| Method | One route |
| Controller | All routes in controller |
| Global | Entire app |
@UseGuards(AuthGuard)@Controller('users')
👉 Applies to all /users routes
A real production setup looks like this:
// main.tsapp.useGlobalPipes(new ValidationPipe());app.useGlobalFilters(new HttpExceptionFilter());app.useGlobalInterceptors(new TransformInterceptor());
// controller@UseGuards(AuthGuard)@Controller('users')export class UsersController {@Get()findAll(@Query('page', ParseIntPipe) page: number) {return this.service.findAll(page);}}
| Layer | Responsibility |
|---|---|
| Middleware | Request logging |
| Guards | Access control |
| Pipes | Input validation |
| Interceptors | Response handling |
| Filters | Error handling |
any everywhereNestJS is powerful because:
“Each building block does one thing—and does it well.”
If asked:
👉 “How does NestJS structure applications?”
You answer:
“NestJS uses middleware, guards, pipes, interceptors, and filters to handle different concerns in the request lifecycle, making the application modular and scalable.”
Once you master these building blocks, you stop writing “code that works” and start building systems that scale.
If you want: