Dwex Logo

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:

user.module.ts
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:

  1. Defined in the same module as the consumer
  2. Exported by an imported module
  3. 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

database.module.ts
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

app.module.ts
@Module({
  imports: [
    DatabaseModule.forRoot({
      host: "localhost",
      port: 5432,
      database: "myapp",
    }),
  ],
})
export class AppModule {}

Async Configuration

For configuration that requires async initialization:

database.module.ts
@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:

app.module.ts
@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:

user.module.ts
@Module({
  imports: [DatabaseModule],
  controllers: [UserController],
  providers: [UserService, UserRepository],
  exports: [UserService],
})
export class UserModule {}
auth.module.ts
@Module({
  imports: [UserModule, JwtModule],
  controllers: [AuthController],
  providers: [AuthService, AuthGuard],
  exports: [AuthGuard],
})
export class AuthModule {}
app.module.ts
@Module({
  imports: [UserModule, AuthModule],
})
export class AppModule {}

Root Module

Every application has exactly one root module passed to DwexFactory.create():

main.ts
import { DwexFactory } from "@dwex/core";
import { AppModule } from "./app.module";

const app = await DwexFactory.create(AppModule);

The root module typically imports all feature modules:

app.module.ts
@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 broad

2. 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 {}

Next Steps