Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/wevm/incur/llms.txt

Use this file to discover all available pages before exploring further.

incur can mount any HTTP API that implements the Web Fetch API as a CLI. This works seamlessly with frameworks like Hono, Bun, Deno, and Elysia.

When to Mount APIs

Mount APIs as CLIs when you:
  • Want to expose HTTP endpoints via CLI without writing separate handlers
  • Need a unified interface for both web and CLI access
  • Have an existing API and want instant CLI support
  • Want to test API endpoints directly from the command line

Basic API Mounting

1
Create an API with Fetch Handler
2
Any framework that exposes a fetch handler works:
3
import { Hono } from 'hono'

const app = new Hono()
  .get('/users', (c) => c.json({ users: [{ id: 1, name: 'Alice' }] }))
  .post('/users', async (c) => c.json({ created: true, ...(await c.req.json()) }, 201))
4
Mount with fetch Property
5
Pass the fetch handler to Cli.create():
6
import { Cli } from 'incur'

Cli.create('my-cli', {
  description: 'My CLI',
  fetch: app.fetch,  // Mount the entire API
}).serve()
7
Use curl-style Syntax
8
Arguments become path segments, flags become options:
9
my-cli api users
# → users[1]{id,name}:
# →   1,Alice

my-cli api users -X POST -d '{"name":"Bob"}'
# → created: true
# → name: Bob

curl-Style Translation

incur translates CLI arguments to HTTP requests using curl-style conventions:

Path Segments

Bare arguments become path segments:
my-cli api users 42
# → GET /users/42

HTTP Methods

Use -X or --method to set the HTTP method:
my-cli api users -X POST
# → POST /users

my-cli api users 42 --method DELETE
# → DELETE /users/42
Default method is GET, or POST if a body is provided via -d or --data.

Request Body

Use -d, --data, or --body to send a request body:
my-cli api users -d '{"name":"Charlie"}'
# → POST /users
# → Body: {"name":"Charlie"}

my-cli api users --data '{"id":1,"name":"Alice"}' -X PUT
# → PUT /users
# → Body: {"id":1,"name":"Alice"}

Headers

Use -H or --header to set headers:
my-cli api users -H "Authorization: Bearer token123"
# → GET /users
# → Header: Authorization: Bearer token123

my-cli api admin -H "X-Api-Key: secret" -H "X-Request-Id: abc"
# → Multiple headers

Query Parameters

Any unknown --flag becomes a query parameter:
my-cli api users --limit 5 --sort name
# → GET /users?limit=5&sort=name

my-cli api search --q hello --page 2
# → GET /search?q=hello&page=2

Framework Examples

Hono

import { Cli } from 'incur'
import { Hono } from 'hono'

const app = new Hono()
  .get('/users', (c) => c.json({ users: [{ id: 1, name: 'Alice' }] }))
  .get('/users/:id', (c) => c.json({ id: Number(c.req.param('id')), name: 'Alice' }))
  .post('/users', async (c) => c.json({ created: true, ...(await c.req.json()) }, 201))

Cli.create('api', {
  description: 'API CLI',
  fetch: app.fetch,
}).serve()
api api users
# → users[1]{id,name}:
# →   1,Alice

api api users 42
# → id: 42
# → name: Alice

api api users -X POST -d '{"name":"Bob"}'
# → created: true
# → name: Bob

Bun

import { Cli } from 'incur'

const bunApp = {
  fetch(req: Request) {
    const url = new URL(req.url)
    if (url.pathname === '/ping') {
      return Response.json({ pong: true })
    }
    return new Response('Not found', { status: 404 })
  },
}

Cli.create('bun-cli', {
  description: 'Bun CLI',
  fetch: bunApp.fetch,
}).serve()
bun-cli api ping
# → pong: true

Deno

import { Cli } from 'incur'

const denoApp = {
  fetch(req: Request) {
    return Response.json({ hello: 'deno' })
  },
}

Cli.create('deno-cli', {
  fetch: denoApp.fetch,
}).serve()

Elysia

import { Cli } from 'incur'
import { Elysia } from 'elysia'

const app = new Elysia()
  .get('/health', () => ({ status: 'ok' }))
  .post('/users', ({ body }) => ({ created: true, ...body }))

Cli.create('elysia-cli', {
  fetch: app.fetch,
}).serve()

Mounting as Commands

Mount APIs on specific commands instead of the root:
import { Cli } from 'incur'
import { Hono } from 'hono'

const app = new Hono()
  .get('/users', (c) => c.json({ users: [{ id: 1, name: 'Alice' }] }))
  .post('/users', async (c) => c.json({ created: true, ...(await c.req.json()) }, 201))

Cli.create('my-cli', { description: 'My CLI' })
  .command('users', { fetch: app.fetch })  // Mount on 'users' command
  .serve()
my-cli users users
# → users[1]{id,name}:
# →   1,Alice

my-cli users users -X POST -d '{"name":"Bob"}'
# → created: true
# → name: Bob

Base Path

Specify a base path to strip from CLI arguments:
Cli.create('my-cli', {})
  .command('api', {
    description: 'API commands',
    fetch: app.fetch,
    basePath: '/v1',  // CLI paths will be prefixed with /v1
  })
  .serve()
my-cli api users
# → GET /v1/users (not /users)

OpenAPI Integration

Pass an OpenAPI spec alongside fetch to generate typed subcommands:
import { Cli } from 'incur'
import { app, spec } from './my-hono-openapi-app.js'

