NestJS Comprehensive Learning Guide

// table of contents

Comprehensive Learning Guide for NestJS (Version: Latest Stable)

This guide provides a comprehensive, structured, and up-to-date resource for software engineers looking to enhance their skills in NestJS. It focuses on the latest stable features, architectural patterns, and best practices, building upon foundational knowledge of previous NestJS versions or equivalent general programming experience.


Introduction to NestJS (Latest Stable)

NestJS has evolved into a robust, scalable, and versatile framework for building efficient and maintainable server-side applications. The latest stable versions continue to refine its modular architecture, enhance performance, and introduce features that simplify complex application development. Key advancements often focus on improved developer experience, better tooling, and more resilient microservices patterns.

What’s New and Why It Matters

While NestJS maintains its core principles (modules, providers, controllers, dependency injection), recent iterations have brought refined type safety, enhanced monorepo support, improved microservice communication, and more streamlined ways to handle common cross-cutting concerns. These updates aim to:

  • Improve Developer Productivity: By providing opinionated structures and powerful CLI tools.
  • Increase Application Scalability: Through microservice patterns and efficient resource management.
  • Enhance Maintainability: Via a strong architectural foundation inspired by Angular.
  • Boost Performance: With optimizations and efficient module loading.

Prerequisites and Setup

To follow this guide, you should have:

  • Node.js (LTS Version): Installed on your system.
  • TypeScript: Basic understanding of TypeScript syntax and concepts.
  • NPM or Yarn: Package managers.
  • Foundational NestJS Knowledge: Familiarity with basic modules, controllers, and services.

To set up a new NestJS project, use the Nest CLI:

npm install -g @nestjs/cli
nest new project-name
cd project-name
npm run start:dev

Chapter 1: Core Concepts Refresher and Advanced Application

NestJS is built on several fundamental concepts that enable its modularity and scalability. Understanding these deeply is crucial for leveraging the framework effectively.

1.1 Modules and Feature Organization

What it is: Modules are fundamental building blocks that organize your application into cohesive units. Each NestJS application has at least one root module (AppModule), and can have many feature modules. They encapsulate providers, controllers, and other modules, defining the application’s structure.

Why it matters: Modules promote a clear separation of concerns, improve code readability, and facilitate maintainability and scalability, especially in large applications.

How it works: Modules are classes annotated with the @Module() decorator, which takes an object defining imports, controllers, providers, and exports.

Simple Example:

// cats.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
  exports: [CatsService], // Allows other modules to use CatsService
})
export class CatsModule {}

Complex Example (Feature Module Import):

// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CatsModule } from './cats/cats.module';
import { UsersModule } from './users/users.module';

@Module({
  imports: [CatsModule, UsersModule], // Importing feature modules
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Tip: Organize your application by domain or feature. For example, src/cats, src/users, src/auth, each containing their own module, controllers, services, and DTOs.

1.2 Providers (Services, Factories, Value Providers)

What it is: Providers are fundamental NestJS concepts. They are plain JavaScript classes that are decorated with @Injectable(). They are primarily responsible for encapsulating business logic and data access. NestJS’s Dependency Injection (DI) system manages their instantiation and lifecycle.

Why it matters: Providers enable loose coupling and reusability. By injecting dependencies, components don’t need to know how their dependencies are created, making testing and maintenance easier.

How it works: Providers are declared in the providers array of a module. NestJS’s DI container handles their instantiation and injects them where needed.

Simple Example (Service):

// cats.service.ts
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

Complex Example (Factory and Value Providers):

Sometimes you need to create a provider dynamically or use a predefined value.

// app.constants.ts
export const APP_CONFIG = {
  appName: 'My NestJS App',
  version: '1.0.0',
};

// app.module.ts
import { Module } from '@nestjs/common';
import { APP_CONFIG } from './app.constants';

// A value provider
const configProvider = {
  provide: 'APP_CONFIG_TOKEN', // Token to inject the value
  useValue: APP_CONFIG,
};

// A factory provider (can inject other dependencies)
const connectionFactory = {
  provide: 'DATABASE_CONNECTION',
  useFactory: () => {
    // Imagine some complex database connection logic here
    return { connectionString: 'mongodb://localhost/test' };
  },
};

@Module({
  providers: [configProvider, connectionFactory],
})
export class AppModule {}

// In a service, injecting the value and factory provider
import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class MyService {
  constructor(
    @Inject('APP_CONFIG_TOKEN') private config: any,
    @Inject('DATABASE_CONNECTION') private dbConnection: any,
  ) {
    console.log('App Config:', this.config);
    console.log('DB Connection:', this.dbConnection);
  }
}

Tip: Use useClass for class-based providers, useValue for static values, and useFactory for dynamic or complex instantiation.

1.3 Controllers and Request Handling

What it is: Controllers are responsible for handling incoming requests and returning responses. They are decorated with @Controller() and use HTTP method decorators (e.g., @Get(), @Post(), @Put(), @Delete()) to map requests to specific handler methods.

Why it matters: Controllers provide a structured way to define API endpoints, separating routing logic from business logic (which resides in services/providers).

How it works: NestJS maps incoming HTTP requests to controller methods based on the path and HTTP method specified in the decorators.

Simple Example:

// cats.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { Cat } from './interfaces/cat.interface';

@Controller('cats') // Base path for all routes in this controller
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

Complex Example (Route Parameters, Query Parameters, Response Object):

// cats.controller.ts
import { Controller, Get, Post, Body, Param, Query, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express'; // Import Response type if using express
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto, @Res() res: Response) {
    this.catsService.create(createCatDto);
    // Directly manipulating response object for fine-grained control
    res.status(HttpStatus.CREATED).json({ message: 'Cat created successfully' });
  }

  @Get(':id') // Route parameter
  findOne(@Param('id') id: string): string {
    return `This action returns a cat with ID: ${id}`;
  }

  @Get()
  findAll(@Query('name') name?: string): Cat[] { // Query parameter
    if (name) {
      return this.catsService.findAll().filter(cat => cat.name.includes(name));
    }
    return this.catsService.findAll();
  }
}

Tip: For most cases, return values directly from your controller methods, and NestJS will handle the response serialization. Use @Res() only when you need full control over the response object (e.g., setting headers, redirects).

1.4 Decorators: Custom and Built-in

What it is: Decorators are a special kind of declaration that can be attached to classes, methods, accessors, properties, or parameters. In NestJS, they are heavily used for metadata reflection, defining behavior, and integrating with the dependency injection system.

Why it matters: Decorators provide a declarative way to add functionality or configuration to your code, reducing boilerplate and making the code more readable and expressive.

How it works: NestJS leverages TypeScript’s experimental decorators feature. You can create your own custom decorators using @SetMetadata() or by composing existing decorators.

Simple Example (Built-in Decorators):

Already seen in previous sections: @Module(), @Controller(), @Injectable(), @Get(), @Post(), @Body(), @Param(), @Query(), @Res().

Complex Example (Custom Decorator):

Let’s create a custom decorator to get a specific header from the request.

// common/decorators/user-agent.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const UserAgent = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.headers['user-agent'];
  },
);

// In a controller:
import { Controller, Get } from '@nestjs/common';
import { UserAgent } from '../common/decorators/user-agent.decorator';

@Controller('info')
export class InfoController {
  @Get('user-agent')
  getUserAgent(@UserAgent() userAgent: string): string {
    return `Your user agent is: ${userAgent}`;
  }
}

Tip: Custom decorators are excellent for extracting common request data, injecting authenticated user information, or applying cross-cutting concerns in a declarative way.

1.5 Dependency Injection in Depth

What it is: Dependency Injection (DI) is a design pattern in which a class receives its dependencies from external sources rather than creating them itself. NestJS has a powerful and highly configurable DI container.

Why it matters: DI promotes loose coupling, testability, and maintainability. It simplifies managing complex object graphs and their lifecycles.

How it works: NestJS resolves dependencies recursively. When a component (e.g., a controller) requires a provider (e.g., a service), NestJS’s DI container looks up that provider in the current module’s providers array (or imported modules) and instantiates it, injecting any of its own dependencies in turn.

Circular Dependencies and Solutions:

What it is: A circular dependency occurs when module A depends on module B, and module B simultaneously depends on module A. This creates a chicken-and-egg problem during instantiation.

Why it matters: Left unaddressed, circular dependencies can lead to runtime errors, undefined behavior, or application crashes.

How it works (Solutions):

NestJS provides forwardRef() for resolving circular dependencies. It allows you to create a reference that can be resolved later.

Example:

Suppose UsersService needs AuthService and AuthService needs UsersService.

// users/users.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { UsersService } from './users.service';
import { AuthModule } from '../auth/auth.module'; // This would cause circular dependency

@Module({
  imports: [forwardRef(() => AuthModule)], // Use forwardRef here
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

// auth/auth.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module'; // This would cause circular dependency

@Module({
  imports: [forwardRef(() => UsersModule)], // Use forwardRef here
  providers: [AuthService],
  exports: [AuthService],
})
export class AuthModule {}

Important: You also need to use forwardRef in the services if they directly inject each other:

// users/users.service.ts
import { Injectable, Inject, forwardRef } from '@nestjs/common';
import { AuthService } from '../auth/auth.service';

@Injectable()
export class UsersService {
  constructor(
    @Inject(forwardRef(() => AuthService))
    private authService: AuthService,
  ) {}

  // ...
}

// auth/auth.service.ts
import { Injectable, Inject, forwardRef } from '@nestjs/common';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(
    @Inject(forwardRef(() => UsersService))
    private usersService: UsersService,
  ) {}

  // ...
}

Tip: While forwardRef() solves the technical problem, it’s often a sign of a design smell. Consider refactoring your code to break the circular dependency if possible (e.g., introduce a new shared service, or rethink responsibilities).

Optional Dependencies:

What it is: Sometimes a provider might depend on another provider that may or may not be available. You can mark these dependencies as optional.

Why it matters: This allows for more flexible module design, especially in scenarios where certain features are conditional or configured based on environment.

How it works: Use the @Optional() decorator in conjunction with @Inject().

Example:

// my-optional.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class MyOptionalService {
  getData(): string {
    return 'Data from optional service!';
  }
}

// my-consumer.service.ts
import { Injectable, Optional, Inject } from '@nestjs/common';
import { MyOptionalService } from './my-optional.service';

@Injectable()
export class MyConsumerService {
  constructor(
    @Optional() @Inject(MyOptionalService) private readonly optionalService: MyOptionalService,
  ) {
    if (this.optionalService) {
      console.log('Optional service is available:', this.optionalService.getData());
    } else {
      console.log('Optional service is NOT available.');
    }
  }
}

// app.module.ts (demonstrating both cases)
import { Module } from '@nestjs/common';
import { MyConsumerService } from './my-consumer.service';
import { MyOptionalService } from './my-optional.service';

@Module({
  // Case 1: MyOptionalService is provided, MyConsumerService will get it
  // providers: [MyConsumerService, MyOptionalService],

  // Case 2: MyOptionalService is NOT provided, MyConsumerService will get null
  providers: [MyConsumerService],
})
export class AppModule {}

Chapter 2: Enhancing Functionality with Advanced NestJS Features

NestJS provides powerful building blocks to handle common web application concerns like validation, authentication, authorization, logging, and error handling. These are implemented using Pipes, Guards, Interceptors, and Exception Filters.

2.1 Pipes: Validation, Transformation, and Custom Pipes

What it is: Pipes are classes annotated with @Injectable() that implement the PipeTransform interface. They are used to validate or transform incoming data before it reaches the route handler.

Why it was introduced: To separate validation and transformation logic from the business logic within controllers, promoting cleaner code and reusability.

How it works: Pipes execute sequentially. NestJS applies pipes automatically based on decorators like @Body(), @Param(), @Query(), or you can apply them globally or at method/parameter level.

Simple Example (Built-in Validation Pipe):

NestJS often integrates with class-validator and class-transformer for powerful validation.

First, install the necessary packages:

npm install class-validator class-transformer
// dto/create-cat.dto.ts
import { IsString, IsInt, Min, Max } from 'class-validator';

export class CreateCatDto {
  @IsString()
  name: string;

  @IsInt()
  @Min(0)
  @Max(20)
  age: number;

  @IsString()
  breed: string;
}

// cats.controller.ts
import { Controller, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  // Apply ValidationPipe at method level
  async create(@Body() createCatDto: CreateCatDto) {
    // If validation fails, a BadRequestException is thrown automatically
    this.catsService.create(createCatDto);
  }
}

// To apply ValidationPipe globally (recommended for most apps)
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true, // Strips properties that are not defined in the DTO
    forbidNonWhitelisted: true, // Throws an error if non-whitelisted properties are present
    transform: true, // Automatically transform incoming data to DTO instance
  }));
  await app.listen(3000);
}
bootstrap();

Complex Example (Custom Transformation Pipe):

Let’s create a pipe that transforms incoming strings to uppercase.

// common/pipes/uppercase.pipe.ts
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';

@Injectable()
export class UppercasePipe implements PipeTransform {
  transform(value: string, metadata: ArgumentMetadata) {
    if (typeof value === 'string') {
      return value.toUpperCase();
    }
    return value; // Return original value if not a string
  }
}

// cats.controller.ts
import { Controller, Post, Body, UsePipes } from '@nestjs/common';
import { UppercasePipe } from '../common/pipes/uppercase.pipe';
import { CatsService } from './cats.service';

