Skip to main content

Module Patterns

This article covers advanced module patterns and best practices for organizing your NexusDI modules effectively. Your modules are like Bob's various clones - each thinks they're the most important one, but they all need to stop arguing and work together if you want anything to actually get done.

Advanced Module Patterns

1. Feature Modules

Organize by application features:

// User feature module
@Module({
providers: [
UserService,
UserRepository,
UserValidator,
{ token: USER_CONFIG, useValue: userConfig },
],
})
class UserModule {}

// Order feature module
@Module({
providers: [
OrderService,
OrderRepository,
PaymentService,
{ token: PAYMENT_GATEWAY, useClass: StripeGateway },
],
})
class OrderModule {}

// Main application module
@Module({
imports: [UserModule, OrderModule],
providers: [AppService, { token: APP_CONFIG, useValue: appConfig }],
})
class AppModule {}

2. Infrastructure Modules

Separate infrastructure concerns:

@Module({
providers: [
DatabaseService,
ConnectionPool,
{ token: DATABASE_CONFIG, useValue: dbConfig },
],
})
class DatabaseModule {}

@Module({
providers: [
LoggerService,
LogFormatter,
{ token: LOG_LEVEL, useValue: process.env.LOG_LEVEL },
],
})
class LoggingModule {}

@Module({
providers: [
EmailService,
TemplateEngine,
{ token: SMTP_CONFIG, useValue: smtpConfig },
],
})
class EmailModule {}

3. Environment-Specific Modules

Different modules for different environments:

// Development module
@Module({
providers: [
DevLogger,
DevDatabase,
{ token: LOG_LEVEL, useValue: 'debug' },
{ token: DATABASE_URL, useValue: 'sqlite://dev.db' },
],
})
class DevelopmentModule {}

// Production module
@Module({
providers: [
ProductionLogger,
PostgresDatabase,
{ token: LOG_LEVEL, useValue: 'info' },
{ token: DATABASE_URL, useValue: process.env.DATABASE_URL },
],
})
class ProductionModule {}

// Use based on environment
const container = new Nexus();
if (process.env.NODE_ENV === 'production') {
container.set(ProductionModule);
} else {
container.set(DevelopmentModule);
}

Testing with Modules

Unit Testing Modules

import { describe, it, expect, beforeEach } from 'vitest';
import { Nexus } from '@nexusdi/core';
import { UserModule } from './modules/user.module';

describe('UserModule', () => {
let container: Nexus;

beforeEach(() => {
container = new Nexus();
container.set(UserModule);
});

it('should provide UserService', () => {
const userService = container.get(USER_SERVICE);
expect(userService).toBeInstanceOf(UserService);
});

it('should inject dependencies correctly', () => {
const userService = container.get(USER_SERVICE);
const result = userService.getUser('123');
expect(result).toBeDefined();
});
});

Mocking Module Dependencies

// Create a test module with mocked dependencies
@Module({
providers: [
UserService,
{ token: DATABASE, useValue: mockDatabase },
{ token: LOGGER, useValue: mockLogger },
],
})
class TestUserModule {}

describe('UserModule with mocks', () => {
it('should work with mocked dependencies', () => {
const container = new Nexus();
container.set(TestUserModule);

const userService = container.get(USER_SERVICE);
// Test with mocked dependencies
});
});

Testing with async dynamic modules (see Dynamic Modules doc for details)

const config = await SomeModule.configAsync(options);
container.set(config); // Always await the result of configAsync() before passing it to set. See Dynamic Modules for details.

Best Practices

1. Single Responsibility

Each module should have a single, well-defined responsibility:

// ✅ Good - focused on user management
@Module({
providers: [
UserService,
UserRepository,
UserValidator,
{ token: USER_CONFIG, useValue: userConfig },
],
})
class UserModule {}

// ❌ Bad - mixing unrelated concerns
@Module({
providers: [
UserService,
EmailService,
PaymentService,
LoggerService,
{ token: USER_CONFIG, useValue: userConfig },
{ token: EMAIL_CONFIG, useValue: emailConfig },
{ token: PAYMENT_CONFIG, useValue: paymentConfig },
],
})
class EverythingModule {}

2. Clear Dependencies

Make module dependencies explicit through imports:

// ✅ Good - explicit dependencies
@Module({
providers: [UserService],
imports: [DatabaseModule, LoggingModule],
})
class UserModule {}

// ❌ Bad - hidden dependencies
@Module({
providers: [UserService],
// Missing imports, but UserService depends on DatabaseModule
})
class UserModule {}

3. Consistent Naming

Use consistent naming conventions:

// ✅ Good - consistent naming
class UserModule {}
class OrderModule {}
class DatabaseModule {}

// ❌ Bad - inconsistent naming
class UserModule {}
class Orders {}
class DB {}

4. Documentation

Document your modules with clear descriptions:

/**
* User management module
*
* Provides user-related services including:
* - User creation and management
* - Authentication and authorization
* - User profile operations
*
* Dependencies:
* - DatabaseModule for data persistence
* - LoggingModule for audit trails
*/
@Module({
providers: [
UserService,
UserRepository,
UserValidator,
{ token: USER_CONFIG, useValue: userConfig },
],
imports: [DatabaseModule, LoggingModule],
})
class UserModule {}

