Skip to content

Framework GuidesHono

Hono

Hono server adapter using courierHandler — works on Node.js, Bun, Deno, and Cloudflare Workers.

4 min readUpdated proxy mode

The @noverax/courier-hono adapter lets you host the Courier proxy route on any Hono-supported runtime: Node.js, Bun, Deno, and Cloudflare Workers.

In one command

bash
npx @noverax/create-courier

In a Hono server it adds routes for the proxy and a page at /courier, mounted on your app, with a typed example API and its mock. See Create Courier.

Installation

bash
npm install @noverax/courier @noverax/courier-hono hono

Define your API sources

lib/sources.ts
import { courier, defineApi, defineGroup } from "@noverax/courier/define"

const { endpoint } = courier()

type User = { id: number; name: string; email: string }

const users = defineGroup({
  prefix: "users",
  // A page may call these through the proxy; an endpoint no mark opens stays the server's
  expose: true,
  endpoints: {
    getAll: endpoint.get("").response<User[]>(),
    getById: endpoint.get(":id").variables<{ id: number }>().response<User>(),
    create: endpoint.post("").variables<{ name: string; email: string }>().response<User>(),
  },
})

export const api = defineApi({
  baseURL: process.env.API_URL ?? "http://localhost:4000",
  groups: { users },
})

export const sources = { myApi: api }

Setup

Create a Hono app with the Courier handler:

src/index.ts
import { Hono } from "hono"
import { cors } from "hono/cors"
import { registerSources } from "@noverax/courier"
import { courierHandler } from "@noverax/courier-hono"
import { sources } from "./lib/sources"

registerSources(sources)

const app = new Hono()

app.use("/api/*", cors())

app.all("/api/request", courierHandler())

export default app

That is all you need. The courierHandler function returns a standard Hono handler that processes incoming Courier proxy requests and forwards them to your backend.

Runtime-specific entry points

Node.js

src/index.ts
import { serve } from "@hono/node-server"
import { Hono } from "hono"
import { cors } from "hono/cors"
import { registerSources } from "@noverax/courier"
import { courierHandler } from "@noverax/courier-hono"
import { sources } from "./lib/sources"

registerSources(sources)

const app = new Hono()

app.use("/api/*", cors())
app.all("/api/request", courierHandler())

serve({ fetch: app.fetch, port: 3001 }, (info) => {
  console.log(`Server running on http://localhost:${info.port}`)
})

Bun

src/index.ts
import { Hono } from "hono"
import { cors } from "hono/cors"
import { registerSources } from "@noverax/courier"
import { courierHandler } from "@noverax/courier-hono"
import { sources } from "./lib/sources"

registerSources(sources)

const app = new Hono()

app.use("/api/*", cors())
app.all("/api/request", courierHandler())

export default {
  port: 3001,
  fetch: app.fetch,
}

Deno

src/index.ts
import { Hono } from "hono"
import { cors } from "hono/cors"
import { registerSources } from "@noverax/courier"
import { courierHandler } from "@noverax/courier-hono"
import { sources } from "./lib/sources.ts"

registerSources(sources)

const app = new Hono()

app.use("/api/*", cors())
app.all("/api/request", courierHandler())

Deno.serve({ port: 3001 }, app.fetch)

Cloudflare Workers

src/index.ts
import { Hono } from "hono"
import { cors } from "hono/cors"
import { registerSources } from "@noverax/courier"
import { courierHandler } from "@noverax/courier-hono"
import { sources } from "./lib/sources"

registerSources(sources)

const app = new Hono()

app.use("/api/*", cors())
app.all("/api/request", courierHandler())

export default app

Same handler, any runtime

The courierHandler function uses the standard Web API Request/Response objects, so it works identically across all Hono-supported runtimes with zero configuration changes.

Adding other routes

The Courier handler only handles the proxy route. You can add any other Hono routes alongside it:

src/index.ts
import { Hono } from "hono"
import { cors } from "hono/cors"
import { registerSources } from "@noverax/courier"
import { courierHandler } from "@noverax/courier-hono"
import { sources } from "./lib/sources"

registerSources(sources)

const app = new Hono()

app.use("/api/*", cors())

// Courier proxy route
app.all("/api/request", courierHandler())

// Your own routes
app.get("/health", (c) => c.json({ status: "ok" }))

app.get("/api/stats", (c) => {
  return c.json({ uptime: process.uptime() })
})

export default app

The user's session

An API that keeps a session gets the user's session with every call a route, a service or a step it awaits makes, once the request is served with it at hand:

src/index.ts
import { requestContext } from "@noverax/courier-hono/request-context"

app.use(requestContext())

Add it before the routes that call APIs. The session cookies the API sets reach the browser on the response the route answers with, beside the route's own — on a fetch's response too. The request is kept in AsyncLocalStorage, which Node, Bun and Deno provide, and Cloudflare Workers with the nodejs_compat flag; the main entry imports none of it, so an application that keeps no session runs anywhere Hono does. The user's address is the connection's where the runtime reports one. Hono 4 is checked on @hono/node-server. See User Sessions.

Studio integration & appOrigin

Wire your Hono app into Studio with a single attachRuntime() call. In development it loads courier.config.ts from disk, registers its sources, and opens the connection to Studio; in production the app's own registerSources() call registers them. Studio's Playground sends its proxy calls to the app's origin, which in development the app learns from the server itself — the socket the first request to a Courier route arrived on — so open a page that calls your API before the first Playground call. Where Studio cannot see the server, set courier.config.ts.runtime.appOrigin or the COURIER_APP_URL env var — see How the app learns its origin.

src/index.ts
import { serve } from "@hono/node-server"
import { Hono } from "hono"
import { courierHandler } from "@noverax/courier-hono"

if (process.env.NODE_ENV !== "production") {
  const { attachRuntime } = await import("@noverax/courier-studio-core/runtime")
  await attachRuntime()
}

const app = new Hono()
app.all("/api/request", courierHandler())

serve({ fetch: app.fetch, port: 3001 })

Alternatively, set COURIER_APP_URL instead of pinning appOrigin in the config:

bash
COURIER_APP_URL=http://localhost:3001 node src/index.js

Edge runtimes

attachRuntime() only runs on Node-compatible runtimes (Bun also works). Cloudflare Workers and Deno do not currently host Studio's Socket.IO bridge — for those targets the Studio integration is a no-op and Playground proxy mode is unavailable. Use the standalone server for development against edge deployments.

No magic default

There is no localhost:3001 auto-fallback. Until the config, the environment or the server itself names the origin, a Playground call in proxy mode throws ConfigurationError("APP_ORIGIN_NOT_CONFIGURED") with a pointer to how the app learns its origin. The correct value depends on your runtime — custom port, Docker host, HTTPS, reverse proxy — and a wrong guess would silently route requests to nowhere.

Client-side setup

On the client, point your caller to this server's proxy URL. Choose the guide for your frontend:

  • React (Vite / CRA) — createProxyQueryCaller with React Query hooks
  • Vue — createProxyCaller with composables
  • Next.js — full-stack with apiRoute and createProxyQueryCaller
  • Nuxt — full-stack with courierHandler and createProxyCaller

The caller takes the routes Courier writes into courier.routes.ts from the endpoints your sources mark .expose(), never the definition itself — see Exposing Endpoints:

lib/caller.ts
import { createProxyCaller } from "@noverax/courier"
import { myApiRoutes } from "../courier.routes"

export const caller = createProxyCaller(myApiRoutes, {
  proxyPath: { http: "http://localhost:3001/api/request" },
})