Menu

React & Next.js Topic

NestJs Services & Dependency Injection

1. Why Dependency Injection Matters

Dependency Injection (DI) is a design pattern where dependencies are "injected" into a class rather than the class creating them itself. NestJS has a built-in DI container that manages this automatically.

Benefits of DI in NestJS:

  1. Loose Coupling: Classes depend on abstractions (interfaces) rather than concrete implementations

  2. Testability: Easy to mock dependencies for unit testing

  3. Reusability: Services can be shared across multiple modules

  4. Maintainability: Easy to swap implementations without changing dependent classes

  5. Lifecycle Management: NestJS manages the creation and destruction of instances

Without DI (Tight Coupling):

typescript

// Bad: Tight coupling
class UserController {
  private userService: UserService;
  
  constructor() {
    this.userService = new UserService(); // Creates dependency
  }
}

With DI (Loose Coupling):

typescript

// Good: Dependency injection
class UserController {
  constructor(private userService: UserService) {} // Injected dependency
}

2. Understanding Providers

In NestJS, a Provider is any class that can be injected as a dependency. The most common type is a Service, but providers can also be:

  • Services

  • Repositories

  • Factories

  • Helpers

  • Values

  • Any class with business logic

Provider Registration:

typescript

// app.module.ts
@Module({
  providers: [UserService, EmailService, LoggerService], // Register providers
  controllers: [UserController],
})
export class AppModule {}

3. Creating Services

Basic Service Creation:

typescript

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

@Injectable() // Mark as injectable
export class UsersService {
  private users = [
    { id: 1, name: 'John', email: 'john@example.com' },
    { id: 2, name: 'Jane', email: 'jane@example.com' },
  ];

  findAll() {
    return this.users;
  }

  findOne(id: number) {
    return this.users.find(user => user.id === id);
  }

  create(userData: any) {
    const newUser = { id: this.users.length + 1, ...userData };
    this.users.push(newUser);
    return newUser;
  }
}

Service with Dependencies:

typescript

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

@Injectable()
export class EmailService {
  sendWelcomeEmail(email: string, name: string) {
    console.log(`Sending welcome email to ${email}`);
    // Email sending logic
    return { success: true, message: `Welcome email sent to ${name}` };
  }

  sendPasswordResetEmail(email: string) {
    console.log(`Sending password reset to ${email}`);
    return { success: true, message: 'Password reset email sent' };
  }
}

Service Using Another Service:

typescript

// users.service.ts with EmailService dependency
import { Injectable } from '@nestjs/common';
import { EmailService } from './email.service';

@Injectable()
export class UsersService {
  private users = [];

  constructor(private readonly emailService: EmailService) {
    // EmailService is automatically injected
  }

  async create(userData: any) {
    const newUser = { 
      id: this.users.length + 1, 
      ...userData,
      createdAt: new Date()
    };
    
    this.users.push(newUser);
    
    // Use the injected service
    await this.emailService.sendWelcomeEmail(
      newUser.email, 
      newUser.name
    );
    
    return newUser;
  }
}

4. Dependency Injection Patterns

1. Constructor Injection (Most Common)

typescript

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

@Controller('users')
export class UsersController {
  constructor(
    private readonly usersService: UsersService,
    // Multiple dependencies can be injected
    private readonly logger: LoggerService,
    private readonly configService: ConfigService,
  ) {}

  @Get()
  findAll() {
    return this.usersService.findAll();
  }
}

2. Property Injection (Less Common)

typescript

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

@Injectable()
export class UsersService {
  @Inject(EmailService)
  private readonly emailService: EmailService;

  @Inject('CONFIG_OPTIONS')
  private readonly configOptions: any;
}

3. Method Injection

typescript

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

@Injectable()
export class NotificationService {
  sendNotification(
    @Inject('SMS_SERVICE') smsService: any,
    message: string,
  ) {
    return smsService.send(message);
  }
}

Module Setup for DI:

typescript

// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { EmailService } from '../email/email.service';
import { LoggerService } from '../logger/logger.service';
import { DatabaseModule } from '../database/database.module';

@Module({
  imports: [DatabaseModule], // Import other modules
  controllers: [UsersController],
  providers: [
    UsersService,     // Service will be available for injection
    EmailService,     // Another service
    LoggerService,    // Shared service
  ],
  exports: [UsersService], // Export to make available to other modules
})
export class UsersModule {}

Circular Dependencies:

typescript

// Circular dependency problem
// UserService → AuthService → UserService

// Solution 1: ForwardRef
import { forwardRef, Module } from '@nestjs/common';

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

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

// Solution 2: Inject forwardRef in constructor
@Injectable()
export class UserService {
  constructor(
    @Inject(forwardRef(() => AuthService))
    private authService: AuthService,
  ) {}
}

5. Custom Providers

1. Value Providers

typescript

// constants.ts
export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';
export const API_KEY = 'super-secret-key';

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

@Module({
  providers: [
    {
      provide: 'API_KEY', // Token
      useValue: 'my-secret-key-123', // Value
    },
    {
      provide: DATABASE_CONNECTION,
      useValue: {
        host: 'localhost',
        port: 5432,
        username: 'admin',
        // ... other config
      },
    },
    {
      provide: 'CONFIG',
      useValue: {
        timeout: 5000,
        retries: 3,
        environment: process.env.NODE_ENV,
      },
    },
  ],
})
export class AppModule {}

// Usage in service
import { Injectable, Inject } from '@nestjs/common';
import { DATABASE_CONNECTION } from './constants';

@Injectable()
export class DatabaseService {
  constructor(
    @Inject('API_KEY') private readonly apiKey: string,
    @Inject(DATABASE_CONNECTION) private readonly connection: any,
    @Inject('CONFIG') private readonly config: any,
  ) {
    console.log('API Key:', apiKey);
    console.log('Database config:', connection);
  }
}

2. Class Providers (Aliasing)

typescript

// logger.interface.ts
export interface Logger {
  log(message: string): void;
  error(message: string): void;
}

// console-logger.service.ts
import { Injectable } from '@nestjs/common';
import { Logger } from './logger.interface';

@Injectable()
export class ConsoleLogger implements Logger {
  log(message: string) {
    console.log(`[LOG] ${message}`);
  }
  
  error(message: string) {
    console.error(`[ERROR] ${message}`);
  }
}

// file-logger.service.ts
import { Injectable } from '@nestjs/common';
import { Logger } from './logger.interface';

@Injectable()
export class FileLogger implements Logger {
  log(message: string) {
    // Write to file
  }
  
  error(message: string) {
    // Write error to file
  }
}

// app.module.ts
@Module({
  providers: [
    ConsoleLogger, // Regular provider
    {
      provide: 'LoggerService', // Token for injection
      useClass: process.env.NODE_ENV === 'production' 
        ? FileLogger 
        : ConsoleLogger, // Conditional class
    },
    {
      provide: 'AnotherLogger',
      useExisting: ConsoleLogger, // Alias to existing provider
    },
  ],
})
export class AppModule {}

// Usage
import { Injectable, Inject } from '@nestjs/common';
import { Logger } from './logger.interface';

@Injectable()
export class UsersService {
  constructor(
    @Inject('LoggerService') private readonly logger: Logger,
  ) {
    this.logger.log('UserService initialized');
  }
}

3. Factory Providers

typescript

// database.providers.ts
import { DataSource } from 'typeorm';

export const databaseProviders = [
  {
    provide: 'DATA_SOURCE',
    useFactory: async () => {
      const dataSource = new DataSource({
        type: 'postgres',
        host: process.env.DB_HOST,
        port: parseInt(process.env.DB_PORT),
        username: process.env.DB_USERNAME,
        password: process.env.DB_PASSWORD,
        database: process.env.DB_NAME,
        entities: [__dirname + '/../**/*.entity{.ts,.js}'],
        synchronize: process.env.NODE_ENV === 'development',
      });
      
      return await dataSource.initialize();
    },
  },
];

