Keel's HTTP layer is Hono — an ultrafast, web-standard router that runs on Node, Cloudflare Workers, Deno, Bun, and more. Keel adds the container, providers, routing sugar, and helpers on top; everything Hono can do is available to you underneath.
Keel's convenience helpers (json(), param(), request, response, view())
are thin wrappers over Hono's context. You never have to use them — you can
always take the context (c) directly and use the full Hono API.
What Hono provides, what Keel adds
The division of labor is worth holding in your head, because it tells you which docs to reach for:
| Concern | Owned by |
|---|---|
The fetch handler, request matching, method routing |
Hono |
Context — c.req, c.json, c.html, cookies, headers |
Hono |
JSX rendering (hono/jsx), streaming, SSE, WebSockets |
Hono |
| Runtime adapters (Node, Workers, Deno, Bun, Lambda) | Hono |
| The service container, providers, config, the console | Keel |
| Fluent routing: names, groups, resources, param matchers, URL generation | Keel (routing) |
[Controller, method] handlers resolved from the container |
Keel (controllers) |
Request/response helpers that reach c without threading it |
Keel (request & response) |
view(), error rendering, exceptions, validation |
Keel |
Hono is the engine; Keel is the wheelhouse. Keel never hides Hono — it sits
beside it. When Keel wraps a Hono feature it's for ergonomics (fluent routes,
container DI, ambient helpers), and the raw feature is always one c away.
The context (c)
Every closure handler receives Hono's Context, and controller methods can too:
router.get("/users/:id", (c) => {
c.req.param("id"); // route param
c.req.query("q"); // query string
c.req.header("authorization");
await c.req.json(); // parse a JSON body
return c.json({ ok: true }); // c.text() · c.html() · c.body() · c.redirect()
});
Keel's Ctx type is exactly Hono's Context — it's a re-export, not a wrapper:
import type { Ctx } from "@shaferllc/keel/core";
// type Ctx = import("hono").Context
So a Keel handler and a Hono handler have the identical signature. Anything that
accepts a Hono Context accepts a Keel Ctx, and vice versa — there is no
adapter, boxing, or conversion between the two. When a guide says "the request
context," it means this object.
Common context surface: c.req.{param, query, header, json, parseBody, valid, path, method, url, raw},
c.{json, text, html, body, redirect, status, header, notFound}, c.set/c.get
for request-scoped variables, and on Workers c.env (bindings like D1/KV/R2) and
c.executionCtx (waitUntil). Full reference:
hono.dev/docs/api/context.
Keel does set a few request-scoped variables of its own on the context, which
you can read with c.get(...):
c.get("app")— the service container for this request.c.get("route")— the matched route ({ name, pattern, methods }).c.get("subdomains")— captured subdomain params on domain-bound routes.
These are exactly what the ambient request helpers read
under the hood. That's the trade: the helpers are terse and don't need c
passed around, but they only work inside a request; c is explicit and works
anywhere you're handed it.
Hono middleware works as-is
Any Hono middleware — built-in or third-party — drops straight into Keel's kernel or onto a route, because Keel middleware is Hono middleware:
import { cors } from "hono/cors";
import { secureHeaders } from "hono/secure-headers";
import { compress } from "hono/compress";
// app/Http/Kernel.ts
this.use(cors());
this.use(secureHeaders());
this.use(compress());
Hono ships CORS, Secure Headers, Body Limit, Cache, Compress, ETag, Basic/Bearer Auth, JWT, Logger, and more — see hono.dev/docs/middleware/builtin. Keel just gives you nicer places to attach them: global in the kernel, or per-route and per-group via the fluent router. Middleware covers the ordering and named-middleware conveniences Keel layers on top.
What else you get from Hono
Because Keel is Hono underneath, these are all available directly:
| Hono feature | Use it in Keel |
|---|---|
| JSX | Keel views are Hono JSX (hono/jsx) |
| Cookies | hono/cookie; Keel wraps common cases in request.cookie / response.cookie |
| Streaming / SSE | hono/streaming — return a streamed Response from a handler |
| WebSockets | Hono's upgrade helpers on supported runtimes |
| Testing | hono.request(path, init) — exactly what Keel's own test suite uses |
| Validators / RPC | hono/validator, the hc typed client |
| Runtime adapters | Node (@hono/node-server), Workers, Deno, Bun, Lambda |
Views are the clearest example of Keel building on a Hono
primitive: a Keel view is a Hono JSX function component, and view() just
renders it to a full HTML document through the View service. Drop the helper
and return c.html(<Page />) and you get the same result — Keel's version only
adds the doctype and props typing.
When to drop to raw Hono
Reach for c and the Hono API directly when:
- You need a context feature Keel doesn't wrap —
c.executionCtx.waitUntil, streaming responses, content negotiation beyond the helpers, Workers bindings onc.env. - You're pulling in a Hono (or Hono-ecosystem) middleware — it already speaks
the native
Context, so hand itcunchanged. - You want the typed RPC client (
hc) orhono/validator'sc.req.valid(...).
Reach for Keel's helpers and fluent router when you want named routes, groups,
resource routes, container-resolved controllers, or the ambient
request/response accessors. The two mix freely in the same handler — start
with Keel's ergonomics and drop to c for the exact spot that needs it. Nothing
you do at the Hono level is "off the map"; it's the same object either way.
Reaching the Hono app
The HTTP kernel compiles your routes onto a Hono instance and returns it —
that's the fetch handler you serve (Node) or export (Workers). If you need to
attach something at the Hono level, do it where you build the kernel:
const hono = new Kernel(app).build(); // a Hono instance
// hono.get(...), hono.use(...), export default hono, serve({ fetch: hono.fetch })
Because build() hands back a plain Hono app, the same kernel serves every
runtime — serve({ fetch: hono.fetch }) under Node, export default hono on
Workers. That single return value is the seam that keeps Keel edge-portable (see
Architecture).
For anything HTTP-layer that Keel doesn't wrap yet, drop down to Hono — the docs at hono.dev apply directly.
Serving over HTTP/2
There's nothing to configure in Keel for HTTP/2 — it's a transport concern, not a
framework one. Keel's handlers are Hono's fetch-based Request/Response, which
are HTTP-version-agnostic, so the protocol is decided entirely by whatever serves
the fetch handler:
On the edge (Cloudflare Workers — the headline target), the platform terminates HTTP/2 and HTTP/3 for you. Nothing to do, nothing to control from app code.
In Node production, HTTP/2 is almost always terminated at a reverse proxy or CDN (nginx, Cloudflare, ALB) in front of the process — the usual setup.
In-process h2, if you really want it, is a
@hono/node-serveroption — handserve()anode:http2server. No Keel change:import { serve } from "@hono/node-server"; import { createSecureServer } from "node:http2"; import { readFileSync } from "node:fs"; const hono = new Kernel(app).build(); serve({ fetch: hono.fetch, createServer: createSecureServer, serverOptions: { key: readFileSync("key.pem"), cert: readFileSync("cert.pem") }, });
So HTTP/2 is available on every Keel deployment without the framework
implementing anything — the same fetch handler just gets served over it.