Skip to main content
Version: 4.0.0-preview

Modules

Modules, or container modules in ExpressoTS, are collections of services, primarily Controllers and their dependencies, managed by the framework's container. These modules help organize your application into logical domains, register components, and resolve Controllers efficiently.

SOLID module pattern with one controller per action

ExpressoTS encourages the creation of controllers following the SOLID principles. In this approach, each controller is responsible for a specific action, such as creating, updating, or deleting a product. For example, within a Product Module, you might have separate controllers like CreateController, UpdateController, and DeleteController. Each of these controllers would then call their unique service or use case, which contains the actual implementation logic. This structure helps maintain a clean and organized codebase, where each controller has a single responsibility.

info

ExpressoTS provides the flexibility to choose the approach that best fits your project's needs, whether it's adhering to SOLID principles for maximum maintainability or following the MVC pattern for simplicity and clarity.

Creating a Module

To create a module using the CLI tool, run the following command:

expressots g mo module-name

This command will generate a new module with the following structure:

import { CreateModule } from "@expressots/core";

export const ProductModule = CreateModule([]);

Registering Controllers

To register controllers within a module, pass them as an array to the CreateModule function:

import { CreateModule } from "@expressots/core";
import { ProductController } from "./product.controller";

export const ProductModule = CreateModule([ProductController]);

Module Scope

Developers can set the scope of a module using the Scope enum. All controllers registered within the module will inherit the module's scope.

ScopeLifecyclePerformanceUse CaseMemory
RequestNew per requestFastStateless services (default)Low
SingletonSingle instanceFastestDB connections, configHigh
TransientNew every injectionSlowestPure functions, disposableHighest

When creating a module, you can pass the scope as a second argument:

import { CreateModule, Scope } from "@expressots/core";

export const ProductModule = CreateModule([ProductController], Scope.Singleton);

If not provided, the default scope is acquired from the AppContainer which is Request.

Custom Bindings

Modules support custom bindings for advanced dependency injection patterns:

Basic Custom Binding

export const AppModule = CreateModule([UserController], (bind) => {
bind<ILogger>("ILogger").to(ConsoleLogger).inSingletonScope();
bind<ICache>("ICache").to(RedisCache);
});

Module with Scope AND Bindings

import { CreateModule, Scope } from "@expressots/core";

export const DatabaseModule = CreateModule(
[DbController],
Scope.Singleton,
(bind) => {
bind<IDatabase>("IDatabase").to(PostgresDB);
}
);

Binding Types

// Class binding
bind<IService>("IService").to(MyService);

// Constant value
bind<IConfig>("IConfig").toConstantValue(config);

// Dynamic value (factory)
bind<ILogger>("ILogger").toDynamicValue((context) => {
return new Logger(context.container.get("IConfig"));
});

Binding Scopes

.inSingletonScope() // Single instance
.inRequestScope() // New per request
.inTransientScope() // New every injection

Conditional Binding

export const Module = CreateModule(
[Controller],
(bind, unbind, isBound, rebind) => {
if (!isBound("ILogger")) {
bind<ILogger>("ILogger").to(Logger);
}

if (isBound("OldCache")) {
rebind<ICache>("ICache").to(NewCache);
}
}
);

Advanced Module Patterns

Using createModule Helper

import { createModule } from "@expressots/core";

export const LoggingModule = createModule((bind) => {
bind<ILogger>("ILogger").to(WinstonLogger).inSingletonScope();
});

Combining Modules

import { combineModules, CreateModule } from "@expressots/core";

export const AppModule = combineModules(
CreateModule([UserController]),
CreateModule([AuthController]),
LoggingModule
);

Scope Precedence

The hierarchy of scope precedence is: AppContainer > Module > Controller

This ensures a versatile and hierarchical approach to dependency scope management.

  1. AppContainer (Global)

    • Services registered here have the broadest scope and are shared across all modules and controllers.
  2. Module (Modular)

    • Dependencies registered in a module are available to all controllers within that module.
  3. Controller (Specific)

    • Dependencies at this level are only available within the individual controller.

Using @scope() Decorator

To override controller scope within a module, use the @scope() decorator:

import { scope, Scope } from "@expressots/core";
import { controller } from "@expressots/adapter-express";

@scope(Scope.Singleton)
@controller("/cache")
export class CacheController {
// Single instance regardless of module scope
}
tip

Use @scope() decorator when you need a specific controller to have a different lifecycle than its parent module.

Scope Selection Guide

  1. Use Request scope (default) for stateless services
  2. Use Singleton for expensive initialization (DB, cache)
  3. Avoid Transient unless necessary (disposable objects)

Support the Project

ExpressoTS is MIT-licensed open source. See the support guide to contribute.