// config.factory.ts
export const configFactory = {
  provide: 'APP_CONFIG',
  useFactory: () => {
    return {
      apiUrl: process.env.API_URL || 'http://localhost:3000',
      timeout: parseInt(process.env.TIMEOUT) || 5000,
      features: {
        enableCache: process.env.ENABLE_CACHE === 'true',
        enableLogging: process.env.ENABLE_LOGGING !== 'false',
      },
    };
  },
};

// app.module.ts
@Module({
  providers: [
    databaseProviders[0], // Database provider
    configFactory,       // Config factory
    {
      provide: 'PAYMENT_GATEWAY',
      useFactory: (configService: ConfigService) => {
        const apiKey = configService.get('PAYMENT_API_KEY');
        const endpoint = configService.get('PAYMENT_ENDPOINT');
        return new PaymentGateway(apiKey, endpoint);
      },
      inject: [ConfigService], // Dependencies for factory
    },
  ],
})
export class AppModule {}

4. Async Providers

typescript

@Module({
  providers: [
    {
      provide: 'ASYNC_DATA',
      useFactory: async () => {
        // Simulate async operation
        const data = await fetchRemoteData();
        return data;
      },
    },
    {
      provide: 'CONFIG_LOADER',
      useFactory: async (httpService: HttpService) => {
        const response = await httpService.get('/config').toPromise();
        return response.data;
      },
      inject: [HttpService],
    },
  ],
})
export class AsyncModule {}

6. Provider Lifecycle

NestJS manages provider instances through a lifecycle:

Lifecycle Events:

text

1. OnModuleInit     → After module initialization
2. OnApplicationBootstrap → After app bootstrap
3. OnModuleDestroy  → Before module destruction
4. BeforeApplicationShutdown → Before app shutdown
5. OnApplicationShutdown → On app shutdown

Implementing Lifecycle Hooks:

typescript

import { 
  Injectable, 
  OnModuleInit, 
  OnModuleDestroy,
  OnApplicationBootstrap,
  BeforeApplicationShutdown,
  OnApplicationShutdown,
} from '@nestjs/common';

@Injectable()
export class DatabaseService implements 
  OnModuleInit, 
  OnModuleDestroy,
  OnApplicationBootstrap,
  BeforeApplicationShutdown,
  OnApplicationShutdown {
  
  private connection: any;

  // 1. Called after the module is initialized
  async onModuleInit() {
    console.log('DatabaseService: Module initialized');
    this.connection = await this.connectToDatabase();
  }

  // 2. Called after the application fully starts
  onApplicationBootstrap() {
    console.log('DatabaseService: Application bootstrapped');
    this.runMigrations();
  }

  // 3. Called before the module is destroyed
  async onModuleDestroy() {
    console.log('DatabaseService: Module destroying...');
    await this.closeDatabaseConnections();
  }

  // 4. Called before application shutdown
  beforeApplicationShutdown(signal?: string) {
    console.log(`DatabaseService: App shutting down (signal: ${signal})`);
    this.logShutdown(signal);
  }

  // 5. Called on application shutdown
  onApplicationShutdown(signal?: string) {
    console.log('DatabaseService: App shutdown complete');
    this.cleanup();
  }

  private async connectToDatabase() {
    // Connection logic
    return { connected: true };
  }

  private runMigrations() {
    // Migration logic
  }

  private async closeDatabaseConnections() {
    // Cleanup logic
  }

  private logShutdown(signal: string) {
    // Log shutdown
  }

  private cleanup() {
    // Final cleanup
  }
}

Scope-Based Lifecycle:

1. DEFAULT Scope (Singleton - Default)

typescript

@Injectable({ scope: Scope.DEFAULT })
export class SingletonService {
  constructor() {
    console.log('SingletonService created');
  }
}
// One instance shared across the entire application

2. REQUEST Scope

typescript

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

