Skip to main content
Version: 4.x

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.