Skip to main content

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

Handler
Handler<vars, env>
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

context
Context<vars, env>
The middleware context object. See Context Type below.
next
() => Promise<void>
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

agent
boolean
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()
})
command
string
The resolved command path (e.g. 'deploy' or 'pr create').
middleware(async (c, next) => {
  console.log(`Executing: ${c.command}`)
  await next()
})
name
string
The CLI name.
env
InferEnv<env>
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()
  }
)
var
InferVars<vars>
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()
})
version
string | undefined
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()
})

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:
  1. CLI-level middleware — registered with cli.use()
  2. Group-level middleware — registered on mounted sub-CLIs
  3. Command-level middleware — defined in command definition
  4. 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

Build docs developers (and LLMs) love