Skip to content

Framework GuidesHono

Hono

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

4 min readproxy 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",
  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. The appOrigin field for Playground proxy SSE/WS resolution lives in courier.config.ts.runtime.appOrigin (or set the COURIER_APP_URL env var to override per-environment).

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. If appOrigin is missing, clicking "Subscribe" on an SSE/WS endpoint in Studio Playground proxy mode throws ConfigurationError("APP_ORIGIN_NOT_CONFIGURED") with a pointer back to this section. 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
lib/caller.ts
import { createProxyCaller } from "@noverax/courier"
import type { api } from "../shared/sources"

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