@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {
  private requestId: string;

  constructor() {
    this.requestId = Math.random().toString(36).substr(2, 9);
    console.log(`RequestScopedService created for request: ${this.requestId}`);
  }

  getRequestId() {
    return this.requestId;
  }
}
// New instance created for each incoming request

3. TRANSIENT Scope

typescript

@Injectable({ scope: Scope.TRANSIENT })
export class TransientService {
  private instanceId: string;

  constructor() {
    this.instanceId = Math.random().toString(36).substr(2, 9);
    console.log(`TransientService created: ${this.instanceId}`);
  }

  getInstanceId() {
    return this.instanceId;
  }
}
// New instance every time it's injected

Lifecycle with Scopes Example:

typescript

// users.controller.ts
import { Controller, Get, Scope } from '@nestjs/common';
import { UsersService } from './users.service';
import { RequestScopedService } from './request-scoped.service';

@Controller({ path: 'users', scope: Scope.REQUEST }) // Controller can also be request-scoped
export class UsersController {
  constructor(
    private readonly usersService: UsersService, // Default scope
    private readonly requestScopedService: RequestScopedService, // Request scope
  ) {}

  @Get()
  findAll() {
    return {
      users: this.usersService.findAll(),
      requestId: this.requestScopedService.getRequestId(),
    };
  }
}

// app.module.ts
@Module({
  controllers: [UsersController],
  providers: [
    UsersService, // Default (singleton) scope
    {
      provide: 'REQUEST_SCOPED_SERVICE',
      useClass: RequestScopedService,
      scope: Scope.REQUEST,
    },
  ],
})
export class AppModule {}

7. Advanced DI Concepts

Optional Dependencies:

typescript

import { Injectable, Optional, Inject } from '@nestjs/common';

@Injectable()
export class NotificationService {
  constructor(
    @Optional() // This dependency is optional
    @Inject('EMAIL_SERVICE')
    private readonly emailService?: any,
  ) {
    if (this.emailService) {
      console.log('Email service available');
    } else {
      console.log('Email service not configured');
    }
  }
}
Property-based Injection with Setters:
typescript

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

@Injectable()
export class PaymentService {
  private paymentGateway: any;

  @Inject('PAYMENT_GATEWAY')
  setGateway(gateway: any) {
    this.paymentGateway = gateway;
  }

  processPayment(amount: number) {
    return this.paymentGateway.charge(amount);
  }
}

Dynamic Module Configuration:

typescript

// database.module.ts
import { DynamicModule, Module } from '@nestjs/common';
import { DatabaseService } from './database.service';

export interface DatabaseModuleOptions {
  host: string;
  port: number;
  username: string;
  password: string;
}

@Module({})
export class DatabaseModule {
  static forRoot(options: DatabaseModuleOptions): DynamicModule {
    return {
      module: DatabaseModule,
      providers: [
        {
          provide: 'DATABASE_OPTIONS',
          useValue: options,
        },
        DatabaseService,
      ],
      exports: [DatabaseService],
    };
  }
}

// app.module.ts
@Module({
  imports: [
    DatabaseModule.forRoot({
      host: 'localhost',
      port: 5432,
      username: 'admin',
      password: 'secret',
    }),
  ],
})
export class AppModule {}

Service Injection with Interfaces:

typescript

// strategy.interface.ts
export interface PaymentStrategy {
  pay(amount: number): Promise<boolean>;
}

// credit-card.strategy.ts
@Injectable()
export class CreditCardStrategy implements PaymentStrategy {
  async pay(amount: number): Promise<boolean> {
    // Credit card logic
    return true;
  }
}

// paypal.strategy.ts
@Injectable()
export class PaypalStrategy implements PaymentStrategy {
  async pay(amount: number): Promise<boolean> {
    // PayPal logic
    return true;
  }
}

// payment.service.ts
@Injectable()
export class PaymentService {
  constructor(
    @Inject('PAYMENT_STRATEGY')
    private readonly paymentStrategy: PaymentStrategy,
  ) {}

