Skip to content

Hono + SSR + JWT

This example builds a small Hono API with 3 routes:

  1. GET /ping for health checks.
  2. POST /ssr to SSR a user card.
  3. POST /jwt to issue a valid signed JWT for a user.

This example is split into three files:

  1. hono_knitting.ts (the Hono server, plus a Knitting worker pool).
  2. hono_componets_ssr.tsx (SSR task: parse + defaults + render).
  3. hono_components_jwt.ts (JWT task: validate + sign + return JSON string).
  • Hono: a small, fast routing layer. It keeps the request path minimal so most overhead is in your actual route work.
  • @hono/node-server: a thin adapter that runs a Hono fetch handler on Node/Bun.
  • React SSR (react-dom/server): renders a tiny HTML page for /ssr so you can simulate CPU-heavy server work.
  • hono/jwt: signs a JWT (HS256) for /jwt so the example includes common auth-like CPU work.
  • Knitting (knitting): runs selected transforms in a worker pool (threads). This is the core “offload expensive work” technique the example demonstrates.

The JWT route uses hono/jwt and signs with HS256. Set JWT_SECRET in production.

This example imports TSX and npm packages. For Deno, keep a root deno.json like this:

{
"nodeModulesDir": "auto",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}

Without this, TSX files can fail with:

Uncaught SyntaxError: Unexpected token '<'
bun.sh
JWT_SECRET="replace-me" bun src/hono_knitting.ts
Terminal window
# Ping
curl -s http://localhost:3000/ping
# SSR (returns HTML)
curl -s http://localhost:3000/ssr \
-H 'content-type: application/json' \
-d '{"name":"Ari","plan":"pro","bio":"Building on Knitting","projects":17}'
# JWT (returns JSON string with token)
curl -s http://localhost:3000/jwt \
-H 'content-type: application/json' \
-d '{"user":{"id":"u_42","email":"ari@example.com","role":"admin"},"ttlSec":900}'

Measurements: Hono 16-core benchmark.

One worker, with /ping, /ssr, and /jwt loaded concurrently.

RouteHono onlyHono + KnittingDelta
/ping2,815 rps8,486 rps+201%
/ssr2,815 rps4,816 rps+71%
/jwt2,578 rps4,712 rps+83%

One worker, with each route offered 2,000 rps.

RouteHono only p50Knitting p50Hono only p99Knitting p99
/ping0.81ms0.40ms16.93ms2.31ms
/ssr0.84ms2.22ms14.32ms8.71ms
/jwt1.03ms2.58ms25.18ms9.76ms
  • ping stays cheap and synchronous.
  • You can benchmark workers vs host-only with the same route behavior.
  • In Knitting mode, the JWT task returns stringified JSON to reduce structured-clone overhead.
  • Heavy route logic stays in one shared file, keeping both server entrypoints small.
hono_knitting.ts
import { serve } from "@hono/node-server";
import { createPool } from "knitting";
import { Hono } from "hono";
import { issueJwt } from "./hono_components_jwt.ts";
import { renderSsrPage } from "./hono_componets_ssr.tsx";
const handlers = createPool({
})({
issueJwt,
renderSsrPage,
});
async function main() {
const app = new Hono();
app.get("/ping", (c) => {
return c.json({
ok: true,
pong: true,
runtime: process.release?.name ?? "unknown",
ts: new Date().toISOString(),
});
});
app.post("/ssr", async (c) => {
const html = await handlers.call.renderSsrPage(c.req.arrayBuffer());
return c.html(html);
});
app.post("/jwt", async (c) => {
const responseJson = await handlers.call.issueJwt(c.req.arrayBuffer());
return c.body(responseJson ?? "Bad request", responseJson ? 200 : 400, {
"content-type": "application/json; charset=utf-8",
});
});
const server = serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log("GET /ping");
console.log("POST /ssr body: { name?, plan?, bio?, projects? }");
console.log("POST /jwt body: { user: { id, email?, role? }, ttlSec? }");
});
const close = () => {
// IMPORTANT TO CLOSE CONNECTION
handlers.shutdown();
server.close();
};
process.on("SIGINT", close);
process.on("SIGTERM", close);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});