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​