Advanced Configuration Patterns

Environment-Specific Configuration

You can achieve environment-specific configuration by creating separate modules for each environment:

@Module({
providers: [LoggerService],
providers: [
{ token: LOG_CONFIG, useValue: { level: 'debug', format: 'detailed' } },
],
})
class DevelopmentLoggingModule {}

@Module({
providers: [LoggerService],
providers: [
{ token: LOG_CONFIG, useValue: { level: 'info', format: 'json' } },
],
})
class ProductionLoggingModule {}

@Module({
providers: [LoggerService],
providers: [
{ token: LOG_CONFIG, useValue: { level: 'error', format: 'minimal' } },
],
})
class TestingLoggingModule {}

// Usage
const container = new Nexus();

if (process.env.NODE_ENV === 'production') {
container.set(ProductionLoggingModule);
} else if (process.env.NODE_ENV === 'test') {
container.set(TestingLoggingModule);
} else {
container.set(DevelopmentLoggingModule);
}

Feature-Based Configuration

Create different modules for different feature configurations:

interface EmailConfig {
provider: 'smtp' | 'sendgrid' | 'mailgun';
apiKey?: string;
smtpConfig?: {
host: string;
port: number;
secure: boolean;
};
}

@Module({
providers: [EmailService],
providers: [{ token: EMAIL_CONFIG, useValue: { provider: 'sendgrid' } }],
})
class SendGridEmailModule {}

@Module({
providers: [EmailService],
providers: [{ token: EMAIL_CONFIG, useValue: { provider: 'mailgun' } }],
})
class MailgunEmailModule {}

@Module({
providers: [EmailService],
providers: [{ token: EMAIL_CONFIG, useValue: { provider: 'smtp' } }],
})
class SmtpEmailModule {}

// Usage
const container = new Nexus();

// Choose email provider based on configuration
if (process.env.EMAIL_PROVIDER === 'sendgrid') {
container.set(SendGridEmailModule);
} else if (process.env.EMAIL_PROVIDER === 'mailgun') {
container.set(MailgunEmailModule);
} else {
container.set(SmtpEmailModule);
}

Composite Configuration

Create composite modules that import other modules:

@Module({
providers: [DatabaseService],
providers: [
{
token: DATABASE_CONFIG,
useValue: { host: 'localhost', port: 5432, database: 'myapp' },
},
],
})
class DatabaseModule {}

@Module({
providers: [EmailService],
providers: [
{
token: EMAIL_CONFIG,
useValue: { provider: 'sendgrid', apiKey: process.env.SENDGRID_API_KEY },
},
],
})
class EmailModule {}

@Module({
providers: [LoggerService],
providers: [
{
token: LOG_CONFIG,
useValue: {
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
},
},
],
})
class LoggingModule {}

@Module({
providers: [AppService],
imports: [DatabaseModule, EmailModule, LoggingModule],
})
class AppModule {}

// Usage
const container = new Nexus();
container.set(AppModule);

Configuration Validation

You can validate configuration by creating factory functions that validate before creating modules:

function validateDatabaseConfig(config: DatabaseConfig): void {
if (!config.host) {
throw new Error('Database host is required');
}
if (!config.port || config.port < 1 || config.port > 65535) {
throw new Error('Database port must be between 1 and 65535');
}
if (!config.database) {
throw new Error('Database name is required');
}
}

function createDatabaseModule(config: DatabaseConfig) {
validateDatabaseConfig(config);

@Module({
providers: [DatabaseService],
providers: [{ token: DATABASE_CONFIG, useValue: config }],
})
class ValidatedDatabaseModule {}

return ValidatedDatabaseModule;
}

// Usage
const container = new Nexus();
const DatabaseModule = createDatabaseModule({
host: 'localhost',
port: 5432,
database: 'myapp',
});
container.set(DatabaseModule);

Testing with Dynamic Modules

You can test different configurations by creating test-specific modules:

describe('DatabaseModule', () => {
it('should work with test configuration', () => {
const container = new Nexus();

@Module({
providers: [DatabaseService],
providers: [
{
token: DATABASE_CONFIG,
useValue: {
host: 'localhost',
port: 5432,
database: 'test_db',
},
},
],
})
class TestDatabaseModule {}

container.set(TestDatabaseModule);

const databaseService = container.get(DATABASE_SERVICE);
expect(databaseService).toBeInstanceOf(DatabaseService);
});

it('should validate configuration', () => {
expect(() => {
createDatabaseModule({
host: '', // Invalid
port: 5432,
database: 'test_db',
});
}).toThrow('Database host is required');
});
});

Advanced Patterns & Further Reading

Summary

Module patterns help you create well-organized, maintainable applications:

  • Feature modules organize by business domain
  • Infrastructure modules separate technical concerns
  • Environment-specific modules handle different deployment scenarios
  • Testing patterns ensure reliable module testing
  • Configuration patterns provide flexible setup options
  • Best practices guide you toward maintainable code

For dynamic module configuration with runtime settings, see Dynamic Modules.

Next Steps