Deploy and operate Hono
Model runtime bindings
Type and inject databases, secrets, caches, and environment configuration according to the target runtime.
Bindings are capabilities the deployment environment gives you: D1, KV, secrets, connection strings. Type them, inject them, and never read undeclared globals inside handlers.
The Worker receives a database binding through env. The Node adapter injects a repository built from process configuration:
// src/app.js
export function createApp(deps) {
const app = new Hono()
app.use('*', async (c, next) => {
c.set('bookmarks', deps.bookmarks)
await next()
})
app.get('/bookmarks', async (c) => {
const store = c.get('bookmarks')
return c.json(await store.list())
})
return app
}
Worker entry:
import { createApp } from './app.js'
import { D1BookmarkStore } from './d1-store.js'
export default {
fetch(request, env) {
const app = createApp({ bookmarks: new D1BookmarkStore(env.DB) })
return app.fetch(request, env)
}
}
Node entry:
import { serve } from '@hono/node-server'
import { createApp } from './app.js'
import { PostgresBookmarkStore } from './postgres-store.js'
const app = createApp({
bookmarks: new PostgresBookmarkStore(process.env.DATABASE_URL)
})
serve({ fetch: app.fetch, port: 3000 })
On Node, curl works when Postgres is wired:
curl -s http://localhost:3000/bookmarks
You get [] or a JSON array. A realistic failure: a handler reads c.env.DB copied from a Worker tutorial. On Node, c.env is undefined, curl returns 500, and the log says Cannot read properties of undefined (reading 'DB'). Fix by injecting the store through createApp(deps) and deleting direct c.env reads from shared route code.
Define a Worker env contract in TypeScript:
type Env = { DB: D1Database; SESSION_SECRET: string }
Vitest passes { bookmarks: new MemoryBookmarkStore() } into createApp. No Wrangler or Postgres required. Output looks like:
✓ GET /bookmarks returns empty list
Validate required config at Node startup. Missing DATABASE_URL should throw before you accept traffic.
The same pattern works for KV caches and session secrets. Wrap each binding behind an interface your tests can fake. Production entry files choose the real implementation; route files stay identical.
When you add a Worker-only binding, update wrangler.jsonc and the Env type together. Wrangler dev without the binding reproduces the same 500 curl sees in production.
Document which deps are injected in the README so the next developer does not reach for c.env inside shared handlers.
MemoryBookmarkStore in Vitest should behave like production stores: same method names, same error types on failure.
If a handler imports process.env directly, the Worker build may still bundle but fail at runtime. Grep shared files for process and c.env before deploy.
Try this on your own project: thread one dependency through createApp() and delete any direct env.DB read from route files.
Lesson completed