Lesson 33-NestJS Source Code Core Module Analysis

Startup Process of @nestjs/core

The startup process of NestJS is primarily handled by the NestFactory class. Let’s examine how the create method of NestFactory works.

NestFactory.create

In @nestjs/core, the create method of NestFactory is responsible for creating and initializing a Nest application instance.

// nest/packages/core/nest-application.ts
export class NestFactory {
  public static async create<T extends Type<any>>(
    module: T | ModuleRef | DynamicModule,
    httpAdapterHost?: HttpAdapterHost,
    options?: NestFactoryOptions,
  ): Promise<NestApplication<T>> {
    // ...
    const application = new NestApplication<T>(module, httpAdapterHost, options);
    // ...
    await application.init();
    return application;
  }
}

In this method, a NestApplication instance is created, and the init method is called to initialize the application.

Implementation of Decorators

NestJS uses decorators to mark classes, methods, and properties, enabling features such as route mapping and dependency injection. Decorators are parsed at runtime to build the application’s metadata.

Route Decorators

For example, the @Controller and @Get decorators are used to define HTTP routes.

import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

The internal workings of decorators are as follows:

Defining a Decorator:

export function Controller(path?: string) {
  return (target: any) => {
    // Save controller path information
    Reflect.defineMetadata(CONTROLLER_PATH_METADATA, path, target);
  };
}

Processing Decorators:
In @nestjs/core, decorator information is collected and used to build the routing table.

// nest/packages/core/router/router.ts
export class Router {
  // ...
  registerRoutes(routes: Route[]) {
    routes.forEach(route => {
      // Register routes based on decorator information
      this.registerRoute(route);
    });
  }
  // ...
}

Principles of Dependency Injection

Dependency injection is another core feature of NestJS, enabling loose coupling between components and improving code maintainability and testability.

Injection Tokens

In NestJS, dependency injection is based on tokens. A token can be any unique identifier, typically a class or a string.

@Injectable()
export class CatsService {}

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

Resolving Dependencies

  • Dependency Resolution: When creating a CatsController, NestJS automatically resolves the dependency for CatsService.
  • Module Registration: Modules register providers and services using the @Module decorator.
@Module({
  providers: [CatsService],
  controllers: [CatsController],
})
export class CatsModule {}
  • Service Instantiation: When the application starts, NestJS creates service instances based on module configuration and injects them into controllers.
// nest/packages/core/module-ref.ts
export class ModuleRef {
  // ...
  get<T>(token: Type<T> | Factory<T>, options?: InjectionOptions): T {
    // ...
    const instance = this.resolver.resolve(token, options);
    // ...
    return instance;
  }
  // ...
}

Example Code

Here’s a simple example demonstrating the use of decorators and dependency injection.

CatsService

// src/services/cats.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class CatsService {
  findAll() {
    return 'This action returns all cats';
  }
}

CatsController

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

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

  @Get()
  findAll(): string {
    return this.catsService.findAll();
  }
}

CatsModule

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

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

AppModule

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

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

Main Application

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

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

Execution of Route Handlers

In NestJS, route handlers are defined using decorators such as @Get, @Post, @Put, @Delete, etc. These decorators bind request-handling functions to specific HTTP methods and paths.

Route Handler Execution Process

When an HTTP request arrives, NestJS follows these steps to execute the route handler:

  1. Parse Request:
    NestJS parses the incoming HTTP request, including the request method and path.
  2. Match Route:
    Based on the request method and path, NestJS locates the corresponding route handler.
  3. Execute Middleware:
    If global or local middleware is registered, the middleware logic is executed first.
  4. Execute Guards:
    If guards are registered, their logic is executed to determine whether the request should proceed.
  5. Execute Filters:
    If an exception occurs, filters capture and handle it.
  6. Execute Interceptors:
    If interceptors are registered, their logic is triggered before and after the route handler execution.
  7. Execute Route Handler:
    The actual route handler function is executed.
  8. Return Response:
    The result of the handler function is sent back to the client as the response.

Code Example

Here’s a simple example demonstrating the route handler execution process.

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

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

Service Instantiation and Dependency Injection

In NestJS, service instantiation and dependency injection are managed by the dependency injection container. The container handles the lifecycle of services and ensures they are properly instantiated and injected where needed.

Service Instantiation Process

  1. Define Service:
    Use the @Injectable() decorator to define a service class.
  2. Register Service:
    Register the service in a module using the providers property of the @Module decorator.
  3. Inject Service:
    Inject the service into controllers or other services using constructor injection.
  4. Instantiate Service:
    When creating a controller instance, NestJS automatically instantiates the required dependencies.

Code Example

Here’s an example demonstrating service instantiation and dependency injection.

// src/services/cats.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class CatsService {
  findAll() {
    return 'This action returns all cats';
  }
}

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

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

  @Get()
  findAll(): string {
    return this.catsService.findAll();
  }
}

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

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

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

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

Internal Implementation of Service Instantiation

