typescriptintermediate

Middleware Chain Pattern

Implement the middleware/pipeline pattern for composable request processing without Express.

typescript
type Context = {
  request: { method: string; path: string; headers: Record<string, string>; body?: unknown };
  response: { status: number; body: unknown; headers: Record<string, string> };
  state: Record<string, unknown>;
};

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

class Pipeline {
  private middlewares: Middleware[] = [];

  use(mw: Middleware): this {
    this.middlewares.push(mw);
    return this;
  }

  async execute(ctx: Context): Promise<Context> {
    let index = -1;
    const dispatch = async (i: number): Promise<void> => {
      if (i <= index) throw new Error('next() called multiple times');
      index = i;
      const mw = this.middlewares[i];
      if (mw) await mw(ctx, () => dispatch(i + 1));
    };
    await dispatch(0);
    return ctx;
  }
}

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

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

// Handler
const handler: Middleware = async (ctx, next) => {
  ctx.response = {
    status: 200,
    body: { message: 'Hello', userId: ctx.state.userId },
    headers: { 'content-type': 'application/json' },
  };
  await next();
};

// Usage
async function main() {
  const app = new Pipeline().use(logger).use(auth).use(handler);

  const ctx: Context = {
    request: { method: 'GET', path: '/api/me', headers: { authorization: 'Bearer token' } },
    response: { status: 200, body: null, headers: {} },
    state: {},
  };

  const result = await app.execute(ctx);
  console.log('Response:', result.response);
}

main();

Use Cases

  • Custom HTTP framework
  • Request processing pipelines
  • Plugin architecture

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.