Hono foundations
Choose a runtime adapter
Separate portable application code from Node, Workers, Bun, Deno, or another runtime entry point.
The Hono app is portable. The entry point is not. Keep routes and middleware in one module, and put runtime wiring in separate files.
Our bookmarks app exports the app from src/app.js:
import { Hono } from 'hono'
export const app = new Hono()
app.get('/health', (c) => c.json({ ok: true }))
On Node we listen on a port with @hono/node-server:
import { serve } from '@hono/node-server'
import { app } from './app.js'
serve({ fetch: app.fetch, port: 3000 })
On Cloudflare Workers we export fetch and pass platform bindings through env:
import { app } from './app.js'
export default {
fetch: (request, env) => app.fetch(request, env)
}
Notice what moved and what stayed. Routes never call serve() or read process.env directly. Startup, sockets, and bindings live at the edge. That split is what lets you test the app with app.request() without starting a real server.
Framework portability does not mean every npm package works everywhere. A Node-only file reader or a Worker-only KV binding still belongs outside the shared core. When a dependency imports node:fs, mark it as Node-only in your notes and keep it out of Worker bundles.
Bun and Deno follow the same pattern: import the app, call app.fetch or their local serve helper, inject env-specific deps at the boundary. The route file stays identical.
Run the same /health route under Node and Wrangler. List every file and dependency that differs between the two entry points. That list is your portability boundary, not a marketing claim.
If you copy the Node entry into a Worker and patch globals one at a time, you will debug for days. Split the files up front even when you only deploy to one runtime today.
Try this on your own project: extract src/app.js today, even if you only target one runtime for now. The refactor is cheap early and painful later.
Lesson completed