Building a CLI Adapter for Hono
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:
process.env(base)options.env(adapter config)--envflags (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
listPostRoutesinspects 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: