spinny:~/writing $ less hono-framework-guide.md
12Jarenlang betekende het kiezen van een JavaScript-webframework een afweging accepteren: Express was universeel maar traag en aan Node gebonden, Fastify was snel maar alleen Node, Next.js was volledig maar zwaar. Toen edge-runtimes — Cloudflare Workers, Deno Deploy, Bun, Vercel Edge — verschenen, toonden die frameworks hun grenzen.34[Hono](https://hono.dev) (Japans voor "vlam" 🔥) is het moderne antwoord. Een framework van minder dan 14KB, volledig gebouwd op Web Standards (`Request`, `Response`, `fetch`), dat draait waar er een JavaScript-runtime is. Dezelfde code wordt zonder wijziging gedeployed naar Cloudflare Workers, Bun, Deno, Node.js, Vercel, Netlify en AWS Lambda.56## Waarom Hono781. **Performance.** `RegExpRouter` compileert alle routepatronen in één regex. Benchmarks halen meer dan 400.000 ops/s.92. **Portabiliteit.** Web Standards betekent nul Node-afhankelijkheden. Dezelfde `app.fetch` wordt als default geëxporteerd in een Cloudflare Worker, doorgegeven aan `Bun.serve`, gemount in een Deno-server of geadapteerd met `@hono/node-server`.103. **TypeScript-first DX.** Pad-parameters afgeleid als literal types, end-to-end type-safe RPC-client.1112```mermaid13graph LR14 Client[Client] -->|Request| App[app.fetch]15 App --> MW1[Middleware 1]16 MW1 --> MW2[Middleware 2]17 MW2 --> Router[RegExpRouter]18 Router --> Handler[Route Handler]19 Handler --> Context[c.json / c.text]20 Context -->|Response| Client21 App -.->|deploy| CF[Cloudflare Workers]22 App -.->|deploy| Bun[Bun]23 App -.->|deploy| Deno[Deno]24 App -.->|deploy| Node[Node.js]25 App -.->|deploy| Vercel[Vercel]26```2728## Beginnen2930```bash31npm create hono@latest my-api32cd my-api33npm install34npm run dev35```3637De starter vraagt welke template: `cloudflare-workers`, `bun`, `deno`, `nodejs`, `vercel`, `aws-lambda`, `nextjs` en meer.3839```typescript40// src/index.ts41import { Hono } from 'hono'4243const app = new Hono()4445app.get('/', (c) => c.text('Hello Hono!'))4647export default app48```4950Op Cloudflare Workers is dit genoeg. Op Bun: `Bun.serve({ fetch: app.fetch, port: 3000 })`. Op Node: `serve({ fetch: app.fetch })` uit `@hono/node-server`.5152## Routing5354```typescript55import { Hono } from 'hono'5657const app = new Hono()5859app.get('/', (c) => c.text('Home'))60app.get('/posts/:id', (c) => {61 const id = c.req.param('id')62 return c.json({ id })63})64app.get('/posts/:id/comments/:commentId', (c) => {65 const { id, commentId } = c.req.param()66 return c.json({ id, commentId })67})68app.get('/files/*', (c) => c.text('Wildcard'))69app.post('/posts', async (c) => {70 const body = await c.req.json()71 return c.json({ created: body }, 201)72})73```7475### Routes groeperen7677```typescript78// routes/posts.ts79import { Hono } from 'hono'8081const posts = new Hono()82posts.get('/', (c) => c.json({ posts: [] }))83posts.get('/:id', (c) => c.json({ id: c.req.param('id') }))84export default posts8586// src/index.ts87import { Hono } from 'hono'88import posts from './routes/posts'8990const app = new Hono()91app.route('/posts', posts)92```9394## Het Context-object9596```typescript97app.post('/echo', async (c) => {98 const userAgent = c.req.header('User-Agent')99 const page = c.req.query('page')100 const body = await c.req.json()101 const env = c.env102103 c.set('requestId', crypto.randomUUID())104 const id = c.get('requestId')105106 c.header('X-Request-Id', id)107 c.status(200)108 return c.json({ userAgent, page, body, id })109})110```111112## Middleware: het ui-model113114```typescript115import { Hono } from 'hono'116import { logger } from 'hono/logger'117import { cors } from 'hono/cors'118import { secureHeaders } from 'hono/secure-headers'119120const app = new Hono()121122app.use('*', logger())123app.use('*', secureHeaders())124app.use('/api/*', cors({ origin: 'https://spinny.dev' }))125126app.use('*', async (c, next) => {127 const start = performance.now()128 await next()129 c.header('X-Response-Time', `${performance.now() - start}ms`)130})131```132133### Ingebouwde middleware134135| Middleware | Doel |136|-----------|---------|137| `logger` | Gestructureerde logs van methode, pad, status, duur |138| `cors` | CORS configureerbaar per origin, methoden, headers |139| `csrf` | Origin-gebaseerde CSRF-bescherming |140| `secureHeaders` | Stelt CSP, HSTS, X-Frame-Options in |141| `bearerAuth` / `basicAuth` | Out-of-the-box Bearer/Basic auth |142| `jwt` | JWT verifiëren/ondertekenen met `jose` |143| `etag` | Genereert ETag en behandelt 304 |144| `cache` | Cache via Web Cache API |145| `compress` | gzip/deflate op response |146| `bodyLimit` | Weigert bodies boven drempel |147| `timing` | Server-Timing-header voor profiling |148149### Type-safe custom middleware150151```typescript152import { createMiddleware } from 'hono/factory'153154type AuthVars = { userId: string; role: 'user' | 'admin' }155156export const requireAuth = createMiddleware<{ Variables: AuthVars }>(157 async (c, next) => {158 const token = c.req.header('Authorization')?.replace('Bearer ', '')159 if (!token) return c.json({ error: 'Unauthorized' }, 401)160161 const payload = await verifyJwt(token)162 c.set('userId', payload.sub)163 c.set('role', payload.role)164 await next()165 }166)167168app.get('/me', requireAuth, (c) => {169 const userId = c.var.userId170 return c.json({ userId })171})172```173174## Validatie met Zod175176```typescript177import { Hono } from 'hono'178import { zValidator } from '@hono/zod-validator'179import { z } from 'zod'180181const createPost = z.object({182 title: z.string().min(1).max(200),183 body: z.string().min(1),184 tags: z.array(z.string()).default([]),185})186187app.post(188 '/posts',189 zValidator('json', createPost),190 (c) => {191 const data = c.req.valid('json')192 return c.json({ ok: true, post: data }, 201)193 }194)195```196197## RPC: end-to-end type-safe client198199```typescript200// server.ts201import { Hono } from 'hono'202import { zValidator } from '@hono/zod-validator'203import { z } from 'zod'204205const app = new Hono()206 .get('/posts/:id', (c) =>207 c.json({ id: c.req.param('id'), title: 'Hello' })208 )209 .post(210 '/posts',211 zValidator('json', z.object({ title: z.string(), body: z.string() })),212 (c) => c.json({ ok: true }, 201)213 )214215export type AppType = typeof app216export default app217```218219```typescript220// client.ts221import { hc } from 'hono/client'222import type { AppType } from './server'223224const client = hc<AppType>('https://api.spinny.dev')225226const res = await client.posts[':id'].$get({ param: { id: '42' } })227if (res.ok) {228 const data = await res.json()229 console.log(data.title)230}231232const created = await client.posts.$post({233 json: { title: 'Hallo', body: 'Hono is een vlam' },234})235```236237### Statuscodediscriminatie238239```typescript240.get('/posts/:id', (c) => {241 const post = findPost(c.req.param('id'))242 if (!post) return c.json({ error: 'not found' }, 404)243 return c.json({ post }, 200)244})245```246247```typescript248const res = await client.posts[':id'].$get({ param: { id } })249if (res.status === 404) {250 const { error } = await res.json()251}252if (res.status === 200) {253 const { post } = await res.json()254}255```256257## Routers en performance258259| Router | Sterke punten | Wanneer gebruiken |260|--------|-----------|-------------|261| `RegExpRouter` | Maximale snelheid, gecompileerde regex | Standaard voor de meeste APIs |262| `TrieRouter` | Ondersteunt alle patronen | Complexe patronen die RegExp niet aankan |263| `SmartRouter` | Kiest automatisch het beste | Aanbevolen standaard |264| `LinearRouter` | Ultrasnelle registratie | One-shot workers, kritische cold start |265| `PatternRouter` | Minimaal bundle (<15KB) | Extreme grootteperken |266267```typescript268import { Hono } from 'hono'269import { LinearRouter } from 'hono/router/linear-router'270271const app = new Hono({ router: new LinearRouter() })272```273274## Multi-runtime deploy275276### Cloudflare Workers277278```typescript279import { Hono } from 'hono'280281type Bindings = { MY_KV: KVNamespace; DB: D1Database }282const app = new Hono<{ Bindings: Bindings }>()283284app.get('/cache/:key', async (c) => {285 const value = await c.env.MY_KV.get(c.req.param('key'))286 return c.json({ value })287})288289export default app290```291292Deploy: `npx wrangler deploy`.293294### Bun295296```typescript297import { Hono } from 'hono'298const app = new Hono()299app.get('/', (c) => c.text('Bun + Hono'))300301Bun.serve({ fetch: app.fetch, port: 3000 })302```303304### Node.js305306```typescript307import { serve } from '@hono/node-server'308import { Hono } from 'hono'309310const app = new Hono()311app.get('/', (c) => c.text('Node + Hono'))312313serve({ fetch: app.fetch, port: 3000 })314```315316### Deno317318```typescript319import { Hono } from 'jsr:@hono/hono'320const app = new Hono()321app.get('/', (c) => c.text('Deno + Hono'))322Deno.serve(app.fetch)323```324325### Vercel326327```typescript328// api/[[...route]].ts329import { Hono } from 'hono'330import { handle } from 'hono/vercel'331332const app = new Hono().basePath('/api')333app.get('/hello', (c) => c.json({ msg: 'Hello from Vercel' }))334335export const GET = handle(app)336export const POST = handle(app)337```338339## Praktijkvoorbeeld: REST API met auth en DB340341```typescript342import { Hono } from 'hono'343import { jwt } from 'hono/jwt'344import { logger } from 'hono/logger'345import { cors } from 'hono/cors'346import { zValidator } from '@hono/zod-validator'347import { z } from 'zod'348349type Bindings = { DB: D1Database; JWT_SECRET: string }350type Variables = { jwtPayload: { sub: string } }351352const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()353354app.use('*', logger())355app.use('/api/*', cors({ origin: 'https://spinny.dev', credentials: true }))356357const auth = (c: any, next: any) =>358 jwt({ secret: c.env.JWT_SECRET })(c, next)359360const api = app.basePath('/api')361362api.get('/posts', async (c) => {363 const { results } = await c.env.DB364 .prepare('SELECT id, title, created_at FROM posts ORDER BY created_at DESC LIMIT 50')365 .all()366 return c.json({ posts: results })367})368369api.post(370 '/posts',371 auth,372 zValidator('json', z.object({373 title: z.string().min(1).max(200),374 body: z.string().min(1),375 })),376 async (c) => {377 const { title, body } = c.req.valid('json')378 const userId = c.var.jwtPayload.sub379 const result = await c.env.DB380 .prepare('INSERT INTO posts (title, body, author_id) VALUES (?, ?, ?) RETURNING id')381 .bind(title, body, userId)382 .first<{ id: number }>()383 return c.json({ id: result?.id }, 201)384 }385)386387api.onError((err, c) => {388 console.error(err)389 return c.json({ error: 'Internal error' }, 500)390})391392export type AppType = typeof api393export default app394```395396## Testen397398```typescript399import { describe, it, expect } from 'vitest'400import app from '../src/index'401402describe('GET /api/posts', () => {403 it('geeft de lijst met posts terug', async () => {404 const res = await app.request('/api/posts')405 expect(res.status).toBe(200)406 const body = await res.json()407 expect(body.posts).toBeInstanceOf(Array)408 })409})410```411412## Best practices413414### 1. Ketting routedefinities415416```typescript417const app = new Hono()418 .get('/posts', handler1)419 .post('/posts', handler2)420 .get('/posts/:id', handler3)421```422423### 2. Exporteer het type, niet de implementatie424425De client moet `AppType` importeren.426427### 3. Eén router per domein428429Sub-app voor `posts`, `users`, `webhooks`.430431### 4. Validatie aan de rand, altijd432433Elke externe input moet door `zValidator`.434435### 5. Steun op bindings, niet globale clients436437Op Cloudflare KV/D1/R2 via `c.env`.438439### 6. Meet voor je de router optimaliseert440441De default `SmartRouter` past in 95% van de gevallen.442443## Conclusie444445Hono is in 2026 de de-facto standaard geworden om edge-ready APIs te bouwen in TypeScript.446447> **Startchecklist:**448>449> - [x] `npm create hono@latest` en kies je runtime-template450> - [x] Definieer routes met chaining (`.get(...).post(...)`)451> - [x] Voeg `logger`, `cors`, `secureHeaders` toe als globale middleware452> - [x] Valideer elke input met `@hono/zod-validator`453> - [x] Exporteer `AppType` en consumeer de API met type-safe `hc`-client454> - [x] Schrijf tests met `app.request()` — geen HTTP-server nodig455> - [x] Deploy met `wrangler deploy` (CF), `vercel deploy` of de bundler van je runtime456
:Hono: het ultrasnelle webframework gebouwd op Web Standardslines 1-456 (END) — press q to close