NestJS
A progressive Node.js framework for building efficient, reliable and scalable server-side applications.
Questions
Explain what NestJS is and how it compares to Express.js. Include key differences in architecture, features, and use cases.
Expert Answer
Posted on May 10, 2025NestJS is a progressive Node.js framework for building efficient, reliable, and scalable server-side applications. It represents an architectural evolution in the Node.js ecosystem, addressing common pain points in developing enterprise-grade applications.
Architectural Comparison with Express.js:
- Design Philosophy: Express.js follows a minimalist, unopinionated approach that provides basic routing and middleware capabilities with no enforced structure. NestJS is opinionated, implementing a structured architecture inspired by Angular that enforces separation of concerns.
- Framework Structure: NestJS implements a modular design with a hierarchical dependency injection container, leveraging decorators for metadata programming and providing clear boundaries between application components.
- TypeScript Integration: While Express.js can be used with TypeScript through additional configuration, NestJS is built with TypeScript from the ground up, offering first-class type safety, enhanced IDE support, and compile-time error checking.
- Underlying Implementation: NestJS actually uses Express.js (or optionally Fastify) as its HTTP server framework under the hood, essentially functioning as a higher-level abstraction layer.
NestJS Architecture Implementation:
// app.module.ts - Module definition
@Module({
imports: [DatabaseModule, ConfigModule],
controllers: [UsersController],
providers: [UsersService],
})
export class AppModule {}
// users.controller.ts - Controller with dependency injection
@Controller("users")
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll(): Promise<User[]> {
return this.usersService.findAll();
}
@Post()
@UsePipes(ValidationPipe)
create(@Body() createUserDto: CreateUserDto): Promise<User> {
return this.usersService.create(createUserDto);
}
}
// users.service.ts - Service with business logic
@Injectable()
export class UsersService {
constructor(@InjectRepository(User) private usersRepository: Repository<User>) {}
findAll(): Promise<User[]> {
return this.usersRepository.find();
}
create(createUserDto: CreateUserDto): Promise<User> {
const user = this.usersRepository.create(createUserDto);
return this.usersRepository.save(user);
}
}
Technical Differentiators:
- Dependency Injection: NestJS implements a robust IoC container that handles object creation and lifetime management, facilitating more testable and maintainable code.
- Middleware System: While Express uses a linear middleware pipeline, NestJS offers multiple levels of middleware: global, module, route, and method-specific.
- Request Pipeline: NestJS provides additional pipeline components like guards, interceptors, pipes, and exception filters that execute at different stages of the request lifecycle.
- API Documentation: NestJS integrates with Swagger through dedicated decorators for automatic API documentation generation.
- Microservice Support: NestJS has first-class support for microservices with various transport mechanisms (Redis, MQTT, gRPC, etc.).
- WebSocket Support: Built-in decorators and adapters for WebSocket protocols.
Performance Considerations:
Express.js | NestJS |
---|---|
Lower memory footprint | Higher memory usage due to metadata reflection |
Slightly faster request processing | Additional overhead from DI container and middleware layers |
Better for small microservices | Better for complex enterprise applications |
NestJS's architectural advantages typically outweigh its slightly higher performance overhead in enterprise settings where code organization, maintainability, and developer productivity are prioritized.
Beginner Answer
Posted on May 10, 2025NestJS is a modern framework for building server-side applications with Node.js. It's often described as a progressive Node.js framework designed for building efficient and scalable server-side applications.
Key Differences from Express.js:
- Architecture: NestJS follows an organized structure inspired by Angular, with modules, controllers, and services. Express.js is minimalist with no enforced structure.
- TypeScript: NestJS is built with TypeScript, offering strong typing and better tooling. Express.js uses plain JavaScript (though TypeScript can be added).
- Built-in Features: NestJS has many features pre-built (validation, security, dependency injection) while Express.js requires additional packages.
Simple NestJS Controller Example:
@Controller("cats")
export class CatsController {
@Get()
findAll(): string {
return "This returns all cats";
}
}
Equivalent Express.js Example:
const express = require("express");
const app = express();
app.get("/cats", (req, res) => {
res.send("This returns all cats");
});
Tip: NestJS actually uses Express.js under the hood by default, but adds structure and features on top of it!
Describe the main components of a NestJS application and how they work together. Include information about modules, controllers, providers, and the request lifecycle.
Expert Answer
Posted on May 10, 2025NestJS implements a modular, layered architecture influenced by Angular's design principles and leveraging TypeScript's decorators for metadata programming. The framework is built around several core architectural concepts that together create a robust application structure optimized for testability, maintainability, and scalability.
Core Architectural Components
1. Modules
Modules are the foundational organizational units in NestJS, implementing the modular design pattern. They encapsulate related components and provide clear boundaries between functional areas of the application.
- Root Module: The application's entry point module that bootstraps the application
- Feature Modules: Domain-specific modules that encapsulate related functionality
- Shared Modules: Reusable modules that export common providers/components
- Core Module: Often used for singleton services that are needed application-wide
2. Controllers
Controllers are responsible for handling incoming HTTP requests and returning responses to the client. They define routes using decorators and delegate business logic to providers.
- Use route decorators:
@Get()
,@Post()
,@Put()
, etc. - Handle parameter extraction through decorators:
@Param()
,@Body()
,@Query()
, etc. - Focus solely on HTTP concerns, not business logic
3. Providers
Providers are classes annotated with @Injectable()
decorator. They encapsulate business logic and are injected into controllers or other providers.
- Services: Implement business logic
- Repositories: Handle data access logic
- Factories: Create and return providers dynamically
- Helpers: Utility providers with common functionality
4. Dependency Injection System
NestJS implements a powerful IoC (Inversion of Control) container that manages dependencies between components.
- Constructor-based injection is the primary pattern
- Provider scope management (default: singleton, also transient and request-scoped available)
- Circular dependency resolution
- Custom providers with complex initialization
Request Lifecycle Pipeline
Requests in NestJS flow through a well-defined pipeline with multiple interception points:
Request Lifecycle Diagram:
Incoming Request ↓ ┌─────────────────┐ │ Global Middleware │ └─────────────────┘ ↓ ┌─────────────────┐ │ Module Middleware │ └─────────────────┘ ↓ ┌─────────────────┐ │ Guards │ └─────────────────┘ ↓ ┌─────────────────┐ │ Request Interceptors │ └─────────────────┘ ↓ ┌─────────────────┐ │ Pipes │ └─────────────────┘ ↓ ┌─────────────────┐ │ Route Handler (Controller) │ └─────────────────┘ ↓ ┌─────────────────┐ │ Response Interceptors │ └─────────────────┘ ↓ ┌─────────────────┐ │ Exception Filters (if error) │ └─────────────────┘ ↓ Response
1. Middleware
Function/class executed before route handlers, with access to request and response objects. Provides integration point with Express middleware.
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: Function) {
console.log(`Request to ${req.url}`);
next();
}
}
2. Guards
Responsible for determining if a request should be handled by the route handler, primarily used for authorization.
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private readonly jwtService: JwtService) {}
canActivate(context: ExecutionContext): boolean | Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization?.split(" ")[1];
if (!token) return false;
try {
const decoded = this.jwtService.verify(token);
request.user = decoded;
return true;
} catch {
return false;
}
}
}
3. Interceptors
Classes that can intercept the execution of a method, allowing transformation of request/response data and implementation of cross-cutting concerns.
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const req = context.switchToHttp().getRequest();
const method = req.method;
const url = req.url;
console.log(`[${method}] ${url} - ${new Date().toISOString()}`);
const now = Date.now();
return next.handle().pipe(
tap(() => console.log(`[${method}] ${url} - ${Date.now() - now}ms`))
);
}
}
4. Pipes
Classes that transform input data, used primarily for validation and type conversion.
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
const { metatype } = metadata;
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToClass(metatype, value);
const errors = validateSync(object);
if (errors.length > 0) {
throw new BadRequestException("Validation failed");
}
return value;
}
private toValidate(metatype: Function): boolean {
return metatype !== String && metatype !== Boolean &&
metatype !== Number && metatype !== Array;
}
}
5. Exception Filters
Handle exceptions thrown during request processing, allowing custom exception responses.
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: exception.message
});
}
}
Architectural Patterns
NestJS facilitates several architectural patterns:
- MVC Pattern: Controllers (route handling), Services (business logic), and Models (data representation)
- CQRS Pattern: Separate command and query responsibilities
- Microservices Architecture: Built-in support for various transport layers (TCP, Redis, MQTT, gRPC, etc.)
- Event-Driven Architecture: Through the EventEmitter pattern
- Repository Pattern: Typically implemented with TypeORM or Mongoose
Complete Module Structure Example:
// users.module.ts
@Module({
imports: [
TypeOrmModule.forFeature([User]),
AuthModule,
ConfigModule,
],
controllers: [UsersController],
providers: [
UsersService,
UserRepository,
{
provide: APP_GUARD,
useClass: RolesGuard,
},
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
exports: [UsersService],
})
export class UsersModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes({ path: "users", method: RequestMethod.ALL });
}
}
Advanced Tip: NestJS applications can be configured to use Fastify instead of Express as the underlying HTTP framework for improved performance, using:
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter()
);
Beginner Answer
Posted on May 10, 2025NestJS applications are built using a clear architecture with several main components that work together. This structure helps organize code and makes applications easier to maintain.
Main Components:
- Modules: These are containers that group related code. Every NestJS app has at least one module (the root module).
- Controllers: These handle incoming requests and return responses to clients. Think of them as traffic directors.
- Providers/Services: These contain the business logic. Controllers use services to perform complex operations.
- DTOs (Data Transfer Objects): Simple objects that define how data is sent over the network.
Basic NestJS Architecture Diagram:
┌─────────────────────────────────────┐ │ Module │ │ ┌──────────────┐ ┌─────────────┐ │ │ │ Controller │ │ Service │ │ │ │ (Routing) │←→│ (Business │ │ │ │ │ │ Logic) │ │ │ └──────────────┘ └─────────────┘ │ └─────────────────────────────────────┘
Simple Example of a NestJS App Structure:
// app.module.ts
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class AppModule {}
// cats.controller.ts
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
@Get()
findAll() {
return this.catsService.findAll();
}
}
// cats.service.ts
@Injectable()
export class CatsService {
private cats = [];
findAll() {
return this.cats;
}
}
How Requests Flow Through a NestJS Application:
- Request arrives at the application
- Middleware processes the request (optional)
- Guards check if the request should proceed (optional)
- The appropriate controller method handles the request
- The controller calls services to perform business logic
- The response is sent back to the client
Tip: Think of modules as organizing containers, controllers as traffic directors, and services as workers that do the actual tasks.
Explain the role of controllers in NestJS, how they fit into the architecture, and provide an example of how to create a basic controller.
Expert Answer
Posted on May 10, 2025Controllers in NestJS implement the Controller layer in the MVC architecture pattern, serving as the entry point for client requests within the application. They are TypeScript classes annotated with the @Controller()
decorator, which binds routes to class methods through metadata.
Technical Implementation Details:
- Route Registration: Controllers employ decorators to register routes with the underlying HTTP server implementation (Express by default, or Fastify)
- Dependency Injection: Controllers leverage NestJS's DI system to inject services and other providers
- Request Pipeline: Controllers participate in the NestJS middleware, guard, interceptor, and pipe execution chain
- Metadata Reflection: The TypeScript metadata reflection API enables NestJS to inspect and utilize the type information of controller parameters
Comprehensive Controller Implementation:
import {
Controller,
Get,
Post,
Put,
Delete,
Param,
Body,
HttpStatus,
HttpException,
Query,
UseGuards,
UseInterceptors,
UsePipes,
ValidationPipe
} from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto, UpdateUserDto } from './dto';
import { AuthGuard } from '../guards/auth.guard';
import { LoggingInterceptor } from '../interceptors/logging.interceptor';
import { User } from './user.entity';
@Controller('users')
@UseInterceptors(LoggingInterceptor)
export class UsersController {
constructor(private readonly userService: UserService) {}
@Get()
async findAll(@Query('page') page: number = 1, @Query('limit') limit: number = 10): Promise {
return this.userService.findAll(page, limit);
}
@Get(':id')
async findOne(@Param('id') id: string): Promise {
const user = await this.userService.findOne(id);
if (!user) {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}
return user;
}
@Post()
@UseGuards(AuthGuard)
@UsePipes(new ValidationPipe({ transform: true }))
async create(@Body() createUserDto: CreateUserDto): Promise {
return this.userService.create(createUserDto);
}
@Put(':id')
@UseGuards(AuthGuard)
async update(
@Param('id') id: string,
@Body() updateUserDto: UpdateUserDto
): Promise {
return this.userService.update(id, updateUserDto);
}
@Delete(':id')
@UseGuards(AuthGuard)
async remove(@Param('id') id: string): Promise {
return this.userService.remove(id);
}
}
Advanced Controller Concepts:
1. Route Parameters Extraction:
NestJS provides various parameter decorators to extract data from the request:
@Request()
,@Req()
: Access the entire request object@Response()
,@Res()
: Access the response object (using this disables automatic response handling)@Param(key?)
: Extract route parameters@Body(key?)
: Extract the request body or a specific property@Query(key?)
: Extract query parameters@Headers(name?)
: Extract headers@Session()
: Access the session object
2. Controller Registration and Module Integration:
// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService]
})
export class UsersModule {}
3. Custom Route Declaration and Versioning:
// Multiple path prefixes
@Controller(['users', 'people'])
export class UsersController {}
// Versioning with URI path
@Controller({
path: 'users',
version: '1'
})
export class UsersControllerV1 {}
// Versioning with headers
@Controller({
path: 'users',
version: '2',
versioningOptions: {
type: VersioningType.HEADER,
header: 'X-API-Version'
}
})
export class UsersControllerV2 {}
Advanced Tip: To optimize performance, you can leverage controller method return type metadata to automatically transform responses. NestJS uses this information to determine how to handle the response, including serialization.
Beginner Answer
Posted on May 10, 2025In NestJS, controllers are responsible for handling incoming requests from clients and returning responses. Think of controllers as traffic controllers that direct requests to the appropriate code in your application.
Key Points About Controllers:
- Purpose: They receive HTTP requests and determine what code should run in response
- Annotation-based: They use decorators like
@Controller()
to define their behavior - Routing: They help map specific URL paths to methods in your code
Creating a Basic Controller:
// users.controller.ts
import { Controller, Get } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get()
findAll() {
return ['user1', 'user2', 'user3']; // Just a simple example
}
}
Tip: After creating a controller, remember to include it in the module's controllers
array to make it available to your application.
How to Create a Controller:
- Create a new file named [name].controller.ts
- Import the necessary decorators from @nestjs/common
- Create a class and add the @Controller() decorator
- Define methods with HTTP method decorators (@Get, @Post, etc.)
- Register the controller in a module
You can also use the NestJS CLI to generate a controller automatically:
nest generate controller users
# or shorter:
nest g co users
Describe how routing works in NestJS, including route paths, HTTP methods, and how to implement various request handlers like GET, POST, PUT, and DELETE.
Expert Answer
Posted on May 10, 2025Routing in NestJS is implemented through a sophisticated combination of TypeScript decorators and metadata reflection. The framework's routing system maps HTTP requests to controller methods based on route paths, HTTP methods, and applicable middleware.
Routing Architecture:
- Route Registration: Routes are registered during the application bootstrap phase, leveraging metadata collected from controller decorators
- Route Execution: The NestJS runtime examines incoming requests and matches them against registered routes
- Route Resolution: Once a match is found, the request traverses through the middleware pipeline before reaching the handler
- Handler Execution: The appropriate controller method executes with parameters extracted from the request
Comprehensive HTTP Method Handler Implementation:
import {
Controller,
Get, Post, Put, Patch, Delete, Options, Head, All,
Param, Query, Body, Headers, Req, Res,
HttpCode, Header, Redirect,
UseGuards, UseInterceptors, UsePipes
} from '@nestjs/common';
import { Request, Response } from 'express';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ProductService } from './product.service';
import { CreateProductDto, UpdateProductDto, ProductQueryParams } from './dto';
import { Product } from './product.entity';
import { AuthGuard } from '../guards/auth.guard';
import { ValidationPipe } from '../pipes/validation.pipe';
import { TransformInterceptor } from '../interceptors/transform.interceptor';
@Controller('products')
export class ProductsController {
constructor(private readonly productService: ProductService) {}
// GET with query parameters and response transformation
@Get()
@UseInterceptors(TransformInterceptor)
findAll(@Query() query: ProductQueryParams): Observable {
return this.productService.findAll(query).pipe(
map(products => products.map(p => ({ ...p, featured: !!p.featured })))
);
}
// Dynamic route parameter with specific parameter extraction
@Get(':id')
@HttpCode(200)
@Header('Cache-Control', 'none')
findOne(@Param('id') id: string): Promise {
return this.productService.findOne(id);
}
// POST with body validation and custom status code
@Post()
@HttpCode(201)
@UsePipes(new ValidationPipe())
@UseGuards(AuthGuard)
async create(@Body() createProductDto: CreateProductDto): Promise {
return this.productService.create(createProductDto);
}
// PUT with route parameter and request body
@Put(':id')
update(
@Param('id') id: string,
@Body() updateProductDto: UpdateProductDto
): Promise {
return this.productService.update(id, updateProductDto);
}
// PATCH for partial updates
@Patch(':id')
partialUpdate(
@Param('id') id: string,
@Body() partialData: Partial
): Promise {
return this.productService.patch(id, partialData);
}
// DELETE with proper status code
@Delete(':id')
@HttpCode(204)
async remove(@Param('id') id: string): Promise {
await this.productService.remove(id);
}
// Route with redirect
@Get('redirect/:id')
@Redirect('https://docs.nestjs.com', 301)
redirect(@Param('id') id: string) {
// Can dynamically change redirect with returned object
return { url: `https://example.com/products/${id}`, statusCode: 302 };
}
// Full request/response access (Express objects)
@Get('raw')
getRaw(@Req() req: Request, @Res() res: Response) {
// Using Express response means YOU handle the response lifecycle
res.status(200).json({
message: 'Using raw response object',
headers: req.headers
});
}
// Resource OPTIONS handler
@Options()
getOptions(@Headers() headers) {
return {
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
requestHeaders: headers
};
}
// Catch-all wildcard route
@All('*')
catchAll() {
return 'This catches any HTTP method to /products/* that isn't matched by other routes';
}
// Sub-resource route
@Get(':id/variants')
getVariants(@Param('id') id: string): Promise {
return this.productService.findVariants(id);
}
// Nested dynamic parameters
@Get(':categoryId/items/:itemId')
getItemInCategory(
@Param('categoryId') categoryId: string,
@Param('itemId') itemId: string
) {
return `Item ${itemId} in category ${categoryId}`;
}
}
Advanced Routing Techniques:
1. Route Versioning:
// main.ts
import { VersioningType } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableVersioning({
type: VersioningType.URI, // or VersioningType.HEADER, VersioningType.MEDIA_TYPE
prefix: 'v'
});
await app.listen(3000);
}
// products.controller.ts
@Controller({
path: 'products',
version: '1'
})
export class ProductsControllerV1 {
// Accessible at /v1/products
}
@Controller({
path: 'products',
version: '2'
})
export class ProductsControllerV2 {
// Accessible at /v2/products
}
2. Asynchronous Handlers:
NestJS supports various ways of handling asynchronous operations:
- Promises
- Observables (RxJS)
- Async/Await
3. Route Wildcards and Complex Path Patterns:
@Get('ab*cd')
findByWildcard() {
// Matches: abcd, ab_cd, ab123cd, etc.
}
@Get('files/:filename(.+)') // Uses RegExp
getFile(@Param('filename') filename: string) {
// Matches: files/image.jpg, files/document.pdf, etc.
}
4. Route Registration Internals:
The routing system in NestJS is built on a combination of:
- Decorator Pattern: Using TypeScript decorators to attach metadata to classes and methods
- Reflection API: Leveraging
Reflect.getMetadata
to retrieve type information - Express/Fastify Routing: Ultimately mapping to the underlying HTTP server's routing system
// Simplified version of how method decorators work internally
function Get(path?: string): MethodDecorator {
return (target, key, descriptor) => {
Reflect.defineMetadata('path', path || '', target, key);
Reflect.defineMetadata('method', RequestMethod.GET, target, key);
return descriptor;
};
}
Advanced Tip: For high-performance applications, consider using the Fastify adapter instead of Express. You can switch by using NestFactory.create(AppModule, new FastifyAdapter())
and it works with the same controller-based routing system.
Beginner Answer
Posted on May 10, 2025Routing in NestJS is how the framework knows which code to execute when a specific URL is requested with a particular HTTP method. It's like creating a map that connects web addresses to the functions in your application.
Basic Routing Concepts:
- Route Path: The URL pattern that a request must match
- HTTP Method: GET, POST, PUT, DELETE, etc.
- Handler: The method that will be executed when the route is matched
Basic Route Examples:
import { Controller, Get, Post, Put, Delete, Param, Body } from '@nestjs/common';
@Controller('products') // Base path for all routes in this controller
export class ProductsController {
@Get() // Handles GET /products
findAll() {
return ['Product 1', 'Product 2', 'Product 3'];
}
@Get(':id') // Handles GET /products/123
findOne(@Param('id') id: string) {
return `Product with ID ${id}`;
}
@Post() // Handles POST /products
create(@Body() productData) {
return `Created a new product with name: ${productData.name}`;
}
@Put(':id') // Handles PUT /products/123
update(@Param('id') id: string, @Body() updateData) {
return `Updated product ${id} with new name: ${updateData.name}`;
}
@Delete(':id') // Handles DELETE /products/123
remove(@Param('id') id: string) {
return `Removed product ${id}`;
}
}
Key HTTP Method Decorators:
- @Get(): For retrieving data
- @Post(): For creating new resources
- @Put(): For updating existing resources
- @Patch(): For partial updates
- @Delete(): For removing resources
- @Options(), @Head(), @All(): For other HTTP methods
Tip: You can access route parameters with @Param()
, query parameters with @Query()
, and request body data with @Body()
.
Route Parameters:
You can define dynamic parameters in your routes using the colon syntax:
@Get(':id/details')
getProductDetails(@Param('id') id: string) {
return `Details for product ${id}`;
}
Wildcards in Routes:
You can use wildcards to match any combination of characters:
@Get('ab*cd')
findAllWildcard() {
// Will match abcd, abXcd, ab123cd, etc.
return 'This route uses a wildcard';
}
Explain the concept of providers in NestJS, their purpose, and how they are registered in a NestJS application.
Expert Answer
Posted on May 10, 2025Providers are a core concept in NestJS that form the backbone of the dependency injection system. They represent services, repositories, factories, helpers, or any class that manages specific functionality and can be injected into other components.
Provider Registration and Resolution:
NestJS creates a dependency injection container during application bootstrapping. The container maintains a provider registry based on module definitions and handles the creation and caching of provider instances.
Provider Definition Formats:
@Module({
providers: [
// Standard provider (shorthand)
UsersService,
// Standard provider (expanded form)
{
provide: UsersService,
useClass: UsersService,
},
// Value provider
{
provide: 'API_KEY',
useValue: 'secret_key_123',
},
// Factory provider
{
provide: 'ASYNC_CONNECTION',
useFactory: async (configService: ConfigService) => {
const dbHost = configService.get('DB_HOST');
const dbPort = configService.get('DB_PORT');
return await createConnection({host: dbHost, port: dbPort});
},
inject: [ConfigService], // dependencies for the factory
},
// Existing provider (alias)
{
provide: 'CACHED_SERVICE',
useExisting: CacheService,
},
]
})
Provider Scopes:
NestJS supports three different provider scopes that determine the lifecycle of provider instances:
Scope | Description | Usage |
---|---|---|
DEFAULT | Singleton scope (default) - single instance shared across the entire application | Stateless services, configuration |
REQUEST | New instance created for each incoming request | Request-specific state, per-request caching |
TRANSIENT | New instance created each time the provider is injected | Lightweight stateful providers |
Custom Provider Scope:
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {
private requestId: string;
constructor() {
this.requestId = Math.random().toString(36).substring(2);
console.log(`RequestScopedService created with ID: ${this.requestId}`);
}
}
Technical Considerations:
- Circular Dependencies: NestJS handles circular dependencies using forward references:
@Injectable() export class ServiceA { constructor( @Inject(forwardRef(() => ServiceB)) private serviceB: ServiceB, ) {} }
- Custom Provider Tokens: Using symbols or strings as provider tokens can help avoid naming collisions in large applications:
export const USER_REPOSITORY = Symbol('USER_REPOSITORY'); // In module providers: [ { provide: USER_REPOSITORY, useClass: UserRepository, } ] // In service constructor(@Inject(USER_REPOSITORY) private userRepo: UserRepository) {}
- Provider Lazy Loading: Some providers can be instantiated on-demand using module reference:
@Injectable() export class LazyService { constructor(private moduleRef: ModuleRef) {} async doSomething() { // Get instance only when needed const service = await this.moduleRef.resolve(HeavyService); return service.performTask(); } }
Advanced Tip: In test environments, you can use custom provider configurations to mock dependencies without changing your application code.
Beginner Answer
Posted on May 10, 2025Providers in NestJS are a fundamental concept that allows you to organize your code into reusable, injectable classes. Think of providers as services that your application needs to function.
Key Points About Providers:
- What They Are: Providers are classes marked with the
@Injectable()
decorator that can be injected into controllers or other providers. - Common Types: Services, repositories, factories, helpers - any class that handles a specific piece of functionality.
- Purpose: They help keep your code organized, maintainable, and testable by separating concerns.
Basic Provider Example:
// users.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
private users = [];
findAll() {
return this.users;
}
create(user) {
this.users.push(user);
return user;
}
}
How to Register Providers:
Providers are registered in the module's providers
array:
// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService] // Optional: makes this service available to other modules
})
export class UsersModule {}
Tip: Once registered, NestJS automatically handles the creation and injection of providers when needed. You don't need to manually create instances!
Describe how dependency injection works in NestJS and how to implement it with services. Include examples of how to inject and use services in controllers and other providers.
Expert Answer
Posted on May 10, 2025Dependency Injection (DI) in NestJS is implemented through an IoC (Inversion of Control) container that manages class dependencies. The NestJS DI system is built on top of reflection and decorators from TypeScript, enabling a highly flexible dependency resolution mechanism.
Core Mechanisms of NestJS DI:
NestJS DI relies on three key mechanisms:
- Type Metadata Reflection: Uses TypeScript's metadata reflection API to determine constructor parameter types
- Provider Registration: Maintains a registry of providers that can be injected
- Dependency Resolution: Recursively resolves dependencies when instantiating classes
Type Metadata and How NestJS Knows What to Inject:
// This is how NestJS identifies the types to inject
import 'reflect-metadata';
import { Injectable } from '@nestjs/common';
@Injectable()
class ServiceA {}
@Injectable()
class ServiceB {
constructor(private serviceA: ServiceA) {}
}
// At runtime, NestJS can access the type information:
const paramTypes = Reflect.getMetadata('design:paramtypes', ServiceB);
console.log(paramTypes); // [ServiceA]
Advanced DI Techniques:
1. Custom Providers with Non-Class Dependencies:
// app.module.ts
@Module({
providers: [
{
provide: 'CONFIG', // Using a string token
useValue: {
apiUrl: 'https://api.example.com',
timeout: 3000
}
},
{
provide: 'CONNECTION',
useFactory: (config) => {
return new DatabaseConnection(config.apiUrl);
},
inject: ['CONFIG'] // Inject dependencies to the factory
},
ServiceA
]
})
export class AppModule {}
// In your service:
@Injectable()
export class ServiceA {
constructor(
@Inject('CONFIG') private config: any,
@Inject('CONNECTION') private connection: DatabaseConnection
) {}
}
2. Controlling Provider Scope:
import { Injectable, Scope } from '@nestjs/common';
// DEFAULT scope (singleton) is the default if not specified
@Injectable({ scope: Scope.DEFAULT })
export class GlobalService {}
// REQUEST scope - new instance per request
@Injectable({ scope: Scope.REQUEST })
export class RequestService {
constructor(private readonly globalService: GlobalService) {}
}
// TRANSIENT scope - new instance each time it's injected
@Injectable({ scope: Scope.TRANSIENT })
export class TransientService {}
3. Circular Dependencies:
import { Injectable, forwardRef, Inject } from '@nestjs/common';
@Injectable()
export class ServiceA {
constructor(
@Inject(forwardRef(() => ServiceB))
private serviceB: ServiceB,
) {}
getFromA() {
return 'data from A';
}
}
@Injectable()
export class ServiceB {
constructor(
@Inject(forwardRef(() => ServiceA))
private serviceA: ServiceA,
) {}
getFromB() {
return this.serviceA.getFromA() + ' with B';
}
}
Architectural Considerations for DI:
When to Use Different Injection Techniques:
Technique | Use Case | Benefits |
---|---|---|
Constructor Injection | Most dependencies | Type safety, mandatory dependencies |
Property Injection (@Inject()) | Optional dependencies | No need to modify constructors |
Factory Providers | Dynamic dependencies, configuration | Runtime decisions for dependency creation |
useExisting Provider | Aliases, backward compatibility | Multiple tokens for the same service |
DI in Testing:
One of the major benefits of DI is testability. NestJS provides a powerful testing module that makes it easy to mock dependencies:
// users.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
describe('UsersController', () => {
let controller: UsersController;
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useValue: {
findAll: jest.fn().mockReturnValue([
{ id: 1, name: 'Test User' }
]),
findOne: jest.fn().mockImplementation((id) =>
({ id, name: 'Test User' })
),
}
}
],
}).compile();
controller = module.get(UsersController);
service = module.get(UsersService);
});
it('should return all users', () => {
expect(controller.findAll()).toEqual([
{ id: 1, name: 'Test User' }
]);
expect(service.findAll).toHaveBeenCalled();
});
});
Advanced Tip: In large applications, consider using hierarchical DI containers with module boundaries to encapsulate services. This will help prevent DI tokens from becoming global and keep your application modular.
Performance Considerations:
While DI is powerful, it does come with performance costs. With large applications, consider:
- Using
Scope.DEFAULT
(singleton) for services without request-specific state - Being cautious with
Scope.TRANSIENT
providers in performance-critical paths - Using lazy loading for modules that contain many providers but are infrequently used
Beginner Answer
Posted on May 10, 2025Dependency Injection (DI) in NestJS is a technique where one object (a class) receives other objects (dependencies) that it needs to work. Rather than creating these dependencies itself, the class "asks" for them.
The Basic Concept:
- Instead of creating dependencies: Your class receives them automatically
- Makes testing easier: You can substitute real dependencies with mock versions
- Reduces coupling: Your code doesn't need to know how to create its dependencies
How DI works in NestJS:
1. Create an injectable service:
// users.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
private users = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];
findAll() {
return this.users;
}
findOne(id: number) {
return this.users.find(user => user.id === id);
}
}
2. Register the service in a module:
// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService]
})
export class UsersModule {}
3. Inject and use the service in a controller:
// users.controller.ts
import { Controller, Get, Param } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
// The service is injected via the constructor
constructor(private usersService: UsersService) {}
@Get()
findAll() {
// We can now use the service methods
return this.usersService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(+id);
}
}
Tip: The key part is the constructor. When NestJS creates your controller, it sees that it needs a UsersService and automatically provides it. You don't have to write this.usersService = new UsersService()
anywhere!
Injecting Services into Other Services:
You can also inject services into other services:
// auth.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(private usersService: UsersService) {}
async validateUser(username: string, password: string) {
const user = await this.usersService.findByUsername(username);
if (user && user.password === password) {
return user;
}
return null;
}
}
Just remember that if you're using a service from another module, you need to export it from its original module and import that module where you need to use the service.
Explain the concept of modules in NestJS and their significance in application architecture.
Expert Answer
Posted on May 10, 2025Modules in NestJS are a fundamental architectural concept that implement the Modular Design Pattern, enabling modular organization of the application. They serve as the primary mechanism for organizing the application structure in accordance with SOLID principles.
Module Architecture and Decorators:
A NestJS module is a class annotated with the @Module()
decorator, which provides metadata for the Nest dependency injection container. The decorator takes a single object with the following properties:
- providers: Services, repositories, factories, helpers, etc. that will be instantiated by the Nest injector and shared across this module.
- controllers: The set of controllers defined in this module that must be instantiated.
- imports: List of modules required by this module. Any exported providers from these imported modules will be available in our module.
- exports: Subset of providers that are provided by this module and should be available in other modules that import this module.
Module Implementation Example:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { UserRepository } from './user.repository';
import { User } from './entities/user.entity';
import { AuthModule } from '../auth/auth.module';
@Module({
imports: [
TypeOrmModule.forFeature([User]),
AuthModule
],
controllers: [UsersController],
providers: [UsersService, UserRepository],
exports: [UsersService]
})
export class UsersModule {}
Module Registration Patterns:
NestJS supports several module registration patterns:
Module Registration Patterns:
Pattern | Use Case | Example |
---|---|---|
Static Module | Basic module registration | imports: [UsersModule] |
Dynamic Modules (forRoot) | Global configuration with options | imports: [ConfigModule.forRoot({ isGlobal: true })] |
Dynamic Modules (forFeature) | Feature-specific configurations | imports: [TypeOrmModule.forFeature([User])] |
Global Modules | Module needed throughout the app | @Global() decorator + module exports |
Module Dependency Resolution:
NestJS utilizes circular dependency resolution algorithms when dealing with complex module relationships. This ensures proper instantiation order and dependency injection even in complex module hierarchies.
Technical Detail: The module system in NestJS uses topological sorting to resolve dependencies, which enables the framework to handle circular dependencies via forward referencing using forwardRef()
.
Module Encapsulation:
NestJS enforces strong encapsulation for modules, meaning that providers not explicitly exported remain private to the module. This implements the Information Hiding principle and provides well-defined boundaries between application components.
The module system forms the foundation of NestJS's dependency injection container, allowing for loosely coupled architecture that facilitates testing, maintenance, and scalability.
Beginner Answer
Posted on May 10, 2025In NestJS, modules are organizational units that help structure your application into logical, related parts. Think of modules like containers that group together related features.
Key Points About NestJS Modules:
- Organization: Modules help organize code by grouping related functionality together.
- Encapsulation: Each module encapsulates its components, preventing unwanted access from other parts of the application.
- Reusability: Modules can be reused across different applications.
Basic Module Example:
// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
Tip: Every NestJS application has at least one module - the root AppModule.
Why Modules Are Important:
- Structure: They give your application a clear, organized structure.
- Maintainability: Easier to maintain and understand code in smaller, focused units.
- Separation of Concerns: Each module handles its own specific functionality.
- Dependency Management: Modules help manage dependencies between different parts of your application.
Describe the best practices for structuring a NestJS application with modules and how different modules should interact with each other.
Expert Answer
Posted on May 10, 2025Organizing a NestJS application with modules involves implementing a modular architecture that follows Domain-Driven Design (DDD) principles and adheres to SOLID design patterns. The module organization strategy should address scalability, maintainability, and testability concerns.
Strategic Module Organization Patterns:
Module Organization Approaches:
Organization Pattern | Use Case | Benefits |
---|---|---|
Feature-based Modules | Organizing by business domain/feature | Strong cohesion, domain isolation |
Layer-based Modules | Separation of technical concerns | Clear architectural boundaries |
Hybrid Approach | Complex applications with clear domains | Balances domain and technical concerns |
Recommended Project Structure:
src/ ├── app.module.ts # Root application module ├── config/ # Configuration module │ ├── config.module.ts │ ├── configuration.ts │ └── validation.schema.ts ├── core/ # Core module (application-wide concerns) │ ├── core.module.ts │ ├── interceptors/ │ ├── filters/ │ └── guards/ ├── shared/ # Shared module (common utilities) │ ├── shared.module.ts │ ├── dtos/ │ ├── interfaces/ │ └── utils/ ├── database/ # Database module │ ├── database.module.ts │ ├── migrations/ │ └── seeds/ ├── domain/ # Domain modules (feature modules) │ ├── users/ │ │ ├── users.module.ts │ │ ├── controllers/ │ │ ├── services/ │ │ ├── repositories/ │ │ ├── entities/ │ │ ├── dto/ │ │ └── interfaces/ │ ├── products/ │ │ └── ... │ └── orders/ │ └── ... └── main.ts # Application entry point
Module Interaction Patterns:
Strategic Module Exports and Imports:
// core.module.ts
import { Module, Global } from '@nestjs/common';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { LoggingInterceptor } from './interceptors/logging.interceptor';
@Global() // Makes providers available application-wide
@Module({
providers: [JwtAuthGuard, LoggingInterceptor],
exports: [JwtAuthGuard, LoggingInterceptor],
})
export class CoreModule {}
// users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersController } from './controllers/users.controller';
import { UsersService } from './services/users.service';
import { UserRepository } from './repositories/user.repository';
import { User } from './entities/user.entity';
import { SharedModule } from '../../shared/shared.module';
@Module({
imports: [
TypeOrmModule.forFeature([User]),
SharedModule,
],
controllers: [UsersController],
providers: [UsersService, UserRepository],
exports: [UsersService], // Strategic exports
})
export class UsersModule {}
Advanced Module Organization Techniques:
- Dynamic Module Configuration: Implement module factories for configurable modules.
// database.module.ts import { Module, DynamicModule } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; @Module({}) export class DatabaseModule { static forRoot(options: any): DynamicModule { return { module: DatabaseModule, imports: [TypeOrmModule.forRoot(options)], global: true, }; } }
- Module Composition: Use composite modules to organize related feature modules.
// e-commerce.module.ts (Composite module) import { Module } from '@nestjs/common'; import { ProductsModule } from './products/products.module'; import { OrdersModule } from './orders/orders.module'; import { CartModule } from './cart/cart.module'; @Module({ imports: [ProductsModule, OrdersModule, CartModule], }) export class ECommerceModule {}
- Lazy-loaded Modules: For performance optimization in larger applications (especially with NestJS in a microservices context).
Architectural Insight: Consider organizing modules based on bounded contexts from Domain-Driven Design. This creates natural boundaries that align with business domains and facilitates potential microservice extraction in the future.
Cross-Cutting Concerns:
Handle cross-cutting concerns through specialized modules:
- ConfigModule: Environment-specific configuration using dotenv or config service
- AuthModule: Authentication and authorization logic
- LoggingModule: Centralized logging functionality
- HealthModule: Application health checks and monitoring
Testing Considerations:
Proper modularization facilitates both unit and integration testing:
// users.service.spec.ts
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
// Import only what's needed for testing this service
SharedModule,
TypeOrmModule.forFeature([User]),
],
providers: [UsersService, UserRepository],
}).compile();
service = module.get(UsersService);
});
// Tests...
});
A well-modularized NestJS application adheres to the Interface Segregation and Dependency Inversion principles from SOLID, enabling a loosely coupled architecture that can evolve with changing requirements while maintaining clear boundaries between different domains of functionality.
Beginner Answer
Posted on May 10, 2025Organizing a NestJS application with modules helps keep your code clean and maintainable. Here's a simple approach to structuring your application:
Basic Structure of a NestJS Application:
- Root Module: Every NestJS application has a root module, typically called
AppModule
. - Feature Modules: Create separate modules for different features or parts of your application.
- Shared Modules: For code that will be used across multiple feature modules.
Typical Project Structure:
src/ ├── app.module.ts # Root module ├── app.controller.ts # Main controller ├── app.service.ts # Main service ├── users/ # Users feature module │ ├── users.module.ts │ ├── users.controller.ts │ ├── users.service.ts │ └── dto/ ├── products/ # Products feature module │ ├── products.module.ts │ ├── products.controller.ts │ ├── products.service.ts │ └── dto/ └── shared/ # Shared module ├── shared.module.ts └── services/
Steps to Organize Your NestJS Application:
- Create feature modules for different parts of your application
- Keep related files together (controllers, services, etc.) in their module folder
- Import modules where they are needed
- Export providers that need to be used in other modules
Example of Module Organization:
// app.module.ts (Root Module)
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
import { ProductsModule } from './products/products.module';
import { SharedModule } from './shared/shared.module';
@Module({
imports: [UsersModule, ProductsModule, SharedModule],
})
export class AppModule {}
// users.module.ts (Feature Module)
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { SharedModule } from '../shared/shared.module';
@Module({
imports: [SharedModule],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // Export if other modules need this service
})
export class UsersModule {}
Tip: Keep your modules focused on specific functionality. If a module gets too large, consider breaking it down into smaller, more manageable modules.
Benefits of This Organization:
- Better readability: Code is easier to find and understand
- Improved maintainability: Changes to one feature don't affect others
- Easier testing: Modules can be tested in isolation
- Reusability: Modules can be reused in other projects
Explain the different ways to access and handle request data (body, params, query) in NestJS controllers.
Expert Answer
Posted on May 10, 2025NestJS provides a comprehensive system for extracting and validating request data through its decorator-based approach. Understanding the nuances of these decorators and how they interact with NestJS's dependency injection system is crucial for building robust APIs.
Request Data Decorators:
- @Body(property?: string): Extracts the request body or a specific property from it
- @Param(param?: string): Extracts route parameters or a specific parameter
- @Query(property?: string): Extracts query parameters or a specific query parameter
- @Headers(header?: string): Extracts HTTP headers or a specific header
- @Req() / @Request(): Provides access to the underlying request object
- @Res() / @Response(): Provides access to the underlying response object (use with caution)
Advanced Implementation with Validation:
import { Controller, Get, Post, Body, Param, Query, ParseIntPipe, ValidationPipe, UsePipes } from '@nestjs/common';
import { CreateUserDto, UserQueryDto } from './dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
// Full body validation with custom DTO
@Post()
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
// Parameter parsing and validation
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id);
}
// Query validation with custom DTO and transformation
@Get()
@UsePipes(new ValidationPipe({ transform: true }))
findAll(@Query() query: UserQueryDto) {
return this.usersService.findAll(query);
}
// Multiple parameter extraction techniques
@Post(':id/profile')
updateProfile(
@Param('id', ParseIntPipe) id: number,
@Body('profile') profile: any,
@Headers('authorization') token: string
) {
// Validate token first
// Then update profile
return this.usersService.updateProfile(id, profile);
}
}
Advanced Techniques:
Custom Parameter Decorators:
You can create custom parameter decorators to extract complex data or perform specialized extraction logic:
// custom-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user; // Assuming authentication middleware adds user
},
);
// Usage in controller
@Get('profile')
getProfile(@CurrentUser() user: UserEntity) {
return this.usersService.getProfile(user.id);
}
Warning: When using @Res()
decorator, you switch to Express's response handling which bypasses NestJS's response interceptors. Use library-specific response objects only when absolutely necessary.
Performance Considerations:
For maximum performance when handling large request payloads:
- Use partial extraction with
@Body(property)
to extract only needed properties - Consider streaming for file uploads or very large payloads
- Use
ValidationPipe
withwhitelist: true
to automatically strip unwanted properties - Employ the
transformOptions
parameter to control object instantiation behavior
Parameter Extraction Approaches:
Approach | Advantages | Disadvantages |
---|---|---|
Dedicated Decorators ( @Body() , @Query() , etc.) |
Clear, explicit, testable, supports pipes | Multiple decorators for complex requests |
Request Object ( @Req() ) |
Access to all request data | Platform-specific, less testable, bypasses NestJS abstractions |
Custom Parameter Decorators | Reusable, complex logic encapsulation | Additional code to maintain |
Beginner Answer
Posted on May 10, 2025In NestJS, handling request data is made simple through decorators that extract different parts of the incoming HTTP request. There are three main types of request data you can access:
Main Request Data Types:
- Request Body: Contains data sent in the request body (often from forms or JSON payloads)
- URL Parameters: Values extracted from the URL path (like IDs in /users/:id)
- Query Parameters: Data sent as URL query strings (like /search?term=nestjs)
Basic Example:
import { Controller, Get, Post, Body, Param, Query } from '@nestjs/common';
@Controller('users')
export class UsersController {
// Handle POST request with body data
@Post()
create(@Body() createUserData: any) {
console.log(createUserData);
return 'User created';
}
// Handle GET request with URL parameter
@Get(':id')
findOne(@Param('id') id: string) {
return `Finding user with id ${id}`;
}
// Handle GET request with query parameters
@Get()
findAll(@Query() query: any) {
const page = query.page || 1;
const limit = query.limit || 10;
return `Fetching users, page ${page}, limit ${limit}`;
}
}
Tip: Always validate your incoming data using validation pipes or DTOs before processing it to ensure it meets your application's requirements.
This approach makes your code clean and readable, as each request data type is clearly marked with decorators.
Explain how to use Data Transfer Objects (DTOs) in NestJS and why they are important.
Expert Answer
Posted on May 10, 2025Data Transfer Objects (DTOs) are a core architectural pattern in NestJS that facilitate clean separation of concerns and robust data validation. They act as contracts between client and server, representing the shape of data as it traverses layer boundaries in your application.
DTO Architecture in NestJS:
DTOs serve multiple purposes in the NestJS ecosystem:
- Request/Response Serialization: Defining the exact structure of data moving in and out of API endpoints
- Input Validation: Combined with class-validator to enforce business rules
- Type Safety: Providing TypeScript interfaces for your data models
- Transformation Logic: Enabling automatic conversion between transport formats and domain models
- API Documentation: Serving as the basis for Swagger/OpenAPI schema generation
- Security Boundary: Acting as a whitelist filter against excessive data exposure
Advanced DTO Implementation:
// user.dto.ts - Base DTO with common properties
import { Expose, Exclude, Type } from 'class-transformer';
import {
IsEmail, IsString, IsInt, IsOptional,
Min, Max, Length, ValidateNested
} from 'class-validator';
// Base entity shared by create/update DTOs
export class UserBaseDto {
@IsString()
@Length(2, 100)
name: string;
@IsEmail()
email: string;
@IsInt()
@Min(0)
@Max(120)
age: number;
}
// Create operation DTO
export class CreateUserDto extends UserBaseDto {
@IsString()
@Length(8, 100)
password: string;
}
// Address nested DTO for complex structures
export class AddressDto {
@IsString()
street: string;
@IsString()
city: string;
@IsString()
@Length(2, 10)
zipCode: string;
}
// Update operation DTO with partial fields and nested object
export class UpdateUserDto {
@IsOptional()
@IsString()
@Length(2, 100)
name?: string;
@IsOptional()
@IsEmail()
email?: string;
@IsOptional()
@ValidateNested()
@Type(() => AddressDto)
address?: AddressDto;
}
// Response DTO (excludes sensitive data)
export class UserResponseDto extends UserBaseDto {
@Expose()
id: number;
@Expose()
createdAt: Date;
@Exclude()
password: string; // This will be excluded from responses
@Type(() => AddressDto)
@ValidateNested()
address?: AddressDto;
}
Advanced Validation Configurations:
// main.ts - Advanced ValidationPipe configuration
import { ValidationPipe, ValidationError, BadRequestException } from '@nestjs/common';
import { useContainer } from 'class-validator';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Configure the global validation pipe
app.useGlobalPipes(new ValidationPipe({
whitelist: true, // Strip properties not defined in DTO
forbidNonWhitelisted: true, // Throw errors if non-whitelisted properties are sent
transform: true, // Transform payloads to be objects typed according to their DTO classes
transformOptions: {
enableImplicitConversion: true, // Implicitly convert types when possible
},
stopAtFirstError: false, // Collect all validation errors
exceptionFactory: (validationErrors: ValidationError[] = []) => {
// Custom formatting of validation errors
const errors = validationErrors.map(error => ({
property: error.property,
constraints: error.constraints
}));
return new BadRequestException({
statusCode: 400,
message: 'Validation failed',
errors
});
}
}));
// Allow dependency injection in custom validators
useContainer(app.select(AppModule), { fallbackOnErrors: true });
await app.listen(3000);
}
bootstrap();
Advanced DTO Techniques:
1. Custom Validation:
// unique-email.validator.ts
import {
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
registerDecorator,
ValidationOptions
} from 'class-validator';
import { Injectable } from '@nestjs/common';
import { UsersService } from './users.service';
@ValidatorConstraint({ async: true })
@Injectable()
export class IsEmailUniqueConstraint implements ValidatorConstraintInterface {
constructor(private usersService: UsersService) {}
async validate(email: string) {
const user = await this.usersService.findByEmail(email);
return !user; // Returns false if user exists (email not unique)
}
defaultMessage(args: ValidationArguments) {
return `Email ${args.value} is already taken`;
}
}
// Custom decorator that uses the constraint
export function IsEmailUnique(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsEmailUniqueConstraint,
});
};
}
// Usage in DTO
export class CreateUserDto {
@IsEmail()
@IsEmailUnique()
email: string;
}
2. DTO Inheritance for API Versioning:
// Base DTO (v1)
export class UserDtoV1 {
@IsString()
name: string;
@IsEmail()
email: string;
}
// Extended DTO (v2) with additional fields
export class UserDtoV2 extends UserDtoV1 {
@IsOptional()
@IsString()
middleName?: string;
@IsPhoneNumber()
phoneNumber: string;
}
// Controller with versioned endpoints
@Controller()
export class UsersController {
@Post('v1/users')
createV1(@Body() userDto: UserDtoV1) {
// V1 implementation
}
@Post('v2/users')
createV2(@Body() userDto: UserDtoV2) {
// V2 implementation using extended DTO
}
}
3. Mapped Types for CRUD Operations:
import { PartialType, PickType, OmitType } from '@nestjs/mapped-types';
// Base DTO with all properties
export class UserDto {
@IsString()
name: string;
@IsEmail()
email: string;
@IsString()
password: string;
@IsDateString()
birthDate: string;
}
// Create DTO (uses all fields)
export class CreateUserDto extends UserDto {}
// Update DTO (all fields optional)
export class UpdateUserDto extends PartialType(UserDto) {}
// Login DTO (only email & password)
export class LoginUserDto extends PickType(UserDto, ['email', 'password'] as const) {}
// Profile DTO (excludes password)
export class ProfileDto extends OmitType(UserDto, ['password'] as const) {}
DTO Design Strategies Comparison:
Strategy | Advantages | Best For |
---|---|---|
Separate DTOs for each operation | Maximum flexibility, clear boundaries | Complex domains with different validation rules per operation |
Inheritance with base DTOs | DRY principle, consistent validation | Similar operations with shared validation logic |
Mapped Types | Automatic type transformations | Standard CRUD operations with predictable patterns |
Composition with nested DTOs | Models complex hierarchical data | Rich domain models with relationship hierarchies |
Performance Considerations:
While DTOs provide significant benefits, they also introduce performance overhead due to validation and transformation. To optimize:
- Use
stopAtFirstError: true
for performance-critical paths - Consider caching validation results for frequently used DTOs
- Selectively apply transformation based on endpoint requirements
- For high-throughput APIs, consider schema validation with JSON Schema validators instead of class-validator
Beginner Answer
Posted on May 10, 2025Data Transfer Objects (DTOs) in NestJS are simple classes that define the structure of data as it moves between your application layers. Think of them as blueprints that describe what data should look like when it's being transferred.
Why Use DTOs?
- Data Validation: They help ensure the data coming into your application is in the correct format
- Type Safety: They provide TypeScript type checking for your request data
- Documentation: They serve as self-documentation for what data your endpoints expect
- Code Organization: They keep your codebase clean by separating data structure definitions
Creating and Using a DTO:
// create-user.dto.ts
export class CreateUserDto {
name: string;
email: string;
age: number;
}
// users.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
@Controller('users')
export class UsersController {
@Post()
create(@Body() createUserDto: CreateUserDto) {
// The incoming data will be shaped according to CreateUserDto
console.log(createUserDto.name);
console.log(createUserDto.email);
console.log(createUserDto.age);
return 'User created';
}
}
Adding Validation:
DTOs become even more powerful when combined with validation decorators from the class-validator package:
// First install these packages:
// npm install class-validator class-transformer
// create-user.dto.ts
import { IsEmail, IsString, IsInt, Min, Max } from 'class-validator';
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
@IsInt()
@Min(0)
@Max(120)
age: number;
}
// Enable validation in your main.ts
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
Tip: Create separate DTOs for different operations. For example, you might have CreateUserDto
and UpdateUserDto
that have slightly different requirements.
With this setup, if someone tries to create a user with invalid data (like an age of 200 or an invalid email format), NestJS will automatically reject the request with appropriate error messages!
What is middleware in NestJS and how does it work? Explain the concept, implementation, and execution flow.
Expert Answer
Posted on May 10, 2025Middleware in NestJS represents functions that execute sequentially in the request-response cycle before the route handler. NestJS middleware is fully compatible with Express middleware, while also providing its own dependency injection and modularity capabilities.
Middleware Architecture in NestJS:
Middleware executes in a specific order within the NestJS request lifecycle:
- Incoming request
- Global middleware
- Module-specific middleware
- Guards
- Interceptors (pre-controller)
- Pipes
- Controller (route handler)
- Service (business logic)
- Interceptors (post-controller)
- Exception filters (if exceptions occur)
- Server response
Implementation Approaches:
1. Function Middleware:
export function loggerMiddleware(req: Request, res: Response, next: NextFunction) {
console.log(`${req.method} ${req.originalUrl}`);
next();
}
2. Class Middleware (with DI support):
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
constructor(private readonly configService: ConfigService) {}
use(req: Request, res: Response, next: NextFunction) {
const logLevel = this.configService.get('LOG_LEVEL');
if (logLevel === 'debug') {
console.log(`${req.method} ${req.originalUrl}`);
}
next();
}
}
Registration Methods:
1. Module-bound Middleware:
@Module({
imports: [ConfigModule],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'users/health', method: RequestMethod.GET },
)
.forRoutes({ path: 'users/*', method: RequestMethod.ALL });
}
}
2. Global Middleware:
// main.ts
const app = await NestFactory.create(AppModule);
app.use(logger); // Function middleware only for global registration
await app.listen(3000);
Technical Implementation Details:
- Execution Chain: NestJS uses a middleware execution chain internally managed by the middleware consumer. When
next()
is called, control passes to the next middleware in the chain. - Route Matching: Middleware can be applied to specific routes using wildcards, regex patterns, and HTTP method filters.
- Lazy Loading: Middleware is instantiated lazily when the module is loaded, allowing proper dependency injection.
- Middleware Consumer: The
MiddlewareConsumer
provides a fluent API to configure middleware, including route targeting and exclusions.
Performance Considerations:
Middleware execution adds overhead to each request, so it's important to:
- Use middleware only when necessary
- Place computationally expensive operations in guards or interceptors instead when possible
- Consider the middleware execution order for optimal performance
- Use the
exclude()
method to prevent middleware execution for specific routes
Advanced Tip: You can implement conditional middleware execution by using a factory pattern:
export function conditionalMiddleware(options: MiddlewareOptions): MiddlewareFunction {
return (req: Request, res: Response, next: NextFunction) => {
if (options.condition(req)) {
// Execute middleware logic
}
next();
};
}
// In your module
consumer
.apply(conditionalMiddleware({ condition: req => req.headers.authorization }))
.forRoutes('*');
Beginner Answer
Posted on May 10, 2025Middleware in NestJS is a function that runs before your route handlers (controllers) process a request. Think of middleware as checkpoints that your request has to pass through before reaching its final destination.
What Middleware Does:
- Executes code: Runs specific functions before the route handler
- Modifies request/response: Can change the request or response objects
- Ends the request cycle: Can stop a request from proceeding further
- Calls the next middleware: Passes control to the next middleware function
Basic Middleware Example:
// A simple logger middleware
export function loggerMiddleware(req, res, next) {
console.log(`Request received: ${req.method} ${req.url}`);
next(); // Important! This passes control to the next middleware
}
// In your module file
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(loggerMiddleware)
.forRoutes('*'); // Apply to all routes
}
}
How Middleware Works in NestJS:
- A request arrives at your NestJS application
- The request passes through each middleware in the order they're defined
- Each middleware can:
- Do its job and call
next()
to continue - End the request-response cycle by sending a response
- Do its job and call
- If all middleware calls
next()
, the request reaches your controller
Tip: Always remember to call next()
in your middleware, or your request will hang!
Explain the different types of middleware in NestJS and when to use each. Cover function middleware, class middleware, global middleware, and module-specific middleware.
Expert Answer
Posted on May 10, 2025NestJS middleware follows the Express middleware pattern but extends it with additional capabilities and integration with the NestJS ecosystem. Understanding the different types and their appropriate use cases is crucial for effective application architecture.
Middleware Classification in NestJS:
1. By Implementation Pattern:
Type | Implementation | DI Support | Technical Characteristics |
---|---|---|---|
Function Middleware | Standard Express-style functions | No | Lightweight, simple access to request/response objects |
Class Middleware | Classes implementing NestMiddleware interface | Yes | Full access to NestJS container, lifecycle hooks, and providers |
2. By Registration Scope:
Type | Registration Method | Application Point | Execution Order |
---|---|---|---|
Global Middleware | app.use() in bootstrap file |
All routes across all modules | First in the middleware chain |
Module-bound Middleware | configure(consumer) in a module implementing NestModule |
Specific routes within the module's scope | After global middleware, in the order defined in the consumer |
Deep Technical Analysis:
1. Function Middleware Implementation:
// Standard Express-compatible middleware function
export function headerValidator(req: Request, res: Response, next: NextFunction) {
const apiKey = req.headers['x-api-key'];
if (!apiKey) {
return res.status(403).json({ message: 'API key missing' });
}
// Store validated data on request object for downstream handlers
req['validatedApiKey'] = apiKey;
next();
}
// Registration in bootstrap
const app = await NestFactory.create(AppModule);
app.use(headerValidator);
2. Class Middleware with Dependencies:
@Injectable()
export class AuthMiddleware implements NestMiddleware {
constructor(
private readonly authService: AuthService,
private readonly configService: ConfigService
) {}
async use(req: Request, res: Response, next: NextFunction) {
const token = this.extractTokenFromHeader(req);
if (!token) {
return res.status(401).json({ message: 'Unauthorized' });
}
try {
const payload = await this.authService.verifyToken(
token,
this.configService.get('JWT_SECRET')
);
req['user'] = payload;
next();
} catch (error) {
return res.status(401).json({ message: 'Invalid token' });
}
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
// Registration in module
@Module({
imports: [AuthModule, ConfigModule],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AuthMiddleware)
.forRoutes(
{ path: 'users/:id', method: RequestMethod.GET },
{ path: 'users/:id', method: RequestMethod.PATCH },
{ path: 'users/:id', method: RequestMethod.DELETE }
);
}
}
3. Advanced Route Configuration:
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
// Multiple middleware in execution order
consumer
.apply(CorrelationIdMiddleware, RequestLoggerMiddleware, AuthMiddleware)
.exclude(
{ path: 'health', method: RequestMethod.GET },
{ path: 'metrics', method: RequestMethod.GET }
)
.forRoutes('*');
// Different middleware for different routes
consumer
.apply(RateLimiterMiddleware)
.forRoutes(
{ path: 'auth/login', method: RequestMethod.POST },
{ path: 'auth/register', method: RequestMethod.POST }
);
// Route-specific middleware with wildcards
consumer
.apply(CacheMiddleware)
.forRoutes({ path: 'products*', method: RequestMethod.GET });
}
}
Middleware Factory Pattern:
For middleware that requires configuration, implement a factory pattern:
export function rateLimiter(options: RateLimiterOptions): MiddlewareFunction {
const limiter = new RateLimit({
windowMs: options.windowMs || 15 * 60 * 1000,
max: options.max || 100,
message: options.message || 'Too many requests, please try again later'
});
return (req: Request, res: Response, next: NextFunction) => {
// Skip rate limiting for certain conditions if needed
if (options.skipIf && options.skipIf(req)) {
return next();
}
// Apply rate limiting
limiter(req, res, next);
};
}
// Usage
consumer
.apply(rateLimiter({
windowMs: 60 * 1000,
max: 10,
skipIf: req => req.ip === '127.0.0.1'
}))
.forRoutes(AuthController);
Decision Framework for Middleware Selection:
Requirement | Recommended Type | Implementation Approach |
---|---|---|
Application-wide with no dependencies | Global Function Middleware | app.use() in main.ts |
Dependent on NestJS services | Class Middleware | Module-bound via consumer |
Conditional application based on route | Module-bound Function/Class Middleware | Configure with specific route patterns |
Cross-cutting concerns with complex logic | Class Middleware with DI | Module-bound with explicit ordering |
Hot-swappable/configurable behavior | Middleware Factory Function | Creating middleware instance with configuration |
Advanced Performance Tip: For computationally expensive operations that don't need to execute on every request, consider conditional middleware execution with early termination patterns:
@Injectable()
export class OptimizedMiddleware implements NestMiddleware {
constructor(private cacheManager: Cache) {}
async use(req: Request, res: Response, next: NextFunction) {
// Early return for excluded paths
if (req.path.startsWith('/public/')) {
return next();
}
// Check cache before heavy processing
const cacheKey = `request_${req.path}`;
const cachedResponse = await this.cacheManager.get(cacheKey);
if (cachedResponse) {
return res.status(200).json(cachedResponse);
}
// Heavy processing only when necessary
const result = await this.heavyComputation(req);
req['processedData'] = result;
next();
}
private async heavyComputation(req: Request) {
// Expensive operation here
}
}
Beginner Answer
Posted on May 10, 2025NestJS offers several types of middleware to help you process requests before they reach your route handlers. Each type is useful in different situations.
Main Types of NestJS Middleware:
Middleware Type | Description | When to Use |
---|---|---|
Function Middleware | Simple functions that take request, response, and next parameters | For quick, simple tasks like logging |
Class Middleware | Classes that implement the NestMiddleware interface | When you need to use dependency injection |
Global Middleware | Applied to every route in the application | For application-wide functionality like CORS or body parsing |
Module-specific Middleware | Applied only to specific modules or routes | When functionality is needed for a specific feature area |
1. Function Middleware
This is the simplest form - just a regular function:
// Function middleware
export function simpleLogger(req, res, next) {
console.log('Request received...');
next();
}
2. Class Middleware
More powerful because it can use NestJS dependency injection:
// Class middleware
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Request received from class middleware...');
next();
}
}
3. Global Middleware
Applied to all routes in your application:
// In main.ts
const app = await NestFactory.create(AppModule);
app.use(simpleLogger); // Apply to all routes
await app.listen(3000);
4. Module-specific Middleware
Applied only to routes in a specific module:
// In your module file
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('cats'); // Only apply to routes starting with "cats"
}
}
Tip: Choose your middleware type based on:
- Scope needed (global vs. specific routes)
- Complexity (simple function vs. class with dependencies)
- Reusability requirements (will you use it in multiple places?)
Explain the concept of pipes in NestJS, their purpose, and how they are used within the framework.
Expert Answer
Posted on May 10, 2025Pipes in NestJS are classes annotated with the @Injectable()
decorator that implement the PipeTransform
interface. They operate on the arguments being processed by a controller route handler, performing data transformation or validation before the handler receives the arguments.
Core Functionality:
- Transformation: Converting input data from one form to another (e.g., string to integer, DTO to entity)
- Validation: Evaluating input data against predefined rules and raising exceptions for invalid data
Pipes run inside the request processing pipeline, specifically after guards and before interceptors and the route handler.
Pipe Execution Context:
Pipes execute in different contexts depending on how they are registered:
- Parameter-scoped pipes: Applied to a specific parameter
- Handler-scoped pipes: Applied to all parameters in a route handler
- Controller-scoped pipes: Applied to all route handlers in a controller
- Global-scoped pipes: Applied to all controllers and route handlers
Implementation Architecture:
export interface PipeTransform<T = any, R = any> {
transform(value: T, metadata: ArgumentMetadata): R;
}
// Example implementation
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed: numeric string expected');
}
return val;
}
}
Binding Pipes:
// Parameter-scoped
@Get('/:id')
findOne(@Param('id', ParseIntPipe) id: number) {}
// Handler-scoped
@Post()
@UsePipes(new ValidationPipe())
create(@Body() createUserDto: CreateUserDto) {}
// Controller-scoped
@Controller('users')
@UsePipes(ValidationPipe)
export class UsersController {}
// Global-scoped
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
Async Pipes:
Pipes can also be asynchronous by returning a Promise or using async/await within the transform method, which is useful for database lookups or external API calls during validation.
Performance Note: While pipes provide powerful validation capabilities, complex validation logic in pipes can impact performance. For high-throughput APIs, consider simpler validation strategies or moving complex validation logic to a separate layer.
Pipe Execution Order:
When multiple pipes are applied to a parameter, they execute in the following order:
- Global pipes
- Controller-level pipes
- Handler-level pipes
- Parameter-level pipes
Beginner Answer
Posted on May 10, 2025Pipes in NestJS are simple classes that help process data before it reaches your route handlers. Think of them like actual pipes in plumbing - data flows through them and they can transform or validate that data along the way.
Main Uses of Pipes:
- Transformation: Converting input data to the desired form (like changing strings to numbers)
- Validation: Checking if data meets certain rules and rejecting it if it doesn't
Example of Built-in Pipes:
@Get('/:id')
findOne(@Param('id', ParseIntPipe) id: number) {
// ParseIntPipe ensures id is a number
// If someone passes "abc" instead of a number, the request fails
return this.usersService.findOne(id);
}
NestJS comes with several built-in pipes:
- ValidationPipe: Validates objects against a class schema
- ParseIntPipe: Converts string to integer
- ParseBoolPipe: Converts string to boolean
- ParseArrayPipe: Converts string to array
Tip: Pipes can be applied at different levels - parameter level, method level, or globally for your entire application.
Describe the process of creating and implementing custom validation pipes in NestJS applications, including the key interfaces and methods required.
Expert Answer
Posted on May 10, 2025Implementing custom validation pipes in NestJS involves creating classes that implement the PipeTransform
interface to perform specialized validation logic tailored to your application's requirements.
Architecture of a Custom Validation Pipe:
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
@Injectable()
export class CustomValidationPipe implements PipeTransform {
// Optional constructor for configuration
constructor(private readonly options?: any) {}
transform(value: any, metadata: ArgumentMetadata) {
// metadata contains:
// - type: 'body', 'query', 'param', 'custom'
// - metatype: The type annotation on the parameter
// - data: The parameter name
// Validation logic here
if (!this.isValid(value)) {
throw new BadRequestException('Validation failed');
}
// Return the original value or a transformed version
return value;
}
private isValid(value: any): boolean {
// Your custom validation logic
return true;
}
}
Advanced Implementation Patterns:
Example 1: Schema-based Validation Pipe
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import * as Joi from 'joi';
@Injectable()
export class JoiValidationPipe implements PipeTransform {
constructor(private schema: Joi.Schema) {}
transform(value: any, metadata: ArgumentMetadata) {
const { error, value: validatedValue } = this.schema.validate(value);
if (error) {
const errorMessage = error.details
.map(detail => detail.message)
.join(', ');
throw new BadRequestException(`Validation failed: ${errorMessage}`);
}
return validatedValue;
}
}
// Usage
@Post()
create(
@Body(new JoiValidationPipe(createUserSchema)) createUserDto: CreateUserDto,
) {
// ...
}
Example 2: Entity Existence Validation Pipe
@Injectable()
export class EntityExistsPipe implements PipeTransform {
constructor(
private readonly repository: Repository,
private readonly entityName: string,
) {}
async transform(value: any, metadata: ArgumentMetadata) {
const entity = await this.repository.findOne(value);
if (!entity) {
throw new NotFoundException(
`${this.entityName} with id ${value} not found`,
);
}
return entity; // Note: returning the actual entity, not just ID
}
}
// Usage with TypeORM
@Get(':id')
findOne(
@Param('id', new EntityExistsPipe(userRepository, 'User'))
user: User, // Now parameter is the actual user entity
) {
return user; // No need to query again
}
Performance and Testing Considerations:
- Caching results: For expensive validations, consider implementing caching
- Dependency injection: Custom pipes can inject services for database queries
- Testing: Pipes should be unit tested independently
// Example of a pipe with dependency injection
@Injectable()
export class UserExistsPipe implements PipeTransform {
constructor(private readonly usersService: UsersService) {}
async transform(value: any, metadata: ArgumentMetadata) {
const user = await this.usersService.findById(value);
if (!user) {
throw new NotFoundException(`User with ID ${value} not found`);
}
return value;
}
}
Unit Testing a Custom Pipe
describe('PositiveIntPipe', () => {
let pipe: PositiveIntPipe;
beforeEach(() => {
pipe = new PositiveIntPipe();
});
it('should transform a positive number string to number', () => {
expect(pipe.transform('42')).toBe(42);
});
it('should throw an exception for non-positive values', () => {
expect(() => pipe.transform('0')).toThrow(BadRequestException);
expect(() => pipe.transform('-1')).toThrow(BadRequestException);
});
it('should throw an exception for non-numeric values', () => {
expect(() => pipe.transform('abc')).toThrow(BadRequestException);
});
});
Integration with Class-validator:
For complex object validation, custom pipes can leverage class-validator and class-transformer:
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';
@Injectable()
export class CustomValidationPipe implements PipeTransform {
constructor(private readonly type: any) {}
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToClass(this.type, value);
const errors = await validate(object);
if (errors.length > 0) {
// Process and format validation errors
const messages = errors.map(error => {
const constraints = error.constraints;
return Object.values(constraints).join(', ');
});
throw new BadRequestException(messages);
}
return object;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
Advanced Tip: For complex validation scenarios, consider combining multiple validation strategies - parameter-level custom pipes for simple validations and body-level pipes using class-validator for complex object validations.
Beginner Answer
Posted on May 10, 2025Custom validation pipes in NestJS allow you to create your own rules for checking data. They're like security guards that ensure only valid data gets through to your application.
Steps to Create a Custom Validation Pipe:
- Create a new class with the
@Injectable()
decorator - Make it implement the
PipeTransform
interface - Add a
transform()
method that does your validation - Return the value if valid, or throw an exception if not
Example: Creating a Simple Positive Number Validation Pipe
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
@Injectable()
export class PositiveIntPipe implements PipeTransform {
transform(value: any) {
// Convert to number and check if positive
const intValue = parseInt(value, 10);
if (isNaN(intValue) || intValue <= 0) {
throw new BadRequestException('Value must be a positive integer');
}
return intValue;
}
}
Using Your Custom Pipe:
@Get('/items/:id')
findItem(@Param('id', PositiveIntPipe) id: number) {
return this.itemsService.findOne(id);
}
Tip: Custom pipes are great for business-specific validations that the built-in pipes don't cover, like checking if a user ID exists in your database.
You can also create custom pipes that work with class-validator to validate whole objects:
// First, create a DTO with validation decorators
export class CreateUserDto {
@IsString()
@MinLength(3)
name: string;
@IsEmail()
email: string;
}
// Then use with ValidationPipe
@Post()
createUser(@Body(new ValidationPipe()) createUserDto: CreateUserDto) {
// At this point, createUserDto has been validated
}
What are guards in NestJS and how do they control access to routes?
Expert Answer
Posted on May 10, 2025Guards in NestJS are execution context evaluators that implement the CanActivate
interface. They serve as a crucial part of NestJS's request lifecycle, specifically for controlling route access based on runtime conditions.
Technical Implementation Details:
Guards sit within the NestJS request pipeline, executing after middleware but before interceptors and pipes. They leverage the power of TypeScript decorators and dependency injection to create a clean separation of concerns.
Guard Interface:
export interface CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean>;
}
Execution Context and Request Evaluation:
The ExecutionContext
provides access to the current execution process, which guards use to extract request details for making authorization decisions:
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedException();
}
try {
const token = authHeader.split(' ')[1];
const payload = await this.jwtService.verifyAsync(token, {
secret: process.env.JWT_SECRET
});
// Attach user to request for use in route handlers
request['user'] = payload;
return true;
} catch (error) {
throw new UnauthorizedException();
}
}
}
Guard Registration and Scope Hierarchy:
Guards can be registered at three different scopes, with a clear hierarchy of specificity:
- Global Guards: Applied to every route handler
// In main.ts
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new JwtAuthGuard());
@UseGuards(RolesGuard)
@Controller('admin')
export class AdminController {
// All methods inherit the RolesGuard
}
@Controller('users')
export class UsersController {
@UseGuards(AdminGuard)
@Get('sensitive-data')
getSensitiveData() {
// Only admin can access this
}
@Get('public-data')
getPublicData() {
// Anyone can access this
}
}
Leveraging Metadata for Enhanced Guards:
NestJS guards can utilize route metadata for more sophisticated decision-making:
// Custom decorator
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
// Guard that utilizes metadata
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
// Usage in controller
@Controller('admin')
export class AdminController {
@Roles('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
@Get('dashboard')
getDashboard() {
// Only admins can access this
}
}
Exception Handling in Guards:
Guards can throw exceptions that are automatically caught by NestJS's exception layer:
// Instead of returning false, throw specific exceptions
if (!user) {
throw new UnauthorizedException();
}
if (!hasPermission) {
throw new ForbiddenException('Insufficient permissions');
}
Advanced Tip: For complex authorization logic, implement a guard that leverages CASL or other policy-based permission libraries to decouple the authorization rules from the guard implementation:
@Injectable()
export class PermissionGuard implements CanActivate {
constructor(
private reflector: Reflector,
private caslAbilityFactory: CaslAbilityFactory,
) {}
canActivate(context: ExecutionContext): boolean {
const requiredPermission = this.reflector.get<PermissionAction>(
'permission',
context.getHandler(),
);
if (!requiredPermission) {
return true;
}
const { user } = context.switchToHttp().getRequest();
const ability = this.caslAbilityFactory.createForUser(user);
return ability.can(requiredPermission.action, requiredPermission.subject);
}
}
Beginner Answer
Posted on May 10, 2025Guards in NestJS are special components that determine whether a request should be handled by the route handler or not. Think of them as bouncers at a club who check if you have the right credentials to enter.
How Guards Work:
- Purpose: Guards control access to routes based on certain conditions like authentication status, user roles, or permissions.
- Execution Timing: They run after middleware but before pipes and interceptors.
- Decision Making: Every guard must implement a
canActivate()
method that returns either true (proceed with request) or false (deny access).
Simple Authentication Guard Example:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
// Check if user is authenticated
return request.isAuthenticated();
}
}
Using Guards in NestJS:
- Controller-level: Applied to all routes in a controller
- Method-level: Applied to specific route handlers
- Global: Applied to the entire application
Applying a Guard:
// Method level
@UseGuards(AuthGuard)
@Get('profile')
getProfile() {
return 'This is a protected route';
}
// Controller level
@UseGuards(AuthGuard)
@Controller('users')
export class UsersController {
// All routes in this controller will be protected
}
Tip: Guards are perfect for implementing authentication and authorization in your NestJS applications. They help keep your route handlers clean by separating the access control logic.
How would you implement role-based authentication using guards in NestJS?
Expert Answer
Posted on May 10, 2025Implementing role-based authentication in NestJS requires a comprehensive approach that leverages NestJS's powerful dependency injection system, guards, decorators, and reflection capabilities. Here's an in-depth implementation strategy:
1. User Domain Architecture
First, establish a robust user domain with role support:
// user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, ManyToMany, JoinTable } from 'typeorm';
import { Role } from '../roles/role.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@Column({ select: false })
password: string;
@ManyToMany(() => Role, { eager: true })
@JoinTable()
roles: Role[];
// Helper method for role checking
hasRole(roleName: string): boolean {
return this.roles.some(role => role.name === roleName);
}
}
// role.entity.ts
@Entity()
export class Role {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
name: string;
@Column()
description: string;
}
2. Authentication Infrastructure
Implement JWT-based authentication with refresh token support:
// auth.service.ts
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
private configService: ConfigService,
) {}
async validateUser(email: string, password: string): Promise<any> {
const user = await this.usersService.findOneWithPassword(email);
if (user && await bcrypt.compare(password, user.password)) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(user: User) {
const payload = {
sub: user.id,
email: user.email,
roles: user.roles.map(role => role.name)
};
return {
accessToken: this.jwtService.sign(payload, {
secret: this.configService.get('JWT_SECRET'),
expiresIn: '15m',
}),
refreshToken: this.jwtService.sign(
{ sub: user.id },
{
secret: this.configService.get('JWT_REFRESH_SECRET'),
expiresIn: '7d',
},
),
};
}
async refreshTokens(userId: string) {
const user = await this.usersService.findOne(userId);
if (!user) {
throw new UnauthorizedException('Invalid user');
}
return this.login(user);
}
}
3. Custom Role-Based Authorization
Create a sophisticated role system with custom decorators:
// role.enum.ts
export enum Role {
USER = 'user',
EDITOR = 'editor',
ADMIN = 'admin',
}
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { Role } from './role.enum';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
// policies.decorator.ts - for more granular permissions
export const POLICIES_KEY = 'policies';
export const Policies = (...policies: string[]) => SetMetadata(POLICIES_KEY, policies);
4. JWT Authentication Guard
Create a guard to authenticate users and attach user object to the request:
// jwt-auth.guard.ts
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private configService: ConfigService,
private userService: UsersService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: this.configService.get('JWT_SECRET')
});
// Enhance security by fetching full user from DB
// This ensures revoked users can't use valid tokens
const user = await this.userService.findOne(payload.sub);
if (!user) {
throw new UnauthorizedException('User no longer exists');
}
// Append user and raw JWT payload to request object
request.user = user;
request.jwtPayload = payload;
return true;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
5. Advanced Roles Guard with Hierarchical Role Support
Create a sophisticated roles guard that understands role hierarchy:
// roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
// Role hierarchy - higher roles include lower role permissions
private readonly roleHierarchy = {
[Role.ADMIN]: [Role.ADMIN, Role.EDITOR, Role.USER],
[Role.EDITOR]: [Role.EDITOR, Role.USER],
[Role.USER]: [Role.USER],
};
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles || requiredRoles.length === 0) {
return true; // No role requirements
}
const { user } = context.switchToHttp().getRequest();
if (!user || !user.roles) {
return false; // No user or roles defined
}
// Get user's highest role
const userRoleNames = user.roles.map(role => role.name);
// Check if any user role grants access to required roles
return requiredRoles.some(requiredRole =>
userRoleNames.some(userRole =>
this.roleHierarchy[userRole]?.includes(requiredRole)
)
);
}
}
6. Policy-Based Authorization Guard
For more fine-grained control, implement policy-based permissions:
// permission.service.ts
@Injectable()
export class PermissionService {
// Define policies (can be moved to database for dynamic policies)
private readonly policies = {
'createUser': (user: User) => user.hasRole(Role.ADMIN),
'editArticle': (user: User, articleId: string) =>
user.hasRole(Role.ADMIN) ||
(user.hasRole(Role.EDITOR) && this.isArticleAuthor(user.id, articleId)),
'deleteComment': (user: User, commentId: string) =>
user.hasRole(Role.ADMIN) ||
this.isCommentAuthor(user.id, commentId),
};
can(policyName: string, user: User, ...args: any[]): boolean {
const policy = this.policies[policyName];
if (!policy) return false;
return policy(user, ...args);
}
// These would be replaced with actual DB queries
private isArticleAuthor(userId: string, articleId: string): boolean {
// Query DB to check if user is article author
return true; // Simplified for example
}
private isCommentAuthor(userId: string, commentId: string): boolean {
// Query DB to check if user is comment author
return true; // Simplified for example
}
}
// policy.guard.ts
@Injectable()
export class PolicyGuard implements CanActivate {
constructor(
private reflector: Reflector,
private permissionService: PermissionService,
) {}
canActivate(context: ExecutionContext): boolean {
const requiredPolicies = this.reflector.getAllAndOverride<string[]>(POLICIES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredPolicies || requiredPolicies.length === 0) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user) {
return false;
}
// Extract context parameters for policy evaluation
const params = {
...request.params,
body: request.body,
};
// Check all required policies
return requiredPolicies.every(policy =>
this.permissionService.can(policy, user, params)
);
}
}
7. Controller Implementation
Apply the guards in your controllers:
// articles.controller.ts
@Controller('articles')
@UseGuards(JwtAuthGuard) // Apply auth to all routes
export class ArticlesController {
constructor(private articlesService: ArticlesService) {}
@Get()
findAll() {
// Public route for authenticated users
return this.articlesService.findAll();
}
@Post()
@Roles(Role.EDITOR, Role.ADMIN) // Only editors and admins can create
@UseGuards(RolesGuard)
create(@Body() createArticleDto: CreateArticleDto, @Req() req) {
return this.articlesService.create(createArticleDto, req.user.id);
}
@Delete(':id')
@Roles(Role.ADMIN) // Only admins can delete
@UseGuards(RolesGuard)
remove(@Param('id') id: string) {
return this.articlesService.remove(id);
}
@Patch(':id')
@Policies('editArticle')
@UseGuards(PolicyGuard)
update(
@Param('id') id: string,
@Body() updateArticleDto: UpdateArticleDto
) {
// PolicyGuard will check if user can edit this particular article
return this.articlesService.update(id, updateArticleDto);
}
}
8. Global Guard Registration
For consistent authentication across the application:
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Optional: Apply JwtAuthGuard globally except for paths marked with @Public()
const reflector = app.get(Reflector);
app.useGlobalGuards(new JwtAuthGuard(
app.get(JwtService),
app.get(ConfigService),
app.get(UsersService),
reflector
));
await app.listen(3000);
}
bootstrap();
// public.decorator.ts
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
// In JwtAuthGuard, add:
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride(
IS_PUBLIC_KEY,
[context.getHandler(), context.getClass()],
);
if (isPublic) {
return true;
}
// Rest of the guard logic...
}
9. Module Configuration
Set up the auth module correctly:
// auth.module.ts
@Module({
imports: [
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get('JWT_SECRET'),
signOptions: { expiresIn: '15m' },
}),
inject: [ConfigService],
}),
UsersModule,
PassportModule,
],
providers: [
AuthService,
JwtStrategy,
LocalStrategy,
RolesGuard,
PolicyGuard,
PermissionService,
],
exports: [
AuthService,
JwtModule,
RolesGuard,
PolicyGuard,
PermissionService,
],
})
export class AuthModule {}
Production Considerations:
- Redis for token blacklisting: Implement token revocation for logout/security breach scenarios
- Rate limiting: Add rate limiting to prevent brute force attacks
- Audit logging: Log authentication and authorization decisions for security tracking
- Database-stored permissions: Move role definitions and policies to database for dynamic management
- Role inheritance: Implement more sophisticated role inheritance with database support
This implementation provides a comprehensive role-based authentication system that is both flexible and secure, leveraging NestJS's architectural patterns to maintain clean separation of concerns.
Beginner Answer
Posted on May 10, 2025Implementing role-based authentication in NestJS allows you to control which users can access specific routes based on their roles (like admin, user, editor, etc.). Let's break down how to do this in simple steps:
Step 1: Set Up Authentication
First, you need a way to authenticate users. This typically involves:
- Creating a user model with a roles property
- Implementing a login system that issues tokens (usually JWT)
- Creating an authentication guard that verifies these tokens
Basic User Model:
// user.entity.ts
export class User {
id: number;
username: string;
password: string;
roles: string[]; // e.g., ['admin', 'user']
}
Step 2: Create a Roles Decorator
Create a custom decorator to mark which roles can access a route:
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
Step 3: Create a Roles Guard
Create a guard that checks if the user has the required role:
// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// Get the roles required for this route
const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
// If no roles required, allow access
if (!requiredRoles) {
return true;
}
// Get the user from the request
const { user } = context.switchToHttp().getRequest();
// Check if user has at least one of the required roles
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
Step 4: Use in Your Controllers
Now you can protect your routes with role requirements:
// users.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { RolesGuard } from '../auth/roles.guard';
import { Roles } from '../auth/roles.decorator';
@Controller('users')
export class UsersController {
@Get()
getAllUsers() {
// Public route - anyone can access
return 'List of all users';
}
@Get('profile')
@UseGuards(JwtAuthGuard) // First check if authenticated
getUserProfile() {
// Any authenticated user can access
return 'User profile';
}
@Get('admin-panel')
@Roles('admin')
@UseGuards(JwtAuthGuard, RolesGuard) // Check auth, then check roles
getAdminPanel() {
// Only users with admin role can access
return 'Admin panel';
}
}
Tip: The order of guards matters! Place the authentication guard (JwtAuthGuard) before the roles guard, as you need to authenticate the user before checking their roles.
Summary:
To implement role-based authentication in NestJS:
- Set up user authentication (usually with JWT)
- Add roles to your user model
- Create a roles decorator to mark required roles for routes
- Create a roles guard that checks if the user has the required roles
- Apply both authentication and roles guards to your routes
This approach is clean, reusable, and follows NestJS's principles of separation of concerns.
Explain the concept of interceptors in NestJS, their purpose in the request-response cycle, and how they are implemented.
Expert Answer
Posted on May 10, 2025Interceptors in NestJS are classes that implement the NestInterceptor
interface and utilize RxJS observables to provide powerful middleware-like capabilities with fine-grained control over the request-response stream.
Technical Implementation:
Interceptors implement the intercept()
method which takes two parameters:
- ExecutionContext: Provides access to request details and the underlying platform (Express/Fastify)
- CallHandler: A wrapper around the route handler, providing the
handle()
method that returns an Observable
Anatomy of an Interceptor:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map, tap, catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable {
// Pre-controller logic
const request = context.switchToHttp().getRequest();
const method = request.method;
const url = request.url;
const now = Date.now();
// Handle() returns an Observable of the controller's result
return next
.handle()
.pipe(
// Post-controller logic: transform the response
map(data => ({
data,
meta: {
timestamp: new Date().toISOString(),
url,
method,
executionTime: `${Date.now() - now}ms`
}
})),
catchError(err => {
// Error handling logic
console.error(`Error in ${method} ${url}:`, err);
return throwError(() => err);
})
);
}
}
Execution Context and Platform Abstraction:
The ExecutionContext
extends ArgumentsHost
and provides methods to access the underlying platform context:
// For HTTP applications
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
// For WebSockets
const client = context.switchToWs().getClient();
// For Microservices
const ctx = context.switchToRpc().getContext();
Integration with Dependency Injection:
Unlike Express middleware, interceptors can inject dependencies via constructor:
@Injectable()
export class CacheInterceptor implements NestInterceptor {
constructor(
private cacheService: CacheService,
private configService: ConfigService
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable {
const cacheKey = this.buildCacheKey(context);
const ttl = this.configService.get('cache.ttl');
const cachedResponse = this.cacheService.get(cacheKey);
if (cachedResponse) {
return of(cachedResponse);
}
return next.handle().pipe(
tap(response => this.cacheService.set(cacheKey, response, ttl))
);
}
}
Binding Mechanisms:
NestJS provides multiple ways to bind interceptors:
- Method-scoped:
@UseInterceptors(LoggingInterceptor)
- Controller-scoped: Applied to all routes in a controller
- Globally-scoped: Using
app.useGlobalInterceptors()
or providers configuration
// Global binding using providers (preferred for DI)
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}
Execution Order:
In the NestJS request lifecycle, interceptors execute:
- After guards (if a guard exists)
- Before pipes and route handlers
- After the route handler returns a response
- Before the response is sent back to the client
Technical Detail: Interceptors leverage RxJS's powerful operators to manipulate the stream. The response manipulation happens in the pipe()
chain after next.handle()
is called, which represents the point where the route handler executes.
Beginner Answer
Posted on May 10, 2025Interceptors in NestJS are special classes that can add extra functionality to incoming requests and outgoing responses, similar to how a security checkpoint works at an airport.
How Interceptors Work:
- Intercept Requests/Responses: They can examine and modify both incoming requests and outgoing responses
- Add Extra Logic: They add cross-cutting functionality like logging, timing, or transforming data
- Run Before and After: They execute code both before a request reaches your handler and after your handler generates a response
Basic Interceptor Example:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable {
console.log('Before...');
const now = Date.now();
return next
.handle()
.pipe(
tap(() => console.log(`After... ${Date.now() - now}ms`)),
);
}
}
Tip: Think of interceptors like middleware, but with more capabilities and better integration with NestJS's dependency injection system.
How to Use Interceptors:
- Create a class that implements the
NestInterceptor
interface - Apply it to your controller/method using
@UseInterceptors()
decorator - You can apply interceptors globally, to specific controllers, or to individual routes
Applying an Interceptor:
// Apply to a specific controller method
@UseInterceptors(LoggingInterceptor)
@Get()
findAll() {
return this.catsService.findAll();
}
// Or apply to an entire controller
@UseInterceptors(LoggingInterceptor)
@Controller('cats')
export class CatsController { ... }
Discuss practical scenarios where interceptors provide value in NestJS applications, and explain how they are implemented for these specific use cases.
Expert Answer
Posted on May 10, 2025NestJS interceptors leverage RxJS operators to provide powerful cross-cutting functionality. Below are comprehensive implementations of key interceptor patterns with technical explanations of their operation and use cases.
1. Telemetry and Performance Monitoring
Advanced logging with correlation IDs, performance metrics, and integration with monitoring systems:
@Injectable()
export class TelemetryInterceptor implements NestInterceptor {
private readonly logger = new Logger(TelemetryInterceptor.name);
constructor(
private readonly metricsService: MetricsService,
@Inject(TRACE_SERVICE) private readonly tracer: TraceService
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable {
const request = context.switchToHttp().getRequest();
const { method, url, ip, headers } = request;
const userAgent = headers['user-agent'] || 'unknown';
// Generate or extract correlation ID
const correlationId = headers['x-correlation-id'] || randomUUID();
request.correlationId = correlationId;
// Create span for this request
const span = this.tracer.startSpan(`HTTP ${method} ${url}`);
span.setTag('http.method', method);
span.setTag('http.url', url);
span.setTag('correlation.id', correlationId);
const startTime = performance.now();
// Set context for downstream services
context.switchToHttp().getResponse().setHeader('x-correlation-id', correlationId);
return next.handle().pipe(
tap({
next: (data) => {
const duration = performance.now() - startTime;
// Record metrics
this.metricsService.recordHttpRequest({
method,
path: url,
status: 200,
duration,
});
// Complete tracing span
span.finish();
this.logger.log({
message: `${method} ${url} completed`,
correlationId,
duration: `${duration.toFixed(2)}ms`,
ip,
userAgent,
status: 'success'
});
},
error: (error) => {
const duration = performance.now() - startTime;
const status = error.status || 500;
// Record error metrics
this.metricsService.recordHttpRequest({
method,
path: url,
status,
duration,
});
// Mark span as failed
span.setTag('error', true);
span.log({
event: 'error',
'error.message': error.message,
stack: error.stack
});
span.finish();
this.logger.error({
message: `${method} ${url} failed`,
correlationId,
error: error.message,
stack: error.stack,
duration: `${duration.toFixed(2)}ms`,
ip,
userAgent,
status
});
}
}),
// Importantly, we don't convert errors here to allow the exception filters to work
);
}
}
2. Response Transformation and API Standardization
Advanced response structure with metadata, pagination support, and hypermedia links:
@Injectable()
export class ApiResponseInterceptor implements NestInterceptor {
constructor(private configService: ConfigService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
return next.handle().pipe(
map(data => {
// Determine if this is a paginated response
const isPaginated = data &&
typeof data === 'object' &&
'items' in data &&
'total' in data &&
'page' in data;
const baseUrl = this.configService.get('app.baseUrl');
const apiVersion = this.configService.get('app.apiVersion');
const result = {
status: 'success',
code: response.statusCode,
message: response.statusMessage || 'Operation successful',
timestamp: new Date().toISOString(),
path: request.url,
version: apiVersion,
data: isPaginated ? data.items : data,
};
// Add pagination metadata if this is a paginated response
if (isPaginated) {
const { page, size, total } = data;
const totalPages = Math.ceil(total / size);
result['meta'] = {
pagination: {
page,
size,
total,
totalPages,
},
links: {
self: `${baseUrl}${request.url}`,
first: `${baseUrl}${this.getUrlWithPage(request.url, 1)}`,
prev: page > 1 ? `${baseUrl}${this.getUrlWithPage(request.url, page - 1)}` : null,
next: page < totalPages ? `${baseUrl}${this.getUrlWithPage(request.url, page + 1)}` : null,
last: `${baseUrl}${this.getUrlWithPage(request.url, totalPages)}`
}
};
}
return result;
})
);
}
private getUrlWithPage(url: string, page: number): string {
const urlObj = new URL(`http://placeholder${url}`);
urlObj.searchParams.set('page', page.toString());
return `${urlObj.pathname}${urlObj.search}`;
}
}
3. Caching with Advanced Strategies
Sophisticated caching with TTL, conditional invalidation, and tenant isolation:
@Injectable()
export class CacheInterceptor implements NestInterceptor {
constructor(
private cacheManager: Cache,
private configService: ConfigService,
private tenantService: TenantService
) {}
async intercept(context: ExecutionContext, next: CallHandler): Promise> {
// Skip caching for non-GET methods or if explicitly disabled
const request = context.switchToHttp().getRequest();
if (request.method !== 'GET' || request.headers['cache-control'] === 'no-cache') {
return next.handle();
}
// Build cache key with tenant isolation
const tenantId = this.tenantService.getCurrentTenant(request);
const urlKey = request.url;
const queryParams = JSON.stringify(request.query);
const cacheKey = `${tenantId}:${urlKey}:${queryParams}`;
try {
// Try to get from cache
const cachedResponse = await this.cacheManager.get(cacheKey);
if (cachedResponse) {
return of(cachedResponse);
}
// Route-specific cache configuration
const handlerName = context.getHandler().name;
const controllerName = context.getClass().name;
const routeConfigKey = `cache.routes.${controllerName}.${handlerName}`;
const defaultTtl = this.configService.get('cache.defaultTtl') || 60; // 60 seconds default
const ttl = this.configService.get(routeConfigKey) || defaultTtl;
// Execute route handler and cache the response
return next.handle().pipe(
tap(async (response) => {
// Don't cache null/undefined responses
if (response !== undefined && response !== null) {
// Add cache header for browser caching
context.switchToHttp().getResponse().setHeader(
'Cache-Control',
`private, max-age=${ttl}``
);
// Store in server cache
await this.cacheManager.set(cacheKey, response, ttl * 1000);
// Register this cache key for the resource to support invalidation
if (response.id) {
const resourceType = controllerName.replace('Controller', '').toLowerCase();
const resourceId = response.id;
const invalidationKey = `invalidation:${resourceType}:${resourceId}`;
// Get existing cache keys for this resource or initialize empty array
const existingKeys = await this.cacheManager.get(invalidationKey) || [];
// Add current key if not already in the list
if (!existingKeys.includes(cacheKey)) {
existingKeys.push(cacheKey);
await this.cacheManager.set(invalidationKey, existingKeys);
}
}
}
})
);
} catch (error) {
// If cache fails, don't crash the app, just skip caching
return next.handle();
}
}
}
4. Request Rate Limiting
Advanced rate limiting with sliding window algorithm and multiple limiting strategies:
@Injectable()
export class RateLimitInterceptor implements NestInterceptor {
constructor(
@Inject('REDIS') private readonly redisClient: Redis,
private configService: ConfigService,
private authService: AuthService,
) {}
async intercept(context: ExecutionContext, next: CallHandler): Promise> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
// Identify the client by user ID or IP
const user = request.user;
const clientId = user ? `user:${user.id}` : `ip:${request.ip}`;
// Determine rate limit parameters (different for authenticated vs anonymous)
const isAuthenticated = !!user;
const endpoint = `${request.method}:${request.route.path}`;
const defaultLimit = isAuthenticated ?
this.configService.get('rateLimit.authenticated.limit') :
this.configService.get('rateLimit.anonymous.limit');
const defaultWindow = isAuthenticated ?
this.configService.get('rateLimit.authenticated.windowSec') :
this.configService.get('rateLimit.anonymous.windowSec');
// Check for endpoint-specific limits
const endpointConfig = this.configService.get(`rateLimit.endpoints.${endpoint}`);
const limit = (endpointConfig?.limit) || defaultLimit;
const windowSec = (endpointConfig?.windowSec) || defaultWindow;
// If user has special permissions, they might have higher limits
if (user && await this.authService.hasPermission(user, 'rate-limit:bypass')) {
return next.handle();
}
// Implement sliding window algorithm
const now = Math.floor(Date.now() / 1000);
const windowStart = now - windowSec;
const key = `ratelimit:${clientId}:${endpoint}`;
// Record this request
await this.redisClient.zadd(key, now, `${now}:${randomUUID()}`);
// Remove old entries outside the window
await this.redisClient.zremrangebyscore(key, 0, windowStart);
// Set expiry on the set itself
await this.redisClient.expire(key, windowSec * 2);
// Count requests in current window
const requestCount = await this.redisClient.zcard(key);
// Set rate limit headers
response.header('X-RateLimit-Limit', limit.toString());
response.header('X-RateLimit-Remaining', Math.max(0, limit - requestCount).toString());
response.header('X-RateLimit-Reset', (now + windowSec).toString());
if (requestCount > limit) {
const retryAfter = windowSec;
response.header('Retry-After', retryAfter.toString());
throw new HttpException(
`Rate limit exceeded. Try again in ${retryAfter} seconds.`,
HttpStatus.TOO_MANY_REQUESTS
);
}
return next.handle();
}
}
5. Request Timeout Management
Graceful handling of long-running operations with timeout control:
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
constructor(
private configService: ConfigService,
private logger: LoggerService
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable {
const request = context.switchToHttp().getRequest();
const controller = context.getClass().name;
const handler = context.getHandler().name;
// Get timeout configuration
const defaultTimeout = this.configService.get('http.timeout.default') || 30000; // 30 seconds
const routeTimeout = this.configService.get(`http.timeout.routes.${controller}.${handler}`);
const timeout = routeTimeout || defaultTimeout;
return next.handle().pipe(
// Use timeout operator from RxJS
timeoutWith(
timeout,
throwError(() => {
this.logger.warn(`Request timeout: ${request.method} ${request.url} exceeded ${timeout}ms`);
return new RequestTimeoutException(
`Request processing time exceeded the limit of ${timeout/1000} seconds`
);
}),
// Add scheduler for more precise timing
asyncScheduler
)
);
}
}
Interceptor Execution Order Considerations:
First in Chain | Middle of Chain | Last in Chain |
---|---|---|
|
|
|
Technical Insight: When using multiple global interceptors, remember they execute in reverse registration order due to NestJS's middleware composition pattern. Consider using APP_INTERCEPTOR
with precise provider ordering to control execution sequence.
Beginner Answer
Posted on May 10, 2025Interceptors in NestJS are like helpful assistants that can enhance your application in various ways without cluttering your main code. Here are the most common use cases:
Common Use Cases for NestJS Interceptors:
1. Logging Requests and Responses
Track who's using your application and how long operations take:
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable {
const request = context.switchToHttp().getRequest();
const method = request.method;
const url = request.url;
console.log(`[${new Date().toISOString()}] ${method} ${url}`);
const start = Date.now();
return next.handle().pipe(
tap(() => {
console.log(`[${new Date().toISOString()}] ${method} ${url} - ${Date.now() - start}ms`);
})
);
}
}
2. Transforming Response Data
Format your responses consistently across the application:
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable {
return next.handle().pipe(
map(data => ({
status: 'success',
data,
timestamp: new Date().toISOString()
}))
);
}
}
3. Error Handling
Catch and transform errors in a consistent way:
@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable {
return next.handle().pipe(
catchError(err => {
return throwError(() => new BadRequestException('Something went wrong'));
})
);
}
}
Other Common Use Cases:
- Caching Responses: Store responses to avoid unnecessary processing for repeated requests
- Tracking User Activity: Record user actions for analytics
- Setting Response Headers: Add security headers or other metadata to all responses
- Measuring API Performance: Track how long your endpoints take to respond
- Authentication Context: Add user information to requests for easier access in controllers
Tip: Interceptors are great for code that needs to run for many different routes. This keeps your controller methods focused on their primary job without repeating the same code everywhere.
Explain the concept of exception filters in NestJS, their purpose, and how they work within the NestJS request lifecycle.
Expert Answer
Posted on May 10, 2025Exception filters in NestJS are powerful constructs that provide granular control over the exception handling process. They intercept exceptions thrown within the application and allow for custom response transformations, logging, and exception processing within the request/response pipeline.
Architecture and Implementation:
Exception filters operate within NestJS's request lifecycle as one of the execution context pipelines. They implement the ExceptionFilter
interface, which requires a catch()
method for processing exceptions. The @Catch()
decorator determines which exceptions the filter handles.
Comprehensive Exception Filter Implementation:
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch() // Catches all exceptions
export class GlobalExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(GlobalExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
// Handle HttpExceptions differently than system exceptions
const status =
exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const message =
exception instanceof HttpException
? exception.getResponse()
: 'Internal server error';
// Structured logging for all exceptions
this.logger.error(
`${request.method} ${request.url} ${status}: ${
exception instanceof Error ? exception.stack : 'Unknown error'
}`
);
// Structured response
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
message,
correlationId: request.headers['x-correlation-id'] || 'unknown',
});
}
}
Exception Filter Binding Mechanisms:
Exception filters can be bound at different levels of the application, with different scopes:
- Method-scoped:
@UseFilters(new HttpExceptionFilter())
- instance-based, allowing for constructor injection - Controller-scoped: Same decorator at controller level
- Globally-scoped: Multiple approaches:
- Imperative:
app.useGlobalFilters(new HttpExceptionFilter())
- Dependency Injection aware:
import { Module } from '@nestjs/common'; import { APP_FILTER } from '@nestjs/core'; @Module({ providers: [ { provide: APP_FILTER, useClass: GlobalExceptionFilter, }, ], }) export class AppModule {}
- Imperative:
Request/Response Context Switching:
The ArgumentsHost
parameter provides a powerful abstraction for accessing the underlying platform-specific execution context:
// For HTTP (Express/Fastify)
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
// For WebSockets
const ctx = host.switchToWs();
const client = ctx.getClient();
const data = ctx.getData();
// For Microservices
const ctx = host.switchToRpc();
const data = ctx.getData();
Inheritance and Filter Chaining:
Multiple filters can be applied at different levels, and they execute in a specific order:
- Global filters
- Controller-level filters
- Route-level filters
Filters at more specific levels take precedence over broader scopes.
Advanced Pattern: For enterprise applications, consider implementing a filter hierarchy:
@Catch()
export class BaseExceptionFilter implements ExceptionFilter {
constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
catch(exception: unknown, host: ArgumentsHost) {
// Base implementation
}
protected getHttpAdapter() {
return this.httpAdapterHost.httpAdapter;
}
}
@Catch(HttpException)
export class HttpExceptionFilter extends BaseExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
// HTTP-specific handling
super.catch(exception, host);
}
}
@Catch(QueryFailedError)
export class DatabaseExceptionFilter extends BaseExceptionFilter {
catch(exception: QueryFailedError, host: ArgumentsHost) {
// Database-specific handling
super.catch(exception, host);
}
}
Performance Considerations:
Exception filters should be lightweight to avoid introducing performance bottlenecks. For computationally intensive operations (like logging to external systems), consider:
- Using asynchronous processing for I/O-bound operations
- Implementing bulking for database operations
- Utilizing message queues for heavy processing
Exception filters are a critical part of NestJS's exception handling architecture, enabling robust error handling while maintaining clean separation of concerns between business logic and error processing.
Beginner Answer
Posted on May 10, 2025Exception filters in NestJS are special components that help handle errors in your application. Think of them as safety nets that catch errors before they reach your users and allow you to respond in a consistent way.
Basic Concept:
- Purpose: They transform unhandled exceptions into user-friendly HTTP responses
- Default Behavior: NestJS has a built-in filter that catches exceptions and automatically converts them to appropriate responses
- Custom Handling: You can create your own filters to handle specific types of errors differently
Example of a Basic Exception Filter:
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception.getStatus();
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: exception.message,
});
}
}
How to Use Exception Filters:
- Create a filter class that implements the ExceptionFilter interface
- Use the @Catch() decorator to specify which exceptions it should handle
- Implement the catch() method to process the exception
- Apply the filter to a controller, method, or globally
Tip: You can apply filters at different levels:
- Controller method:
@UseFilters(new HttpExceptionFilter())
- Controller: Same decorator but affects all routes
- Globally: In your main.ts with
app.useGlobalFilters(new HttpExceptionFilter())
In simple terms, exception filters let you customize how your app responds when things go wrong, so you can give users helpful error messages instead of scary technical details.
Describe the approach to implement custom exception handling in NestJS, including creating custom exceptions, filtering them, and providing consistent error responses across an application.
Expert Answer
Posted on May 10, 2025Implementing robust custom exception handling in NestJS requires a comprehensive approach that combines several architectural patterns. This involves creating a layered exception handling system that maintains separation of concerns, provides consistent error responses, and facilitates debugging while following RESTful best practices.
1. Exception Hierarchy Architecture
First, establish a well-structured exception hierarchy:
// base-exception.ts
export abstract class BaseException extends Error {
abstract statusCode: number;
abstract errorCode: string;
constructor(
public readonly message: string,
public readonly metadata?: Record
) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
// api-exception.ts
import { HttpStatus } from '@nestjs/common';
export class ApiException extends BaseException {
constructor(
public readonly statusCode: number,
public readonly errorCode: string,
message: string,
metadata?: Record
) {
super(message, metadata);
}
static badRequest(errorCode: string, message: string, metadata?: Record) {
return new ApiException(HttpStatus.BAD_REQUEST, errorCode, message, metadata);
}
static notFound(errorCode: string, message: string, metadata?: Record) {
return new ApiException(HttpStatus.NOT_FOUND, errorCode, message, metadata);
}
static forbidden(errorCode: string, message: string, metadata?: Record) {
return new ApiException(HttpStatus.FORBIDDEN, errorCode, message, metadata);
}
static unauthorized(errorCode: string, message: string, metadata?: Record) {
return new ApiException(HttpStatus.UNAUTHORIZED, errorCode, message, metadata);
}
static internalError(errorCode: string, message: string, metadata?: Record) {
return new ApiException(HttpStatus.INTERNAL_SERVER_ERROR, errorCode, message, metadata);
}
}
// domain-specific exceptions
export class EntityNotFoundException extends ApiException {
constructor(entityName: string, identifier: string | number) {
super(
HttpStatus.NOT_FOUND,
'ENTITY_NOT_FOUND',
`${entityName} with identifier ${identifier} not found`,
{ entityName, identifier }
);
}
}
export class ValidationException extends ApiException {
constructor(errors: Record) {
super(
HttpStatus.BAD_REQUEST,
'VALIDATION_ERROR',
'Validation failed',
{ errors }
);
}
}
2. Comprehensive Exception Filter
Create a global exception filter that handles all types of exceptions:
// global-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
Injectable
} from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
import { Request } from 'express';
import { ApiException } from './exceptions/api-exception';
import { ConfigService } from '@nestjs/config';
interface ExceptionResponse {
statusCode: number;
timestamp: string;
path: string;
method: string;
errorCode: string;
message: string;
metadata?: Record;
stack?: string;
correlationId?: string;
}
@Catch()
@Injectable()
export class GlobalExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(GlobalExceptionFilter.name);
private readonly isProduction: boolean;
constructor(
private readonly httpAdapterHost: HttpAdapterHost,
configService: ConfigService
) {
this.isProduction = configService.get('NODE_ENV') === 'production';
}
catch(exception: unknown, host: ArgumentsHost) {
// Get the HTTP adapter
const { httpAdapter } = this.httpAdapterHost;
const ctx = host.switchToHttp();
const request = ctx.getRequest();
let responseBody: ExceptionResponse;
// Handle different types of exceptions
if (exception instanceof ApiException) {
responseBody = this.handleApiException(exception, request);
} else if (exception instanceof HttpException) {
responseBody = this.handleHttpException(exception, request);
} else {
responseBody = this.handleUnknownException(exception, request);
}
// Log the exception
this.logException(exception, responseBody);
// Send the response
httpAdapter.reply(
ctx.getResponse(),
responseBody,
responseBody.statusCode
);
}
private handleApiException(exception: ApiException, request: Request): ExceptionResponse {
return {
statusCode: exception.statusCode,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
errorCode: exception.errorCode,
message: exception.message,
metadata: exception.metadata,
stack: this.isProduction ? undefined : exception.stack,
correlationId: request.headers['x-correlation-id'] as string
};
}
private handleHttpException(exception: HttpException, request: Request): ExceptionResponse {
const status = exception.getStatus();
const response = exception.getResponse();
let message: string;
let metadata: Record | undefined;
if (typeof response === 'string') {
message = response;
} else if (typeof response === 'object') {
const responseObj = response as Record;
message = responseObj.message || 'An error occurred';
// Extract metadata, excluding known fields
const { statusCode, error, message: _, ...rest } = responseObj;
metadata = Object.keys(rest).length > 0 ? rest : undefined;
} else {
message = 'An error occurred';
}
return {
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
errorCode: 'HTTP_ERROR',
message,
metadata,
stack: this.isProduction ? undefined : exception.stack,
correlationId: request.headers['x-correlation-id'] as string
};
}
private handleUnknownException(exception: unknown, request: Request): ExceptionResponse {
return {
statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
errorCode: 'INTERNAL_ERROR',
message: 'Internal server error',
stack: this.isProduction
? undefined
: exception instanceof Error
? exception.stack
: String(exception),
correlationId: request.headers['x-correlation-id'] as string
};
}
private logException(exception: unknown, responseBody: ExceptionResponse): void {
const { statusCode, path, method, errorCode, message, correlationId } = responseBody;
const logContext = {
path,
method,
statusCode,
errorCode,
correlationId
};
if (statusCode >= 500) {
this.logger.error(
message,
exception instanceof Error ? exception.stack : 'Unknown error',
logContext
);
} else {
this.logger.warn(message, logContext);
}
}
}
3. Register the Global Filter
Register the filter using dependency injection to enable proper DI in the filter:
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { GlobalExceptionFilter } from './filters/global-exception.filter';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
// other imports
],
providers: [
{
provide: APP_FILTER,
useClass: GlobalExceptionFilter,
},
],
})
export class AppModule {}
4. Exception Interceptor for Service-Layer Transformations
Add an interceptor to transform domain exceptions into API exceptions:
// exception-transform.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
NotFoundException,
BadRequestException,
InternalServerErrorException
} from '@nestjs/common';
import { Observable, catchError, throwError } from 'rxjs';
import { ApiException } from './exceptions/api-exception';
import { EntityNotFoundError } from 'typeorm';
@Injectable()
export class ExceptionTransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable {
return next.handle().pipe(
catchError(error => {
// Transform domain or ORM exceptions to API exceptions
if (error instanceof EntityNotFoundError) {
// Transform TypeORM not found error
return throwError(() => ApiException.notFound(
'ENTITY_NOT_FOUND',
error.message
));
}
// Re-throw API exceptions unchanged
if (error instanceof ApiException) {
return throwError(() => error);
}
// Transform other exceptions
return throwError(() => error);
}),
);
}
}
5. Integration with Validation Pipe
Customize the validation pipe to use your exception structure:
// validation.pipe.ts
import {
PipeTransform,
Injectable,
ArgumentMetadata,
ValidationError
} from '@nestjs/common';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { ValidationException } from './exceptions/api-exception';
@Injectable()
export class CustomValidationPipe implements PipeTransform {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToInstance(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
// Transform validation errors to a structured format
const formattedErrors = this.formatErrors(errors);
throw new ValidationException(formattedErrors);
}
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
private formatErrors(errors: ValidationError[]): Record {
return errors.reduce((acc, error) => {
const property = error.property;
if (!acc[property]) {
acc[property] = [];
}
if (error.constraints) {
acc[property].push(...Object.values(error.constraints));
}
// Handle nested validation errors
if (error.children && error.children.length > 0) {
const nestedErrors = this.formatErrors(error.children);
Object.entries(nestedErrors).forEach(([nestedProp, messages]) => {
const fullProperty = `${property}.${nestedProp}`;
acc[fullProperty] = messages;
});
}
return acc;
}, {} as Record);
}
}
6. Centralized Error Codes Management
Implement a centralized error code registry to maintain consistent error codes:
// error-codes.ts
export enum ErrorCode {
// Authentication errors: 1XXX
UNAUTHORIZED = '1000',
INVALID_TOKEN = '1001',
TOKEN_EXPIRED = '1002',
// Validation errors: 2XXX
VALIDATION_ERROR = '2000',
INVALID_INPUT = '2001',
// Resource errors: 3XXX
RESOURCE_NOT_FOUND = '3000',
RESOURCE_ALREADY_EXISTS = '3001',
// Business logic errors: 4XXX
BUSINESS_RULE_VIOLATION = '4000',
INSUFFICIENT_PERMISSIONS = '4001',
// External service errors: 5XXX
EXTERNAL_SERVICE_ERROR = '5000',
// Server errors: 9XXX
INTERNAL_ERROR = '9000',
}
// Extended API exception class that uses centralized error codes
export class EnhancedApiException extends ApiException {
constructor(
statusCode: number,
errorCode: ErrorCode,
message: string,
metadata?: Record
) {
super(statusCode, errorCode, message, metadata);
}
}
7. Documenting Exceptions with Swagger
Document your exceptions in API documentation:
// user.controller.ts
import { Controller, Get, Param, NotFoundException } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
import { UserService } from './user.service';
import { ErrorCode } from '../exceptions/error-codes';
@ApiTags('users')
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get(':id')
@ApiOperation({ summary: 'Get user by ID' })
@ApiParam({ name: 'id', description: 'User ID' })
@ApiResponse({
status: 200,
description: 'User found',
type: UserDto
})
@ApiResponse({
status: 404,
description: 'User not found',
schema: {
type: 'object',
properties: {
statusCode: { type: 'number', example: 404 },
timestamp: { type: 'string', example: '2023-01-01T12:00:00.000Z' },
path: { type: 'string', example: '/users/123' },
method: { type: 'string', example: 'GET' },
errorCode: { type: 'string', example: ErrorCode.RESOURCE_NOT_FOUND },
message: { type: 'string', example: 'User with id 123 not found' },
correlationId: { type: 'string', example: 'abcd-1234-efgh-5678' }
}
}
})
async findOne(@Param('id') id: string) {
const user = await this.userService.findOne(id);
if (!user) {
throw new EntityNotFoundException('User', id);
}
return user;
}
}
Advanced Patterns:
- Error Isolation: Wrap external service calls in a try/catch block to translate 3rd-party exceptions into your domain exceptions
- Circuit Breaking: Implement circuit breakers for external service calls to fail fast when services are down
- Correlation IDs: Use a middleware to generate and attach correlation IDs to every request for easier debugging
- Feature Flagging: Use feature flags to control the level of error detail shown in different environments
- Metrics Collection: Track exception frequencies and types for monitoring and alerting
8. Testing Exception Handling
Write tests specifically for your exception handling logic:
// global-exception.filter.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { HttpAdapterHost } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import { GlobalExceptionFilter } from './global-exception.filter';
import { ApiException } from '../exceptions/api-exception';
import { HttpStatus } from '@nestjs/common';
describe('GlobalExceptionFilter', () => {
let filter: GlobalExceptionFilter;
let httpAdapterHost: HttpAdapterHost;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
GlobalExceptionFilter,
{
provide: HttpAdapterHost,
useValue: {
httpAdapter: {
reply: jest.fn(),
},
},
},
{
provide: ConfigService,
useValue: {
get: jest.fn().mockReturnValue('test'),
},
},
],
}).compile();
filter = module.get(GlobalExceptionFilter);
httpAdapterHost = module.get(HttpAdapterHost);
});
it('should handle ApiException correctly', () => {
const exception = ApiException.notFound('TEST_ERROR', 'Test error');
const host = createMockArgumentsHost();
filter.catch(exception, host);
expect(httpAdapterHost.httpAdapter.reply).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
statusCode: HttpStatus.NOT_FOUND,
errorCode: 'TEST_ERROR',
message: 'Test error',
}),
HttpStatus.NOT_FOUND
);
});
// Helper to create a mock ArgumentsHost
function createMockArgumentsHost() {
const mockRequest = {
url: '/test',
method: 'GET',
headers: { 'x-correlation-id': 'test-id' },
};
return {
switchToHttp: () => ({
getRequest: () => mockRequest,
getResponse: () => ({}),
}),
} as any;
}
});
This comprehensive approach to exception handling creates a robust system that maintains clean separation of concerns, provides consistent error responses, supports debugging, and follows RESTful API best practices while being maintainable and extensible.
Beginner Answer
Posted on May 10, 2025Custom exception handling in NestJS helps you create a consistent way to deal with errors in your application. Instead of letting errors crash your app or show technical details to users, you can control how errors are processed and what responses users see.
Basic Steps for Custom Exception Handling:
- Create custom exception classes
- Build exception filters to handle these exceptions
- Apply these filters to your controllers or globally
Step 1: Create Custom Exception Classes
// business-error.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';
export class BusinessException extends HttpException {
constructor(message: string) {
super(message, HttpStatus.BAD_REQUEST);
}
}
// not-found.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';
export class NotFoundException extends HttpException {
constructor(resource: string) {
super(`${resource} not found`, HttpStatus.NOT_FOUND);
}
}
Step 2: Create an Exception Filter
// http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
const status = exception.getStatus();
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message: exception.message,
});
}
}
Step 3: Apply the Filter
You can apply the filter at different levels:
- Method level: Affects only one endpoint
- Controller level: Affects all endpoints in a controller
- Global level: Affects the entire application
Method Level:
@Get()
@UseFilters(new HttpExceptionFilter())
findAll() {
throw new BusinessException('Something went wrong');
}
Global Level (in main.ts):
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter());
await app.listen(3000);
}
bootstrap();
Step 4: Using Your Custom Exceptions
Now you can use your custom exceptions in your services or controllers:
@Get(':id')
findOne(@Param('id') id: string) {
const user = this.usersService.findOne(id);
if (!user) {
throw new NotFoundException('User');
}
return user;
}
Tip: For even better organization, create a separate folder structure for your exceptions:
src/ ├── exceptions/ │ ├── business.exception.ts │ ├── not-found.exception.ts │ └── index.ts (export all exceptions) └── filters/ └── http-exception.filter.ts
By implementing custom exception handling, you make your application more robust and user-friendly, providing clear error messages while keeping the technical details hidden from users.