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:
-
Loose Coupling: Classes depend on abstractions (interfaces) rather than concrete implementations
-
Testability: Easy to mock dependencies for unit testing
-
Reusability: Services can be shared across multiple modules
-
Maintainability: Easy to swap implementations without changing dependent classes
-
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:
-
Use Constructor Injection for most dependencies
-
Mark services with @Injectable() decorator
-
Register providers in module's providers array
-
Export services to make them available to other modules
-
Use appropriate scopes for different use cases
-
Leverage custom providers for complex scenarios
-
Implement lifecycle hooks for cleanup and initialization
-
Follow single responsibility principle in services
-
Use interfaces/abstractions for loose coupling
-
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!