Preloader Logo
NestJS icon

NestJS

Backend Frameworks

A progressive Node.js framework for building efficient, reliable and scalable server-side applications.

40 Questions

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, 2025

NestJS 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, 2025

NestJS 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, 2025

NestJS 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, 2025

NestJS 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:

  1. Request arrives at the application
  2. Middleware processes the request (optional)
  3. Guards check if the request should proceed (optional)
  4. The appropriate controller method handles the request
  5. The controller calls services to perform business logic
  6. 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, 2025

Controllers 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, 2025

In 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:

  1. Create a new file named [name].controller.ts
  2. Import the necessary decorators from @nestjs/common
  3. Create a class and add the @Controller() decorator
  4. Define methods with HTTP method decorators (@Get, @Post, etc.)
  5. 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, 2025

Routing 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, 2025

Routing 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, 2025

Providers 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, 2025

Providers 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, 2025

Dependency 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:

  1. Type Metadata Reflection: Uses TypeScript's metadata reflection API to determine constructor parameter types
  2. Provider Registration: Maintains a registry of providers that can be injected
  3. 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, 2025

Dependency 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, 2025

Modules 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, 2025

In 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, 2025

Organizing 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, 2025

Organizing 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:

  1. Create feature modules for different parts of your application
  2. Keep related files together (controllers, services, etc.) in their module folder
  3. Import modules where they are needed
  4. 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, 2025

NestJS 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 with whitelist: 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, 2025

In 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, 2025

Data 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, 2025

Data 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, 2025

Middleware 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:

  1. Incoming request
  2. Global middleware
  3. Module-specific middleware
  4. Guards
  5. Interceptors (pre-controller)
  6. Pipes
  7. Controller (route handler)
  8. Service (business logic)
  9. Interceptors (post-controller)
  10. Exception filters (if exceptions occur)
  11. 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, 2025

Middleware 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:

  1. A request arrives at your NestJS application
  2. The request passes through each middleware in the order they're defined
  3. Each middleware can:
    • Do its job and call next() to continue
    • End the request-response cycle by sending a response
  4. 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, 2025

NestJS 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, 2025

NestJS 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, 2025

Pipes 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:

  1. Global pipes
  2. Controller-level pipes
  3. Handler-level pipes
  4. Parameter-level pipes

Beginner Answer

Posted on May 10, 2025

Pipes 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, 2025

Implementing 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, 2025

Custom 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:

  1. Create a new class with the @Injectable() decorator
  2. Make it implement the PipeTransform interface
  3. Add a transform() method that does your validation
  4. 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, 2025

Guards 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());
            
  • Controller Guards: Applied to all route handlers within a controller
  • 
    @UseGuards(RolesGuard)
    @Controller('admin')
    export class AdminController {
      // All methods inherit the RolesGuard
    }
            
  • Handler Guards: Applied to specific route handlers
  • 
    @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, 2025

Guards 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, 2025

Implementing 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, 2025

Implementing 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:

  1. Set up user authentication (usually with JWT)
  2. Add roles to your user model
  3. Create a roles decorator to mark required roles for routes
  4. Create a roles guard that checks if the user has the required roles
  5. 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, 2025

Interceptors 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:

  1. After guards (if a guard exists)
  2. Before pipes and route handlers
  3. After the route handler returns a response
  4. 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, 2025

Interceptors 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:

  1. Create a class that implements the NestInterceptor interface
  2. Apply it to your controller/method using @UseInterceptors() decorator
  3. 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, 2025

NestJS 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
  • Authentication
  • Rate Limiting
  • Timeout
  • Logging
  • Caching
  • Validation
  • Data Transformation
  • Response Transformation
  • Compression
  • Error Handling

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, 2025

Interceptors 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, 2025

Exception 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 {}
                      

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:

  1. Global filters
  2. Controller-level filters
  3. 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, 2025

Exception 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:

  1. Create a filter class that implements the ExceptionFilter interface
  2. Use the @Catch() decorator to specify which exceptions it should handle
  3. Implement the catch() method to process the exception
  4. 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, 2025

Implementing 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, 2025

Custom 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:

  1. Create custom exception classes
  2. Build exception filters to handle these exceptions
  3. 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.