  processPayment(amount: number) {
    return this.paymentStrategy.pay(amount);
  }
}

// payment.module.ts
@Module({
  providers: [
    CreditCardStrategy,
    PaypalStrategy,
    {
      provide: 'PAYMENT_STRATEGY',
      useClass: process.env.PAYMENT_METHOD === 'paypal' 
        ? PaypalStrategy 
        : CreditCardStrategy,
    },
    PaymentService,
  ],
  exports: [PaymentService],
})
export class PaymentModule {}

8. Best Practices & Common Patterns

1. Service Layer Pattern

typescript

// users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateUserDto, UpdateUserDto } from './dto/user.dto';
import { EmailService } from '../email/email.service';
import { LoggerService } from '../logger/logger.service';

@Injectable()
export class UsersService {
  constructor(
    private readonly emailService: EmailService,
    private readonly logger: LoggerService,
    @Inject('USER_REPOSITORY')
    private readonly userRepository: any,
  ) {}

  async findAll() {
    this.logger.log('Fetching all users');
    return await this.userRepository.find();
  }

  async findOne(id: number) {
    const user = await this.userRepository.findOne(id);
    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }
    return user;
  }

  async create(createUserDto: CreateUserDto) {
    const user = await this.userRepository.create(createUserDto);
    await this.emailService.sendWelcomeEmail(user.email, user.name);
    this.logger.log(`User created: ${user.email}`);
    return user;
  }

  async update(id: number, updateUserDto: UpdateUserDto) {
    await this.findOne(id); // Check if exists
    await this.userRepository.update(id, updateUserDto);
    return await this.findOne(id);
  }

  async remove(id: number) {
    const user = await this.findOne(id);
    await this.userRepository.delete(id);
    this.logger.log(`User deleted: ${user.email}`);
    return { message: 'User deleted successfully' };
  }
}

2. Repository Pattern

typescript

// user.repository.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UserRepository {
  constructor(
    @InjectRepository(User)
    private readonly repository: Repository<User>,
  ) {}

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

  async findById(id: number): Promise<User> {
    return this.repository.findOne({ where: { id } });
  }

  async create(userData: Partial<User>): Promise<User> {
    const user = this.repository.create(userData);
    return this.repository.save(user);
  }

  async update(id: number, userData: Partial<User>): Promise<void> {
    await this.repository.update(id, userData);
  }

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

3. Factory Pattern

typescript

// notification.factory.ts
import { Injectable } from '@nestjs/common';

export interface Notification {
  send(message: string): Promise<void>;
}

@Injectable()
export class EmailNotification implements Notification {
  async send(message: string): Promise<void> {
    // Send email
  }
}

@Injectable()
export class SMSNotification implements Notification {
  async send(message: string): Promise<void> {
    // Send SMS
  }
}

@Injectable()
export class PushNotification implements Notification {
  async send(message: string): Promise<void> {
    // Send push notification
  }
}

@Injectable()
export class NotificationFactory {
  create(type: 'email' | 'sms' | 'push'): Notification {
    switch (type) {
      case 'email':
        return new EmailNotification();
      case 'sms':
        return new SMSNotification();
      case 'push':
        return new PushNotification();
      default:
        throw new Error('Invalid notification type');
    }
  }
}

// Using factory pattern
@Injectable()
export class NotificationService {
  constructor(private readonly factory: NotificationFactory) {}

  async sendNotification(type: 'email' | 'sms' | 'push', message: string) {
    const notification = this.factory.create(type);
    await notification.send(message);
  }
}

4. Configuration Service Pattern

typescript

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

@Injectable()
export class ConfigService {
  private config: Record<string, any>;

  constructor() {
    this.config = {
      database: {
        host: process.env.DB_HOST || 'localhost',
        port: parseInt(process.env.DB_PORT) || 5432,
      },
      app: {
        port: parseInt(process.env.PORT) || 3000,
        environment: process.env.NODE_ENV || 'development',
      },
      auth: {
        jwtSecret: process.env.JWT_SECRET,
        expiresIn: '7d',
      },
    };
  }

