Middleware Model
Strata's middleware system lets you intercept and modify requests and responses at three distinct points in the processing lifecycle. Unlike Express-style middleware (a single function chain), Strata middleware uses three explicit hooks, making it clear exactly when your code runs.
The Three Hooks
Every middleware can implement up to three hooks:
beforeRequest(request)
Called before the operation handler runs. This is the only hook that can modify the incoming request. It can also short-circuit processing by calling request.succeed() or request.fail() directly -- if it does, the remaining beforeRequest hooks and the operation handler are skipped, and execution jumps straight to success() or failure().
success(request)
Called after the operation handler succeeds. Can modify the outgoing response payload and messages. Cannot modify the original request.
failure(request)
Called after the operation handler fails (either by calling request.fail() or throwing an exception). Can modify the error response. Cannot modify the original request.
Request Flow
flowchart TD
A[Incoming Request] --> B["beforeRequest()"]
B --> C{Short-circuited?}
C -->|No| D[Operation Handler]
C -->|Yes, succeeded| F
C -->|Yes, failed| G
D --> E{Result}
E -->|Succeeded| F["success()"]
E -->|Failed| G["failure()"]
F --> H[Send Response]
G --> HThe beforeRequest hook runs first. If it doesn't short-circuit, the operation handler executes. Based on the result, either success() or failure() runs. Then the response is sent. The success() and failure() hooks always run -- they cannot be skipped.
Registration Levels
Middleware can be registered at three levels, each with a different scope:
Global (Service-level)
Applies to every request the service processes, across all contexts and operations.
import { StrataService } from '@strata-js/strata';
const service = new StrataService(config);
service.useMiddleware(new AuthMiddleware());Context-level
Applies to every operation within a specific context.
import { StrataContext } from '@strata-js/strata';
const users = new StrataContext('users');
users.useMiddleware(new RateLimitMiddleware());Operation-level
Applies to a single operation only. Passed as an array in the fourth argument to registerOperation().
users.registerOperation('get', async (request) =>
{
return { user: await db.findUser(request.payload.userId) };
}, [], [ new CacheMiddleware({ ttl: 60000 }) ]);Calling Order
Middleware executes outside-in for beforeRequest() and inside-out for success() / failure():
flowchart LR
G["Global Middleware"] -->|"beforeRequest()"| C["Context Middleware"]
C -->|"beforeRequest()"| O["Operation Middleware"]
O -->|"Call Handler"| H["Operation Handler"]
H -->|"Return Result"| O2["Operation Middleware"]
O2 -->|"success() / failure()"| C2["Context Middleware"]
C2 -->|"success() / failure()"| G2["Global Middleware"]beforeRequest() order: Global -> Context -> Operation
success() / failure() order: Operation -> Context -> Global
This means global middleware wraps everything. It sees the request first and the response last -- useful for cross-cutting concerns like logging, authentication, or tracing.
Writing a Middleware
A middleware is any object that implements beforeRequest() and optionally success() and failure(). The recommended approach is a class:
import type { OperationMiddleware } from '@strata-js/strata';
import type { StrataRequest } from '@strata-js/strata';
class TimingMiddleware implements OperationMiddleware
{
#startTimes = new Map<string, number>();
async beforeRequest(request : StrataRequest) : Promise<StrataRequest>
{
this.#startTimes.set(request.id, Date.now());
return request;
}
async success(request : StrataRequest) : Promise<StrataRequest>
{
const start = this.#startTimes.get(request.id);
if(start)
{
const duration = Date.now() - start;
console.log(`[${ request.context }/${ request.operation }] completed in ${ duration }ms`);
this.#startTimes.delete(request.id);
}
return request;
}
async failure(request : StrataRequest) : Promise<StrataRequest>
{
const start = this.#startTimes.get(request.id);
if(start)
{
const duration = Date.now() - start;
console.error(`[${ request.context }/${ request.operation }] failed after ${ duration }ms`);
this.#startTimes.delete(request.id);
}
return request;
}
}Key points:
- All hooks are async. Even if the work is synchronous, the interface expects promises.
- Hooks run serially. A slow middleware delays the entire chain. Keep hooks fast.
- You must instantiate the class. Register
new TimingMiddleware(), not the class itself. - Return the request. Each hook receives a
StrataRequestand should return it (potentially modified). If you returnundefined, the original request is used.