Context Decorators
Decorators in Archibald are specialized classes responsible for enriching the ServerContext before a request reaches a Controller. They act as a modular middleware system that ensures the environment, headers, and application state are correctly initialized for each request.
Why Do We Need Them?
Archibald is an isomorphic framework. This means code needs to access things like "the current configuration" or "request headers" even when it's deep inside a service, far away from the Hapi.js request object. Decorators:
- Decouple Framework Logic: They separate the logic of extracting data (like parsing headers) from the logic of using it.
- Enable Isomorphism: By populating the
ServerContext, they allow services to use@archibald/storageto retrieve request-specific data without passing therequestobject through every function call. - Ensure Consistency: They guarantee that every request has a standardized set of properties (Config, Headers, AppClient) regardless of which controller handles it.
Core Decorators: Deep Dive
1. ConfigDecorator
The ConfigDecorator transforms the static application configuration into a request-aware context.
- Source Logic: It utilizes the
ConfigServiceto perform a multi-stage lookup:- Hostname Check: It scans the hostname (e.g.,
myshop.devsmyshop.at) to identify the target country. - Header Check: It looks for internal headers like
x-arc-countryto allow for programmatic overrides (useful in testing or edge-side routing). - Config Merging: Once a country is identified, it uses
deepmergeto overlay country-specific settings (found in thecountrieskey of the base config) onto the default configuration.
- Hostname Check: It scans the hostname (e.g.,
- Result: It attaches a tailored
context.configto the storage. - Architectural Benefit: This enables Multi-Tenant/Multi-Country support without branching logic in business services. A service simply calls
configService.get('api.url'), and it receives the correct URL for the specific country context resolved by the decorator.
2. HeaderDecorator
The HeaderDecorator standardizes the communication layer.
- Source Logic: It wraps Hapi's raw
request.headersobject using theHeadersHelper.convert()utility. This creates a formal instance of the Web APIHeadersclass. - Result: Attaches
context.headersto the storage. - Architectural Benefit: This ensures consistency and standard compliance. By using a Fetch-compatible
Headersobject, the framework avoids issues with header case-sensitivity and provides a unified API (.get(),.has()) that works identically on both the server and the browser.
3. AppDecorator
The AppDecorator initializes the state synchronization engine for Isomorphic React.
- Source Logic: It instantiates a fresh
AppClientfor the request. TheAppClientis a specializedSubscriberMapthat tracks the application's "life signs" during the request lifecycle (e.g.,language,httpStatus,cacheControl). - Result: Attaches
context.appto the storage. - Architectural Benefit: It acts as the SSR-to-Client State Bridge. Services can update
context.app.httpStatus = 404deep in the logic. This state is then:- Read by the
Rendererto set the actual HTTP response code. - Serialized into a compressed JSON blob and injected into the HTML as
window.__INITIAL_APP_CACHE__. - Restored on the client-side to ensure the React hydration exactly matches the server's output.
- Read by the
Route-Level Integration
Decorators are not applied globally at the server level (like traditional Hapi plugins); instead, they are dynamically hooked into every route handler during the registration phase.
How it Works
When CoreServer.registerRoute() is called, the framework wraps the actual controller action or handler function inside a call to decorateController. This ensures that the context is freshly built and isolated for every single incoming request.
// Simplified internal logic of registerRoute
this.server.route({
path: routePath,
method: route.method,
handler: (request, h) => {
// The decoration happens HERE, inside the Hapi handler
return this.decorateController({ request, response: h, skip: route.skipDecorators }, async () => {
return await controller.action(request, h);
});
}
});
Granular Control: skipDecorators
The DefaultRouteConfig interface allows for granular control over this pipeline. If a specific route (e.g., a simple health check or a high-performance webhook) does not require the overhead of resolving country configs or initializing the AppClient, it can be bypassed.
skipDecorators: true: ThedecorateControllerfunction will bypass the decorator pipeline, providing only the bare-minimum context (request and response) to the storage.- Default: All registered decorators are executed.
Under the Hood: The Decoration Pipeline
Decorators are executed sequentially during the route handling phase. The CoreServer manages a Set of decorators and applies them using a functional reduce pattern.
Sequence Diagram
The reduce Implementation
The core logic resides in CoreServer.decorateController. It ensures that even if a decorator fails or is skipped, the basic context structure remains intact.
public decorateController({ request, response, skip = false }, cb) {
const context = (skip ? [] : Array.from(this.decorators)).reduce(
(currentContext, decorator) => {
return decorator.decorate(currentContext);
},
{
request,
response,
path: request.path,
coreServer: this,
instance: this.server
}
);
return runInServerContext(context, cb);
}
Connection Points
CoreServer: Holds the registry of decorators inthis.decorators.@archibald/storage: Provides therunInServerContextutility that makes the decorated context available globally within the request's async stack.- Controllers & Services: Consume the decorated data. Instead of looking at
request.headers, they look atcontext.headersvia the storage helper. - Frontend Components: Use the state initialized by
AppDecorator(e.g.,useAppClient().language) to drive isomorphic rendering.