Skip to main content

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:

  1. Decouple Framework Logic: They separate the logic of extracting data (like parsing headers) from the logic of using it.
  2. Enable Isomorphism: By populating the ServerContext, they allow services to use @archibald/storage to retrieve request-specific data without passing the request object through every function call.
  3. 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 ConfigService to perform a multi-stage lookup:
    1. Hostname Check: It scans the hostname (e.g., myshop.de vs myshop.at) to identify the target country.
    2. Header Check: It looks for internal headers like x-arc-country to allow for programmatic overrides (useful in testing or edge-side routing).
    3. Config Merging: Once a country is identified, it uses deepmerge to overlay country-specific settings (found in the countries key of the base config) onto the default configuration.
  • Result: It attaches a tailored context.config to 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.headers object using the HeadersHelper.convert() utility. This creates a formal instance of the Web API Headers class.
  • Result: Attaches context.headers to the storage.
  • Architectural Benefit: This ensures consistency and standard compliance. By using a Fetch-compatible Headers object, 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 AppClient for the request. The AppClient is a specialized SubscriberMap that tracks the application's "life signs" during the request lifecycle (e.g., language, httpStatus, cacheControl).
  • Result: Attaches context.app to the storage.
  • Architectural Benefit: It acts as the SSR-to-Client State Bridge. Services can update context.app.httpStatus = 404 deep in the logic. This state is then:
    1. Read by the Renderer to set the actual HTTP response code.
    2. Serialized into a compressed JSON blob and injected into the HTML as window.__INITIAL_APP_CACHE__.
    3. Restored on the client-side to ensure the React hydration exactly matches the server's output.

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: The decorateController function 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 in this.decorators.
  • @archibald/storage: Provides the runInServerContext utility 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 at context.headers via the storage helper.
  • Frontend Components: Use the state initialized by AppDecorator (e.g., useAppClient().language) to drive isomorphic rendering.