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 (@vixeny/knitting): runs selected transforms in a worker pool (threads). This is the core “offload expensive work” technique the example demonstrates.
deno.sh
deno add --npm jsr:@vixeny/knitting
deno add npm:hono npm:@hono/node-server npm:react npm:react-dom npm:zod

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}'

Performance notes (how to talk about it correctly)

Section titled “Performance notes (how to talk about it correctly)”

What the numbers say (hono_only → hono+knitting)

Section titled “What the numbers say (hono_only → hono+knitting)”
RouteHono onlyHono + KnittingDelta
/ping29985471+82%
/ssr29985421+81%
/jwt29165454+87%

So: roughly ~1.8× throughput across the board. Offloading the heavy work frees the main thread, so even the “cheap” routes handle more requests per second when Knitting is in play.

PercentileHono onlyHono + KnittingDelta
p5016.32ms8.84ms-46%
p9927.05ms14.52ms-46%
p99.971.01ms64.59ms-9%
p99.99128.70ms99.44ms-23%
slowest129.25ms99.90ms-23%
PercentileHono onlyHono + KnittingDelta
p5016.32ms8.71ms-47%
p9927.32ms17.35ms-36%
p99.971.96ms63.97ms-11%
p99.99128.61ms98.75ms-23%
slowest129.31ms106.73ms-17%
PercentileHono onlyHono + KnittingDelta
p5016.47ms8.71ms-47%
p9934.49ms15.91ms-54%
p99.9114.95ms59.48ms-48%
p99.99121.47ms101.07ms-17%
slowest161.28ms138.48ms-14%

Headline: the median and p99 basically halve, and the JWT tail (p99.9) improves dramatically.

  • 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 "@vixeny/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;
});