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:
- Initial args:
{ name: string } - After
timingMiddleware:{ name: string, startTime: number, requestId: string } - 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
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