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
- MVC

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.

ExpressoTS also supports the traditional MVC (Model-View-Controller) pattern. In this approach, a single controller can manage multiple routes related to a specific resource. For instance, within a Product Module, you could have a ProductController that handles various routes like create, update, and delete. This style is beneficial for smaller applications or when you prefer to group related actions within the same controller, providing a more compact and centralized structure.
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.
| Scope | Lifecycle | Performance | Use Case | Memory |
|---|---|---|---|---|
| Request | New per request | Fast | Stateless services (default) | Low |
| Singleton | Single instance | Fastest | DB connections, config | High |
| Transient | New every injection | Slowest | Pure functions, disposable | Highest |
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.
-
AppContainer (Global)
- Services registered here have the broadest scope and are shared across all modules and controllers.
-
Module (Modular)
- Dependencies registered in a module are available to all controllers within that module.
-
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
}
Use @scope() decorator when you need a specific controller to have a different lifecycle than its parent module.
Scope Selection Guide
- Use Request scope (default) for stateless services
- Use Singleton for expensive initialization (DB, cache)
- Avoid Transient unless necessary (disposable objects)
Support the Project
ExpressoTS is MIT-licensed open source. See the support guide to contribute.