React & Next.js Topic
NestJs Modules
1. Understanding Modules
Modules are the fundamental building blocks of NestJS applications. Each module is a class annotated with @Module() decorator that organizes related functionality.
Basic Module Structure:
typescript
import { Module } from '@nestjs/common';
@Module({
imports: [], // Other modules to import
controllers: [], // Controllers in this module
providers: [], // Services/Providers in this module
exports: [], // What to make available to other modules
})
export class ModuleName {}
2. Project Structure Organization
Common Project Structures:
1. Domain-Based Structure (Recommended)
text
src/ ├── modules/ │ ├── auth/ │ │ ├── auth.module.ts │ │ ├── auth.controller.ts │ │ ├── auth.service.ts │ │ ├── strategies/ │ │ ├── guards/ │ │ └── dto/ │ ├── users/ │ │ ├── users.module.ts │ │ ├── users.controller.ts │ │ ├── users.service.ts │ │ ├── entities/ │ │ └── dto/ │ ├── products/ │ │ ├── products.module.ts │ │ └── ... │ └── orders/ │ ├── orders.module.ts │ └── ... ├── shared/ │ ├── common/ │ ├── utils/ │ ├── decorators/ │ └── interceptors/ ├── config/ ├── database/ └── app.module.ts
2. Layer-Based Structure
text
src/ ├── controllers/ ├── services/ ├── repositories/ ├── entities/ ├── dto/ ├── middleware/ └── modules/
3. Feature-Based Structure
text
src/ ├── features/ │ ├── user-management/ │ ├── product-catalog/ │ └── order-processing/ ├── core/ ├── infrastructure/ └── api/
Recommended Structure (Domain-Driven):
typescript
// Directory structure src/ ├── common/ # Shared utilities, decorators, filters ├── config/ # Configuration files ├── database/ # Database configuration, migrations ├── modules/ # Feature modules │ ├── auth/ │ ├── users/ │ ├── products/ │ └── orders/ ├── app.module.ts # Root module └── main.ts # Application entry point
3. Feature Modules
Feature modules organize code around specific features or business domains.
Creating a Feature Module:
bash
# Generate a complete module structure nest generate module users # Generate module with controller and service nest generate resource users
Complete User Module Example:
typescript
// users/user.entity.ts
export class User {
id: number;
name: string;
email: string;
createdAt: Date;
}
// users/dto/create-user.dto.ts
export class CreateUserDto {
name: string;
email: string;
password: string;
}
// users/dto/update-user.dto.ts
export class UpdateUserDto {
name?: string;
email?: string;
}
// users/users.repository.ts
import { Injectable } from '@nestjs/common';
import { User } from './user.entity';
@Injectable()
export class UsersRepository {
private users: User[] = [];
findAll(): User[] {
return this.users;
}
findById(id: number): User {
return this.users.find(user => user.id === id);
}
create(user: Partial<User>): User {
const newUser = {
id: this.users.length + 1,
...user,
createdAt: new Date(),
};
this.users.push(newUser);
return newUser;
}
}
// users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { UsersRepository } from './users.repository';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Injectable()
export class UsersService {
constructor(private readonly usersRepository: UsersRepository) {}
findAll() {
return this.usersRepository.findAll();
}
findOne(id: number) {
const user = this.usersRepository.findById(id);
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
create(createUserDto: CreateUserDto) {
return this.usersRepository.create(createUserDto);
}
update(id: number, updateUserDto: UpdateUserDto) {
const user = this.findOne(id);
return { ...user, ...updateUserDto };
}
remove(id: number) {
this.findOne(id); // Verify exists
return { message: `User ${id} deleted` };
}
}
// users/users.controller.ts
import {
Controller,
Get,
Post,
Body,
Param,
ParseIntPipe,
Put,
Delete,
HttpCode,
HttpStatus
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
findAll() {
return this.usersService.findAll();
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id);
}
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
@Put(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto,
) {
return this.usersService.update(id, updateUserDto);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id', ParseIntPipe) id: number) {
return this.usersService.remove(id);
}
}
// users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { UsersRepository } from './users.repository';
@Module({
controllers: [UsersController],
providers: [UsersService, UsersRepository],
exports: [UsersService], // Export if other modules need to use UsersService
})
export class UsersModule {}
Product Module Example:
typescript
// products/products.module.ts
import { Module } from '@nestjs/common';
import { ProductsController } from './products.controller';
import { ProductsService } from './products.service';
import { CategoriesService } from './categories.service';
import { ProductsRepository } from './repositories/products.repository';
import { CategoriesRepository } from './repositories/categories.repository';
@Module({
controllers: [ProductsController],
providers: [
ProductsService,
CategoriesService,
ProductsRepository,
CategoriesRepository,
],
exports: [ProductsService, CategoriesService],
})
export class ProductsModule {}
4. Shared Modules
Shared modules contain providers that need to be used across multiple feature modules.
Creating Shared Modules:
typescript
// common/common.module.ts
import { Global, Module } from '@nestjs/common';
import { LoggerService } from './logger.service';
import { ConfigService } from './config.service';
import { ValidationService } from './validation.service';
@Module({
providers: [LoggerService, ConfigService, ValidationService],
exports: [LoggerService, ConfigService, ValidationService],
})
export class CommonModule {}
Database Shared Module:
typescript
// database/database.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DatabaseService } from './database.service';
import { DatabaseConfig } from './database.config';
@Module({
imports: [
TypeOrmModule.forRootAsync({
useClass: DatabaseConfig,
}),
],
providers: [DatabaseService],
exports: [DatabaseService, TypeOrmModule],
})
export class DatabaseModule {}
Utility Shared Module:
typescript
// utils/utils.module.ts
import { Module } from '@nestjs/common';
import { EmailService } from './email/email.service';
import { SmsService } from './sms/sms.service';
import { FileService } from './file/file.service';
import { EncryptionService } from './encryption/encryption.service';
@Module({
providers: [EmailService, SmsService, FileService, EncryptionService],
exports: [EmailService, SmsService, FileService, EncryptionService],
})
export class UtilsModule {}
Using Shared Modules:
typescript
// users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
import { CommonModule } from '../common/common.module';
import { DatabaseModule } from '../database/database.module';
import { UtilsModule } from '../utils/utils.module';
@Module({
imports: [CommonModule, DatabaseModule, UtilsModule],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
// orders/orders.module.ts
import { Module } from '@nestjs/common';
import { OrdersController } from './orders.controller';
import { OrdersService } from './orders.service';
import { CommonModule } from '../common/common.module';
import { DatabaseModule } from '../database/database.module';
import { UtilsModule } from '../utils/utils.module';
@Module({
imports: [CommonModule, DatabaseModule, UtilsModule],
controllers: [OrdersController],
providers: [OrdersService],
})
export class OrdersModule {}
5. Global Modules
Global modules are available throughout the entire application without needing to import them in every module.
Creating Global Modules:
typescript
// config/config.module.ts
import { Global, Module } from '@nestjs/common';
import { ConfigService } from './config.service';
@Global() // Makes this module global
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {}
// logger/logger.module.ts
import { Global, Module } from '@nestjs/common';
import { LoggerService } from './logger.service';
@Global()
@Module({
providers: [LoggerService],
exports: [LoggerService],
})
export class LoggerModule {}
Using Global Modules:
typescript
// Any module can now use ConfigService without importing ConfigModule
import { Injectable } from '@nestjs/common';
import { ConfigService } from '../config/config.service';
import { LoggerService } from '../logger/logger.service';
@Injectable()
export class UsersService {
constructor(
private readonly configService: ConfigService, // Available globally
private readonly logger: LoggerService, // Available globally
) {
const dbHost = this.configService.get('DATABASE_HOST');
this.logger.log(`UsersService initialized with DB host: ${dbHost}`);
}
}
Example: Global Cache Module:
typescript
// cache/cache.module.ts
import { Global, Module } from '@nestjs/common';
import { CacheService } from './cache.service';
@Global()
@Module({
providers: [CacheService],
exports: [CacheService],
})
export class CacheModule {}
// cache/cache.service.ts
import { Injectable, OnModuleDestroy } from '@nestjs/common';
@Injectable()
export class CacheService implements OnModuleDestroy {
private cache = new Map<string, any>();
set(key: string, value: any, ttl?: number) {
this.cache.set(key, value);
if (ttl) {
setTimeout(() => this.delete(key), ttl);
}
}
get<T = any>(key: string): T | undefined {
return this.cache.get(key);
}
delete(key: string) {
return this.cache.delete(key);
}
clear() {
this.cache.clear();
}
onModuleDestroy() {
this.clear();
}
}
// Using cache service globally
import { Injectable } from '@nestjs/common';
import { CacheService } from '../cache/cache.service';
@Injectable()
export class ProductsService {
constructor(private readonly cache: CacheService) {}
async findOne(id: number) {
const cacheKey = `product:${id}`;
const cached = this.cache.get(cacheKey);
if (cached) {
return cached;
}
// Fetch from database
const product = await this.fetchFromDatabase(id);
this.cache.set(cacheKey, product, 60000); // Cache for 60 seconds
return product;
}
}
6. Dynamic Modules
Dynamic modules allow you to configure modules at runtime.
Creating Dynamic Modules:
typescript
// database/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;
database: string;
}
@Module({})
export class DatabaseModule {
static forRoot(options: DatabaseModuleOptions): DynamicModule {
return {
module: DatabaseModule,
providers: [
{
provide: 'DATABASE_OPTIONS',
useValue: options,
},
DatabaseService,
],
exports: [DatabaseService],
};
}
static forFeature(entities: any[]): DynamicModule {
return {
module: DatabaseModule,
imports: [/* register entities */],
exports: [/* entity repositories */],
};
}
}
// Using dynamic module
@Module({
imports: [
DatabaseModule.forRoot({
host: 'localhost',
port: 5432,
username: 'admin',
password: 'secret',
database: 'mydb',
}),
],
})
export class AppModule {}
Configurable Logger Module:
typescript
// logger/logger.module.ts
import { DynamicModule, Module } from '@nestjs/common';
import { LoggerService } from './logger.service';
export interface LoggerModuleOptions {
level: 'debug' | 'info' | 'warn' | 'error';
format: 'json' | 'text';
}
@Module({})
export class LoggerModule {
static forRoot(options: LoggerModuleOptions): DynamicModule {
return {
module: LoggerModule,
providers: [
{
provide: 'LOGGER_OPTIONS',
useValue: options,
},
LoggerService,
],
exports: [LoggerService],
};
}
}
Async Configuration Module:
typescript
// config/config.module.ts
import { DynamicModule, Module } from '@nestjs/common';
import { ConfigService } from './config.service';
export interface ConfigModuleAsyncOptions {
useFactory?: (...args: any[]) => Promise<any> | any;
inject?: any[];
}
@Module({})
export class ConfigModule {
static forRootAsync(options: ConfigModuleAsyncOptions): DynamicModule {
return {
module: ConfigModule,
providers: [
{
provide: 'CONFIG_OPTIONS',
useFactory: options.useFactory,
inject: options.inject || [],
},
ConfigService,
],
exports: [ConfigService],
};
}
}
// Using async configuration
@Module({
imports: [
ConfigModule.forRootAsync({
useFactory: async (httpService: HttpService) => {
const response = await httpService.get('/config').toPromise();
return response.data;
},
inject: [HttpService],
}),
],
})
export class AppModule {}
7. Avoiding Circular Dependencies
Circular dependencies occur when Module A depends on Module B, and Module B depends on Module A.
Problem Scenario:
typescript
// Circular dependency
// users.module.ts → depends on → auth.module.ts
// auth.module.ts → depends on → users.module.ts
// users.module.ts
@Module({
imports: [AuthModule], // Depends on AuthModule
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
// auth.module.ts
@Module({
imports: [UsersModule], // Depends on UsersModule
providers: [AuthService],
})
export class AuthModule {}
Solution 1: Use forwardRef()
typescript
// users.module.ts
import { forwardRef, Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { AuthModule } from '../auth/auth.module';
@Module({
imports: [forwardRef(() => AuthModule)], // Forward reference
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
// auth.module.ts
import { forwardRef, Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
@Module({
imports: [forwardRef(() => UsersModule)], // Forward reference
providers: [AuthService],
exports: [AuthService],
})
export class AuthModule {}
Solution 2: Create Shared Module
typescript
// shared/auth-users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { AuthService } from '../auth/auth.service';
@Module({
providers: [UsersService, AuthService],
exports: [UsersService, AuthService],
})
export class AuthUsersModule {}
// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { AuthUsersModule } from '../shared/auth-users.module';
@Module({
imports: [AuthUsersModule],
controllers: [UsersController],
})
export class UsersModule {}
// auth.module.ts
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthUsersModule } from '../shared/auth-users.module';
@Module({
imports: [AuthUsersModule],
controllers: [AuthController],
})
export class AuthModule {}
Solution 3: Refactor Dependencies
typescript
// Extract common functionality to a separate module
// common/security.module.ts
import { Module } from '@nestjs/common';
import { PasswordService } from './password.service';
import { TokenService } from './token.service';
@Module({
providers: [PasswordService, TokenService],
exports: [PasswordService, TokenService],
})
export class SecurityModule {}
// users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { SecurityModule } from '../common/security.module';
@Module({
imports: [SecurityModule],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
// auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UsersModule } from '../users/users.module';
import { SecurityModule } from '../common/security.module';
@Module({
imports: [UsersModule, SecurityModule], // No circular dependency
providers: [AuthService],
})
export class AuthModule {}
Solution 4: Use Module Reference
typescript
// auth.service.ts
import { Injectable, Inject, forwardRef } from '@nestjs/common';
@Injectable()
export class AuthService {
constructor(
@Inject(forwardRef(() => UsersService))
private readonly usersService: UsersService,
) {}
}
8. Module Re-exporting
Re-export modules to simplify imports.
Basic Re-exporting:
typescript
// shared/shared.module.ts
import { Module } from '@nestjs/common';
import { CommonModule } from '../common/common.module';
import { DatabaseModule } from '../database/database.module';
import { UtilsModule } from '../utils/utils.module';
@Module({
imports: [CommonModule, DatabaseModule, UtilsModule],
exports: [CommonModule, DatabaseModule, UtilsModule],
})
export class SharedModule {}
// Now other modules only need to import SharedModule
@Module({
imports: [SharedModule], // Import all shared modules at once
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
Feature Aggregation Module:
typescript
// features/features.module.ts
import { Module } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module';
import { UsersModule } from '../users/users.module';
import { ProductsModule } from '../products/products.module';
import { OrdersModule } from '../orders/orders.module';
@Module({
imports: [AuthModule, UsersModule, ProductsModule, OrdersModule],
exports: [AuthModule, UsersModule, ProductsModule, OrdersModule],
})
export class FeaturesModule {}
// app.module.ts
import { Module } from '@nestjs/common';
import { FeaturesModule } from './features/features.module';
@Module({
imports: [FeaturesModule],
})
export class AppModule {}
9. Best Practices & Patterns
1. Lazy Loading Modules
typescript
// app.module.ts
import { Module } from '@nestjs/common';
@Module({
imports: [
{
module: UsersModule,
// Lazy load conditionally
condition: process.env.ENABLE_USERS_MODULE === 'true',
},
{
module: AdminModule,
// Load only in admin mode
condition: process.env.ADMIN_MODE === 'true',
},
],
})
export class AppModule {}
2. Module Facade Pattern
typescript
// payment/payment.module.ts
import { Module } from '@nestjs/common';
import { PaymentController } from './payment.controller';
import { PaymentFacadeService } from './payment-facade.service';
import { StripeService } from './providers/stripe.service';
import { PayPalService } from './providers/paypal.service';
import { BankTransferService } from './providers/bank-transfer.service';
@Module({
controllers: [PaymentController],
providers: [
PaymentFacadeService,
StripeService,
PayPalService,
BankTransferService,
],
exports: [PaymentFacadeService],
})
export class PaymentModule {}
// payment/payment-facade.service.ts
import { Injectable } from '@nestjs/common';
import { StripeService } from './providers/stripe.service';
import { PayPalService } from './providers/paypal.service';
@Injectable()
export class PaymentFacadeService {
constructor(
private readonly stripeService: StripeService,
private readonly paypalService: PayPalService,
) {}
async processPayment(method: string, amount: number) {
switch (method) {
case 'stripe':
return this.stripeService.charge(amount);
case 'paypal':
return this.paypalService.createPayment(amount);
default:
throw new Error('Unsupported payment method');
}
}
}
3. Plugin Modules
typescript
// plugins/analytics/analytics.module.ts
import { DynamicModule, Module } from '@nestjs/common';
export interface AnalyticsModuleOptions {
apiKey: string;
enabled: boolean;
}
@Module({})
export class AnalyticsModule {
static forRoot(options: AnalyticsModuleOptions): DynamicModule {
if (!options.enabled) {
return {
module: AnalyticsModule,
providers: [],
};
}
return {
module: AnalyticsModule,
providers: [
{
provide: 'ANALYTICS_OPTIONS',
useValue: options,
},
AnalyticsService,
],
exports: [AnalyticsService],
};
}
}
4. Module Configuration Pattern
typescript
// modules/configurable-module.ts
import { DynamicModule, Module } from '@nestjs/common';
export interface ConfigurableModuleOptions {
featureFlag: boolean;
maxRetries?: number;
timeout?: number;
}
export const DEFAULT_OPTIONS: Partial<ConfigurableModuleOptions> = {
maxRetries: 3,
timeout: 5000,
};
@Module({})
export class ConfigurableModule {
static forRoot(options: ConfigurableModuleOptions): DynamicModule {
const mergedOptions = { ...DEFAULT_OPTIONS, ...options };
return {
module: ConfigurableModule,
providers: [
{
provide: 'MODULE_OPTIONS',
useValue: mergedOptions,
},
ConfigurableService,
],
exports: [ConfigurableService],
};
}
}
5. Complete E-commerce Application Structure
typescript
// app.module.ts (Root Module)
import { Module } from '@nestjs/common';
import { ConfigModule } from './config/config.module';
import { DatabaseModule } from './database/database.module';
import { CacheModule } from './cache/cache.module';
import { AuthModule } from './modules/auth/auth.module';
import { UsersModule } from './modules/users/users.module';
import { ProductsModule } from './modules/products/products.module';
import { OrdersModule } from './modules/orders/orders.module';
import { SharedModule } from './shared/shared.module';
@Module({
imports: [
// Global modules
ConfigModule.forRoot(),
DatabaseModule.forRoot({
type: 'postgres',
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT),
}),
CacheModule,
// Feature modules
AuthModule,
UsersModule,
ProductsModule,
OrdersModule,
// Shared utilities
SharedModule,
],
})
export class AppModule {}
// modules/orders/orders.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { OrdersController } from './orders.controller';
import { OrdersService } from './orders.service';
import { OrderItemsService } from './order-items.service';
import { PaymentsModule } from '../payments/payments.module';
import { ProductsModule } from '../products/products.module';
import { UsersModule } from '../users/users.module';
import { ShippingModule } from '../shipping/shipping.module';
import { EmailModule } from '../../shared/email/email.module';
@Module({
imports: [
forwardRef(() => PaymentsModule), // Use forwardRef to avoid circular deps
ProductsModule,
UsersModule,
ShippingModule,
EmailModule,
],
controllers: [OrdersController],
providers: [OrdersService, OrderItemsService],
exports: [OrdersService],
})
export class OrdersModule {}
// modules/products/products.module.ts
import { Module } from '@nestjs/common';
import { ProductsController } from './products.controller';
import { ProductsService } from './products.service';
import { CategoriesService } from './categories.service';
import { InventoryService } from './inventory.service';
import { ReviewsService } from './reviews.service';
import { DatabaseModule } from '../../database/database.module';
import { CacheModule } from '../../cache/cache.module';
@Module({
imports: [DatabaseModule, CacheModule],
controllers: [ProductsController],
providers: [
ProductsService,
CategoriesService,
InventoryService,
ReviewsService,
],
exports: [
ProductsService,
CategoriesService,
InventoryService,
],
})
export class ProductsModule {}
// shared/shared.module.ts
import { Global, Module } from '@nestjs/common';
import { LoggerService } from './logger/logger.service';
import { ValidationService } from './validation/validation.service';
import { EncryptionService } from './encryption/encryption.service';
import { FileService } from './file/file.service';
import { EmailService } from './email/email.service';
@Global() // Make available globally
@Module({
providers: [
LoggerService,
ValidationService,
EncryptionService,
FileService,
EmailService,
],
exports: [
LoggerService,
ValidationService,
EncryptionService,
FileService,
EmailService,
],
})
export class SharedModule {}
Module Organization Rules:
-
Single Responsibility: Each module should handle one domain/feature
-
Explicit Exports: Only export what other modules need
-
Avoid Deep Nesting: Keep module hierarchy flat
-
Use Shared Modules: For cross-cutting concerns
-
Global Modules Sparingly: Only for truly global services
-
ForwardRef for Circular Dependencies: Use when necessary
-
Dynamic Modules for Configuration: When runtime configuration is needed
-
Re-export Aggregately: Simplify imports with shared module exports
-
Feature Flags: Use conditional loading for optional features
-
Testability: Design modules to be easily testable in isolation
This modular approach ensures your NestJS application is maintainable, scalable, and easy to understand as it grows.
Comments (0)
No comments yet. Be the first to comment!