Cli.create('my-cli', { description: 'My CLI' })
  .command('api', {
    fetch: app.fetch,
    openapi: spec,  // Generate commands from spec
  })
  .serve()

Generated Commands

incur extracts:
  • Command names from operationId
  • Descriptions from summary or description
  • Arguments from path parameters
  • Options from query parameters and request body properties
my-cli api --help
# Commands:
#   listUsers    List users
#   createUser   Create a user
#   getUser      Get a user by ID

Typed Subcommands

Each operation becomes a typed command:
my-cli api listUsers --limit 5
# → users: ...

my-cli api getUser 42
# → id: 42
# → name: Alice

my-cli api createUser --name Bob
# → created: true
# → name: Bob
OpenAPI integration uses @readme/openapi-parser to resolve all $ref pointers automatically. The spec can be passed as a plain object or imported JSON.

Example OpenAPI App

import { createRoute, OpenAPIHono, z } from '@hono/zod-openapi'

const app = new OpenAPIHono()

app.openapi(
  createRoute({
    method: 'get',
    path: '/users',
    operationId: 'listUsers',
    summary: 'List users',
    request: {
      query: z.object({
        limit: z.coerce.number().default(10).openapi({ description: 'Number of users' }),
      }),
    },
    responses: {
      200: {
        description: 'List of users',
        content: {
          'application/json': {
            schema: z.object({
              users: z.array(z.object({ id: z.number(), name: z.string() })),
            }),
          },
        },
      },
    },
  }),
  (c) => {
    const limit = c.req.query('limit')
    return c.json({ users: [{ id: 1, name: 'Alice' }] })
  },
)

app.openapi(
  createRoute({
    method: 'get',
    path: '/users/{id}',
    operationId: 'getUser',
    summary: 'Get a user by ID',
    request: {
      params: z.object({
        id: z.coerce.number().openapi({ description: 'User ID' }),
      }),
    },
    responses: {
      200: {
        description: 'User details',
        content: {
          'application/json': {
            schema: z.object({ id: z.number(), name: z.string() }),
          },
        },
      },
    },
  }),
  (c) => {
    const id = Number(c.req.param('id'))
    return c.json({ id, name: 'Alice' })
  },
)

export { app }
export const spec = app.getOpenAPIDocument({
  openapi: '3.1.0',
  info: { title: 'My API', version: '1.0.0' },
})

Usage

my-cli api listUsers --limit 5
# → users[1]{id,name}:
# →   1,Alice

my-cli api getUser 42
# → id: 42
# → name: Alice

Fetch Handler Help

Fetch handlers have specialized help text:
my-cli api --help
# my-cli api
#
# Usage: my-cli api <path> [options]
#
# Arguments:
#   path  API path (e.g., users, users/42)
#
# Options:
#   -X, --method <method>   HTTP method (GET, POST, PUT, DELETE, etc.)
#   -d, --data <json>       Request body (implies POST)
#   -H, --header <header>   Request header (format: "Name: value")
#   --<key> <value>         Query parameters
#
# Examples:
#   my-cli api users
#   my-cli api users/42
#   my-cli api users -X POST -d '{"name":"Alice"}'
#   my-cli api users --limit 10 --sort name

Streaming Responses

APIs that return NDJSON streams are automatically handled:
const app = new Hono()
  .get('/logs', (c) => {
    const stream = new ReadableStream({
      start(controller) {
        controller.enqueue(new TextEncoder().encode(JSON.stringify({ line: 'Log 1' }) + '\n'))
        controller.enqueue(new TextEncoder().encode(JSON.stringify({ line: 'Log 2' }) + '\n'))
        controller.close()
      },
    })
    return new Response(stream, {
      headers: { 'content-type': 'application/x-ndjson' },
    })
  })

Cli.create('logs', { fetch: app.fetch }).serve()
logs api logs
# → line: Log 1
# → line: Log 2
Streaming only works for responses with content-type: application/x-ndjson. Each line is parsed as JSON and output incrementally.

Error Handling

HTTP error responses are automatically converted to CLI errors:
const app = new Hono()
  .get('/users/:id', (c) => {
    const id = Number(c.req.param('id'))
    if (id > 100) {
      return c.json({ message: 'User not found' }, 404)
    }
    return c.json({ id, name: 'Alice' })
  })

Cli.create('api', { fetch: app.fetch }).serve()
api api users 999
# Error (HTTP_404): User not found

Best Practices

Use OpenAPI for Complex APIs

For APIs with many endpoints, use OpenAPI to get typed commands automatically:
// Good: typed commands from spec
Cli.create('api', { fetch: app.fetch, openapi: spec })

// Avoid: manual curl-style for complex APIs
Cli.create('api', { fetch: app.fetch })

Mount on Subcommands

Keep the root clean by mounting APIs on subcommands:
// Good: clear namespace
Cli.create('my-cli', {})
  .command('api', { fetch: app.fetch })
  .command('db', { fetch: dbApp.fetch })

// Avoid: API at root
Cli.create('my-cli', { fetch: app.fetch })

Provide Base Paths

Use basePath to simplify CLI arguments:
// Good: clean CLI paths
Cli.create('api', { fetch: app.fetch, basePath: '/api/v1' })

// Avoid: users type full paths
Cli.create('api', { fetch: app.fetch })
Mounted APIs receive all remaining CLI arguments as path segments and flags. Don’t mix fetch handlers with manual command definitions in the same CLI level.