Skip to main content

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 AuthModule can 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.

  1. Resolution: If a class is passed, it's instantiated. If a ModuleProvider ({ module, options }) is passed, the options are injected into the constructor.
  2. Inversion of Control: By passing this (the CoreServer instance) to the module's register method, the module gains the power to call server.registerServices(), server.registerRoutes(), and so on.
  3. 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 PrometheusModule to the initModules list. 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 CoreServer base.

Impact Analysis: How the System Changes

When a module calls registration methods on the server, it affects the application state in the following ways:

ComponentImpact of Module Registration
ServicesRegistered into the global Container. They become available for @Inject() in any other controller or service, even those registered outside the module.
ControllersAdded to the CoreServer's internal map. They become addressable by any route registered subsequently.
RoutesAppended to the Hapi.js routing table. If they use string handlers, they are resolved against the newly updated controller registry.
DecoratorsAdded 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 ExtensionsInjected 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.