Modules
Organize your application into cohesive, reusable modules
What are Modules?
Modules are the fundamental building blocks of a Dwex application. A module is a class decorated with @Module() that organizes related components (controllers, providers, etc.) into cohesive units.
Basic Module
Here's a simple module:
import { Module } from "@dwex/core";
import { UserController } from "./user.controller";
import { UserService } from "./user.service";
@Module({
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}Module Metadata
The @Module() decorator accepts an object with the following properties:
controllers
Controllers defined in this module. These handle incoming HTTP requests.
@Module({
controllers: [UserController, ProfileController],
})
export class UserModule {}providers
Injectable services and other providers available in this module.
@Module({
providers: [UserService, EmailService, ValidationPipe],
})
export class UserModule {}imports
Other modules whose exported providers should be available in this module.
@Module({
imports: [DatabaseModule, LoggerModule],
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}exports
Providers that should be available to other modules that import this module.
@Module({
providers: [UserService, UserRepository],
exports: [UserService], // UserRepository is private to this module
})
export class UserModule {}Importing Modules
To use providers from another module, import that module:
@Module({
imports: [UserModule], // Imports UserService
controllers: [PostController],
providers: [PostService],
})
export class PostModule {}Now PostService can inject UserService:
@Injectable()
export class PostService {
constructor(private readonly userService: UserService) {}
async createPost(userId: string, title: string) {
const user = await this.userService.findOne(userId);
// ...
}
}Module Encapsulation: Services can only be injected if they are:
- Defined in the same module as the consumer
- Exported by an imported module
- Provided by a global module
If you try to inject a service that isn't accessible, you'll get a helpful error message pointing you to the solution.
Global Modules
If a module should be available everywhere without explicit imports, mark it as global:
import { Module, Global } from "@dwex/core";
@Global()
@Module({
providers: [ConfigService, CacheService],
exports: [ConfigService, CacheService],
})
export class SharedModule {}Now any module can inject ConfigService or CacheService without importing SharedModule.
Best Practice: Only make truly shared utilities global. Overusing global modules can make dependencies unclear.
Dynamic Modules
Dynamic modules allow configuration at runtime. This is useful for modules that need different settings in different contexts.
Creating a Dynamic Module
import { Module, type DynamicModule } from "@dwex/core";
@Module({})
export class DatabaseModule {
static forRoot(options: DatabaseOptions): DynamicModule {
return {
module: DatabaseModule,
providers: [
{
provide: "DATABASE_OPTIONS",
useValue: options,
},
DatabaseService,
],
exports: [DatabaseService],
};
}
}Using a Dynamic Module
@Module({
imports: [
DatabaseModule.forRoot({
host: "localhost",
port: 5432,
database: "myapp",
}),
],
})
export class AppModule {}Async Configuration
For configuration that requires async initialization:
@Module({})
export class DatabaseModule {
static async forRootAsync(
options: DatabaseAsyncOptions
): Promise<DynamicModule> {
const config = await options.useFactory();
return {
module: DatabaseModule,
providers: [
{
provide: "DATABASE_OPTIONS",
useValue: config,
},
DatabaseService,
],
exports: [DatabaseService],
};
}
}Usage:
@Module({
imports: [
await DatabaseModule.forRootAsync({
useFactory: async () => {
const config = await loadConfig();
return config.database;
},
}),
],
})
export class AppModule {}Module Re-exporting
You can re-export imported modules to simplify imports:
@Module({
imports: [UserModule, PostModule, CommentModule],
exports: [UserModule, PostModule, CommentModule],
})
export class ContentModule {}Now other modules only need to import ContentModule:
@Module({
imports: [ContentModule], // Gets User, Post, and Comment modules
})
export class AppModule {}Feature Modules
Organize your app by features:
@Module({
imports: [DatabaseModule],
controllers: [UserController],
providers: [UserService, UserRepository],
exports: [UserService],
})
export class UserModule {}@Module({
imports: [UserModule, JwtModule],
controllers: [AuthController],
providers: [AuthService, AuthGuard],
exports: [AuthGuard],
})
export class AuthModule {}@Module({
imports: [UserModule, AuthModule],
})
export class AppModule {}Root Module
Every application has exactly one root module passed to DwexFactory.create():
import { DwexFactory } from "@dwex/core";
import { AppModule } from "./app.module";
const app = await DwexFactory.create(AppModule);The root module typically imports all feature modules:
@Module({
imports: [
LoggerModule,
DatabaseModule.forRoot({ url: process.env.DATABASE_URL }),
UserModule,
AuthModule,
PostModule,
],
})
export class AppModule {}Best Practices
1. Single Responsibility
Each module should focus on one feature or domain:
// Good
@Module({ ... })
export class UserModule {}
@Module({ ... })
export class AuthModule {}
// Avoid
@Module({ ... })
export class UserAndAuthModule {} // Too broad2. Export Only What's Needed
Keep module internals private:
@Module({
providers: [UserService, UserRepository, UserValidator],
exports: [UserService], // Only expose the service
})
export class UserModule {}Breaking Change in v1.0+: Module encapsulation is now strictly enforced. If a service isn't exported, other modules cannot inject it, even if they import the module. This prevents tight coupling and makes dependencies explicit.
Error Example:
No provider found for UserRepository in PostModule.
However, this provider exists in:
- UserModule (not exported) - Add UserRepository to 'UserModule.exports'Solution: Either export the provider or create a public service that uses it internally.
3. Use Shared Modules Wisely
Create a SharedModule for truly common utilities:
@Global()
@Module({
providers: [ConfigService, LoggerService],
exports: [ConfigService, LoggerService],
})
export class SharedModule {}4. Lazy Loading (Future)
While not yet implemented, plan your modules for potential lazy loading:
// Each feature in its own module
@Module({ ... })
export class AdminModule {}
@Module({ ... })
export class PublicModule {}