Middleware Composition

Demonstrates how to compose middleware from separate modules. Each middleware adds properties to the args object, and TypeScript correctly infers the accumulated type in the final handler.

Type Definitions

Define the context types that middleware will add to args. These types flow through the middleware chain and are available in the handler with full type safety.

1/**
2 * Represents an authenticated user in the system.
3 */
4export interface User {
5  id: string;
6  name: string;
7  role: 'admin' | 'user';
8}
9
10/**
11 * Context added by the auth middleware.
12 */
13export interface AuthContext {
14  user: User;
15  authenticated: boolean;
16}
17
18/**
19 * Context added by the timing middleware.
20 */
21export interface TimingContext {
22  startTime: number;
23  requestId: string;
24}

Timing Middleware

The timing middleware adds request tracking context. It runs first in our chain, establishing a start time that the handler can use to measure execution duration.

1import type { TimingContext } from '../types';
2
3/**
4 * Generates a simple request ID for tracking.
5 */
6function generateRequestId(): string {
7  return `req-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
8}
9
10/**
11 * Timing middleware that adds request timing context.
12 *
13 * The startTime can be used by handlers to calculate execution duration.
14 * The requestId provides a correlation ID for logging.
15 */
16export function timingMiddleware<T>(args: T): T & TimingContext {
17  return {
18    ...args,
19    startTime: Date.now(),
20    requestId: generateRequestId(),
21  };
22}
23
24/**
25 * Helper to calculate elapsed time from a start timestamp.
26 */
27export function getElapsedMs(startTime: number): number {
28  return Date.now() - startTime;
29}

Authentication Middleware

The auth middleware adds user context. Because it runs after the timing middleware, it receives args that already include startTime and requestId.

1import type { AuthContext, User } from '../types';
2
3/**
4 * Simulates looking up a user. In a real application, this might
5 * query a database or validate a token.
6 */
7function lookupUser(): User {
8  return {
9    id: 'user-123',
10    name: 'Jane Developer',
11    role: 'admin',
12  };
13}
14
15/**
16 * Authentication middleware that adds user context to the args.
17 *
18 * This middleware demonstrates how to add typed properties that
19 * downstream middleware and handlers can rely on.
20 */
21export function authMiddleware<T>(args: T): T & AuthContext {
22  const user = lookupUser();
23
24  return {
25    ...args,
26    user,
27    authenticated: true,
28  };
29}

Composing the CLI

The main CLI file imports middleware from separate modules and applies them in order. TypeScript correctly infers the accumulated type at each step of the chain.

1import { cli } from 'cli-forge';
2
3import { authMiddleware } from './middleware/auth';
4import { timingMiddleware, getElapsedMs } from './middleware/timing';
5
6const app = cli('middleware-demo')
7  .command('greet', {
8    builder: (cmd) =>
9      cmd
10        .option('name', {
11          type: 'string',
12          description: 'Name to greet',
13          required: true,
14        })
15        // Middleware is applied in order. Each one adds to the args type.
16        .middleware(timingMiddleware)
17        .middleware(authMiddleware),
18
19    handler: (args) => {
20      // TypeScript knows args has: name, startTime, requestId, user, authenticated
21      console.log(`[${args.requestId}] Hello, ${args.name}!`);
22      console.log(`  Authenticated as: ${args.user.name} (${args.user.role})`);
23      console.log(`  authenticated: ${args.authenticated}`);
24      console.log(`  Request completed in ${getElapsedMs(args.startTime)}ms`);
25    },
26  });
27
28export default app;
29
30if (require.main === module) {
31  app.forge();
32}

How Types Flow

When middleware is applied via .middleware(), the return type extends the args:

  1. Initial args: { name: string }
  2. After timingMiddleware: { name: string, startTime: number, requestId: string }
  3. After authMiddleware: { name: string, startTime: number, requestId: string, user: User, authenticated: boolean }

The handler receives the fully composed type, with autocomplete and type checking for all properties.


All Example Files

FILE EXPLORER
cli.ts
1import { cli } from 'cli-forge';
2
3import { authMiddleware } from './middleware/auth';
4import { timingMiddleware, getElapsedMs } from './middleware/timing';
5
6const app = cli('middleware-demo')
7  .command('greet', {
8    builder: (cmd) =>
9      cmd
10        .option('name', {
11          type: 'string',
12          description: 'Name to greet',
13          required: true,
14        })
15        // Middleware is applied in order. Each one adds to the args type.
16        .middleware(timingMiddleware)
17        .middleware(authMiddleware),
18
19    handler: (args) => {
20      // TypeScript knows args has: name, startTime, requestId, user, authenticated
21      console.log(`[${args.requestId}] Hello, ${args.name}!`);
22      console.log(`  Authenticated as: ${args.user.name} (${args.user.role})`);
23      console.log(`  authenticated: ${args.authenticated}`);
24      console.log(`  Request completed in ${getElapsedMs(args.startTime)}ms`);
25    },
26  });
27
28export default app;
29
30if (require.main === module) {
31  app.forge();
32}
33