Internally, service instantiation in NestJS is managed by the ModuleRef class. ModuleRef is a factory class that provides a get method to retrieve service instances.

// nest/packages/core/module-ref.ts
export class ModuleRef {
  // ...
  get<T>(token: Type<T> | Factory<T>, options?: InjectionOptions): T {
    // ...
    const instance = this.resolver.resolve(token, options);
    // ...
    return instance;
  }
  // ...
}

When the AppModule is started, NestJS creates instances of CatsController and CatsService based on the configuration of CatsModule and injects CatsService into CatsController.

Modularization and Decorator Design Patterns

In NestJS, modularization and decorators are two critical design patterns that support the framework’s core functionality.

Modularization

Modularization is a way to organize code in NestJS, allowing developers to group related classes, services, and controllers into a module. Modularization enhances code readability and maintainability.

Module Definition
Modules are defined using the @Module decorator, which can include controllers, services, and other modules.

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

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

Module Import
Modules can import other modules using the imports property.

import { Module } from '@nestjs/common';
import { CatsModule } from './modules/cats.module';

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

Decorators

Decorators are a TypeScript feature used to modify class behavior. In NestJS, decorators are widely used to define routes, services, controllers, and more.

Route Decorators
Route decorators such as @Get, @Post, @Put, @Delete define HTTP request handlers.

import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

Service Decorators
Services are defined with the @Injectable() decorator, indicating that the class can be managed by the dependency injection system.

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

@Injectable()
export class CatsService {
  findAll() {
    return 'This action returns all cats';
  }
}

Implementation of Event-Driven Programming

Event-driven programming is a paradigm where the program flow is driven by events. In NestJS, the @nestjs/event-emitter package can be used to implement event-driven programming.

Installing the Event Package

First, install the @nestjs/event-emitter package.

npm install --save @nestjs/event-emitter

Creating an Event Service

Create an event service to manage event publishing and subscription.

import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';

@Injectable()
export class EventsService extends EventEmitter2 {}

Registering the Event Service

Register the event service in a module.

import { Module } from '@nestjs/common';
import { EventsService } from './events.service';

@Module({
  providers: [EventsService],
})
export class EventsModule {}

Publishing Events

Publish events in controllers or services.

import { Injectable } from '@nestjs/common';
import { Inject } from '@nestjs/common';
import { EventsService } from './events.service';

@Injectable()
export class CatsService {
  constructor(@Inject(EventsService) private events: EventsService) {}

  createCat() {
    // Publish an event
    this.events.emit('cat.created', { name: 'Kitty' });
  }
}

Subscribing to Events

Subscribe to events in other services.

import { Injectable } from '@nestjs/common';
import { Inject } from '@nestjs/common';
import { EventsService } from './events.service';

@Injectable()
export class LogsService {
  constructor(@Inject(EventsService) private events: EventsService) {
    // Subscribe to an event
    this.events.on('cat.created', (data) => {
      console.log(`New cat created: ${data.name}`);
    });
  }
}

Code Example

Here’s a complete example demonstrating modularization, decorators, and event-driven programming in NestJS.

CatsService

// src/services/cats.service.ts
import { Injectable } from '@nestjs/common';
import { Inject } from '@nestjs/common';
import { EventsService } from './events.service';

@Injectable()
export class CatsService {
  constructor(@Inject(EventsService) private events: EventsService) {}

  createCat() {
    // Publish an event
    this.events.emit('cat.created', { name: 'Kitty' });
  }
}

LogsService

// src/services/logs.service.ts
import { Injectable } from '@nestjs/common';
import { Inject } from '@nestjs/common';
import { EventsService } from './events.service';

@Injectable()
export class LogsService {
  constructor(@Inject(EventsService) private events: EventsService) {
    // Subscribe to an event
    this.events.on('cat.created', (data) => {
      console.log(`New cat created: ${data.name}`);
    });
  }
}

EventsService

// src/services/events.service.ts
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';

@Injectable()
export class EventsService extends EventEmitter2 {}

CatsModule

// src/modules/cats.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './controllers/cats.controller';
import { CatsService } from './services/cats.service';
import { EventsService } from './services/events.service';
import { LogsService } from './services/logs.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService, EventsService, LogsService],
})
export class CatsModule {}

AppModule

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

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

Summary

Modularization:

  • Modularization is a way to organize code in NestJS, allowing developers to group related classes, services, and controllers into a module.
  • Modules are defined using the @Module decorator and can import other modules via the imports property.

Decorators:

  • Decorators are used to define routes, services, controllers, and more.
  • Route decorators like @Get, @Post, @Put, @Delete define HTTP request handlers.
  • Services are defined with the @Injectable() decorator, indicating they can be managed by the dependency injection system.

Event-Driven Programming:

  • Event-driven programming is a paradigm where program flow is driven by events.
  • In NestJS, the @nestjs/event-emitter package is used to implement event-driven programming.
  • The event service manages event publishing and subscription.
Share your love