Middleware Engine
Ordered, context-aware middleware chains with automatic error propagation.
The middleware engine is a Koa-style pipeline. Middleware functions run in the order they were added, share a single context object, and can short-circuit or handle errors downstream.
Pipeline<T>
A middleware container and invoker.
class Pipeline<T> {
constructor(...middlewares: Middleware<T>[]);
use(...mw: Middleware<T>[]): Pipeline<T>;
execute(context: T): Promise<void>;
}Prop
Type
T is the context type shared between middleware functions.
Middleware<T>
The signature every middleware function must satisfy.
type Middleware<T> = (
context: T,
next: Next<T>,
error?: Error
) => Promise<void> | void;next accepts an optional Error, so a middleware can hand a failure to the
next error-handling middleware instead of throwing:
type Next<T> = (error?: Error) => Promise<Middleware<T>> | Promise<void>;MiddlewarePipe<T>
A structural interface for anything that can act as a middleware pipe.
Prop
Type
Basic usage
import { Pipeline } from '@hyperse/pipeline';
interface Context {
[key: string]: any;
}
// Create a pipeline with initial middleware
const engine = new Pipeline<Context>((ctx, next) => {
ctx.foobar = 'baz';
await next();
});
// Add more middleware
engine.use(async (ctx, next) => {
await new Promise((res) => setTimeout(res, 2000));
ctx.another = 123;
await next();
});
// Execute the pipeline
const context: Context = {};
await engine.execute(context);
console.log(context);
// => { foobar: "baz", another: 123 }Error handling
Because next accepts an error, error-handling middleware is simply middleware
that checks its third argument. Register the handler after the middleware it
should protect, and error propagation flows in declaration order.
import { Pipeline } from '@hyperse/pipeline';
const engine = new Pipeline<Context>(async (ctx, next) => {
try {
await next();
} catch (err) {
// Convert a thrown error into a "passed" error, which skips ahead to the
// next middleware that declares an `error` parameter.
await next(err as Error);
}
});
engine.use(async (ctx, next) => {
// Any throw here propagates to the error handler above.
await readFile(ctx.path);
await next();
});
engine.use(async (ctx, next, error) => {
if (error) {
ctx.failed = true;
return;
}
await next();
});Error parameter position
The error argument is always the third parameter. A middleware that declares it opts into error handling for everything the previous middleware forwards.
Chaining
use() is chainable and returns the same instance, so you can register batches
of middleware fluently:
const engine = new Pipeline<Context>()
.use(loggingMiddleware)
.use(authMiddleware)
.use(routerMiddleware)
.use(errorMiddleware);