// Assume CreateCatDto has a 'name' property
import { CreateCatDto } from './dto/create-cat.dto';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post('name')
  // Apply custom pipe to a specific parameter
  async createCatWithName(@Body('name', UppercasePipe) name: string) {
    console.log('Transformed name:', name); // Will be uppercase
    // this.catsService.create({ name, age: 5, breed: 'Unknown' });
    return `Cat name processed: ${name}`;
  }

  @Post()
  // You can also apply it to the whole body if needed
  async create(@Body(new UppercasePipe()) body: CreateCatDto) {
    // If CreateCatDto properties are strings, they'd be uppercase
    console.log('Transformed body:', body);
    return body;
  }
}

Tip: Use ValidationPipe globally to ensure all incoming data conforms to your DTOs. Custom pipes are excellent for specific data transformations (e.g., parsing dates, converting IDs).

2.2 Guards: Authentication and Authorization

What it is: Guards are classes annotated with @Injectable() that implement the CanActivate interface. They determine whether a given request should be handled by the route handler. They are executed before any interceptors or pipes.

Why it was introduced: To provide a declarative way to handle authentication and authorization logic, keeping it separate from controllers.

How it works: A guard’s canActivate() method returns a boolean, a Promise, or an Observable. If it returns true, the request proceeds. If false, NestJS throws an ForbiddenException (or a custom exception if specified).

Simple Example (Basic Auth Guard):

// common/guards/auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    // Simulate a simple authentication check
    return request.headers['authorization'] === 'Bearer secret-token';
  }
}

// cats.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '../common/guards/auth.guard';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Get('protected')
  @UseGuards(AuthGuard) // Apply the guard to this specific route
  async findProtectedCats(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

Complex Example (Role-Based Authorization Guard with Custom Decorator):

This example combines a custom decorator to define required roles with a guard that checks the user’s roles.

// common/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

// common/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (!requiredRoles) {
      return true; // No roles defined, allow access
    }

    const { user } = context.switchToHttp().getRequest(); // Assuming user is attached by an auth middleware/guard
    if (!user || !user.roles) {
      return false; // No user or roles found
    }

    return requiredRoles.some((role) => user.roles.includes(role));
  }
}

// cats.controller.ts
import { Controller, Get, UseGuards, Req } from '@nestjs/common';
import { Roles } from '../common/decorators/roles.decorator';
import { RolesGuard } from '../common/guards/roles.guard';
import { AuthGuard } from '../common/guards/auth.guard'; // Use a real auth guard in production

// Dummy Request type for illustration
interface CustomRequest extends Request {
  user: { roles: string[] };
}

@Controller('cats')
@UseGuards(AuthGuard, RolesGuard) // Guards are executed in order
export class CatsController {
  @Get('admin-only')
  @Roles('admin') // Only users with 'admin' role can access
  getAdminCats(@Req() req: CustomRequest): string {
    return `Hello Admin! Your roles: ${req.user.roles.join(', ')}`;
  }

  @Get('viewer-or-admin')
  @Roles('viewer', 'admin') // Users with 'viewer' or 'admin' role can access
  getViewerOrAdminCats(@Req() req: CustomRequest): string {
    return `Hello ${req.user.roles.join(', ')}!`;
  }
}

Tip: Guards are great for authorization and rate limiting. Use Reflector to read metadata set by custom decorators. Remember that guards run before pipes and interceptors, so if your guard needs validated data from the request body, you might need a custom approach or an earlier middleware.

2.3 Interceptors: Aspect-Oriented Programming

What it is: Interceptors are classes annotated with @Injectable() that implement the NestInterceptor interface. They allow you to “intercept” incoming requests and outgoing responses, enabling powerful aspect-oriented programming (AOP) techniques.

Why it was introduced: To provide a clean way to add cross-cutting concerns (e.g., logging, caching, transforming responses, error handling) without polluting the core business logic.

How it works: Interceptors’ intercept() method receives an ExecutionContext and a CallHandler. The CallHandler’s handle() method returns an Observable representing the stream of the route handler’s execution. You can manipulate this observable to pre-process requests or post-process responses.

Simple Example (Logging Interceptor):

// common/interceptors/logging.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now();
    const req = context.switchToHttp().getRequest();
    const method = req.method;
    const url = req.url;

    return next
      .handle()
      .pipe(
        tap(() => console.log(`${method} ${url} - Request completed in ${Date.now() - now}ms`)),
      );
  }
}

// cats.controller.ts
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from '../common/interceptors/logging.interceptor';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
@UseInterceptors(LoggingInterceptor) // Apply globally or at controller/method level
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

Complex Example (Caching Interceptor):

This interceptor caches responses for GET requests.

// common/interceptors/cache.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';

// In a real app, use a proper cache manager like 'cache-manager'
const cache = new Map<string, any>();

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    if (request.method !== 'GET') {
      return next.handle(); // Only cache GET requests
    }

    const url = request.originalUrl || request.url; // Use originalUrl for full path with query params
    const cachedResponse = cache.get(url);

    if (cachedResponse) {
      console.log(`Cache hit for ${url}`);
      return of(cachedResponse); // Return cached data immediately
    }

    return next.handle().pipe(
      tap((response) => {
        console.log(`Caching response for ${url}`);
        cache.set(url, response);
      }),
    );
  }
}

// cats.controller.ts
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { CacheInterceptor } from '../common/interceptors/cache.interceptor';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
@UseInterceptors(CacheInterceptor)
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Get('cached')
  async findAndCacheCats(): Promise<Cat[]> {
    // This method's response will be cached
    console.log('Fetching cats from service (uncached)');
    return this.catsService.findAll();
  }
}

Complex Example (Response Transformation Interceptor):

This interceptor transforms the response structure.

// common/interceptors/transform.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface Response<T> {
  statusCode: number;
  message: string;
  data: T;
}

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
    const httpContext = context.switchToHttp();
    const response = httpContext.getResponse();
    const statusCode = response.statusCode;

    return next.handle().pipe(
      map(data => ({
        statusCode: statusCode,
        message: 'Success', // Or derive from context/data
        data: data,
      })),
    );
  }
}

// main.ts (Apply globally)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { TransformInterceptor } from './common/interceptors/transform.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
  app.useGlobalInterceptors(new TransformInterceptor()); // Apply globally
  await app.listen(3000);
}
bootstrap();

// Any controller method returning data will now be wrapped like:
// { "statusCode": 200, "message": "Success", "data": [...] }

Tip: Interceptors are incredibly powerful for AOP. Use them for logging, caching, error mapping, response transformation, and any other cross-cutting concerns. They operate on the Observable stream, making them ideal for reactive programming patterns.

2.4 Exception Filters: Custom Error Handling

What it is: Exception filters are classes annotated with @Catch() that implement the ExceptionFilter interface. They are responsible for catching unhandled exceptions and sending a custom response to the client.

Why it was introduced: To provide a centralized and flexible mechanism for error handling, allowing you to control the exact response format for different types of exceptions.

How it works: When an unhandled exception occurs, NestJS’s exception layer catches it. If an exception filter is registered for that specific exception type (or for all exceptions), its catch() method is invoked.

Simple Example (Catching HttpExceptions):

// common/filters/http-exception.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException) // Catches only HttpException and its subclasses
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();
    const errorResponse = exception.getResponse() as Record<string, any>; // Get the actual response from HttpException

    const errorDetails = typeof errorResponse === 'string'
      ? { message: errorResponse }
      : errorResponse;

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      ...errorDetails,
      // You can add more context here
      // method: request.method,
    });
  }
}

// main.ts (Apply globally)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
  app.useGlobalFilters(new HttpExceptionFilter()); // Apply globally
  await app.listen(3000);
}
bootstrap();

// In a controller, throwing an exception:
import { Controller, Get, HttpStatus } from '@nestjs/common';
import { HttpException } from '@nestjs/common';

@Controller('error-demo')
export class ErrorDemoController {
  @Get('not-found')
  throwNotFound() {
    throw new HttpException('Resource not found custom message', HttpStatus.NOT_FOUND);
  }

  @Get('bad-request')
  throwBadRequest() {
    // This will be caught by the ValidationPipe's internal error handling first
    // If you explicitly throw, this is how it works:
    throw new HttpException({
      status: HttpStatus.BAD_REQUEST,
      error: 'Specific error details',
      reason: 'Invalid input provided',
    }, HttpStatus.BAD_REQUEST);
  }
}

Complex Example (Catching All Exceptions):

To catch any unhandled exception (including non-HttpException errors like TypeError or ReferenceError), you can use @Catch() without any arguments.

// common/filters/all-exceptions.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch() // Catches all exceptions
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR;

    const message =
      exception instanceof HttpException
        ? (exception.getResponse() as any).message || exception.message
        : 'Internal server error';

    // Log the error for debugging
    console.error(`Caught exception: ${exception instanceof Error ? exception.message : JSON.stringify(exception)}`);
    console.error(exception instanceof Error ? exception.stack : 'No stack trace');


    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: message, // Provide a generic message for security if it's an unknown error
    });
  }
}

// main.ts (Apply globally, and ensure it's the LAST filter in the array for fallback)
// NestJS applies filters in the order they are provided.
// Specific filters (e.g., HttpExceptionFilter) should come before generic ones.
// In practice, you might use a single AllExceptionsFilter and handle HttpExceptions inside it.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // This filter catches all errors, including internal ones not explicitly thrown as HttpExceptions
  app.useGlobalFilters(new AllExceptionsFilter());
  await app.listen(3000);
}
bootstrap();

// In a controller, throw a non-HttpException error:
import { Controller, Get } from '@nestjs/common';

@Controller('error-demo')
export class ErrorDemoController {
  @Get('internal-error')
  throwInternalError() {
    // This will be caught by AllExceptionsFilter
    throw new Error('Something unexpected happened on the server!');
  }
}

Tip: Use global filters for consistent error responses across your application. Create specific filters for particular exception types (e.g., database errors, authentication errors) when you need very specific error handling or logging for those cases. Remember that filters run after guards and interceptors.


Chapter 3: Asynchronous Operations and Microservices

NestJS excels at building highly scalable and distributed systems, leveraging its robust module system and support for various communication patterns.

3.1 Event-Driven Architecture with NestJS

What it is: Event-Driven Architecture (EDA) is a software architecture paradigm promoting the production, detection, consumption of, and reaction to events. In NestJS, this can be implemented using internal event emitters or by integrating with message brokers for distributed systems.

Why it was introduced: EDA promotes loose coupling between services, improves responsiveness, and enhances scalability, making it easier to evolve complex systems.

How it works: NestJS provides the @nestjs/event-emitter package for in-process events and the CQRS module for more structured command/query/event handling.

Simple Example (Event Emitter Module - In-process Events):

First, install the package:

npm install @nestjs/event-emitter
// app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { OrderListener } from './order.listener';
import { OrderService } from './order.service';

@Module({
  imports: [
    EventEmitterModule.forRoot(), // Import and configure the event emitter module
  ],
  controllers: [AppController],
  providers: [AppService, OrderService, OrderListener], // Register listener as a provider
})
export class AppModule {}

// order.event.ts
export class OrderCreatedEvent {
  constructor(public readonly orderId: string, public readonly userId: string, public readonly amount: number) {}
}

// order.listener.ts
import { Injectable, Logger } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { OrderCreatedEvent } from './order.event';

@Injectable()
export class OrderListener {
  private readonly logger = new Logger(OrderListener.name);

  @OnEvent('order.created') // Listen for 'order.created' event
  handleOrderCreatedEvent(payload: OrderCreatedEvent) {
    this.logger.log(`Order Created Listener: Processing order ${payload.orderId} for user ${payload.userId}`);
    // Simulate some async processing, e.g., send email, update inventory
    // console.log(`Email sent for order ${payload.orderId}`);
    // console.log(`Inventory updated for order ${payload.orderId}`);
  }

  @OnEvent('order.*') // Wildcard to listen for any event starting with 'order.'
  handleAnyOrderEvent(payload: any) {
    this.logger.log(`Wildcard Listener: An order event occurred: ${JSON.stringify(payload)}`);
  }
}

// order.service.ts
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { OrderCreatedEvent } from './order.event';

@Injectable()
export class OrderService {
  constructor(private eventEmitter: EventEmitter2) {}

  createOrder(userId: string, amount: number): string {
    const orderId = `order-${Date.now()}`;
    console.log(`OrderService: Creating order ${orderId} for user ${userId} with amount ${amount}`);

    // Emit the event
    this.eventEmitter.emit(
      'order.created',
      new OrderCreatedEvent(orderId, userId, amount),
    );
    return orderId;
  }
}

// app.controller.ts (to trigger the service)
import { Controller, Get, Post } from '@nestjs/common';
import { OrderService } from './order.service';

@Controller()
export class AppController {
  constructor(private readonly orderService: OrderService) {}

  @Post('orders')
  createOrder(): string {
    return this.orderService.createOrder('user123', 99.99);
  }
}

Complex Example (CQRS - Command Query Responsibility Segregation Basics):

CQRS separates the concerns of modifying data (commands) from reading data (queries). NestJS provides a dedicated @nestjs/cqrs package.

First, install:

npm install @nestjs/cqrs
// app.module.ts
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { HeroesGameModule } from './heroes/heroes-game.module'; // Our CQRS feature module

@Module({
  imports: [CqrsModule, HeroesGameModule],
})
export class AppModule {}


