> ## 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.

# Mounting APIs as CLIs

> Mount HTTP APIs directly as CLI commands using the fetch property

incur can mount any HTTP API that implements the [Web Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/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

<Steps>
  ### Create an API with Fetch Handler

  Any framework that exposes a `fetch` handler works:

  ```ts theme={null}
  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))
  ```

  ### Mount with fetch Property

  Pass the `fetch` handler to `Cli.create()`:

  ```ts theme={null}
  import { Cli } from 'incur'

  Cli.create('my-cli', {
    description: 'My CLI',
    fetch: app.fetch,  // Mount the entire API
  }).serve()
  ```

  ### Use curl-style Syntax

  Arguments become path segments, flags become options:

  ```bash theme={null}
  my-cli api users
  # → users[1]{id,name}:
  # →   1,Alice

  my-cli api users -X POST -d '{"name":"Bob"}'
  # → created: true
  # → name: Bob
  ```
</Steps>

## curl-Style Translation

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

### Path Segments

Bare arguments become path segments:

```bash theme={null}
my-cli api users 42
# → GET /users/42
```

### HTTP Methods

Use `-X` or `--method` to set the HTTP method:

```bash theme={null}
my-cli api users -X POST
# → POST /users

my-cli api users 42 --method DELETE
# → DELETE /users/42
```

<Note>
  Default method is `GET`, or `POST` if a body is provided via `-d` or `--data`.
</Note>

### Request Body

Use `-d`, `--data`, or `--body` to send a request body:

```bash theme={null}
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:

```bash theme={null}
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:

```bash theme={null}
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

```ts theme={null}
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()
```

```bash theme={null}
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

```ts theme={null}
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()
```

```bash theme={null}
bun-cli api ping
# → pong: true
```

### Deno

```ts theme={null}
import { Cli } from 'incur'

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

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

### Elysia

```ts theme={null}
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:

```ts theme={null}
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()
```

```bash theme={null}
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:

```ts theme={null}
Cli.create('my-cli', {})
  .command('api', {
    description: 'API commands',
    fetch: app.fetch,
    basePath: '/v1',  // CLI paths will be prefixed with /v1
  })
  .serve()
```

```bash theme={null}
my-cli api users
# → GET /v1/users (not /users)
```

## OpenAPI Integration

Pass an OpenAPI spec alongside `fetch` to generate typed subcommands:

```ts theme={null}
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

```bash theme={null}
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:

```bash theme={null}
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
```

<Note>
  OpenAPI integration uses `@readme/openapi-parser` to resolve all `$ref` pointers automatically. The spec can be passed as a plain object or imported JSON.
</Note>

### Example OpenAPI App

<CodeGroup>
  ```ts app.ts theme={null}
  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' },
  })
  ```

  ```ts cli.ts theme={null}
  import { Cli } from 'incur'
  import { app, spec } from './app.js'

  Cli.create('my-cli', { description: 'My CLI' })
    .command('api', { fetch: app.fetch, openapi: spec })
    .serve()
  ```
</CodeGroup>

### Usage

```bash theme={null}
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:

```bash theme={null}
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:

```ts theme={null}
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()
```

```bash theme={null}
logs api logs
# → line: Log 1
# → line: Log 2
```

<Note>
  Streaming only works for responses with `content-type: application/x-ndjson`. Each line is parsed as JSON and output incrementally.
</Note>

## Error Handling

HTTP error responses are automatically converted to CLI errors:

```ts theme={null}
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()
```

```bash theme={null}
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:

```ts theme={null}
// 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:

```ts theme={null}
// 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:

```ts theme={null}
// Good: clean CLI paths
Cli.create('api', { fetch: app.fetch, basePath: '/api/v1' })

// Avoid: users type full paths
Cli.create('api', { fetch: app.fetch })
```

<Warning>
  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.
</Warning>
