spinny:~/writing $ less hono-framework-guide.md
12Jahrelang bedeutete die Wahl eines JavaScript-Web-Frameworks einen Kompromiss: Express war universell, aber langsam und an Node gebunden, Fastify war schnell, aber nur für Node, Next.js war vollständig, aber schwer. Mit dem Aufkommen von Edge-Runtimes - Cloudflare Workers, Deno Deploy, Bun, Vercel Edge - zeigten diese Frameworks ihre Grenzen: inkompatible Abhängigkeiten, riesige Bundles, an `req`/`res` gebundene APIs.34[Hono](https://hono.dev) (japanisch für "Flamme" 🔥) ist die moderne Antwort. Ein Framework unter 14KB, vollständig auf Web Standards (`Request`, `Response`, `fetch`) aufgebaut, das überall läuft, wo es eine JavaScript-Runtime gibt. Derselbe Code wird ohne Änderung auf Cloudflare Workers, Bun, Deno, Node.js, Vercel, Netlify und AWS Lambda deployt.56## Warum Hono78Hono macht drei Dinge besser als jeder andere:9101. **Performance.** Der `RegExpRouter` kompiliert alle Route-Patterns zu einer einzigen Regex und vermeidet die linearen Schleifen traditioneller Router. Benchmarks zeigen über 400.000 Ops/s und reihen Hono unter die schnellsten Router des JavaScript-Ökosystems ein.112. **Portabilität.** Web Standards bedeutet null Node-Abhängigkeiten. Dasselbe `app.fetch` wird als Default in einem Cloudflare Worker exportiert, an `Bun.serve` übergeben, in einem Deno-Server gemountet oder mit `@hono/node-server` adaptiert.123. **TypeScript-First DX.** Path-Parameter als Literal-Types, ein End-to-End-typsicherer RPC-Client, Validatoren mit Input- und Output-Inferenz. Die Autovervollständigung ist nahezu telepathisch.1314```mermaid15graph LR16 Client[Client] -->|Request| App[app.fetch]17 App --> MW1[Middleware 1]18 MW1 --> MW2[Middleware 2]19 MW2 --> Router[RegExpRouter]20 Router --> Handler[Route Handler]21 Handler --> Context[c.json / c.text]22 Context -->|Response| Client23 App -.->|deploy| CF[Cloudflare Workers]24 App -.->|deploy| Bun[Bun]25 App -.->|deploy| Deno[Deno]26 App -.->|deploy| Node[Node.js]27 App -.->|deploy| Vercel[Vercel]28```2930## Erste Schritte3132Der schnellste Weg ist der offizielle Starter, der das Projekt für die gewählte Runtime erstellt.3334```bash35npm create hono@latest my-api36cd my-api37npm install38npm run dev39```4041Der Starter fragt nach einem Template: `cloudflare-workers`, `bun`, `deno`, `nodejs`, `vercel`, `aws-lambda`, `nextjs` und mehr. Er wählt passende Konfigurationen und Deploy-Skripte. Für einen schnellen Test reicht auch eine einzelne Datei:4243```typescript44// src/index.ts45import { Hono } from 'hono'4647const app = new Hono()4849app.get('/', (c) => c.text('Hello Hono!'))5051export default app52```5354Auf Cloudflare Workers reicht das. Auf Bun: `Bun.serve({ fetch: app.fetch, port: 3000 })`. Auf Node: `serve({ fetch: app.fetch })` aus `@hono/node-server`. Die Oberfläche ist identisch.5556## Routing5758Routes werden mit HTTP-Verb-Methoden deklariert und unterstützen Parameter, Wildcards und Regex.5960```typescript61import { Hono } from 'hono'6263const app = new Hono()6465app.get('/', (c) => c.text('Home'))66app.get('/posts/:id', (c) => {67 const id = c.req.param('id') // string, typisiert68 return c.json({ id })69})70app.get('/posts/:id/comments/:commentId', (c) => {71 const { id, commentId } = c.req.param()72 return c.json({ id, commentId })73})74app.get('/files/*', (c) => c.text('Wildcard'))75app.post('/posts', async (c) => {76 const body = await c.req.json()77 return c.json({ created: body }, 201)78})79```8081Parameter werden als Literal-Types abgeleitet: TypeScript weiß, dass `c.req.param('id')` nur dann ein `string` zurückgibt, wenn `:id` im Pattern deklariert wurde. Ein Tippfehler ist ein Compile-Time-Error.8283### Routes gruppieren8485Mit `app.route()` lassen sich Sub-Anwendungen als Module komponieren, jede mit eigenem Präfix.8687```typescript88// routes/posts.ts89import { Hono } from 'hono'9091const posts = new Hono()92posts.get('/', (c) => c.json({ posts: [] }))93posts.get('/:id', (c) => c.json({ id: c.req.param('id') }))94export default posts9596// src/index.ts97import { Hono } from 'hono'98import posts from './routes/posts'99100const app = new Hono()101app.route('/posts', posts)102```103104Verschachtelte Routes erben den Base-Path und den Typ der Root-App, sodass der RPC-Client die gesamte Struktur sieht.105106## Das Context-Objekt107108Jeder Handler erhält ein `c` - den Context für die aktuelle Anfrage. Es ist die einzige API, die du lernen musst, um Eingaben zu lesen und Antworten zu produzieren.109110```typescript111app.post('/echo', async (c) => {112 // Lesen113 const userAgent = c.req.header('User-Agent')114 const page = c.req.query('page')115 const body = await c.req.json()116 const env = c.env // Bindings (KV, D1, Secrets auf Cloudflare)117118 // Variablen zwischen Middleware und Handler geteilt119 c.set('requestId', crypto.randomUUID())120 const id = c.get('requestId')121122 // Antwort123 c.header('X-Request-Id', id)124 c.status(200)125 return c.json({ userAgent, page, body, id })126})127```128129Die wichtigsten Response-Methoden sind `c.text`, `c.json`, `c.html`, `c.body` (raw) und `c.redirect`. Alle akzeptieren einen Statuscode als zweites Argument.130131## Middleware: das Onion-Modell132133Middleware sind `(c, next) => ...`-Funktionen, die Code vor und nach dem Handler ausführen können. Komponiert ergeben sie das klassische Onion-Modell: die zuerst registrierte Middleware startet als erste und endet als letzte.134135```typescript136import { Hono } from 'hono'137import { logger } from 'hono/logger'138import { cors } from 'hono/cors'139import { secureHeaders } from 'hono/secure-headers'140141const app = new Hono()142143app.use('*', logger())144app.use('*', secureHeaders())145app.use('/api/*', cors({ origin: 'https://spinny.dev' }))146147app.use('*', async (c, next) => {148 const start = performance.now()149 await next()150 c.header('X-Response-Time', `${performance.now() - start}ms`)151})152```153154`await next()` übergibt die Kontrolle an die nächste Middleware. Alles, was danach kommt, läuft in der Response-Phase, nachdem der Handler ein Ergebnis produziert hat. Das Zurückgeben einer `Response` vor `next()` bricht die Kette ab.155156### Eingebaute Middleware157158Hono liefert eine reichhaltige Auswahl produktionsreifer Middleware aus, importierbar aus `hono/...`:159160| Middleware | Zweck |161|-----------|---------|162| `logger` | Strukturierte Logs zu Methode, Pfad, Status, Dauer |163| `cors` | CORS konfigurierbar nach Origin, Methoden, Headern |164| `csrf` | Origin-basierter CSRF-Schutz |165| `secureHeaders` | Setzt CSP, HSTS, X-Frame-Options |166| `bearerAuth` / `basicAuth` | Bearer/Basic-Auth out of the box |167| `jwt` | JWT-Verify/Sign mit `jose` |168| `etag` | Generiert ETag und behandelt 304 |169| `cache` | Caching über die Web Cache API |170| `compress` | gzip/deflate auf der Response |171| `bodyLimit` | Lehnt Bodies über einer Grenze ab |172| `timing` | Server-Timing-Header für Profiling |173174### Typsichere Custom-Middleware175176Um den `Context` mit typisierten Variablen zu erweitern, nutze `createMiddleware`:177178```typescript179import { createMiddleware } from 'hono/factory'180181type AuthVars = { userId: string; role: 'user' | 'admin' }182183export const requireAuth = createMiddleware<{ Variables: AuthVars }>(184 async (c, next) => {185 const token = c.req.header('Authorization')?.replace('Bearer ', '')186 if (!token) return c.json({ error: 'Unauthorized' }, 401)187188 const payload = await verifyJwt(token)189 c.set('userId', payload.sub)190 c.set('role', payload.role)191 await next()192 }193)194195// Verwendung196app.get('/me', requireAuth, (c) => {197 const userId = c.var.userId // string, typisiert198 return c.json({ userId })199})200```201202Nach der Middleware ist `c.var.userId` ohne Cast typisiert. Das pflanzt sich durch die gesamte Kette fort.203204## Validierung mit Zod205206`@hono/zod-validator` bindet Zod in den Request-Zyklus ein. Du definierst ein Schema, wendest es auf die Route an und erhältst bereits validierte, typisierte Eingaben.207208```typescript209import { Hono } from 'hono'210import { zValidator } from '@hono/zod-validator'211import { z } from 'zod'212213const createPost = z.object({214 title: z.string().min(1).max(200),215 body: z.string().min(1),216 tags: z.array(z.string()).default([]),217})218219app.post(220 '/posts',221 zValidator('json', createPost),222 (c) => {223 const data = c.req.valid('json') // typisiert nach Schema224 return c.json({ ok: true, post: data }, 201)225 }226)227```228229Schlägt die Validierung fehl, antwortet Hono mit 400 und dem Zod-Fehler, ohne den Handler aufzurufen. Du kannst auch `query`, `param`, `header`, `cookie` und `form` validieren.230231## RPC: End-to-End-typsicherer Client232233Das Feature, das Hono wirklich abhebt, ist der **RPC-Modus**. Du exportierst den Typ deiner App vom Server, der `hc`-Client importiert ihn und du erhältst vollständige Autovervollständigung - inklusive Pfade, Query, Body, Header und Response - ohne Codegen oder OpenAPI.234235```typescript236// server.ts237import { Hono } from 'hono'238import { zValidator } from '@hono/zod-validator'239import { z } from 'zod'240241const app = new Hono()242 .get('/posts/:id', (c) =>243 c.json({ id: c.req.param('id'), title: 'Hello' })244 )245 .post(246 '/posts',247 zValidator('json', z.object({ title: z.string(), body: z.string() })),248 (c) => c.json({ ok: true }, 201)249 )250251export type AppType = typeof app252export default app253```254255```typescript256// client.ts257import { hc } from 'hono/client'258import type { AppType } from './server'259260const client = hc<AppType>('https://api.spinny.dev')261262const res = await client.posts[':id'].$get({ param: { id: '42' } })263if (res.ok) {264 const data = await res.json() // { id: string, title: string }265 console.log(data.title)266}267268const created = await client.posts.$post({269 json: { title: 'Hallo', body: 'Hono ist eine Flamme' }, // validiert270})271```272273Benenne eine Route auf dem Server um und das TypeScript des Clients bricht sofort in CI. Derselbe Vorteil wie tRPC, aber über Standard-HTTP, ohne spezifische Middleware und mit einem winzigen Bundle.274275### Statuscode-Diskriminierung276277Wenn du verschiedene Status zurückgibst, diskriminiert der Client sie automatisch.278279```typescript280.get('/posts/:id', (c) => {281 const post = findPost(c.req.param('id'))282 if (!post) return c.json({ error: 'not found' }, 404)283 return c.json({ post }, 200)284})285```286287```typescript288const res = await client.posts[':id'].$get({ param: { id } })289if (res.status === 404) {290 const { error } = await res.json() // { error: string }291}292if (res.status === 200) {293 const { post } = await res.json() // { post: Post }294}295```296297## Router und Performance298299Hono bietet fünf Router mit verschiedenen Trade-offs. Default ist der `SmartRouter`, der beim Start misst, welcher Router deine Routes am besten bedient, und sich darauf festlegt.300301| Router | Stärken | Wann einsetzen |302|--------|---------|----------------|303| `RegExpRouter` | Höchste Geschwindigkeit, kompilierte Regex | Default für die meisten APIs |304| `TrieRouter` | Unterstützt jedes Pattern | Komplexe Patterns, die RegExp nicht abdeckt |305| `SmartRouter` | Wählt automatisch den besten | Empfohlener Default |306| `LinearRouter` | Ultraschnelle Registrierung | One-Shot-Worker, kritische Cold Starts |307| `PatternRouter` | Kleinstes Bundle (<15KB) | Extreme Größenbeschränkungen |308309Für stateless Worker mit häufigen Cold Starts vermeidet `LinearRouter` die initialen Kompilierungskosten und ist 33-mal schneller als `find-my-way`, wenn Registrierung plus Matching gemessen werden.310311```typescript312import { Hono } from 'hono'313import { LinearRouter } from 'hono/router/linear-router'314315const app = new Hono({ router: new LinearRouter() })316```317318## Multi-Runtime-Deployment319320Dieselbe `export default app` wechselt das Ziel, indem nur die Entry-Datei getauscht wird.321322### Cloudflare Workers323324```typescript325// src/index.ts326import { Hono } from 'hono'327328type Bindings = { MY_KV: KVNamespace; DB: D1Database }329const app = new Hono<{ Bindings: Bindings }>()330331app.get('/cache/:key', async (c) => {332 const value = await c.env.MY_KV.get(c.req.param('key'))333 return c.json({ value })334})335336export default app337```338339Deploy: `npx wrangler deploy`. KV/D1/R2/Queues-Bindings sind nativ.340341### Bun342343```typescript344import { Hono } from 'hono'345const app = new Hono()346app.get('/', (c) => c.text('Bun + Hono'))347348Bun.serve({ fetch: app.fetch, port: 3000 })349```350351### Node.js352353```typescript354import { serve } from '@hono/node-server'355import { Hono } from 'hono'356357const app = new Hono()358app.get('/', (c) => c.text('Node + Hono'))359360serve({ fetch: app.fetch, port: 3000 })361```362363### Deno364365```typescript366import { Hono } from 'jsr:@hono/hono'367const app = new Hono()368app.get('/', (c) => c.text('Deno + Hono'))369Deno.serve(app.fetch)370```371372### Vercel373374```typescript375// api/[[...route]].ts376import { Hono } from 'hono'377import { handle } from 'hono/vercel'378379const app = new Hono().basePath('/api')380app.get('/hello', (c) => c.json({ msg: 'Hello from Vercel' }))381382export const GET = handle(app)383export const POST = handle(app)384```385386Gleicher Business-Code, gleiches Routing, gleiche Middleware. Nur der Adapter ändert sich.387388## Praxisbeispiel: REST-API mit Auth und DB389390Alles zusammengeführt. Eine Blog-API auf Cloudflare Workers + D1, mit JWT-Auth, Validierung und RPC.391392```typescript393import { Hono } from 'hono'394import { jwt } from 'hono/jwt'395import { logger } from 'hono/logger'396import { cors } from 'hono/cors'397import { zValidator } from '@hono/zod-validator'398import { z } from 'zod'399400type Bindings = { DB: D1Database; JWT_SECRET: string }401type Variables = { jwtPayload: { sub: string } }402403const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()404405app.use('*', logger())406app.use('/api/*', cors({ origin: 'https://spinny.dev', credentials: true }))407408const auth = (c: any, next: any) =>409 jwt({ secret: c.env.JWT_SECRET })(c, next)410411const api = app.basePath('/api')412413api.get('/posts', async (c) => {414 const { results } = await c.env.DB415 .prepare('SELECT id, title, created_at FROM posts ORDER BY created_at DESC LIMIT 50')416 .all()417 return c.json({ posts: results })418})419420api.post(421 '/posts',422 auth,423 zValidator('json', z.object({424 title: z.string().min(1).max(200),425 body: z.string().min(1),426 })),427 async (c) => {428 const { title, body } = c.req.valid('json')429 const userId = c.var.jwtPayload.sub430 const result = await c.env.DB431 .prepare('INSERT INTO posts (title, body, author_id) VALUES (?, ?, ?) RETURNING id')432 .bind(title, body, userId)433 .first<{ id: number }>()434 return c.json({ id: result?.id }, 201)435 }436)437438api.onError((err, c) => {439 console.error(err)440 return c.json({ error: 'Internal error' }, 500)441})442443export type AppType = typeof api444export default app445```446447Ein React-Client konsumiert sie mit voller Typsicherheit:448449```typescript450import { hc } from 'hono/client'451import type { AppType } from '../api/src/index'452453const api = hc<AppType>(import.meta.env.VITE_API_URL)454455const res = await api.posts.$post({456 json: { title: 'Hono in 2026', body: '...' },457}, {458 headers: { Authorization: `Bearer ${token}` },459})460```461462## Testing463464`app.request()` erlaubt das Testen von Routes, ohne einen HTTP-Server zu starten. Es ist derselbe Pfad, den du in Produktion ausführen würdest, im Speicher ausgeführt.465466```typescript467import { describe, it, expect } from 'vitest'468import app from '../src/index'469470describe('GET /api/posts', () => {471 it('liefert die Liste der Posts', async () => {472 const res = await app.request('/api/posts')473 expect(res.status).toBe(200)474 const body = await res.json()475 expect(body.posts).toBeInstanceOf(Array)476 })477})478```479480Auf Cloudflare Workers führt `@cloudflare/vitest-pool-workers` dieselben Tests in einem echten Worker mit Mock-Bindings aus - maximale Realität, kein Deploy.481482## Best Practices483484### 1. Route-Definitionen verketten485486```typescript487const app = new Hono()488 .get('/posts', handler1)489 .post('/posts', handler2)490 .get('/posts/:id', handler3)491```492493Chaining hält den Typ von `app` mit jeder Route aktuell, was für den RPC-Client essenziell ist. Separate `app.get(...)`-Aufrufe in eigenen Zeilen brechen die Inferenz.494495### 2. Den Typ exportieren, nicht die Implementierung496497Der Client muss `AppType` importieren, nicht die App. `import type` stellt sicher, dass der Frontend-Build keinen Backend-Code enthält.498499### 3. Ein Router pro Domäne500501Eine Sub-App für `posts`, eine für `users`, eine für `webhooks`. Komponiere sie mit `app.route()` und jede besitzt ihre eigene Middleware. Die Struktur skaliert ohne Mega-Dateien.502503### 4. Validierung am Rand, immer504505Jede externe Eingabe (Body, Query, Header) muss durch `zValidator`. Vertraue Daten flussabwärts nicht: selbst ein TypeScript-Cast ohne Runtime-Validierung ist ein Bug, der nur darauf wartet zuzuschlagen.506507### 5. Auf Bindings setzen, nicht auf globale Clients508509Auf Cloudflare greife auf KV/D1/R2 über `c.env` zu. Keine globalen Singletons, keine Verbindungen, die zwischen Workern persistieren. Das stateless Modell ist ein Feature, keine Einschränkung.510511### 6. Vor dem Optimieren des Routers messen512513Der Default-`SmartRouter` reicht für 95 % der Fälle. Wechsle den Router erst nach dem Profiling und einem realen Bottleneck.514515## Fazit516517Hono ist 2026 zum De-facto-Standard geworden, um edge-fähige APIs in TypeScript zu bauen. Die Kombination aus Web Standards, Performance, Typsicherheit und Portabilität löst genau die Probleme, die traditionelle Frameworks zurückhielten: Runtime-Lock-in, schwere Bundles, fragiles Typsystem zwischen Client und Server.518519Du nutzt es für Microservices auf Cloudflare Workers, um Express in einem Node-Monolithen zu ersetzen, für eine Function auf Vercel oder eine API auf Bun. Dasselbe Wissen lässt sich überall einsetzen - und das macht es zu einer soliden Investition für die kommenden Jahre.520521> **Erste-Schritte-Checkliste:**522>523> - [x] `npm create hono@latest` und Runtime-Template wählen524> - [x] Routes mit Chaining definieren (`.get(...).post(...)`)525> - [x] `logger`, `cors`, `secureHeaders` als globale Middleware hinzufügen526> - [x] Jeden Input mit `@hono/zod-validator` validieren527> - [x] `AppType` exportieren und die API mit dem typsicheren `hc`-Client konsumieren528> - [x] Tests mit `app.request()` schreiben - kein HTTP-Server nötig529> - [x] Mit `wrangler deploy` (CF), `vercel deploy` oder dem Runtime-Bundler deployen530
:Hono: Das ultraschnelle Web-Framework auf Basis der Web Standardslines 1-530 (END) — press q to close