typescriptintermediate

Middleware Pattern and Composition

Implement the middleware pattern for composable request processing with next() and error handling.

typescript
// Generic middleware pattern (framework-agnostic)
type Context = {
  method: string;
  path: string;
  headers: Record<string, string>;
  body?: unknown;
  state: Record<string, unknown>;
  response?: { status: number; body: unknown };
};

type Middleware = (ctx: Context, next: () => Promise<void>) => Promise<void>;

// Middleware runner (like Koa)
async function compose(middlewares: Middleware[]): (ctx: Context) => Promise<void> {
  return async (ctx: Context) => {
    let index = -1;

    async function dispatch(i: number): Promise<void> {
      if (i <= index) throw new Error('next() called multiple times');
      index = i;

      if (i >= middlewares.length) return;

      const fn = middlewares[i];
      await fn(ctx, () => dispatch(i + 1));
    }

    await dispatch(0);
  };
}

// Logger middleware
const logger: Middleware = async (ctx, next) => {
  const start = Date.now();
  console.log(`→ ${ctx.method} ${ctx.path}`);
  await next();
  const ms = Date.now() - start;
  console.log(`← ${ctx.response?.status ?? 200} (${ms}ms)`);
};

// Auth middleware
const auth: Middleware = async (ctx, next) => {
  const token = ctx.headers['authorization'];
  if (!token) {
    ctx.response = { status: 401, body: { error: 'Unauthorized' } };
    return; // don't call next
  }
  ctx.state.userId = 'user-123'; // decoded from token
  await next();
};

// Error handler middleware
const errorHandler: Middleware = async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    const message = err instanceof Error ? err.message : 'Unknown error';
    ctx.response = { status: 500, body: { error: message } };
    console.error('Error:', message);
  }
};

// Response time header
const timing: Middleware = async (ctx, next) => {
  const start = process.hrtime.bigint();
  await next();
  const ns = process.hrtime.bigint() - start;
  ctx.headers['x-response-time'] = `${Number(ns / 1_000_000n)}ms`;
};

// Route handler
const handler: Middleware = async (ctx, _next) => {
  ctx.response = {
    status: 200,
    body: { message: 'Hello!', user: ctx.state.userId },
  };
};

// Compose and run
const app = await compose([
  errorHandler,
  logger,
  timing,
  auth,
  handler,
]);

const ctx: Context = {
  method: 'GET',
  path: '/api/profile',
  headers: { authorization: 'Bearer token123' },
  state: {},
};

await app(ctx);
console.log('Response:', ctx.response);

Use Cases

  • Request processing pipelines
  • Express/Koa-style middleware
  • Cross-cutting concerns (logging, auth)

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.