// heroes/commands/implementations/kill-dragon.command.ts
export class KillDragonCommand {
  constructor(public readonly heroId: string, public readonly dragonId: string) {}
}

// heroes/commands/handlers/kill-dragon.handler.ts
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { KillDragonCommand } from '../implementations/kill-dragon.command';
import { Logger } from '@nestjs/common';

@CommandHandler(KillDragonCommand)
export class KillDragonHandler implements ICommandHandler<KillDragonCommand> {
  private readonly logger = new Logger(KillDragonHandler.name);

  async execute(command: KillDragonCommand) {
    this.logger.log(`KillDragonCommand executed for hero ${command.heroId} killing dragon ${command.dragonId}`);
    // Here you would implement the actual logic:
    // 1. Fetch hero and dragon from repository
    // 2. Perform combat logic
    // 3. Update status (e.g., dragon is dead, hero gets experience)
    // 4. Emit an event (e.g., DragonKilledEvent)
    return 'Dragon killed successfully!';
  }
}

// heroes/queries/implementations/get-heroes.query.ts
export class GetHeroesQuery {}

// heroes/queries/handlers/get-heroes.handler.ts
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { GetHeroesQuery } from '../implementations/get-heroes.query';
import { Logger } from '@nestjs/common';

@QueryHandler(GetHeroesQuery)
export class GetHeroesHandler implements IQueryHandler<GetHeroesQuery> {
  private readonly logger = new Logger(GetHeroesHandler.name);

  async execute(query: GetHeroesQuery) {
    this.logger.log('GetHeroesQuery executed');
    // Here you would implement the actual logic:
    // 1. Fetch heroes from a read model or database
    return [{ id: 'hero-1', name: 'Batman' }, { id: 'hero-2', name: 'Superman' }];
  }
}

// heroes/heroes-game.module.ts
import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { HeroGameController } from './hero-game.controller';
import { KillDragonHandler } from './commands/handlers/kill-dragon.handler';
import { GetHeroesHandler } from './queries/handlers/get-heroes.handler';

// List all command and query handlers
const commandHandlers = [KillDragonHandler];
const queryHandlers = [GetHeroesHandler];

@Module({
  imports: [CqrsModule],
  controllers: [HeroGameController],
  providers: [
    ...commandHandlers,
    ...queryHandlers,
  ],
})
export class HeroesGameModule {}


// heroes/hero-game.controller.ts (to interact with CQRS)
import { Controller, Post, Get, Body } from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { KillDragonCommand } from './commands/implementations/kill-dragon.command';
import { GetHeroesQuery } from './queries/implementations/get-heroes.query';

@Controller('hero-game')
export class HeroGameController {
  constructor(
    private readonly commandBus: CommandBus,
    private readonly queryBus: QueryBus,
  ) {}

  @Post('kill-dragon')
  async killDragon(@Body('heroId') heroId: string, @Body('dragonId') dragonId: string) {
    return this.commandBus.execute(new KillDragonCommand(heroId, dragonId));
  }

  @Get('heroes')
  async getHeroes() {
    return this.queryBus.execute(new GetHeroesQuery());
  }
}

Tip: EventEmitterModule is great for simple, in-process event handling. For complex domain events, CQRS provides a more structured approach, especially useful in microservices where commands and queries might span different services.

3.2 Microservices with NestJS

What it is: NestJS has first-class support for building microservices using various transport layers. Microservices are small, independent services that communicate with each other, often over a network.

Why it was introduced: To simplify the creation of distributed systems, providing clear communication patterns and consistent API definitions across services.

How it works: NestJS uses @MessagePattern() and @EventPattern() decorators to define handlers for incoming messages and events, respectively. It supports various transport layers like TCP, Redis, NATS, Kafka, and gRPC.

Complex Example (TCP Microservice - Request-Response Pattern):

Service A (Client):

// app.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'MATH_SERVICE', // Token to inject the client
        transport: Transport.TCP,
        options: { host: 'localhost', port: 8888 },
      },
    ]),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

// app.controller.ts
import { Controller, Get, Inject } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { AppService } from './app.service';
import { Observable } from 'rxjs';

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    @Inject('MATH_SERVICE') private client: ClientProxy, // Inject the microservice client
  ) {}

  @Get('sum')
  sum(): Observable<number> {
    // Send a message with a data payload to the 'sum' message pattern
    return this.client.send<number, number[]>('sum', [1, 2, 3, 4, 5]);
  }

  @Get('hello-microservice')
  hello(): Observable<string> {
    return this.client.send<string, string>('hello', 'World');
  }
}

Service B (Microservice Server):

// main.ts (for the microservice)
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { MathModule } from './math.module'; // The microservice module

async function bootstrap() {
  const app = await NestFactory.createMicroservice<MicroserviceOptions>(
    MathModule,
    {
      transport: Transport.TCP,
      options: { host: '0.0.0.0', port: 8888 }, // Listen on all interfaces
    },
  );
  await app.listen();
  console.log('Math Microservice is running on port 8888');
}
bootstrap();

// math.module.ts
import { Module } from '@nestjs/common';
import { MathController } from './math.controller';

@Module({
  controllers: [MathController],
})
export class MathModule {}

// math.controller.ts
import { Controller } from '@nestjs/common';
import { MessagePattern, EventPattern } from '@nestjs/microservices';

@Controller()
export class MathController {
  @MessagePattern('sum') // Handles messages for the 'sum' pattern (request-response)
  accumulate(data: number[]): number {
    console.log('Received sum request with data:', data);
    return (data || []).reduce((a, b) => a + b);
  }

  @MessagePattern('hello')
  getHello(name: string): string {
    console.log('Received hello request with name:', name);
    return `Hello, ${name} from Microservice!`;
  }

  @EventPattern('user_created') // Handles events for the 'user_created' pattern (event-based)
  handleUserCreated(data: { userId: string; email: string }) {
    console.log('User created event received:', data);
    // Perform async processing like sending welcome email
  }
}

Message Patterns vs. Event Patterns:

  • @MessagePattern() (Request-Response): The client sends a message and expects a response. It’s suitable for operations where the client needs to know the outcome immediately (e.g., calculating a sum, fetching data).
  • @EventPattern() (Event-Based): The client emits an event and does not expect a response. It’s suitable for notifying other services about something that happened (e.g., user_created, order_processed). This promotes loose coupling.

Exception Handling in Microservices:

Exceptions in microservices are crucial. If an error occurs in a @MessagePattern handler, NestJS will propagate an RpcException back to the client.

// math.controller.ts (Microservice Server)
import { Controller, Logger } from '@nestjs/common';
import { MessagePattern, RpcException } from '@nestjs/microservices';

@Controller()
export class MathController {
  private readonly logger = new Logger(MathController.name);

  @MessagePattern('divide')
  divide(data: { numerator: number; denominator: number }): number {
    this.logger.log(`Received divide request: ${data.numerator} / ${data.denominator}`);
    if (data.denominator === 0) {
      throw new RpcException('Division by zero is not allowed!'); // Propagate an RPC exception
    }
    return data.numerator / data.denominator;
  }
}

// app.controller.ts (Client)
import { Controller, Get, Inject } from '@nestjs/common';
import { ClientProxy, RpcException } from '@nestjs/microservices';
import { AppService } from './app.service';
import { Observable, catchError, throwError } from 'rxjs';

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    @Inject('MATH_SERVICE') private client: ClientProxy,
  ) {}

  @Get('divide-by-zero')
  divideByZero(): Observable<number> {
    return this.client.send<number, { numerator: number; denominator: number }>('divide', { numerator: 10, denominator: 0 })
      .pipe(
        catchError(error => {
          console.error('Caught RPC Exception on client:', error.message);
          if (error instanceof RpcException) {
            // Handle specific RpcException here
            return throwError(() => new Error(`Microservice error: ${error.message}`));
          }
          return throwError(() => new Error('An unexpected error occurred.'));
        })
      );
  }
}

API Gateway Patterns:

In a microservices architecture, clients often interact with a single “API Gateway” that routes requests to the appropriate microservices. NestJS can be used to build this gateway, using its HTTP capabilities and microservice clients.

Example (Conceptual API Gateway):

// api-gateway.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { GatewayController } from './gateway.controller';

@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'USER_SERVICE',
        transport: Transport.TCP,
        options: { host: 'localhost', port: 3001 },
      },
      {
        name: 'PRODUCT_SERVICE',
        transport: Transport.TCP,
        options: { host: 'localhost', port: 3002 },
      },
    ]),
  ],
  controllers: [GatewayController],
})
export class ApiGatewayModule {}

// gateway.controller.ts
import { Controller, Get, Inject, Param } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { Observable } from 'rxjs';

@Controller('api')
export class GatewayController {
  constructor(
    @Inject('USER_SERVICE') private readonly userServiceClient: ClientProxy,
    @Inject('PRODUCT_SERVICE') private readonly productServiceClient: ClientProxy,
  ) {}

  @Get('users/:id')
  getUser(@Param('id') id: string): Observable<any> {
    // Forward the request to the user microservice
    return this.userServiceClient.send('get_user', id);
  }

  @Get('products')
  getProducts(): Observable<any> {
    // Forward the request to the product microservice
    return this.productServiceClient.send('get_products', {});
  }
}

Tip: Choose the right transport layer based on your needs (e.g., TCP for simple request/response, Redis/NATS for pub/sub, Kafka for high-throughput streaming, gRPC for strong typed contracts and performance). Always consider fault tolerance and monitoring in a microservices setup.


Chapter 4: Database Integration and ORMs

NestJS doesn’t force a specific database or ORM, offering flexibility to integrate with popular solutions like TypeORM, Mongoose, and Prisma.

4.1 TypeORM Integration

What it is: TypeORM is an ORM (Object Relational Mapper) that can run on Node.js, Browser, React Native, NativeScript, Cordova, and Electron. It supports ActiveRecord and DataMapper patterns and works with TypeScript and JavaScript (ES5, ES6, ES7, ES8). It supports various databases like MySQL, PostgreSQL, SQLite, MS SQL Server, Oracle, SAP Hana, etc.

Why it matters: TypeORM allows you to interact with relational databases using TypeScript classes and objects, abstracting away raw SQL queries and providing powerful features like migrations, relations, and repositories.

How it works: Install @nestjs/typeorm and typeorm packages along with your database driver. Configure the TypeOrmModule in your AppModule and define entities (database tables as classes).

Setup:

npm install @nestjs/typeorm typeorm pg # For PostgreSQL, install your preferred DB driver
npm install @types/node # Needed for TypeORM CLI globally

Simple Example (Entity and Repository):

// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Cat } from './cats/cat.entity';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres', // Or 'mysql', 'sqlite', etc.
      host: 'localhost',
      port: 5432,
      username: 'your_username',
      password: 'your_password',
      database: 'your_database',
      entities: [Cat], // List all your entities here
      synchronize: true, // Use only for development! Auto-creates schema on startup.
    }),
    CatsModule,
  ],
})
export class AppModule {}


// src/cats/cat.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Cat {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 500, unique: true })
  name: string;

  @Column('int')
  age: number;

  @Column()
  breed: string;
}

// src/cats/cats.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Cat } from './cat.entity';
import { CatsService } from './cats.service';
import { CatsController } from './cats.controller';

@Module({
  imports: [TypeOrmModule.forFeature([Cat])], // Register Cat entity for this module
  providers: [CatsService],
  controllers: [CatsController],
  exports: [TypeOrmModule], // If other modules need to access CatRepository
})
export class CatsModule {}

// src/cats/cats.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Cat } from './cat.entity';
import { CreateCatDto } from './dto/create-cat.dto';

@Injectable()
export class CatsService {
  constructor(
    @InjectRepository(Cat) // Inject the repository for the Cat entity
    private catsRepository: Repository<Cat>,
  ) {}

  async create(createCatDto: CreateCatDto): Promise<Cat> {
    const newCat = this.catsRepository.create(createCatDto);
    return this.catsRepository.save(newCat);
  }

  async findAll(): Promise<Cat[]> {
    return this.catsRepository.find();
  }

  async findOne(id: number): Promise<Cat> {
    return this.catsRepository.findOneBy({ id });
  }

  async remove(id: number): Promise<void> {
    await this.catsRepository.delete(id);
  }
}

Complex Example (Relations and Migrations):

Let’s add a User entity and establish a one-to-many relationship where a User can have multiple Cats.

// src/users/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { Cat } from '../cats/cat.entity'; // Import the Cat entity

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  username: string;

  @OneToMany(() => Cat, cat => cat.owner) // One user has many cats, cat.owner refers to the owner property in Cat entity
  cats: Cat[];
}

// src/cats/cat.entity.ts (Modified)
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn } from 'typeorm';
import { User } from '../users/user.entity';

@Entity()
export class Cat {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 500 })
  name: string;

  @Column('int')
  age: number;

  @Column()
  breed: string;

  @ManyToOne(() => User, user => user.cats) // Many cats belong to one user
  @JoinColumn({ name: 'ownerId' }) // This creates a foreign key column named 'ownerId' in the Cat table
  owner: User;
}

// src/app.module.ts (Add User entity)
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Cat } from './cats/cat.entity';
import { User } from './users/user.entity'; // Import User
import { CatsModule } from './cats/cats.module';
import { UsersModule } from './users/users.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'your_username',
      password: 'your_password',
      database: 'your_database',
      entities: [Cat, User], // Add User here
      synchronize: false, // !!! Set to false for production, use migrations instead
      logging: ['query', 'error'],
    }),
    CatsModule,
    UsersModule,
  ],
})
export class AppModule {}

