Skip to content

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

mermaid
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 --> H

The 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.

typescript
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.

typescript
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().

typescript
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():

mermaid
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:

typescript
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 StrataRequest and should return it (potentially modified). If you return undefined, the original request is used.