middleware()
Creates a strictly typed middleware handler. Pass the vars and env schemas as generics for full type inference.
Signature
function middleware<
const vars extends z.ZodObject<any> | undefined = undefined,
const env extends z.ZodObject<any> | undefined = undefined,
>(handler: Handler<vars, env>): Handler<vars, env>
Parameters
handler
Handler<vars, env>
required
The middleware handler function that receives context and next function.
Return Type
Returns the same handler with proper type inference. This is an identity function that exists purely for type safety.
Examples
Basic Middleware
import { middleware } from 'incur'
const logger = middleware(async (c, next) => {
console.log(`[${c.command}] Starting...`)
await next()
console.log(`[${c.command}] Done`)
})
cli.use(logger)
Typed Middleware
Use type parameters for full type inference with vars and env:
import { Cli, middleware, z } from 'incur'
const cli = Cli.create('my-cli', {
vars: z.object({
userId: z.string().default('anonymous'),
requestId: z.string().default('default-id')
}),
env: z.object({
API_TOKEN: z.string()
})
})
// Pass typeof to get full type inference
const auth = middleware<typeof cli.vars, typeof cli.env>(
async (c, next) => {
// c.env.API_TOKEN is typed as string
// c.set() and c.var are fully typed
c.set('userId', 'user-123')
c.set('requestId', crypto.randomUUID())
await next()
}
)
cli.use(auth)
Handler Type
The middleware handler function type.
Type Signature
type Handler<
vars extends z.ZodObject<any> | undefined = undefined,
env extends z.ZodObject<any> | undefined = undefined,
> = (
context: Context<vars, env>,
next: () => Promise<void>
) => Promise<void> | void
Parameters
Function to call the next middleware in the chain or the command handler. Calling next() is optional - omitting it short-circuits the chain.
Context Type
The context object passed to middleware handlers. Contains CLI metadata, environment variables, and variable management.
Properties
Whether the consumer is an agent (stdout is not a TTY).middleware(async (c, next) => {
if (!c.agent) {
console.log('Running for human user')
}
await next()
})
The resolved command path (e.g. 'deploy' or 'pr create').middleware(async (c, next) => {
console.log(`Executing: ${c.command}`)
await next()
})
Parsed environment variables from the CLI-level env schema. Fully typed based on the schema.const cli = Cli.create('my-cli', {
env: z.object({
API_TOKEN: z.string(),
API_URL: z.string().default('https://api.example.com')
})
})
const auth = middleware<typeof cli.vars, typeof cli.env>(
async (c, next) => {
// c.env.API_TOKEN is string
// c.env.API_URL is string with default
const client = new ApiClient(c.env.API_TOKEN, c.env.API_URL)
await next()
}
)
Variables set by upstream middleware. Read-only. Fully typed based on the vars schema.middleware(async (c, next) => {
console.log(`User: ${c.var.userId}`)
await next()
})
The CLI version string, if provided.
set
<key extends keyof InferVars<vars>>(key: key, value: InferVars<vars>[key]) => void
Set a typed variable for downstream middleware and command handlers. Variables must be declared in the CLI’s vars schema.const cli = Cli.create('my-cli', {
vars: z.object({
userId: z.string().default('anonymous'),
timestamp: z.number().default(0)
})
})
cli.use(middleware<typeof cli.vars>(async (c, next) => {
c.set('userId', 'user-123') // Typed!
c.set('timestamp', Date.now()) // Typed!
await next()
}))
error
(options: ErrorOptions) => never
Return an error result, short-circuiting the middleware chain. The command handler will not run.middleware(async (c, next) => {
if (!isAuthenticated) {
return c.error({
code: 'NOT_AUTHENTICATED',
message: 'Please login first',
retryable: false,
cta: {
description: 'Run:',
commands: ['my-cli login']
}
})
}
await next()
})
Error code (e.g. 'NOT_AUTHENTICATED', 'RATE_LIMIT').
Human-readable error message.
Whether the operation can be retried. Helps agents decide if they should retry.
Call-to-action suggestions for the user.
Human-readable label. Defaults to "Suggested commands:".
commands
Array<string | { command: string; description?: string }>
Commands to suggest to the user.
next() Function
The next() function continues the middleware chain or invokes the command handler if this is the last middleware.
Behavior
- Call
await next() to continue execution
- Omit
await next() to short-circuit (useful for auth checks, caching, etc.)
- Code before
next() runs before the command
- Code after
next() runs after the command (cleanup, logging, etc.)
Examples
Before and After Hooks
middleware(async (c, next) => {
const start = Date.now()
console.log(`Starting ${c.command}`)
await next() // Command runs here
const duration = Date.now() - start
console.log(`Finished ${c.command} in ${duration}ms`)
})
Short-Circuit on Error
middleware(async (c, next) => {
if (!isValid(c.env.API_TOKEN)) {
// Don't call next() - stop here
return c.error({
code: 'INVALID_TOKEN',
message: 'API token is invalid'
})
}
await next()
})
Middleware Patterns
Authentication
Check authentication before running commands:
import { Cli, middleware, z } from 'incur'
const cli = Cli.create('api', {
vars: z.object({
userId: z.string().default('anonymous')
}),
env: z.object({
API_TOKEN: z.string()
})
})
const auth = middleware<typeof cli.vars, typeof cli.env>(
async (c, next) => {
// Validate token
const userId = await validateToken(c.env.API_TOKEN)
if (!userId) {
return c.error({
code: 'INVALID_TOKEN',
message: 'Authentication failed',
cta: {
description: 'Get a token:',
commands: ['api login']
}
})
}
c.set('userId', userId)
await next()
}
)
cli.use(auth)
Request Logging
Log all commands with timing:
const logger = middleware(async (c, next) => {
const start = Date.now()
console.log(`→ ${c.command}`)
try {
await next()
console.log(`✓ ${c.command} (${Date.now() - start}ms)`)
} catch (error) {
console.log(`✗ ${c.command} (${Date.now() - start}ms)`)
throw error
}
})
cli.use(logger)
Request ID Tracking
Add a unique ID to each request:
import { randomUUID } from 'crypto'
const cli = Cli.create('my-cli', {
vars: z.object({
requestId: z.string().default('default-id')
})
})
const tracking = middleware<typeof cli.vars>(async (c, next) => {
c.set('requestId', randomUUID())
await next()
})
cli.use(tracking)
// Access in commands:
cli.command('deploy', {
run(c) {
console.log(`Request ID: ${c.var.requestId}`)
return { requestId: c.var.requestId }
}
})
Rate Limiting
Implement rate limiting:
const rateLimiter = middleware(async (c, next) => {
const remaining = await checkRateLimit(c.var.userId)
if (remaining <= 0) {
return c.error({
code: 'RATE_LIMIT_EXCEEDED',
message: 'Too many requests. Please try again later.',
retryable: true
})
}
await next()
})
cli.use(rateLimiter)
Scoped Middleware
Middleware on command groups only runs for that group’s commands:
const cli = Cli.create('my-cli')
// Root middleware - runs for all commands
cli.use(async (c, next) => {
console.log('Root middleware')
await next()
})
// Create a group with its own middleware
const admin = Cli.create('admin', {
description: 'Admin commands'
})
// This middleware only runs for admin commands
admin.use(async (c, next) => {
console.log('Admin middleware')
// Check if user is admin
if (!c.var.isAdmin) {
return c.error({
code: 'FORBIDDEN',
message: 'Admin access required'
})
}
await next()
})
admin.command('reset', {
run() {
return { reset: true }
}
})
cli.command(admin)
// When running "my-cli admin reset":
// 1. Root middleware runs
// 2. Admin middleware runs
// 3. Command runs
Conditional Middleware
Skip middleware based on conditions:
const cache = middleware(async (c, next) => {
// Skip caching for agents
if (c.agent) {
await next()
return
}
// Check cache for humans
const cached = getFromCache(c.command)
if (cached) {
console.log('Using cached result')
return // Don't call next()
}
await next()
})
cli.use(cache)
Error Recovery
Handle errors in middleware:
const errorHandler = middleware(async (c, next) => {
try {
await next()
} catch (error) {
// Log error details
console.error(`Error in ${c.command}:`, error)
// Re-throw to let the CLI handle it
throw error
}
})
cli.use(errorHandler)
Middleware Order
Middleware runs in the order it’s registered:
cli
.use(async (c, next) => {
console.log('1: before')
await next()
console.log('1: after')
})
.use(async (c, next) => {
console.log('2: before')
await next()
console.log('2: after')
})
// Output:
// 1: before
// 2: before
// [command runs]
// 2: after
// 1: after
Middleware Chain Levels
Middleware runs in this order:
- CLI-level middleware — registered with
cli.use()
- Group-level middleware — registered on mounted sub-CLIs
- Command-level middleware — defined in command definition
- Command handler — the
run() function
const cli = Cli.create('my-cli')
.use(async (c, next) => {
console.log('CLI middleware')
await next()
})
const admin = Cli.create('admin')
.use(async (c, next) => {
console.log('Group middleware')
await next()
})
.command('reset', {
middleware: [
async (c, next) => {
console.log('Command middleware')
await next()
}
],
run() {
console.log('Command handler')
return {}
}
})
cli.command(admin)
// Running "my-cli admin reset":
// CLI middleware
// Group middleware
// Command middleware
// Command handler