// Migrations:
// TypeORM CLI is used for migrations. Configure 'ormconfig.json' or 'ormconfig.ts'
// or add scripts to package.json.
// Example package.json scripts:
// "typeorm": "npm run build && npx typeorm",
// "migration:create": "npm run typeorm migration:create ./src/migrations/AddUserAndRelation",
// "migration:run": "npm run typeorm migration:run",
// "migration:revert": "npm run typeorm migration:revert"

// Example ormconfig.ts (in root of project)
/*
import { DataSource } from 'typeorm';
import * as dotenv from 'dotenv';
dotenv.config();

export const AppDataSource = new DataSource({
  type: 'postgres',
  host: process.env.DB_HOST || 'localhost',
  port: parseInt(process.env.DB_PORT || '5432', 10),
  username: process.env.DB_USERNAME || 'your_username',
  password: process.env.DB_PASSWORD || 'your_password',
  database: process.env.DB_DATABASE || 'your_database',
  entities: [__dirname + '/**/*.entity{.ts,.js}'], // Scan for entities
  migrations: [__dirname + '/src/migrations/*.ts'], // Scan for migrations
  synchronize: false, // Set to false for production
  logging: ['query', 'error'],
});
*/

// After setting up ormconfig, generate a migration:
// npm run migration:create src/migrations/AddUserAndCatRelation
// This creates a file like `src/migrations/1678886400000-AddUserAndCatRelation.ts`
// Inside the migration file, TypeORM generates boilerplate. You define your schema changes in `up` and `down` methods.

// Example migration snippet:
/*
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm";

export class AddUserAndCatRelation1678886400000 implements MigrationInterface {
    public async up(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.createTable(new Table({
            name: "user",
            columns: [
                {
                    name: "id",
                    type: "int",
                    isPrimary: true,
                    isGenerated: true,
                    generationStrategy: "increment",
                },
                {
                    name: "username",
                    type: "varchar",
                    isUnique: true,
                },
            ],
        }));

        await queryRunner.addColumn("cat", new TableColumn({
            name: "ownerId",
            type: "int",
            isNullable: true, // Cats might not have an owner initially
        }));

        await queryRunner.createForeignKey("cat", new TableForeignKey({
            columnNames: ["ownerId"],
            referencedColumnNames: ["id"],
            onDelete: "SET NULL", // Or "CASCADE"
            referencedTableName: "user",
        }));
    }

    public async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.dropForeignKey("cat", "FK_cat_ownerId"); // Replace with actual FK name if different
        await queryRunner.dropColumn("cat", "ownerId");
        await queryRunner.dropTable("user");
    }
}
*/

Tip: Always use migrations (synchronize: false) in production environments to manage database schema changes safely. synchronize: true is convenient for rapid development but highly dangerous in production as it can lead to data loss.

4.2 Mongoose Integration (MongoDB)

What it is: Mongoose is an ODM (Object Data Modeling) library for MongoDB and Node.js. It provides a schema-based solution to model your application data and comes with built-in type casting, validation, query building, and business logic hooks.

Why it matters: Mongoose simplifies interactions with MongoDB, providing a more structured and type-safe way to define and manipulate NoSQL data.

How it works: Install @nestjs/mongoose and mongoose packages. Configure MongooseModule in your AppModule and define schemas using Mongoose’s @Schema() decorator and MongooseModule.forFeature().

Setup:

npm install @nestjs/mongoose mongoose

Simple Example (Schema and Model):

// src/app.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [
    MongooseModule.forRoot('mongodb://localhost/nest'), // Connect to MongoDB
    CatsModule,
  ],
})
export class AppModule {}

// src/cats/schemas/cat.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';

export type CatDocument = Cat & Document; // Type for Cat document

@Schema()
export class Cat {
  @Prop({ required: true, unique: true })
  name: string;

  @Prop({ type: Number, min: 0, max: 20 })
  age: number;

  @Prop()
  breed: string;
}

export const CatSchema = SchemaFactory.createForClass(Cat);

// src/cats/cats.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { Cat, CatSchema } from './schemas/cat.schema';
import { CatsService } from './cats.service';
import { CatsController } from './cats.controller';

@Module({
  imports: [
    MongooseModule.forFeature([{ name: Cat.name, schema: CatSchema }]), // Register Cat model for this module
  ],
  providers: [CatsService],
  controllers: [CatsController],
})
export class CatsModule {}

// src/cats/cats.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Cat, CatDocument } from './schemas/cat.schema';
import { CreateCatDto } from './dto/create-cat.dto';

@Injectable()
export class CatsService {
  constructor(
    @InjectModel(Cat.name) private catModel: Model<CatDocument>, // Inject the Mongoose model
  ) {}

  async create(createCatDto: CreateCatDto): Promise<Cat> {
    const createdCat = new this.catModel(createCatDto);
    return createdCat.save();
  }

  async findAll(): Promise<Cat[]> {
    return this.catModel.find().exec();
  }

  async findOne(id: string): Promise<Cat> {
    return this.catModel.findById(id).exec();
  }

  async remove(id: string): Promise<any> {
    return this.catModel.deleteOne({ _id: id }).exec();
  }
}

Complex Example (Population and Virtuals):

Let’s imagine a User schema and populate cats that belong to them.

// src/users/schemas/user.schema.ts
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import mongoose, { Document } from 'mongoose';
import { Cat } from '../../cats/schemas/cat.schema'; // Import Cat schema for reference

export type UserDocument = User & Document;

@Schema()
export class User {
  @Prop({ required: true, unique: true })
  username: string;

  // This is how you reference other documents in Mongoose
  // Note: `type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Cat' }]` is for array of references
  // For a single reference: `type: mongoose.Schema.Types.ObjectId, ref: 'Cat'`
  @Prop({ type: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Cat' }] })
  cats: Cat[]; // Array of Cat document IDs
}

export const UserSchema = SchemaFactory.createForClass(User);

// Virtuals (e.g., full name, or calculated fields)
UserSchema.virtual('catCount').get(function(this: UserDocument) {
  return this.cats ? this.cats.length : 0;
});

// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from './schemas/user.schema';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';

@Module({
  imports: [MongooseModule.forFeature([{ name: User.name, schema: UserSchema }])],
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}

// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User, UserDocument } from './schemas/user.schema';
import { CreateUserDto } from './dto/create-user.dto';

@Injectable()
export class UsersService {
  constructor(@InjectModel(User.name) private userModel: Model<UserDocument>) {}

  async create(createUserDto: CreateUserDto): Promise<User> {
    const createdUser = new this.userModel(createUserDto);
    return createdUser.save();
  }

  async findAllWithCats(): Promise<User[]> {
    // .populate('cats') fetches the actual Cat documents instead of just their IDs
    return this.userModel.find().populate('cats').exec();
  }

  async findOneWithCatCount(id: string): Promise<User> {
    // You can explicitly select virtuals if not configured globally
    return this.userModel.findById(id).select('+catCount').exec();
  }
}

Tip: Use DTOs for incoming request payloads and define strict Mongoose schemas to ensure data integrity and proper indexing. For complex queries, leverage Mongoose’s query builder and aggregation pipeline.

4.3 Prisma ORM: Modern Database Toolkit

What it is: Prisma is an open-source ORM that consists of three main tools: Prisma Client (type-safe query builder), Prisma Migrate (declarative data modeling and migrations), and Prisma Studio (GUI for viewing and editing data). It supports PostgreSQL, MySQL, SQLite, SQL Server, MongoDB (preview), CockroachDB.

Why it matters: Prisma offers a modern developer experience with unparalleled type safety, automatic migrations, and a powerful query API, making database interactions seamless and error-proof.

How it works: You define your database schema in a schema.prisma file. Prisma Migrate then uses this schema to generate SQL migrations and a type-safe Prisma Client. NestJS integrates with Prisma by injecting the generated client.

Setup:

npm install @prisma/client prisma
npx prisma init # This creates prisma/schema.prisma and .env files

Schema Definition and Migrations:

// prisma/schema.prisma
datasource db {
  provider = "postgresql" // or "mysql", "sqlite", "sqlserver", "mongodb"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  posts Post[]
  cats  Cat[] // One-to-many relationship
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
}

model Cat {
  id      Int    @id @default(autoincrement())
  name    String
  age     Int?
  breed   String?
  ownerId Int
  owner   User   @relation(fields: [ownerId], references: [id])
}

After defining your schema, generate a migration and apply it:

npx prisma migrate dev --name init # Creates and applies a new migration

Client Generation and Queries:

NestJS uses a PrismaService to wrap and expose the Prisma Client.

// src/prisma/prisma.service.ts
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    await this.$connect();
  }

  async enableShutdownHooks(app: INestApplication) {
    this.$on('beforeExit', async () => {
      await app.close();
    });
  }
}

// src/app.module.ts
import { Module } from '@nestjs/common';
import { PrismaModule } from './prisma/prisma.module';
import { UsersModule } from './users/users.module';
import { CatsModule } from './cats/cats.module';

@Module({
  imports: [PrismaModule, UsersModule, CatsModule], // Import PrismaModule
})
export class AppModule {}

// src/prisma/prisma.module.ts
import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Module({
  providers: [PrismaService],
  exports: [PrismaService], // Export so other modules can inject it
})
export class PrismaModule {}

// src/users/users.service.ts (Example using Prisma Client)
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { User, Prisma } from '@prisma/client'; // Import generated types

@Injectable()
export class UsersService {
  constructor(private prisma: PrismaService) {}

  async createUser(data: Prisma.UserCreateInput): Promise<User> {
    return this.prisma.user.create({ data });
  }

  async findUserById(id: number): Promise<User | null> {
    return this.prisma.user.findUnique({ where: { id } });
  }

  async findAllUsers(): Promise<User[]> {
    return this.prisma.user.findMany();
  }

  async findUsersWithCats(): Promise<User[]> {
    return this.prisma.user.findMany({
      include: { cats: true }, // Eager loading of related cats
    });
  }
}

// src/cats/cats.service.ts (Example using Prisma Client)
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { Cat, Prisma } from '@prisma/client';

@Injectable()
export class CatsService {
  constructor(private prisma: PrismaService) {}

  async createCat(data: Prisma.CatCreateInput): Promise<Cat> {
    return this.prisma.cat.create({ data });
  }

  async findAllCats(): Promise<Cat[]> {
    return this.prisma.cat.findMany();
  }

  async findCatById(id: number): Promise<Cat | null> {
    return this.prisma.cat.findUnique({ where: { id } });
  }

  async findCatsByOwner(ownerId: number): Promise<Cat[]> {
    return this.prisma.cat.findMany({
      where: { ownerId },
      include: { owner: true }, // Include owner details if needed
    });
  }
}

Tip: Prisma offers exceptional type safety and a predictable API. Always run npx prisma migrate dev when you change your schema.prisma file, and npx prisma generate if you only modified the generator configuration. Prisma Studio (npx prisma studio) is a very useful GUI for inspecting your database data.


Chapter 5: Testing NestJS Applications

Testing is a first-class citizen in NestJS. The framework provides utilities and leverages Jest (by default) to facilitate unit, integration, and end-to-end testing.

5.1 Unit Testing: Controllers, Services, and Modules

What it is: Unit tests focus on individual components (units) in isolation, like a service method or a controller method, ensuring they work correctly independently. Dependencies are usually mocked.

Why it matters: Fast feedback, easy to pinpoint bugs, and ensures small changes don’t break existing functionality.

How it works: NestJS provides @nestjs/testing module. You use Test.createTestingModule to define a test module, allowing you to mock dependencies using useValue, useClass, or useFactory.

Example (Service Unit Test):

// src/cats/cats.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

describe('CatsService', () => {
  let service: CatsService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [CatsService],
    }).compile(); // Compile the test module

    service = module.get<CatsService>(CatsService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  it('should return all cats', () => {
    const result: Cat[] = [{ name: 'TestCat', age: 3, breed: 'TestBreed' }];
    // Directly manipulating private state for simplicity in a true unit test
    // In a more complex scenario with a DB, you'd mock the repository.
    service.create({ name: 'TestCat', age: 3, breed: 'TestBreed' });
    expect(service.findAll()).toEqual(expect.arrayContaining(result));
  });

  it('should create a new cat', () => {
    const newCat = { name: 'Mittens', age: 2, breed: 'Siamese' };
    service.create(newCat);
    expect(service.findAll()).toContainEqual(newCat);
  });
});

Example (Controller Unit Test with Mocked Service):

// src/cats/cats.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

describe('CatsController', () => {
  let controller: CatsController;
  let service: CatsService; // We'll mock this

  const mockCatsService = {
    findAll: jest.fn(() => [{ name: 'TestCat', age: 3, breed: 'TestBreed' }]),
    create: jest.fn((cat: Cat) => cat),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [CatsController],
      providers: [
        {
          provide: CatsService, // The actual service to be mocked
          useValue: mockCatsService, // Our mock object
        },
      ],
    }).compile();

    controller = module.get<CatsController>(CatsController);
    service = module.get<CatsService>(CatsService); // Get the mocked service instance
  });

  it('should be defined', () => {
    expect(controller).toBeDefined();
  });

  it('should return an array of cats', async () => {
    const result = await controller.findAll();
    expect(result).toEqual([{ name: 'TestCat', age: 3, breed: 'TestBreed' }]);
    expect(service.findAll).toHaveBeenCalled(); // Ensure the mocked method was called
  });

  it('should create a cat', async () => {
    const newCat = { name: 'Whiskers', age: 1, breed: 'Tabby' };
    await controller.create(newCat);
    expect(service.create).toHaveBeenCalledWith(newCat);
  });
});

