NestJS Architecture Patterns
Building Clean, Scalable Apps
Open interactive version (quiz + challenge)Real-world analogy
A NestJS app is like a well-organized company. The CEO (AppModule) oversees departments (feature modules). Each department has a receptionist (controller), workers (services), and security (guards). Nobody does someone else's job!
What is it?
NestJS follows a layered architecture: Modules organize features, Controllers handle HTTP, Services contain business logic, and cross-cutting concerns (validation, logging, auth) are handled by Pipes, Guards, Interceptors, and Filters.
Real-world relevance
This architecture pattern (separation of concerns) is used by enterprise companies worldwide. It scales from a 1-person project to a 100-person team without becoming a mess.
Key points
- Modules = Departments — Each feature gets its own module: UserModule, ProductModule, AuthModule. Modules group controllers, services, and providers into self-contained units that can share functionality via imports.
- Pipes = Validators — Pipes validate and transform data before it reaches your handler. ValidationPipe checks DTOs, ParseIntPipe converts strings to numbers. Bad data gets a clear 400 error before your logic runs.
- Interceptors = Middleware++ — Interceptors wrap around handlers, running code before AND after execution. Use them to log timing, transform responses into standard wrappers, cache results, or measure performance with RxJS.
- Exception Filters — Catch errors globally and return consistent, user-friendly responses with proper status codes. Create custom filters to log errors to monitoring services and format clean messages for end users.
- Middleware — Middleware runs BEFORE guards and handlers on every matching request. Use it for logging, CORS headers, cookie parsing, or rate limiting. Works like Express middleware, so existing plugins are compatible.
- Lifecycle Hooks — Control startup and shutdown with hooks: onModuleInit fires when a module loads, onModuleDestroy during shutdown, onApplicationBootstrap after all modules are ready. Great for DB setup and cleanup.
- Guards = Security Checkpoints — Guards decide if a request can proceed or gets rejected. JwtAuthGuard verifies tokens, RolesGuard checks permissions. They run after middleware but before interceptors and pipes in the request lifecycle.
- Custom Decorators — Create reusable decorators like @CurrentUser() to extract the logged-in user, or @Public() to skip auth on specific routes. Custom decorators hide repetitive logic behind a clean @ symbol.
- Provider Scopes — By default, providers are singletons — one shared instance. Change scope to REQUEST (new per request) or TRANSIENT (new per injection). Request scope is great for multi-tenant apps needing per-request context.
- Dynamic Modules — Some modules need runtime config like DB URLs or API keys. Dynamic modules use forRoot() and forRootAsync() to accept config at import time. Example: TypeOrmModule.forRoot({ host, port }).
Code example
// The NestJS Request Lifecycle 🔄
// Request → Middleware → Guards → Interceptors (before)
// → Pipes → Handler → Interceptors (after)
// → Exception Filters → Response
// 1. Custom Pipe — validate incoming data 🔍
@Injectable()
export class ParseObjectIdPipe implements PipeTransform {
transform(value: string) {
if (!Types.ObjectId.isValid(value)) {
throw new BadRequestException('Invalid ID format');
}
return value;
}
}
// Usage: @Param('id', ParseObjectIdPipe) id: string
// 2. Interceptor — log all requests ⏱️
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
const req = context.switchToHttp().getRequest();
const now = Date.now();
console.log(`→ ${req.method} ${req.url}`);
return next.handle().pipe(
tap(() => console.log(`← ${Date.now() - now}ms`)),
);
}
}
// 3. Exception Filter — beautiful error responses 🎨
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse();
const status = exception.getStatus();
response.status(status).json({
success: false,
statusCode: status,
message: exception.message,
timestamp: new Date().toISOString(),
});
}
}Line-by-line walkthrough
- 1. The NestJS Request Lifecycle 🔄
- 2. Request → Middleware → Guards → Interceptors (before)
- 3. → Pipes → Handler → Interceptors (after)
- 4. → Exception Filters → Response
- 5.
- 6. 1. Custom Pipe — validate incoming data 🔍
- 7. Decorator that adds metadata or behavior
- 8. Exporting for use in other files
- 9.
- 10. Conditional check
- 11. Throwing an error
- 12. Closing block
- 13. Returning a value
- 14. Closing block
- 15. Closing block
- 16.
- 17. Usage: @Param('id', ParseObjectIdPipe) id: string
- 18.
- 19. 2. Interceptor — log all requests ⏱️
- 20. Decorator that adds metadata or behavior
- 21. Exporting for use in other files
- 22.
- 23. Declaring a variable
- 24. Declaring a variable
- 25. Printing output to the console
- 26.
- 27. Returning a value
- 28.
- 29. Closing expression
- 30. Closing block
- 31. Closing block
- 32.
- 33. 3. Exception Filter — beautiful error responses 🎨
- 34. Decorator that adds metadata or behavior
- 35. Exporting for use in other files
- 36. Catching any errors from the try block
- 37. Declaring a variable
- 38. Declaring a variable
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46. Closing block
- 47. Closing block
Spot the bug
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
console.log("Before...");
return next.handle();
console.log("After...");
}
}Need a hint?
Can code after a return statement ever execute?
Show answer
The console.log('After...') after return will never run. To log after the handler, use RxJS: return next.handle().pipe(tap(() => console.log('After...')));
Explain like I'm 5
Think of a NestJS app like a school. The principal (AppModule) runs everything. Each classroom (module) has a teacher who talks to students (controller) and a helper who does the work (service). Guards are hall monitors checking passes before you enter!
Fun fact
The NestJS request lifecycle has 7 distinct layers. A single request goes through: Middleware → Guards → Interceptors → Pipes → Handler → Interceptors → Filters. It's like airport security with 7 checkpoints! ✈️
Hands-on challenge
Create a custom LoggingInterceptor that logs the method, URL, and response time for every request. Apply it globally with `app.useGlobalInterceptors()`.
More resources
- NestJS Modules (NestJS Official)
- NestJS Providers (NestJS Official)
- NestJS Controllers (NestJS Official)