Skip to content

Middleware

Middleware wraps downstream execution and can add typed context to the command handler chain. Use derive() when you need typed resolved flags or args before the action. Use middleware when you need next() around the rest of the pipeline.

Defining Middleware

ts
import { middleware } from '@kjanat/dreamcli';

const timing = middleware<{ startTime: number }>(
  async ({ next }) => {
    const startTime = Date.now();
    await next({ startTime });
  },
);

const trace = middleware<{ traceId: string }>(
  async ({ next }) =>
    next({ traceId: crypto.randomUUID() }),
);

The generic parameter declares the context shape this middleware provides. The next() call passes context downstream and continues the chain.

Stacking Middleware

ts
import { command, middleware } from '@kjanat/dreamcli';

const timing = middleware<{ startTime: number }>(
  async ({ next }) => next({ startTime: Date.now() }),
);
const trace = middleware<{ traceId: string }>(
  async ({ next }) =>
    next({ traceId: crypto.randomUUID() }),
);

command('deploy')
  .middleware(timing)
  .middleware(trace)
  .action(({ ctx }) => {
    const { traceId, startTime } = ctx;
    console.log(
      `[${traceId}] deployed in ${Date.now() - startTime}ms`,
    );
  });

Context types intersect: { startTime: number } & { traceId: string }. Each middleware only needs to know about its own context shape.

Middleware Parameters

The middleware handler receives:

ts
import { middleware } from '@kjanat/dreamcli';

middleware(
  async ({ flags, args, ctx, out, meta, next }) => {
    // flags — resolved flag values (type-erased)
    // args  — resolved argument values (type-erased)
    // ctx   — accumulated middleware context (type-erased)
    // out   — output channel
    // meta  — CLI metadata { name, bin, version, command }
    // next  — continue chain, passing context
    return next({});
  },
);

If you need typed command-scoped access to resolved inputs, prefer command(...).derive(...).

Error Handling

Middleware can catch and transform errors:

ts
import { CLIError, middleware } from '@kjanat/dreamcli';

const errorBoundary = middleware(async ({ next, out }) => {
  try {
    return await next({});
  } catch (err) {
    if (err instanceof CLIError) {
      out.error(err.message);
    }
    throw err;
  }
});

What's Next?

Released under the MIT License.