Tip: For unit tests, focus on testing the logic of the component itself, not its dependencies. Mock out database calls, external API calls, or other services.

5.2 Integration Testing: End-to-End API Tests

What it is: Integration tests verify that different parts of your application (e.g., controller, service, database interaction) work together correctly. They involve starting a portion of your NestJS application context.

Why it matters: Ensures that modules and services integrate as expected, catching issues that might not be apparent in isolated unit tests.

How it works: You create a test application instance using Test.createTestingModule and then use Supertest (often alongside) to make actual HTTP requests against it.

Example (Integration Test with Supertest):

First, install Supertest:

npm install --save-dev supertest @types/supertest
// src/cats/cats.e2e-spec.ts (Often named .e2e-spec for end-to-end, but can be used for integration)
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { AppModule } from '../app.module';
import { INestApplication } from '@nestjs/common';
import { CatsService } from './cats.service'; // We might want to mock the service or not, depending on how deep the integration test goes.

describe('Cats (e2e/integration)', () => {
  let app: INestApplication;
  let catsService = { findAll: () => ['test'] }; // A simple mock for this test

  beforeAll(async () => {
    const moduleFixture = await Test.createTestingModule({
      imports: [AppModule], // Import the real application module
    })
      .overrideProvider(CatsService) // Override the service to control its behavior
      .useValue(catsService)
      .compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('/cats (GET)', () => {
    return request(app.getHttpServer())
      .get('/cats')
      .expect(200)
      .expect(catsService.findAll()); // Expect the mocked data
  });

  it('/cats (POST) - invalid data', () => {
    return request(app.getHttpServer())
      .post('/cats')
      .send({ name: 123, age: 'five', breed: [] }) // Invalid data based on DTO
      .expect(400) // Expect Bad Request due to ValidationPipe
      .expect(res => {
        expect(res.body.message).toEqual(expect.arrayContaining([
          'name must be a string',
          'age must be an integer number',
          'breed must be a string'
        ]));
      });
  });

  afterAll(async () => {
    await app.close();
  });
});

Tip: Decide the scope of your integration tests. Do you want to test with a real database (slower, but more realistic) or mock the database interactions (faster, less prone to environmental issues)? Use overrideProvider for selective mocking.

5.3 E2E Testing with Supertest

What it is: End-to-end (E2E) tests simulate user scenarios, interacting with your application as a whole, from the client-side request to the database and back. They cover the entire stack.

Why it matters: Provides high confidence that the entire application flow works correctly in a deployed environment.

How it works: Similar to integration tests, but often involves setting up a test database, running migrations, and potentially more complex data seeding and cleanup. You typically test against the actual HTTP server.

Example (Conceptual E2E Test Flow):

// test/app.e2e-spec.ts
import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import { AppModule } from './../src/app.module'; // Import the full application

describe('AppController (e2e)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    // Re-apply global pipes/interceptors as in main.ts if not part of AppModule import
    app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
    // Example: configure a separate test database connection for E2E
    // (This part is highly dependent on your ORM/DB setup)
    // For TypeORM, you might use different ormconfig.ts for test env.

    await app.init();
  });

  it('/ (GET)', () => {
    return request(app.getHttpServer())
      .get('/')
      .expect(200)
      .expect('Hello World!');
  });

  let createdCatId: string; // Store ID for subsequent tests

  it('/cats (POST) - create a new cat', async () => {
    const createCatDto = { name: 'Buddy', age: 5, breed: 'Lab' };
    const response = await request(app.getHttpServer())
      .post('/cats')
      .send(createCatDto)
      .expect(201); // Expect 201 Created

    expect(response.body).toEqual(expect.objectContaining({ name: 'Buddy', age: 5, breed: 'Lab' }));
    // Assuming the response includes the created ID, adjust based on your actual response
    if (response.body.id) {
        createdCatId = response.body.id;
    } else if (response.body._id) { // For Mongoose
        createdCatId = response.body._id;
    }
  });

  it('/cats/:id (GET) - retrieve the created cat', async () => {
    if (!createdCatId) fail('Cat ID was not set from previous test');
    const response = await request(app.getHttpServer())
      .get(`/cats/${createdCatId}`)
      .expect(200);

    expect(response.body.name).toEqual('Buddy');
    expect(response.body.age).toEqual(5);
  });

  it('/cats/:id (DELETE) - remove the created cat', () => {
    if (!createdCatId) fail('Cat ID was not set from previous test');
    return request(app.getHttpServer())
      .delete(`/cats/${createdCatId}`)
      .expect(200); // Or 204 No Content
  });

  it('/cats/:id (GET) - confirm deletion', () => {
    if (!createdCatId) fail('Cat ID was not set from previous test');
    return request(app.getHttpServer())
      .get(`/cats/${createdCatId}`)
      .expect(404); // Expect Not Found
  });


  afterAll(async () => {
    // Clean up any test data in the database if necessary
    await app.close();
  });
});

Tip: E2E tests are slower and more complex to set up due to database dependencies. Consider dedicated test databases or transactional fixtures for test isolation. Prioritize E2E for critical user flows.

5.4 Mocking and Spying

What it is: Mocking involves creating dummy versions of dependencies to control their behavior during tests. Spying involves monitoring calls to real methods or functions without altering their implementation.

Why it matters: Crucial for isolating components in unit tests, preventing side effects, and verifying interactions.

How it works: Jest provides jest.fn(), jest.spyOn(), and jest.mock() for these purposes. NestJS’s testing module helps override providers with mock implementations.

Example (Using jest.spyOn and jest.fn):

// src/cats/cats.service.spec.ts (revisited for mocking/spying)
import { Test, TestingModule } from '@nestjs/testing';
import { CatsService } from './cats.service';
import { Repository } from 'typeorm'; // Assuming TypeORM setup
import { getRepositoryToken } from '@nestjs/typeorm';
import { Cat } from './cat.entity';

describe('CatsService with Mock Repository', () => {
  let service: CatsService;
  let catsRepository: Repository<Cat>; // The mocked repository

  const mockCat: Cat = { id: 1, name: 'Test Cat', age: 5, breed: 'Persian' };
  const mockCats: Cat[] = [mockCat];

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        CatsService,
        {
          provide: getRepositoryToken(Cat), // How NestJS identifies TypeORM repositories
          useValue: {
            // Mock all methods the service will call on the repository
            find: jest.fn().mockResolvedValue(mockCats), // For async methods, use mockResolvedValue
            findOneBy: jest.fn().mockResolvedValue(mockCat),
            create: jest.fn().mockImplementation((dto) => ({ id: Date.now(), ...dto })), // Mock actual object creation
            save: jest.fn().mockResolvedValue(mockCat),
            delete: jest.fn().mockResolvedValue({ affected: 1 }),
          },
        },
      ],
    }).compile();

    service = module.get<CatsService>(CatsService);
    catsRepository = module.get<Repository<Cat>>(getRepositoryToken(Cat)); // Get the mocked instance
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  it('should find all cats', async () => {
    const result = await service.findAll();
    expect(result).toEqual(mockCats);
    expect(catsRepository.find).toHaveBeenCalled(); // Verify the mock method was called
  });

  it('should create a cat', async () => {
    const createDto = { name: 'New Cat', age: 2, breed: 'Tabby' };
    const result = await service.create(createDto);
    expect(catsRepository.create).toHaveBeenCalledWith(createDto);
    expect(catsRepository.save).toHaveBeenCalled();
    expect(result).toEqual(expect.objectContaining(createDto));
  });

  it('should delete a cat', async () => {
    const catIdToDelete = 1;
    await service.remove(catIdToDelete);
    expect(catsRepository.delete).toHaveBeenCalledWith(catIdToDelete);
  });
});

Tip: Mocking dependencies is crucial for true unit tests. Use jest.fn() to create mock functions, and jest.spyOn() when you want to observe calls to an actual method on a real object without replacing its implementation.


Chapter 6: Deployment, Security, and Performance Optimization

Beyond building the application, deploying it securely and ensuring high performance are critical aspects for any production-ready NestJS application.

6.1 Deployment Strategies (Docker, Serverless)

Docker:

What it is: Docker is a platform for developing, shipping, and running applications in containers. Containers are lightweight, standalone, executable packages of software that include everything needed to run an application.

Why it matters: Ensures consistent environments from development to production, simplifies deployment, and enhances portability.

How it works: You define a Dockerfile that specifies how to build a Docker image for your NestJS application.

Example Dockerfile:

# Stage 1: Build the NestJS application
FROM node:20-alpine AS build
WORKDIR /app

# Copy package.json and install dependencies
COPY package*.json ./
RUN npm install

# Copy source code and build
COPY . .
RUN npm run build

# Stage 2: Run the NestJS application
FROM node:20-alpine AS production
WORKDIR /app

# Copy production dependencies only
COPY package*.json ./
RUN npm install --only=production

# Copy built application from the build stage
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules # Copy all node_modules from build if needed for production
# If using `npm prune --production` in build stage, then `COPY --from=build /app/node_modules ./node_modules` might not be enough.
# Often you just `COPY --from=build /app/dist ./dist` and then `npm install --production` in this stage.
# Let's go with the simpler approach for this example.

# For a production setup, it's better to install only production dependencies here:
# FROM node:20-alpine AS production
# WORKDIR /app
# COPY package.json ./
# COPY package-lock.json ./
# RUN npm install --production
# COPY --from=build /app/dist ./dist

# Environment variables
ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000

CMD ["node", "dist/main"]

Build and Run:

docker build -t nestjs-app .
docker run -p 3000:3000 nestjs-app

Serverless (AWS Lambda with Serverless Framework):

What it is: Serverless computing allows you to build and run applications and services without managing servers. Your cloud provider dynamically provisions and manages the infrastructure. AWS Lambda is a popular Function-as-a-Service (FaaS) offering.

Why it matters: Reduced operational overhead, automatic scaling, and pay-per-execution billing make it cost-effective for many applications, especially APIs with fluctuating traffic.

How it works: You use a framework like Serverless Framework to define your NestJS application as a set of Lambda functions exposed via API Gateway.

Setup:

npm install -g serverless # Install Serverless CLI
npm install --save-dev serverless-offline # For local development
npm install --save @vendia/serverless-express # Adapter for Express/NestJS

Example serverless.yml:

service: nestjs-serverless-api

plugins:
  - serverless-offline # For local development

provider:
  name: aws
  runtime: nodejs20.x # Or a suitable Node.js version
  region: us-east-1
  memorySize: 128
  timeout: 10
  environment:
    NODE_ENV: production
    # Add other environment variables as needed
  apiGateway:
    minimumCompressionSize: 1024 # Enable gzip compression for API Gateway

package:
  individually: true
  patterns:
    - '!node_modules/**'
    - '!src/**'
    - 'dist/**' # Include only compiled JavaScript
    - 'node_modules/**' # Or specify exact needed modules for smaller bundle

functions:
  main:
    handler: dist/main.handler # Path to your compiled NestJS bootstrap handler
    events:
      - http:
          method: any
          path: /{proxy+} # Catch all paths

# src/main.ts (Modified for serverless)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { INestApplication } from '@nestjs/common';
import { ValidationPipe } from '@nestjs/common';
import { Context, APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { createServer, proxy, Response } from 'aws-serverless-express';
import { eventContext } from 'aws-serverless-express/middleware';

let cachedServer: any;

async function bootstrap(): Promise<INestApplication> {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
  app.enableCors(); // Enable CORS for API Gateway
  await app.init();
  return app;
}

export async function handler(
  event: APIGatewayProxyEvent,
  context: Context,
): Promise<APIGatewayProxyResult> {
  if (!cachedServer) {
    const nestApp = await bootstrap();
    // Use aws-serverless-express to create an Express-compatible server
    cachedServer = createServer(nestApp.getHttpAdapter().getInstance());
  }

  // Pass event and context to Express middleware
  return proxy(cachedServer, event, context, 'PROMISE').promise;
}

Deploy:

sls deploy

Tip: Docker is versatile for any cloud or on-premise. Serverless is ideal for event-driven, cost-sensitive, and auto-scaling APIs. Choose based on your infrastructure, cost model, and traffic patterns.

6.2 Security Best Practices

NestJS, being built on Express/Fastify, benefits from their security features but also requires attention to common web vulnerabilities.

CORS (Cross-Origin Resource Sharing):

What it is: A browser security mechanism that restricts web pages from making requests to a different domain than the one that served the web page.

How to implement:

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableCors({
    origin: ['http://localhost:4200', 'https://yourfrontend.com'], // Specific origins
    methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
    credentials: true, // Allow cookies to be sent
  });
  await app.listen(3000);
}
bootstrap();

CSRF (Cross-Site Request Forgery):

What it is: An attack that tricks a user into submitting a malicious request unknowingly.

How to implement: Use the csurf package (for Express) or csrf (for Fastify).

npm install csurf @types/csurf # For Express-based NestJS
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as csurf from 'csurf'; // For Express

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  // app.use(csurf()); // Enable CSRF protection (must be placed before any body parsers)
  // Note: CSRF setup is more complex and depends on cookie/token management.
  // It's often paired with cookie-parser and specific endpoint handling.
  // For API-only applications, JWT or token-based authentication inherently protect against CSRF.
  await app.listen(3000);
}
bootstrap();

