Building a CLI Adapter for Hono

Published: (January 2, 2026 at 08:24 PM EST)
3 min read
Source: Dev.to

Source: Dev.to

Overview

hono-cli-adapter lets you call Hono apps directly from the CLI.
Your business logic stays in Hono, so you can debug with Postman or Insomnia, ship the same code as a CLI, and avoid any stdout writes—the CLI controls all output.
Bonus: trivial MCP server support by swapping entrypoints.

Debugging CLI tools is tedious: run, tweak args, run again—no request history, no easy inspection.
With hono-cli-adapter, your CLI logic lives behind HTTP endpoints, but no actual HTTP server is needed. The library converts CLI arguments into HTTP requests and calls your Hono app’s app.fetch() directly.

Why Hono?

  • Web‑tool friendly – Use Postman/Insomnia while building. Save requests, inspect responses, iterate fast.
  • MCP ready – Swap the entrypoint and you get both local and remote MCP server support. Same logic, different transports.
  • Testable – Your Hono app is the source of truth and can be tested independently.

Installation

npm install hono-cli-adapter

Basic Usage

Create a CLI script (e.g., my-cli.js):

#!/usr/bin/env node
import { cli } from 'hono-cli-adapter';
import { app } from './app.js';

await cli(app);

Make it executable:

chmod +x my-cli.js

Running commands

# Call /hello/:name route
node my-cli.js hello Taro
# → "Hello, Taro!"

# List available routes
node my-cli.js --list

# Show help
node my-cli.js --help

CLI arguments become URL paths and query parameters, then hit app.fetch().

Design Constraints

1. Thin CLI, fat Hono

All business logic lives in Hono; the CLI only handles flags and output. This keeps behavior consistent between CLI and HTTP and makes the Hono app fully testable on its own.

2. No side effects

The library never writes to stdout. You decide how to format output:

const { code, lines } = await runCli(app, process);
for (const l of lines) console.log(l); // or JSON.stringify, pipe somewhere
process.exit(code);

3. POST‑only

CLI commands trigger actions, so they use POST by default. GET support can be added later if needed.

Environment Variable Precedence

Three layers, last wins:

  1. process.env (base)
  2. options.env (adapter config)
  3. --env flags (highest priority)
await cli(app, process, { env: { API_URL: 'https://dev.example.com' } });

node my-cli.js do-thing --env API_KEY=secret-123

Passing JSON Body

Tokens after -- become a JSON body:

node my-cli.js create-user -- name=Taro email=taro@example.com
# Sends: { "name": "Taro", "email": "taro@example.com" }

Transforming Requests per Command

await adaptAndFetch(app, process.argv.slice(2), {
  beforeFetch: {
    upload: async (req, argv) => {
      if (argv.file) {
        const buf = await fs.readFile(argv.file);
        const headers = new Headers(req.headers);
        headers.set('content-type', 'application/octet-stream');
        return new Request(req, { body: buf, headers });
      }
    }
  }
});

Enriching --help with OpenAPI

await runCli(app, process, { openapi: myOpenApiSpec });

The generated help shows parameter types, required/optional flags, and descriptions. It pairs well with hono-openapi.

Implementation Notes

  • listPostRoutes inspects Hono’s internal router structure. This may break on major Hono updates; for production, consider maintaining your own route list.
  • ESM only – No CommonJS support. Requires Node 18+.

Conclusion

Hono + CLI is a pattern that deserves more attention. You get web tooling during development, easy MCP support, and a testable core—all without duplicating logic.

Check it out on GitHub:

Back to Blog

Related posts

Read more »