  get(key: string): any {
    return this.getNestedValue(this.config, key);
  }

  private getNestedValue(obj: any, path: string): any {
    return path.split('.').reduce((acc, part) => acc && acc[part], obj);
  }
}

// Usage
@Injectable()
export class DatabaseService {
  constructor(private readonly configService: ConfigService) {
    const dbConfig = this.configService.get('database');
    console.log(`Connecting to ${dbConfig.host}:${dbConfig.port}`);
  }
}

5. Complete Example: E-commerce Service

typescript

// order.service.ts
import { 
  Injectable, 
  Inject, 
  forwardRef,
  BadRequestException,
  NotFoundException 
} from '@nestjs/common';
import { Order } from './entities/order.entity';
import { PaymentService } from '../payment/payment.service';
import { InventoryService } from '../inventory/inventory.service';
import { NotificationService } from '../notification/notification.service';
import { OrderRepository } from './repositories/order.repository';

@Injectable()
export class OrderService {
  constructor(
    private readonly orderRepository: OrderRepository,
    @Inject(forwardRef(() => PaymentService))
    private readonly paymentService: PaymentService,
    private readonly inventoryService: InventoryService,
    private readonly notificationService: NotificationService,
    @Inject('ORDER_CONFIG')
    private readonly config: any,
  ) {}

  async createOrder(orderData: any): Promise<Order> {
    // 1. Validate inventory
    const isAvailable = await this.inventoryService.checkAvailability(
      orderData.productId, 
      orderData.quantity
    );
    
    if (!isAvailable) {
      throw new BadRequestException('Product not available');
    }

    // 2. Process payment
    const paymentResult = await this.paymentService.processPayment({
      amount: orderData.total,
      currency: 'USD',
      paymentMethod: orderData.paymentMethod,
    });

    if (!paymentResult.success) {
      throw new BadRequestException('Payment failed');
    }

    // 3. Create order
    const order = await this.orderRepository.create({
      ...orderData,
      status: 'confirmed',
      paymentId: paymentResult.id,
    });

    // 4. Update inventory
    await this.inventoryService.updateStock(
      orderData.productId, 
      -orderData.quantity
    );

    // 5. Send notifications
    await this.notificationService.sendOrderConfirmation(order);

    return order;
  }

  async cancelOrder(orderId: number): Promise<Order> {
    const order = await this.orderRepository.findById(orderId);
    
    if (!order) {
      throw new NotFoundException('Order not found');
    }

    if (order.status === 'shipped') {
      throw new BadRequestException('Cannot cancel shipped order');
    }

    // Process refund
    await this.paymentService.refund(order.paymentId);

    // Update inventory
    await this.inventoryService.updateStock(
      order.productId, 
      order.quantity
    );

    // Update order status
    const updatedOrder = await this.orderRepository.update(orderId, {
      status: 'cancelled',
      cancelledAt: new Date(),
    });

    return updatedOrder;
  }

  async getOrderStatistics(): Promise<any> {
    const [totalOrders, totalRevenue, popularProducts] = await Promise.all([
      this.orderRepository.getTotalCount(),
      this.orderRepository.getTotalRevenue(),
      this.orderRepository.getPopularProducts(5),
    ]);

    return {
      totalOrders,
      totalRevenue,
      popularProducts,
      averageOrderValue: totalRevenue / totalOrders,
    };
  }
}

Short notes:

  1. Use Constructor Injection for most dependencies

  2. Mark services with @Injectable() decorator

  3. Register providers in module's providers array

  4. Export services to make them available to other modules

  5. Use appropriate scopes for different use cases

  6. Leverage custom providers for complex scenarios

  7. Implement lifecycle hooks for cleanup and initialization

  8. Follow single responsibility principle in services

  9. Use interfaces/abstractions for loose coupling

  10. Handle errors properly in service layer

This extensive approach to services and dependency injection ensures your NestJS application is maintainable, testable, and scalable.

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?