Helmet:

What it is: A collection of 14 small middleware functions that help set various HTTP headers to improve security.

How to implement:

npm install helmet
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import helmet from 'helmet'; // For Express

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(helmet()); // Apply all default Helmet protections
  // Or configure specific protections:
  // app.use(helmet({ contentSecurityPolicy: false }));
  await app.listen(3000);
}
bootstrap();

Authentication Strategies (JWT, OAuth):

NestJS works seamlessly with Passport.js for various authentication strategies.

JWT (JSON Web Token) Example:

Setup:

npm install @nestjs/passport passport passport-jwt @types/passport-jwt
npm install --save-dev @types/passport # For TypeScript types
npm install bcryptjs # For password hashing
npm install jsonwebtoken # For signing/verifying JWTs

Example Flow (simplified):

  1. Auth Module: Contains authentication logic.
  2. Auth Service: Handles user registration, login, and JWT generation.
  3. JWT Strategy: Validates incoming JWTs.
  4. Auth Guard: Applies JWT strategy to routes.
// src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module'; // Assuming UsersModule exists
import { AuthController } from './auth.controller';
import { JwtStrategy } from './jwt.strategy';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    UsersModule,
    PassportModule,
    ConfigModule, // Import ConfigModule
    JwtModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => ({
        secret: configService.get<string>('JWT_SECRET'),
        signOptions: { expiresIn: '60s' }, // Token expiration
      }),
    }),
  ],
  providers: [AuthService, JwtStrategy],
  controllers: [AuthController],
  exports: [AuthService],
})
export class AuthModule {}

// .env
JWT_SECRET=supersecretkeythatshouldnotbehardcoded

// src/auth/jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private configService: ConfigService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get<string>('JWT_SECRET'),
    });
  }

  async validate(payload: any) {
    // This payload is the decoded JWT.
    // You can fetch the user from the database here to ensure they exist and are active.
    return { userId: payload.sub, username: payload.username, roles: payload.roles };
  }
}

// src/auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service'; // Assume this service handles user data
import * as bcrypt from 'bcryptjs';
import { LoginUserDto } from './dto/login-user.dto';

@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService,
  ) {}

  async validateUser(username: string, pass: string): Promise<any> {
    const user = await this.usersService.findOneByUsername(username);
    if (user && await bcrypt.compare(pass, user.password)) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  async login(loginUserDto: LoginUserDto) {
    const user = await this.validateUser(loginUserDto.username, loginUserDto.password);
    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }
    const payload = { username: user.username, sub: user.id, roles: user.roles };
    return {
      access_token: this.jwtService.sign(payload), // Sign the JWT
    };
  }
}

// src/auth/auth.controller.ts
import { Controller, Post, Body, UseGuards, Request } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LocalAuthGuard } from './local-auth.guard'; // Requires a local strategy
import { JwtAuthGuard } from './jwt-auth.guard'; // Our JWT Guard
import { LoginUserDto } from './dto/login-user.dto';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post('login')
  // @UseGuards(LocalAuthGuard) // If you use username/password login
  async login(@Body() loginUserDto: LoginUserDto) {
    return this.authService.login(loginUserDto);
  }

  @UseGuards(JwtAuthGuard) // Protect this route with JWT
  @Get('profile')
  getProfile(@Request() req) {
    return req.user; // User object attached by JwtStrategy
  }
}

// src/auth/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

Tip: Always use HTTPS in production. Validate and sanitize all user input. Implement rate limiting to prevent brute-force attacks. Keep dependencies updated to patch known vulnerabilities. Never store sensitive data (like passwords) in plain text; use strong hashing algorithms like bcrypt.

6.3 Performance Optimization

Caching (Redis):

What it is: Storing frequently accessed data in a fast, temporary storage layer (like Redis) to reduce database load and improve response times.

How it works: NestJS has a built-in CacheModule that can be configured with various store providers, including Redis.

Setup:

npm install @nestjs/cache-manager cache-manager cache-manager-redis-store redis
// src/app.module.ts
import { Module, CacheModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import * as redisStore from 'cache-manager-redis-store';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }), // For .env variables
    CacheModule.registerAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => ({
        store: redisStore,
        host: configService.get<string>('REDIS_HOST') || 'localhost',
        port: parseInt(configService.get<string>('REDIS_PORT') || '6379', 10),
        ttl: 300, // seconds
      }),
      isGlobal: true, // Make cache module available globally
    }),
    // ... other modules
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

// .env
REDIS_HOST=localhost
REDIS_PORT=6379

// src/cats/cats.controller.ts
import { Controller, Get, UseInterceptors, CacheInterceptor } from '@nestjs/common';
import { CatsService } from './cats.service';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Get('cached')
  @UseInterceptors(CacheInterceptor) // Cache the response of this endpoint
  async findCachedCats() {
    console.log('Fetching cats (potentially from cache)');
    return this.catsService.findAll();
  }
}

Payload Optimization:

  • Minify JSON: Ensure your responses are minified in production (NestJS does this by default for plain JSON).
  • Gzip/Brotli Compression: Use compression to reduce payload size. NestJS can be configured to use compression middleware.

Setup:

npm install compression
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as compression from 'compression';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(compression()); // Enable gzip compression
  await app.listen(3000);
}
bootstrap();

Background Tasks (BullMQ):

What it is: For long-running or resource-intensive tasks, it’s better to process them asynchronously in the background, freeing up the main request thread. BullMQ is a robust, Redis-backed queueing system for Node.js.

Why it matters: Improves application responsiveness, handles fluctuating loads, and provides retry mechanisms for failed jobs.

How it works: You create “queues” to which you add “jobs”. Separate “workers” process these jobs.

Setup:

npm install @nestjs/bull bullmq
# Also need a Redis server running
// src/app.module.ts
import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bull';
import { NotificationModule } from './notifications/notification.module';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    BullModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: async (configService: ConfigService) => ({
        redis: {
          host: configService.get<string>('REDIS_HOST') || 'localhost',
          port: parseInt(configService.get<string>('REDIS_PORT') || '6379', 10),
        },
      }),
    }),
    NotificationModule,
  ],
})
export class AppModule {}

// src/notifications/notification.module.ts
import { Module } from '@nestjs/common';
import { BullModule } from '@nestjs/bull';
import { NotificationProducerService } from './notification-producer.service';
import { NotificationConsumer } from './notification.consumer';
import { NotificationController } from './notification.controller';

@Module({
  imports: [
    BullModule.registerQueue({
      name: 'email-queue', // Define a queue named 'email-queue'
    }),
  ],
  providers: [NotificationProducerService, NotificationConsumer],
  controllers: [NotificationController],
})
export class NotificationModule {}

// src/notifications/notification-producer.service.ts
import { Injectable } from '@nestjs/common';
import { InjectQueue } from '@nestjs/bull';
import { Queue } from 'bullmq';

@Injectable()
export class NotificationProducerService {
  constructor(@InjectQueue('email-queue') private emailQueue: Queue) {}

  async sendWelcomeEmail(userId: string, email: string) {
    await this.emailQueue.add('welcome-email', { userId, email }, {
      attempts: 3, // Retry 3 times on failure
      backoff: { type: 'exponential', delay: 1000 },
    });
    console.log(`Added welcome email job for ${email} to queue`);
  }

  async sendPasswordResetEmail(email: string, resetToken: string) {
    await this.emailQueue.add('password-reset', { email, resetToken });
    console.log(`Added password reset email job for ${email} to queue`);
  }
}

// src/notifications/notification.consumer.ts
import { Processor, Process } from '@nestjs/bull';
import { Job } from 'bullmq';
import { Logger } from '@nestjs/common';

@Processor('email-queue') // Associate this consumer with 'email-queue'
export class NotificationConsumer {
  private readonly logger = new Logger(NotificationConsumer.name);

  @Process('welcome-email') // Process jobs with name 'welcome-email'
  async handleWelcomeEmail(job: Job<{ userId: string; email: string }>) {
    this.logger.log(`Processing welcome email for user ${job.data.userId} to ${job.data.email}`);
    // Simulate long-running task (e.g., actual email sending via third-party service)
    await new Promise(resolve => setTimeout(resolve, 3000));
    this.logger.log(`Welcome email sent to ${job.data.email}`);
    // throw new Error('Simulated email sending failure!'); // Test retry
  }

  @Process('password-reset')
  async handlePasswordReset(job: Job<{ email: string; resetToken: string }>) {
    this.logger.log(`Processing password reset email for ${job.data.email}`);
    await new Promise(resolve => setTimeout(resolve, 2000));
    this.logger.log(`Password reset email sent to ${job.data.email}`);
  }
}

// src/notifications/notification.controller.ts (To trigger job addition)
import { Controller, Post, Body } from '@nestjs/common';
import { NotificationProducerService } from './notification-producer.service';

@Controller('notifications')
export class NotificationController {
  constructor(private notificationProducer: NotificationProducerService) {}

  @Post('send-welcome-email')
  async sendWelcomeEmail(@Body('userId') userId: string, @Body('email') email: string) {
    await this.notificationProducer.sendWelcomeEmail(userId, email);
    return { message: 'Welcome email job added to queue' };
  }
}

Tip: Monitor your application’s performance using tools like Prometheus, Grafana, or APM solutions. Optimize database queries, use indexes, and leverage connection pooling. Consider using FastifyAdapter instead of ExpressAdapter for potentially better performance in high-throughput scenarios, especially if you don’t rely heavily on Express-specific middleware.


Chapter 7: Advanced Topics and Patterns

7.1 Monorepo Setup with NestJS and Nx

What it is: A monorepo is a single repository containing multiple, distinct projects, often managed by a tool like Nx. Nx is an extensible dev kit for monorepos, providing advanced tooling for building, testing, and scaling applications.

Why it matters: Simplifies code sharing, improves consistency, facilitates refactoring across projects, and centralizes dependency management in large organizations.

How it works: Nx provides CLI commands to generate NestJS applications and libraries within a monorepo, allowing easy import and reuse of code.

Setup:

npm install -g nx # Install Nx CLI globally
npx create-nx-workspace my-nest-monorepo --preset=@nrwl/nest # Create a new Nx workspace with Nest preset
cd my-nest-monorepo

Key Nx Commands (inside the monorepo):

  • nx g @nrwl/nest:app my-api: Generates a new NestJS application.
  • nx g @nrwl/nest:lib shared-data --directory=libs: Generates a NestJS library for shared code.
  • nx serve my-api: Runs the my-api application.
  • nx build my-api: Builds the my-api application.
  • nx test shared-data: Runs tests for shared-data library.
  • nx graph: Visualizes the dependency graph of your projects.

Example (Using a Shared Library):

After running nx g @nrwl/nest:app my-api and nx g @nrwl/nest:lib shared-data --directory=libs, you’ll have:

my-nest-monorepo/
├── apps/
│   └── my-api/          # NestJS application
│       ├── src/
│       │   └── app/
│       │       └── app.module.ts
│       └── project.json
├── libs/
│   └── shared-data/     # NestJS library
│       ├── src/
│       │   └── lib/
│       │       └── shared-data.module.ts
│       │       └── shared-data.service.ts
│       └── project.json
└── nx.json              # Nx configuration
└── package.json

Inside libs/shared-data/src/lib/shared-data.service.ts:

import { Injectable } from '@nestjs/common';

@Injectable()
export class SharedDataService {
  getSharedMessage(): string {
    return 'Hello from Shared Data Library!';
  }
}

Inside apps/my-api/src/app/app.module.ts:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { SharedDataModule } from '@my-nest-monorepo/shared-data'; // Import from shared library

@Module({
  imports: [SharedDataModule], // Import the shared library module
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Inside apps/my-api/src/app/app.controller.ts:

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { SharedDataService } from '@my-nest-monorepo/shared-data'; // Inject from shared library

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    private readonly sharedDataService: SharedDataService, // Inject shared service
  ) {}

  @Get()
  getHello(): string {
    return this.appService.getHello() + ' ' + this.sharedDataService.getSharedMessage();
  }
}

Tip: Monorepos with Nx are powerful for larger teams and complex systems, especially when sharing code across multiple front-end and back-end applications. Learn about Nx’s capabilities for affected projects, caching, and running tasks.

7.2 GraphQL Integration with NestJS

What it is: GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. It provides a more efficient, powerful, and flexible alternative to REST. NestJS offers excellent integration with GraphQL, supporting both schema-first and code-first approaches.

Why it matters: Enables clients to request exactly the data they need, reducing over-fetching/under-fetching, and provides a strongly typed API.

How it works: Install @nestjs/graphql and a GraphQL driver (e.g., @nestjs/apollo for Apollo Server). Define your schema using type decorators (code-first) or by writing a .graphql file (schema-first). Use resolvers to fetch and manipulate data.

Setup:

npm install @nestjs/graphql @nestjs/apollo graphql @apollo/server

Code-First Approach:

// src/app.module.ts
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { join } from 'path';
import { AuthorsModule } from './authors/authors.module';

@Module({
  imports: [
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'), // Path to generate schema file
      sortSchema: true, // Sort fields alphabetically for consistent schema
    }),
    AuthorsModule,
  ],
})
export class AppModule {}

// src/authors/models/author.model.ts
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Post } from '../../posts/models/post.model'; // Assuming Post model

@ObjectType()
export class Author {
  @Field(() => Int)
  id: number;

