Skip to main content

DI Middleware

DI middleware wraps container resolution. It runs at construction time, not at call time. Use it to add cross-cutting behaviour to how providers are constructed, not to how their methods are called.

import {
type Middleware,
type MiddlewareFunction,
type MiddlewareNext,
type MiddlewareResult,
createMiddleware,
composeMiddleware,
MiddlewarePipeline,
// Built-ins
RetryMiddleware,
LoggingMiddleware,
CachingMiddleware,
RateLimitMiddleware,
ValidationMiddleware,
TransactionMiddleware,
CircuitBreakerMiddleware,
} from '@omnitron-dev/titan/nexus';

DI middleware ≠ Netron middleware. This page covers wrapping the resolution of providers. For wrapping per-call RPC dispatch, see Netron Middleware.

The Middleware interface

interface Middleware<T = unknown> {
name: string;
execute: (context: MiddlewareContext<T>, next: () => T | Promise<T>) => T | Promise<T>;
priority?: number;
condition?: (context: MiddlewareContext<T>) => boolean;
onError?: (error: Error, context: MiddlewareContext<T>) => void;
}

context carries:

  • token — the token being resolved.
  • scope — current resolution scope.
  • parent — the resolution that triggered this one (depth + cycle tracking).

next() resolves the provider; call exactly once and return the value.

Built-in middleware

MiddlewarePurpose
RetryMiddlewareRetry construction if it throws
LoggingMiddlewareLog every resolution with timing
CachingMiddlewareCache resolved instances by token + context
RateLimitMiddlewareReject resolutions exceeding a threshold
ValidationMiddlewareValidate resolved instance against a schema
TransactionMiddlewareWrap resolution in a transaction (DB-aware contexts)
CircuitBreakerMiddlewareOpen a circuit after consecutive resolution failures

The built-ins are factory functions or pre-built middleware objects exported from nexus/middleware. Consult the source for the constructor signature of each (they accept knob-based options).

Applying middleware

const container = createContainer();

container.useMiddleware([
LoggingMiddleware({ level: 'debug' }),
RetryMiddleware({ /* options */ }),
CachingMiddleware({ /* options */ }),
]);

The order in the array is the outer-to-inner execution order: Logging runs first, then Retry, then Caching, then the provider. The priority field on each middleware can override the array order.

Custom middleware via createMiddleware

import { createMiddleware } from '@omnitron-dev/titan/nexus';

const InstrumentationMiddleware = createMiddleware({
name: 'instrumentation',
execute: async (ctx, next) => {
const t0 = performance.now();
try {
const instance = await next();
metrics.histogram('di.resolve.ms', { token: ctx.token.name })
.observe(performance.now() - t0);
return instance;
} catch (e) {
metrics.counter('di.resolve.errors', { token: ctx.token.name }).inc();
throw e;
}
},
});

container.useMiddleware([InstrumentationMiddleware]);

Composing middleware

composeMiddleware(middlewares) produces a single middleware that chains the inputs. Useful for grouping related middleware as a single unit.

MiddlewarePipeline is the class that drives execution; the container creates one internally, but you can construct one yourself if you need to test middleware in isolation.

Conditional execution

Middleware can opt out per resolution via condition:

const ExpensiveMiddleware: Middleware = {
name: 'expensive',
condition: (ctx) => ctx.token === MY_TOKEN,
execute: async (ctx, next) => { /* … */ return next(); },
};

condition: false means skip — next() runs directly.

Per-token middleware

Some middleware suites support per-token application. Check the specific built-in for whether it accepts a tokens filter, and the provider definition for a middleware: field. The exact surface varies and is documented in the source for each middleware.

When to write DI middleware

Custom DI middleware is rare. The built-ins cover most cases. Write your own when you need:

  • Project-specific telemetry — emit to a metrics backend the built-ins don't know about.
  • Cross-cutting validation — assert that every resolved provider matches a schema.
  • Tracing instrumentation — attach a span to every resolution.
  • Test-only middleware — capture the resolution graph for a test assertion.

Most application code does not need DI middleware at all.

Anti-patterns

  • Putting business logic in DI middleware. It runs at construction time, not per call. Method-call concerns belong in Netron middleware.
  • Side effects in middleware. Middleware should observe and decorate, not mutate the application. A middleware that registers more providers is a smell.
  • Async work that should be in onStart. Heavy setup belongs in lifecycle hooks where the framework can sequence it.

→ Next: Circular Dependencies.