Skip to content

Using Middleware

Middleware lets you run code before and after every request your service handles. Authentication checks, response caching, request logging, payload validation -- these are all common middleware use cases. This guide covers how to install, register, configure, and order middleware in your services.

For how the middleware system works conceptually, see Middleware Model. For how to build your own middleware, see Writing Middleware.

Installing Middleware

Strata publishes first-party middleware as separate npm packages under the @strata-js scope:

bash
npm install @strata-js/mw-cache
npm install @strata-js/mw-message-logging
npm install @strata-js/mw-payload-validation

Each package exports a class (or factory) that you instantiate with options and then register on your service, context, or operation.

Registering Middleware

Middleware can be registered at three levels. The level determines the scope -- which requests the middleware applies to.

Global (Service-level)

Global middleware runs on every request the service processes, across all contexts and operations. Use this for cross-cutting concerns like authentication or request logging.

typescript
import { StrataService } from '@strata-js/strata';
import { MessageLoggingMiddleware } from '@strata-js/mw-message-logging';

const service = new StrataService(config);

// Runs on every request in every context
service.useMiddleware(new MessageLoggingMiddleware());

You can also register multiple middleware at once by passing an array:

typescript
service.useMiddleware([
    new AuthMiddleware(),
    new MessageLoggingMiddleware(),
]);

Context-level

Context-level middleware runs on every operation within a specific context. Use this when you need middleware that only applies to a subset of your operations.

typescript
import { StrataContext } from '@strata-js/strata';
import { PayloadValidationMiddleware } from '@strata-js/mw-payload-validation';

const users = new StrataContext('users');

// Runs on every operation in the 'users' context
users.useMiddleware(new PayloadValidationMiddleware(userSchemas));

Operation-level

Operation-level middleware runs on a single operation only. Pass it as the fourth argument to registerOperation().

typescript
import { CacheMiddleware } from '@strata-js/mw-cache';

users.registerOperation('get', async (request) =>
{
    return { user: await db.findUser(request.payload.userId) };
}, [], [ new CacheMiddleware({ ttl: 60000 }) ]);

The third argument ([]) is for operation function parameters. The fourth is the middleware array.

Configuring Middleware

Each middleware package defines its own configuration options, passed to the constructor. Consult the specific middleware documentation for details:

typescript
// Cache middleware with a 60-second TTL
const cache = new CacheMiddleware({ ttl: 60000 });

// Message logging that only logs failures
const logging = new MessageLoggingMiddleware({ logLevel: 'warn', onlyFailures: true });

Since middleware is just a class instance, you can also configure it dynamically:

typescript
const env = process.env.ENVIRONMENT ?? 'local';
const cache = new CacheMiddleware({
    ttl: env === 'production' ? 300000 : 5000,
});

Middleware Ordering

The order in which middleware is registered matters. Middleware executes in an outside-in / inside-out pattern:

beforeRequest() -- Outside-in

For the beforeRequest() hook, middleware runs in registration order, from broadest scope to narrowest:

  1. Global middleware (in registration order)
  2. Context middleware (in registration order)
  3. Operation middleware (in array order)

success() / failure() -- Inside-out

For the success() and failure() hooks, the order is reversed:

  1. Operation middleware
  2. Context middleware
  3. Global middleware

This means global middleware wraps everything -- it sees the raw request first and the final response last.

mermaid
flowchart LR
    G["Global"] -->|"beforeRequest()"| C["Context"]
    C -->|"beforeRequest()"| O["Operation"]
    O -->|"Handler"| H["Operation Handler"]
    H -->|"Result"| O2["Operation"]
    O2 -->|"success() / failure()"| C2["Context"]
    C2 -->|"success() / failure()"| G2["Global"]

Practical Example

If you register middleware like this:

typescript
// Global
service.useMiddleware(new AuthMiddleware());        // 1st global
service.useMiddleware(new LoggingMiddleware());      // 2nd global

// Context
users.useMiddleware(new ValidationMiddleware());     // 1st context

// Operation
users.registerOperation('create', handler, [], [
    new CacheMiddleware(),                           // 1st operation
]);

Then for a request to users/create:

PhaseOrder
beforeRequest()Auth -> Logging -> Validation -> Cache
success() / failure()Cache -> Validation -> Logging -> Auth

Short-circuiting

If any beforeRequest() hook calls request.succeed() or request.fail(), the remaining beforeRequest() hooks and the operation handler are skipped. Execution jumps directly to success() or failure(). This is how auth middleware rejects unauthorized requests without hitting the handler.

The success() and failure() hooks always run -- they cannot be skipped. This ensures cleanup middleware (like logging or metrics) always executes.

Common Patterns

Authentication

Register auth middleware globally so every request is checked:

typescript
service.useMiddleware(new AuthMiddleware({
    headerField: 'auth',
    validateToken: async (token) => verifyJWT(token),
}));

Caching

Register cache middleware on specific operations that benefit from it:

typescript
users.registerOperation('get', handler, [], [
    new CacheMiddleware({ ttl: 60000 }),
]);

Logging

Register logging middleware globally to capture all traffic:

typescript
service.useMiddleware(new MessageLoggingMiddleware());

Payload Validation

Register validation middleware on a context to validate all operations within it:

typescript
users.useMiddleware(new PayloadValidationMiddleware({
    create: createSchema,
    get: getSchema,
    list: listSchema,
}));

Next Steps