registerModules()
What?
Logical "Feature Bundles" that group related services, routes, and controllers into a single reusable unit (e.g., AuthModule, CMSModule, SearchModule).
Why?
Modules prevent the ShopServer from becoming a "God Object." Instead of the main server knowing about every single service and route, it delegates feature-specific wiring to the modules. This enables:
- Encapsulation: Logic related to a feature stays within its package.
- Reusability: The same
AuthModulecan be used in different project templates. - Clean Architecture: Modules act as "Mini-Servers" that understand their own requirements.
How to use
Modules can be registered either as simple classes or as Module Providers with options.
public async initModules() {
await this.registerModules([
new AuthModule({
strategy: 'cookie',
secret: '...'
}),
SearchModule, // Simple registration without options
new CMSModule({ provider: new CommerceCMSProvider(...) })
]);
}
Under the Hood: The register() Hook
The registerModules method iterates through the list and resolves each module into an instance. The most critical step is the await moduleInstance.register(this) call.
- Resolution: If a class is passed, it's instantiated. If a
ModuleProvider({ module, options }) is passed, the options are injected into the constructor. - Inversion of Control: By passing
this(theCoreServerinstance) to the module'sregistermethod, the module gains the power to callserver.registerServices(),server.registerRoutes(), and so on. - Self-Wiring: The module "wires itself" into the main server infrastructure during this phase.
Deep Dive: Self-Wiring Modules
A Module in Archibald is more than just a configuration block; it is a Lifecycle Participant. Because it receives the CoreServer instance, it can perform complex, multi-stage wiring that would otherwise clutter the ShopServer.
Example: The PrometheusModule
The following example demonstrates a module that not only registers its own components but also hooks into the global Hapi.js request lifecycle.
export class PrometheusModule extends BaseModule {
@Inject()
private readonly prometheusService: PrometheusService;
public override async register(server: CoreServer) {
// 1. Internal Wiring
await server.registerControllers([PrometheusController]);
await server.registerServices([PrometheusService]);
await server.registerRoutes([
{
method: RouteMethod.GET,
path: '/metrics',
handler: 'PrometheusController.index'
}
]);
// 2. Lifecycle Extension: Global request timing
server.internal.ext('onRequest', (request, h) => {
request.app.requestStartedAt = this.prometheusService.getCurrentTime();
return h.continue;
});
// 3. Lifecycle Extension: Observation recording
server.internal.ext('onPostHandler', async (request, h) => {
const start = request.app?.requestStartedAt ?? 0;
const diff = this.prometheusService.getTimeDiff(start);
await this.prometheusService.observe(request, diff);
return h.continue;
});
}
}
Architectural Benefits
- Atomic Portability: A module is a "plug-and-play" feature. To add metrics to a new project, you simply add
PrometheusModuleto theinitModuleslist. All services, routes, and lifecycle hooks come with it automatically. - Encapsulated Scope: The main server doesn't need to know how metrics are collected; it only needs to know that a module handles it.
- Cross-Cutting Concerns: Modules allow for global logic (like security headers, analytics, or error tracking) to be managed in one place without modifying the
CoreServerbase.
Impact Analysis: How the System Changes
When a module calls registration methods on the server, it affects the application state in the following ways:
| Component | Impact of Module Registration |
|---|---|
| Services | Registered into the global Container. They become available for @Inject() in any other controller or service, even those registered outside the module. |
| Controllers | Added to the CoreServer's internal map. They become addressable by any route registered subsequently. |
| Routes | Appended to the Hapi.js routing table. If they use string handlers, they are resolved against the newly updated controller registry. |
| Decorators | Added to the this.decorators Set. Decorators are additive. Adding a decorator in a module does NOT reset existing ones (like ConfigDecorator); it appends a new step to the reduce pipeline for every request. |
| Hapi Extensions | Injected via server.internal.ext. These are Global. A module that hooks into onRequest will affect every single route in the system, providing a powerful way to inject global middleware-like behavior. |
Does the Decorator Configuration Reset?
No. The CoreServer maintains a Set of decorators initialized in the constructor (Config, Header, App). When a module calls registerDecorators(), it simply adds new unique instances to this set. The original decorators remain at the start of the pipeline.
However, because the pipeline is a sequential reduce, a module's decorator can modify the ServerContext created by previous decorators (e.g., adding properties to the app object or overriding headers), allowing for powerful context-driven features.
Orchestration Diagram
This diagram shows the complete sequence, highlighting how Modules act as secondary orchestrators.