  @Field({ nullable: true })
  firstName?: string;

  @Field()
  lastName: string;

  @Field(() => [Post], { nullable: 'items' }) // One author can have many posts
  posts?: Post[];
}

// src/posts/models/post.model.ts
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { Author } from '../../authors/models/author.model';

@ObjectType()
export class Post {
  @Field(() => Int)
  id: number;

  @Field()
  title: string;

  @Field({ nullable: true })
  content?: string;

  @Field(() => Author)
  author: Author; // Each post has one author
}


// src/authors/authors.resolver.ts
import { Resolver, Query, Args, Int, Mutation } from '@nestjs/graphql';
import { Author } from './models/author.model';
import { AuthorsService } from './authors.service';
import { NewAuthorInput } from './dto/new-author.input';

@Resolver(() => Author)
export class AuthorsResolver {
  constructor(private authorsService: AuthorsService) {}

  @Query(() => Author, { name: 'author', nullable: true })
  async getAuthor(@Args('id', { type: () => Int }) id: number): Promise<Author> {
    return this.authorsService.findOneById(id);
  }

  @Query(() => [Author])
  async getAuthors(): Promise<Author[]> {
    return this.authorsService.findAll();
  }

  @Mutation(() => Author)
  async createAuthor(@Args('newAuthorData') newAuthorData: NewAuthorInput): Promise<Author> {
    return this.authorsService.create(newAuthorData);
  }
}

// src/authors/dto/new-author.input.ts
import { InputType, Field } from '@nestjs/graphql';

@InputType()
export class NewAuthorInput {
  @Field({ nullable: true })
  firstName?: string;

  @Field()
  lastName: string;
}

// src/authors/authors.service.ts
import { Injectable } from '@nestjs/common';
import { Author } from './models/author.model';
import { NewAuthorInput } from './dto/new-author.input';

@Injectable()
export class AuthorsService {
  private authors: Author[] = [
    { id: 1, firstName: 'John', lastName: 'Doe', posts: [] },
    { id: 2, firstName: 'Jane', lastName: 'Smith', posts: [] },
  ];
  private nextId = 3;

  findAll(): Author[] {
    return this.authors;
  }

  findOneById(id: number): Author {
    return this.authors.find((a) => a.id === id);
  }

  create(data: NewAuthorInput): Author {
    const newAuthor: Author = { id: this.nextId++, ...data, posts: [] };
    this.authors.push(newAuthor);
    return newAuthor;
  }
}

Schema-First Approach (Conceptual):

Define your schema in a .graphql file, then use NestJS to generate corresponding TypeScript definitions and implement resolvers.

# src/schema.graphql
type Author {
  id: Int!
  firstName: String
  lastName: String!
  posts: [Post!]
}

type Post {
  id: Int!
  title: String!
  content: String
  author: Author!
}

input NewAuthorInput {
  firstName: String
  lastName: String!
}

type Query {
  author(id: Int!): Author
  authors: [Author!]!
}

type Mutation {
  createAuthor(newAuthorData: NewAuthorInput!): Author!
}

Tip: Code-first is generally preferred for its strong type safety and reduced boilerplate. Schema-first is good if you want to involve front-end developers in schema design upfront. Use NestJS’s guards, interceptors, and pipes with GraphQL resolvers as well.

7.3 WebSockets with NestJS

What it is: WebSockets provide a full-duplex communication channel over a single, long-lived TCP connection, enabling real-time, bi-directional communication between a client and a server.

Why it matters: Essential for real-time applications like chat, live notifications, gaming, and collaborative tools.

How it works: NestJS provides @nestjs/platform-socket.io (or @nestjs/platform-ws for native WebSockets) to integrate with WebSocket libraries. You define “gateways” using the @WebSocketGateway() decorator.

Setup:

npm install @nestjs/platform-socket.io socket.io
# OR: npm install @nestjs/platform-ws ws

Example (Socket.IO Gateway - Chat Application):

