Lesson 10-NestJS Execution Context

In NestJS, each module has its own execution context, meaning each module has its own dependency injection container. When a module is lazily loaded, its execution context is created at the time of loading.

The execution context refers to how each request is processed during the runtime of a NestJS application. Understanding the execution context is crucial for debugging and optimizing applications.

Request-Response Cycle

In NestJS, every HTTP request goes through a series of processing steps that make up the request-response cycle. Below are the main components of the request-response cycle:

Interceptors

  • Executed before or after route handlers.
  • Can modify request or response objects.
  • Useful for common operations like logging or performance monitoring.

Guards

  • Executed before route handlers.
  • Used to protect routes, such as verifying user permissions.
  • If a guard returns false or throws an exception, the request is interrupted.

Pipes

  • Executed before route handlers.
  • Used to transform incoming data.
  • Can validate or convert data formats.

Exception Filters

  • Triggered when a component throws an exception.
  • Used for globally handling exceptions, such as standardizing error response formats.

Middlewares

  • Executed before the request reaches the controller.
  • Used for preprocessing tasks like authentication or logging.

Example Code

Below is a simple example demonstrating how to use NestJS execution context components:

Creating a Simple Controller

// app.controller.ts
import { Controller, Get } from '@nestjs/common';

@Controller()
export class AppController {
  @Get()
  getHello(): string {
    return 'Hello World!';
  }
}

Adding a Guard

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

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    // Check user authentication status here
    return true; // Assume the user is authenticated
  }
}

Using the Guard

// app.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from './auth.guard';

@Controller()
export class AppController {
  @Get()
  @UseGuards(AuthGuard)
  getHello(): string {
    return 'Hello World!';
  }
}

Adding an Interceptor

// 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> {
    console.log('Before...');
    const now = Date.now();

    return next.handle().pipe(
      tap(() => console.log(`After... ${Date.now() - now}ms`)),
    );
  }
}

Using the Interceptor

// app.controller.ts
import { Controller, Get, UseGuards, UseInterceptors } from '@nestjs/common';
import { AuthGuard } from './auth.guard';
import { LoggingInterceptor } from './logging.interceptor';

@Controller()
export class AppController {
  @Get()
  @UseGuards(AuthGuard)
  @UseInterceptors(LoggingInterceptor)
  getHello(): string {
    return 'Hello World!';
  }
}

Adding an Exception Filter

// global.exception.filter.ts
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

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

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

Configuring the Exception Filter

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new AllExceptionsFilter());
  await app.listen(3000);
}
bootstrap();

Shared Services

To share a service across multiple modules, you can provide the service in the main module.

// main.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { RouterModule } from '@nestjs/core';
import { SharedService } from './shared.service';

@Module({
  imports: [
    RouterModule.register([
      { path: 'users', module: () => import('./users/users.module').then(m => m.UsersModule) },
      { path: 'posts', module: () => import('./posts/posts.module').then(m => m.PostsModule) },
    ]),
  ],
  controllers: [AppController],
  providers: [AppService, SharedService], // Provide the shared service here
})
export class MainModule {}

// shared.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class SharedService {
  someMethod() {
    return 'Shared service method';
  }
}

// users.controller.ts
import { Controller, Get } from '@nestjs/common';
import { SharedService } from '../shared.service';

@Controller('users')
export class UsersController {
  constructor(private readonly sharedService: SharedService) {}

  @Get('shared')
  getSharedData() {
    return this.sharedService.someMethod();
  }
}

// posts.controller.ts
import { Controller, Get } from '@nestjs/common';
import { SharedService } from '../shared.service';

@Controller('posts')
export class PostsController {
  constructor(private readonly sharedService: SharedService) {}

  @Get('shared')
  getSharedData() {
    return this.sharedService.someMethod();
  }
}
Share your love