// src/events/events.gateway.ts
import {
  WebSocketGateway,
  SubscribeMessage,
  MessageBody,
  WebSocketServer,
  ConnectedSocket,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { Logger } from '@nestjs/common';

@WebSocketGateway({
  cors: {
    origin: '*', // Allow all origins for simplicity in example
  },
  namespace: '/chat', // Optional namespace for separation
})
export class EventsGateway {
  @WebSocketServer()
  server: Server; // Socket.IO server instance

  private readonly logger = new Logger(EventsGateway.name);

  // Called when a client connects
  handleConnection(@ConnectedSocket() client: Socket, ...args: any[]) {
    this.logger.log(`Client connected: ${client.id}`);
    client.emit('message', 'Welcome to the chat!'); // Send message to connecting client
  }

  // Called when a client disconnects
  handleDisconnect(@ConnectedSocket() client: Socket) {
    this.logger.log(`Client disconnected: ${client.id}`);
  }

  @SubscribeMessage('msgToServer') // Listens for 'msgToServer' events from clients
  handleMessage(
    @MessageBody() payload: string, // The data sent from the client
    @ConnectedSocket() client: Socket, // The connecting client socket
  ): void {
    this.logger.log(`Received message from ${client.id}: ${payload}`);
    // Emit the message back to all connected clients in the same namespace
    this.server.emit('msgToClient', `[${client.id}] says: ${payload}`);
  }

  @SubscribeMessage('joinRoom')
  handleJoinRoom(
    @MessageBody('room') room: string,
    @ConnectedSocket() client: Socket,
  ): void {
    client.join(room); // Add client to a room
    this.server.to(room).emit('message', `Client ${client.id} joined room ${room}`);
    this.logger.log(`Client ${client.id} joined room: ${room}`);
  }

  @SubscribeMessage('msgToRoom')
  handleMessageToRoom(
    @MessageBody() payload: { room: string; message: string },
    @ConnectedSocket() client: Socket,
  ): void {
    this.server.to(payload.room).emit('msgToClient', `[${client.room}] ${client.id}: ${payload.message}`);
  }
}

// src/events/events.module.ts
import { Module } from '@nestjs/common';
import { EventsGateway } from './events.gateway';

@Module({
  providers: [EventsGateway], // Register the gateway as a provider
})
export class EventsModule {}

// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { EventsModule } from './events/events.module';

@Module({
  imports: [EventsModule], // Import the EventsModule
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

// Client-side JavaScript (e.g., in a simple HTML file or React app)
/*
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
<script>
  const socket = io('http://localhost:3000/chat'); // Connect to the /chat namespace

  socket.on('connect', () => {
    console.log('Connected to server!');
    socket.emit('msgToServer', 'Hello from client!');
    socket.emit('joinRoom', { room: 'general' });
  });

  socket.on('msgToClient', (data) => {
    console.log('Message from server:', data);
    const messagesDiv = document.getElementById('messages');
    const p = document.createElement('p');
    p.textContent = data;
    messagesDiv.appendChild(p);
  });

  socket.on('disconnect', () => {
    console.log('Disconnected from server.');
  });

  function sendMessage() {
    const input = document.getElementById('messageInput');
    const message = input.value;
    // socket.emit('msgToServer', message); // Send to all in namespace
    socket.emit('msgToRoom', { room: 'general', message: message }); // Send to specific room
    input.value = '';
  }
</script>
<input type="text" id="messageInput" placeholder="Type a message...">
<button onclick="sendMessage()">Send</button>
<div id="messages"></div>
*/

Tip: Use socket.io for a robust, feature-rich solution with fallbacks. Use ws for a simpler, native WebSocket implementation if you don’t need socket.io’s advanced features. Consider authentication for WebSocket connections.

7.4 Task Scheduling

What it is: Running recurring tasks at specific intervals or times (e.g., daily reports, hourly data synchronization).

Why it matters: Automates routine operations, reduces manual intervention, and ensures timely execution of batch jobs.

How it works: NestJS provides @nestjs/schedule module, which leverages node-cron or agenda for scheduling.

Setup:

npm install @nestjs/schedule cron
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule'; // Import ScheduleModule
import { MyScheduledTasksService } from './my-scheduled-tasks.service';

@Module({
  imports: [
    ScheduleModule.forRoot(), // Import and configure ScheduleModule
  ],
  providers: [MyScheduledTasksService], // Register your service with scheduled tasks
})
export class AppModule {}

// src/my-scheduled-tasks.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';

@Injectable()
export class MyScheduledTasksService {
  private readonly logger = new Logger(MyScheduledTasksService.name);

  // Runs every 5 seconds
  @Cron(CronExpression.EVERY_5_SECONDS)
  handleCron() {
    this.logger.debug('Called every 5 seconds');
  }

  // Runs every minute from 1 to 59
  @Cron('*/1 * * * *', {
    name: 'notifications', // Give a unique name for identification
    timeZone: 'Asia/Kolkata', // Specify timezone if needed
  })
  handleNotifications() {
    this.logger.warn('Sending notifications every minute!');
    // Logic to send out notifications
  }

  // Runs once a day at 2:30 AM
  @Cron('30 2 * * *')
  handleDailyReport() {
    this.logger.log('Generating daily report...');
    // Logic to generate and email daily reports
  }

  // Dynamic scheduling (less common, but possible)
  // You would use `SchedulerRegistry` to add/remove cron jobs programmatically.
  // Example: Inject SchedulerRegistry and use `addCronJob`, `deleteCronJob`
}

Tip: For single-instance applications, node-cron (via @nestjs/schedule) is sufficient. For distributed applications (multiple instances), use a distributed task scheduler like BullMQ (as shown earlier) to avoid multiple instances running the same job simultaneously.

7.5 Server-Sent Events (SSE)

What it is: Server-Sent Events (SSE) is a W3C standard that allows a server to push data to the client over a single, long-lived HTTP connection. Unlike WebSockets, SSE is unidirectional (server to client only).

Why it matters: Simpler to implement than WebSockets for scenarios where only server-to-client updates are needed (e.g., live dashboards, stock tickers, activity feeds). Automatically handles re-connections.

How it works: The server responds with Content-Type: text/event-stream and sends data in a specific “event stream” format.

Example:

// src/sse/sse.controller.ts
import { Controller, Sse, MessageEvent } from '@nestjs/common';
import { Observable, interval, map } from 'rxjs';

@Controller('sse')
export class SseController {
  @Sse('events') // Defines an SSE endpoint at /sse/events
  sendEvents(): Observable<MessageEvent> {
    return interval(1000).pipe(
      map((_) => ({
        data: { message: `Hello from SSE! Current time: ${new Date().toLocaleTimeString()}` },
        id: new Date().getTime().toString(), // Optional: unique ID for the event
        event: 'time-update', // Optional: event type for client to listen to specific events
      })),
    );
  }

  @Sse('counter')
  sendCounter(): Observable<MessageEvent> {
    let count = 0;
    return interval(2000).pipe(
      map(() => ({ data: `Counter: ${count++}` })),
    );
  }
}

// src/sse/sse.module.ts
import { Module } from '@nestjs/common';
import { SseController } from './sse.controller';

@Module({
  controllers: [SseController],
})
export class SseModule {}

// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { SseModule } from './sse/sse.module';

@Module({
  imports: [SseModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

// Client-side JavaScript (using EventSource API)
/*
<script>
  // Connect to the 'events' stream
  const eventSourceEvents = new EventSource('http://localhost:3000/sse/events');

  eventSourceEvents.onmessage = (event) => {
    console.log('Received event:', event.data);
    const messagesDiv = document.getElementById('messages');
    const p = document.createElement('p');
    p.textContent = `Default Event: ${event.data}`;
    messagesDiv.appendChild(p);
  };

  eventSourceEvents.addEventListener('time-update', (event) => {
    console.log('Received time-update event:', event.data);
    const messagesDiv = document.getElementById('messages');
    const p = document.createElement('p');
    p.textContent = `Time Update Event: ${event.data}`;
    messagesDiv.appendChild(p);
  });

  eventSourceEvents.onerror = (error) => {
    console.error('EventSource Events Error:', error);
    eventSourceEvents.close(); // Optionally close on error
  };

  // Connect to the 'counter' stream
  const eventSourceCounter = new EventSource('http://localhost:3000/sse/counter');
  eventSourceCounter.onmessage = (event) => {
    console.log('Received counter:', event.data);
    const counterDiv = document.getElementById('counter');
    counterDiv.textContent = event.data;
  };

  eventSourceCounter.onerror = (error) => {
    console.error('EventSource Counter Error:', error);
    eventSourceCounter.close();
  };
</script>
<div id="messages"></div>
<div id="counter">Loading counter...</div>
*/

Tip: Use SSE for simple, one-way push notifications or streaming data from the server. For bi-directional communication, WebSockets are more appropriate.


Guided Projects

These projects integrate various NestJS features discussed in this guide, providing practical hands-on experience.

Project 1: Real-time Chat Application with WebSockets and Microservices

Goal: Build a basic real-time chat application where users can join rooms and send messages. Leverage NestJS microservices for user management and message persistence, and WebSockets for real-time communication.

Features:

  • User registration/login (via REST API).
  • Chat rooms (public or private).
  • Real-time message exchange in rooms.
  • Message persistence to a database.

Technologies:

  • NestJS (API Gateway, WebSocket Gateway)
  • NestJS Microservices (TCP transport for User and Chat Services)
  • TypeORM (for User and Chat Services)
  • Socket.IO

Steps:

  1. Project Setup (Monorepo with Nx - Optional but Recommended):

    npx create-nx-workspace nest-chat-app --preset=@nrwl/nest
    cd nest-chat-app
    nx g @nrwl/nest:app api-gateway # Main NestJS app (HTTP + WS)
    nx g @nrwl/nest:app user-service --directory=apps # Microservice for users
    nx g @nrwl/nest:app chat-service --directory=apps # Microservice for chat
    nx g @nrwl/nest:lib shared-interfaces --directory=libs # Shared DTOs/interfaces
    
  2. Database & TypeORM Setup:

    • Configure ormconfig.ts (or DataSource for main.ts) for user-service and chat-service to use separate databases (e.g., chat_users_db, chat_messages_db) or schemas.
    • Create User entity in user-service (username, passwordHash, roles).
    • Create Message and Room entities in chat-service (Message: text, timestamp, senderId, roomId; Room: name).
  3. Shared Interfaces Library (libs/shared-interfaces):

    • Define DTOs for user creation/login, chat messages, etc. (e.g., CreateUserDto, LoginUserDto, ChatMessageDto).
  4. User Microservice (apps/user-service):

    • Configure main.ts to be a TCP microservice.
    • UserController: @MessagePattern('create_user'), @MessagePattern('validate_user'), @MessagePattern('get_user_by_id').
    • UserService: Handles user creation (hashing passwords with bcrypt), validation, and fetching user data using TypeOrm UserRepository.
  5. Chat Microservice (apps/chat-service):

    • Configure main.ts to be a TCP microservice.
    • ChatController: @MessagePattern('save_message'), @MessagePattern('get_messages_in_room'), @MessagePattern('create_room').
    • ChatService: Handles saving messages, retrieving message history, and room management using TypeOrm MessageRepository and RoomRepository.
  6. API Gateway (apps/api-gateway):

    • Configure main.ts for HTTP server.
    • User Authentication (HTTP):
      • AuthModule: Use JwtModule and PassportModule.
      • AuthService: Inject ClientProxy for user-service to call validate_user. Generates JWT upon successful login.
      • AuthController: /auth/login endpoint.
      • JwtStrategy and JwtAuthGuard.
    • WebSocket Gateway (EventsGateway):
      • Implement WebSocketGateway with Socket.IO.
      • Inject ClientProxy for chat-service and user-service.
      • handleConnection: Authenticate user via WebSocket handshake (pass JWT from client).
      • @SubscribeMessage('joinRoom'): Client sends room name, server joins socket to room and fetches message history from chat-service.
      • @SubscribeMessage('sendMessage'): Client sends message, Gateway saves it to chat-service via microservice call, then emits to all clients in the room via this.server.to(room).emit().
  7. Client-Side (Simple HTML/JS or Frontend Framework):

    • Basic HTML page with login form, chat input, and message display area.
    • Use socket.io-client to connect to the WebSocket gateway.
    • Send and receive messages using socket.emit() and socket.on().
    • Include JWT in WebSocket connection (auth property in io options).

Project 2: E-commerce API with TypeORM and Advanced Authorization

Goal: Develop a robust E-commerce API with user authentication, product management, order processing, and role-based authorization.

Features:

  • Users (Customers, Admins) and Authentication (JWT).
  • Products (CRUD, categories).
  • Orders (create, view, update status).
  • Role-based access control (Admin can manage products/orders, Customer can view products/orders).

Technologies:

  • NestJS (REST API)
  • TypeORM (PostgreSQL/MySQL)
  • Passport.js (JWT strategy)
  • Custom Guards for Role-Based Access Control (RBAC).
  • Pipes for DTO validation.
  • Interceptors for response transformation.

Steps:

  1. Project Setup:

    nest new ecommerce-api
    cd ecommerce-api
    npm install @nestjs/typeorm typeorm pg # + other packages for auth, etc.
    
  2. Database & TypeORM Setup:

    • Configure TypeOrmModule.forRoot() in AppModule.
    • Entities:
      • User: id, username, passwordHash, email, role (e.g., customer, admin).
      • Product: id, name, description, price, stock, category.
      • Order: id, userId, status (e.g., pending, shipped, delivered), orderDate.
      • OrderItem: id, orderId, productId, quantity, price. (Many-to-many through Order and Product)
    • Generate TypeORM Migrations.
  3. Authentication and Authorization:

    • Auth Module: Implement JWT authentication (similar to previous example).
    • Roles Decorator & Guard:
      • @Roles('admin', 'customer') custom decorator.
      • RolesGuard to check user roles from JWT payload using Reflector.
    • User Service: CRUD for users, password hashing.
  4. Product Module:

    • ProductService: Handles CRUD for products.
    • ProductController:
      • @Get('/products'): Publicly accessible to view all products.
      • @Post('/products'): @UseGuards(JwtAuthGuard, RolesGuard) and @Roles('admin') for creation.
      • @Put('/products/:id'), @Delete('/products/:id'): Protected by JwtAuthGuard and RolesGuard('admin').
    • CreateProductDto, UpdateProductDto (with class-validator).
  5. Order Module:

    • OrderService: Handles creating new orders (logic for calculating total, updating product stock), retrieving user’s orders, and updating order status.
    • OrderController:
      • @Post('/orders'): @UseGuards(JwtAuthGuard, RolesGuard('customer')) for placing new orders. Takes items in body.
      • @Get('/orders'): @UseGuards(JwtAuthGuard) to view current user’s orders.
      • @Get('/orders/all'): @UseGuards(JwtAuthGuard, RolesGuard('admin')) to view all orders.
      • @Patch('/orders/:id/status'): @UseGuards(JwtAuthGuard, RolesGuard('admin')) to update order status.
    • CreateOrderDto, UpdateOrderStatusDto.
  6. Global Enhancements:

    • Global ValidationPipe for DTO validation.
    • Global TransformInterceptor to standardize API responses (e.g., { statusCode, message, data }).
    • Global HttpExceptionFilter for consistent error handling.
  7. Testing:

    • Unit tests for services (mocking repositories).
    • Integration tests for controllers (testing endpoint behavior with mocked services).
    • E2E tests for critical flows (user login, product creation by admin, order placement by customer).

Project 3: Serverless API with NestJS and AWS Lambda

Goal: Migrate a simple NestJS API to a serverless architecture using AWS Lambda and API Gateway, focusing on cost-effectiveness and scalability.

Features:

  • Simple CRUD API (e.g., for “Notes”).
  • Deployed as serverless functions on AWS Lambda.
  • Uses a NoSQL database (DynamoDB or MongoDB Atlas) for simplicity in serverless context.

Technologies:

  • NestJS
  • AWS Lambda
  • AWS API Gateway
  • Serverless Framework
  • DynamoDB (via AWS SDK) or Mongoose (for MongoDB Atlas)

Steps:

  1. Project Setup:

    nest new serverless-notes-api
    cd serverless-notes-api
    npm install @vendia/serverless-express serverless-offline serverless-dotenv-plugin # Serverless plugins
    npm install @nestjs/platform-express # Ensure express is installed as adapter if using Express based serverless-express
    # For DynamoDB: npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
    # For MongoDB: npm install @nestjs/mongoose mongoose
    
  2. main.ts for Serverless Deployment:

    • Modify main.ts to use @vendia/serverless-express as described in Chapter 6.1 (Serverless Deployment).
  3. Serverless Configuration (serverless.yml):

    • Configure serverless.yml as shown in Chapter 6.1.
    • Define Lambda function(s) and API Gateway events.
    • Configure IAM roles for Lambda to access DynamoDB or MongoDB.
  4. Database Integration (Choose one):

    • Option A: DynamoDB (NoSQL on AWS):

      • Install AWS SDK for DynamoDB.
      • Create a DynamoDBService (Injectable) that wraps the DynamoDB DocumentClient.
      • NotesService: Use the DynamoDBService to perform CRUD operations on a DynamoDB table (e.g., notes table with id as partition key).
      • Example DynamoDB operations: putItem, getItem, updateItem, deleteItem, scan (for findAll).
    • Option B: MongoDB Atlas (Cloud MongoDB):

      • Install @nestjs/mongoose and mongoose.
      • Configure MongooseModule.forRoot() to connect to your MongoDB Atlas cluster connection string (stored securely in environment variables).
      • Define Note schema (e.g., title, content).
      • NotesService: Use Mongoose model for CRUD operations.
  5. Notes Module:

    • NotesService: Handles CRUD logic.
    • NotesController: REST API endpoints (/notes, /notes/:id).
    • CreateNoteDto, UpdateNoteDto (with class-validator).
  6. Deployment:

    • Configure AWS credentials.
    • Run sls deploy.
    • Test the deployed API Gateway endpoint.
  7. Local Development:

    • Use serverless-offline for local testing: sls offline start. This simulates API Gateway and Lambda locally.

Tips:

  • Cold Starts: Be aware of cold starts in Lambda, especially for Node.js. Techniques like provisioned concurrency or keeping functions “warm” can mitigate this.
  • Database Connections: For relational databases (like PostgreSQL), manage connections carefully in a serverless environment (e.g., AWS RDS Proxy). For NoSQL (DynamoDB), it’s naturally serverless. Mongoose connection pooling is efficient.
  • Logging & Monitoring: Use AWS CloudWatch for logs and metrics.
  • Security: Leverage IAM roles and policies to grant minimal necessary permissions to your Lambda functions.

Further Exploration & Resources

This section provides additional resources to deepen your NestJS knowledge and stay up-to-date.

Blogs/Articles

  • Official NestJS Blog: Stay updated with releases and important announcements.
  • Dev.to (NestJS tag): Many community-written articles on various NestJS topics.
  • Medium (NestJS tag): Similar to Dev.to, a good source for practical guides.
  • Nx Blog: For monorepo best practices and updates.

Video Tutorials/Courses

  • Official NestJS YouTube Channel: While not extensive, sometimes contains useful content.
  • Academind (YouTube Channel): Manuel Lorenz often covers NestJS topics in detail.
  • Udemy/Coursera: Search for “NestJS” courses. Look for highly-rated courses that are regularly updated.
    • NestJS Zero to Hero - Modern Backend Development by Stephen Grider
    • NestJS: A Complete Developers Guide by Maximilian Schwarzmüller
  • Fireship.io (YouTube Channel): Quick, high-level overviews of new technologies, sometimes includes NestJS.

Official Documentation

  • NestJS Official Documentation: (docs.nestjs.com) - The single most important resource. It’s comprehensive, well-maintained, and regularly updated. Always refer to it first.

Community Forums

  • NestJS Discord Server: (link usually found in official docs) - Active community for questions and discussions.
  • Stack Overflow (NestJS tag): For specific technical problems and solutions.
  • GitHub Issues (NestJS repository): For reporting bugs or suggesting features, also a good place to see active development discussions.

Project Ideas

  1. Job Board API: CRUD for jobs, applications, user profiles, authentication, admin panel.
  2. Blog/CMS API: Posts, categories, tags, comments, user management, authentication.
  3. Real-time Dashboard: Integrate WebSockets or SSE to stream live data (e.g., system metrics, stock prices).
  4. Social Media Backend: Users, posts, comments, likes, followers, notifications.
  5. Task Management Tool: Tasks, projects, users, assignments, due dates.
  6. Recipe Sharing Platform: Recipes, ingredients, steps, user reviews, search.
  7. File Sharing Service: Upload/download files, user authentication, access control.
  8. Event Management Platform: Events, attendees, ticketing, registrations.
  9. Online Survey System: Surveys, questions, responses, user participation.
  10. Microservice-based Notification System: Centralized service for sending emails, SMS, push notifications (using queues like BullMQ).

Libraries/Tools

  • Database & ORM/ODM:
    • TypeORM: Relational database ORM.
    • Mongoose: MongoDB ODM.
    • Prisma: Modern database toolkit (ORM/Migration/GUI).
  • Authentication & Authorization:
    • Passport.js: Extensible authentication middleware.
    • bcryptjs: Password hashing.
    • jsonwebtoken: JWT implementation.
    • @nestjs/jwt, @nestjs/passport: NestJS integration for JWT/Passport.
  • Validation:
    • class-validator: Decorator-based validation.
    • class-transformer: Transforms plain objects to class instances.
  • Real-time & Messaging:
    • socket.io: Real-time bidirectional communication (WebSockets with fallbacks).
    • @nestjs/platform-socket.io: NestJS integration for Socket.IO.
    • bullmq: Robust Redis-backed queueing for background jobs.
    • @nestjs/bull: NestJS integration for BullMQ.
    • @nestjs/event-emitter: In-process event handling.
  • GraphQL:
    • @nestjs/graphql, @nestjs/apollo: NestJS integration for GraphQL with Apollo Server.
    • graphql-tools: Tools for building GraphQL schemas.
  • Testing:
    • Jest: Default testing framework.
    • Supertest: HTTP assertions for integration/E2E tests.
    • @nestjs/testing: NestJS testing utilities.
  • Utilities & Security:
    • helmet: Sets various HTTP headers for security.
    • compression: Gzip/Brotli compression middleware.
    • @nestjs/config: Environment variable management.
    • @nestjs/schedule: Declarative task scheduling (cron jobs).
    • @nestjs/cqrs: For Command Query Responsibility Segregation pattern.
    • cache-manager, cache-manager-redis-store: Caching with Redis.
  • Development Tools:
    • Nest CLI: Official command-line interface.
    • Nx CLI: Monorepo management.
    • Docker: Containerization.
    • Serverless Framework: For deploying to FaaS platforms like AWS Lambda.