spinny:~/writing $ vim hono-framework-guide.md
1~2Per anni la scelta di un framework web in JavaScript significava accettare un compromesso: Express era universale ma lento e legato a Node.js, Fastify era veloce ma sempre Node-only, Next.js era completo ma pesante. Quando sono arrivati i runtime edge - Cloudflare Workers, Deno Deploy, Bun, Vercel Edge - quei framework hanno mostrato i loro limiti: dipendenze incompatibili, bundle giganti, API legate a `req`/`res` di Node.3~4[Hono](https://hono.dev) (giapponese per "fiamma" 🔥) e la risposta moderna a quel problema. E un framework di meno di 14KB, costruito interamente sui Web Standards (`Request`, `Response`, `fetch`), che gira ovunque ci sia un runtime JavaScript. Lo stesso codice viene deployato su Cloudflare Workers, Bun, Deno, Node.js, Vercel, Netlify e AWS Lambda senza modifiche.5~6## Perche Hono7~8Hono fa tre cose meglio di chiunque altro:9~101. **Performance.** Il `RegExpRouter` compila tutti i pattern di route in una singola regex, evitando i loop lineari dei router tradizionali. I benchmark mostrano oltre 400.000 op/s, rendendo Hono uno dei router piu veloci nell'ecosistema JavaScript.112. **Portabilita.** Web Standards significa zero dipendenze da Node. Lo stesso `app.fetch` viene esportato come default in un Cloudflare Worker, passato a `Bun.serve`, montato in un Deno server o adattato a `@hono/node-server`.123. **DX TypeScript-first.** Path parameters tipati come literal, RPC client end-to-end type-safe, validatori che inferiscono i tipi di input e output. L'autocompletion e quasi telepatica.13~14```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```29~30## Iniziare31~32Il modo piu rapido e lo starter ufficiale, che genera il progetto e il template per il runtime scelto.33~34```bash35npm create hono@latest my-api36cd my-api37npm install38npm run dev39```40~41Lo starter chiede quale template usare: `cloudflare-workers`, `bun`, `deno`, `nodejs`, `vercel`, `aws-lambda`, `nextjs`, e altri. Sceglie configurazioni e script di deploy adeguati. Per provare al volo, puoi anche partire da un singolo file:42~43```typescript44// src/index.ts45import { Hono } from 'hono'46~47const app = new Hono()48~49app.get('/', (c) => c.text('Hello Hono!'))50~51export default app52```53~54Su Cloudflare Workers questo basta. Su Bun: `Bun.serve({ fetch: app.fetch, port: 3000 })`. Su Node: `serve({ fetch: app.fetch })` da `@hono/node-server`. La superficie e identica.55~56## Routing57~58Le route si dichiarano con i metodi HTTP e supportano parametri, wildcard e regex.59~60```typescript61import { Hono } from 'hono'62~63const app = new Hono()64~65app.get('/', (c) => c.text('Home'))66app.get('/posts/:id', (c) => {67 const id = c.req.param('id') // string, tipato68 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```80~81I parametri sono inferiti come literal type: TypeScript sa che `c.req.param('id')` restituisce `string` solo se hai dichiarato `:id` nel pattern. Sbagliarne il nome e un errore di compilazione.82~83### Raggruppare le route84~85`app.route()` permette di comporre sotto-applicazioni come moduli, ognuna con il suo prefisso.86~87```typescript88// routes/posts.ts89import { Hono } from 'hono'90~91const posts = new Hono()92posts.get('/', (c) => c.json({ posts: [] }))93posts.get('/:id', (c) => c.json({ id: c.req.param('id') }))94export default posts95~96// src/index.ts97import { Hono } from 'hono'98import posts from './routes/posts'99~100const app = new Hono()101app.route('/posts', posts)102```103~104Le route nidificate ereditano il base path e il tipo dell'app radice, quindi il client RPC vede tutta la struttura.105~106## L'oggetto Context107~108Ogni handler riceve un `c` - il Context per la richiesta corrente. E l'unica API che devi imparare per leggere input e produrre output.109~110```typescript111app.post('/echo', async (c) => {112 // Lettura113 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 su Cloudflare)117~118 // Variabili condivise tra middleware e handler119 c.set('requestId', crypto.randomUUID())120 const id = c.get('requestId')121~122 // Risposta123 c.header('X-Request-Id', id)124 c.status(200)125 return c.json({ userAgent, page, body, id })126})127```128~129I metodi di risposta principali sono `c.text`, `c.json`, `c.html`, `c.body` (raw) e `c.redirect`. Tutti accettano uno status code come secondo argomento.130~131## Middleware: il modello a cipolla132~133I middleware sono funzioni `(c, next) => ...` che possono eseguire codice prima e dopo l'handler. Componendoli, ottieni il classico onion model: il primo middleware registrato e il primo a iniziare e l'ultimo a finire.134~135```typescript136import { Hono } from 'hono'137import { logger } from 'hono/logger'138import { cors } from 'hono/cors'139import { secureHeaders } from 'hono/secure-headers'140~141const app = new Hono()142~143app.use('*', logger())144app.use('*', secureHeaders())145app.use('/api/*', cors({ origin: 'https://spinny.dev' }))146~147app.use('*', async (c, next) => {148 const start = performance.now()149 await next()150 c.header('X-Response-Time', `${performance.now() - start}ms`)151})152```153~154`await next()` cede il controllo al middleware successivo. Tutto cio che segue gira in fase di response, dopo che l'handler ha prodotto un risultato. Restituire una `Response` prima di `next()` interrompe la catena.155~156### Middleware integrati157~158Hono spedisce con un set ricco di middleware production-ready, importabili da `hono/...`:159~160| Middleware | Cosa fa |161|-----------|---------|162| `logger` | Log strutturato di metodo, path, status, durata |163| `cors` | CORS configurabile per origin, metodi, headers |164| `csrf` | Protezione CSRF basata su origin |165| `secureHeaders` | Imposta CSP, HSTS, X-Frame-Options |166| `bearerAuth` / `basicAuth` | Auth Bearer/Basic out of the box |167| `jwt` | Verifica e firma JWT con `jose` |168| `etag` | Genera ETag e gestisce 304 |169| `cache` | Cache su Web Cache API |170| `compress` | gzip/deflate sulla response |171| `bodyLimit` | Rifiuta body oltre una soglia |172| `timing` | Server-Timing header per profiling |173~174### Middleware custom type-safe175~176Per estendere il `Context` con variabili tipate, usa `createMiddleware`:177~178```typescript179import { createMiddleware } from 'hono/factory'180~181type AuthVars = { userId: string; role: 'user' | 'admin' }182~183export 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)187~188 const payload = await verifyJwt(token)189 c.set('userId', payload.sub)190 c.set('role', payload.role)191 await next()192 }193)194~195// Uso196app.get('/me', requireAuth, (c) => {197 const userId = c.var.userId // string, tipato198 return c.json({ userId })199})200```201~202A valle del middleware, `c.var.userId` e tipato senza alcun cast. Questo si propaga attraverso tutta la catena.203~204## Validazione con Zod205~206`@hono/zod-validator` integra Zod nel ciclo richiesta. Definisci uno schema, lo applichi alla route e ottieni input gia validati e tipati.207~208```typescript209import { Hono } from 'hono'210import { zValidator } from '@hono/zod-validator'211import { z } from 'zod'212~213const 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})218~219app.post(220 '/posts',221 zValidator('json', createPost),222 (c) => {223 const data = c.req.valid('json') // tipato secondo lo schema224 return c.json({ ok: true, post: data }, 201)225 }226)227```228~229Se il body non valida, Hono risponde 400 con l'errore Zod prima ancora di chiamare l'handler. Puoi validare anche `query`, `param`, `header`, `cookie` e `form`.230~231## RPC: client type-safe end-to-end232~233La feature che differenzia davvero Hono dagli altri framework e l'**RPC mode**. Esporti il tipo della tua app dal server, il client `hc` lo importa e ottieni autocompletion completa - inclusi path, query, body, headers e response - senza generazione di codice o OpenAPI.234~235```typescript236// server.ts237import { Hono } from 'hono'238import { zValidator } from '@hono/zod-validator'239import { z } from 'zod'240~241const 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 )250~251export type AppType = typeof app252export default app253```254~255```typescript256// client.ts257import { hc } from 'hono/client'258import type { AppType } from './server'259~260const client = hc<AppType>('https://api.spinny.dev')261~262const 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}267~268const created = await client.posts.$post({269 json: { title: 'Ciao', body: 'Hono e una fiamma' }, // validato270})271```272~273Cambi un nome di route sul server e il TypeScript del client si rompe immediatamente in CI. E lo stesso vantaggio di tRPC, ma su HTTP standard, senza middleware specifici e con un bundle minuscolo.274~275### Status code discriminanti276~277Se restituisci status diversi, il client li discrimina automaticamente.278~279```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```286~287```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```296~297## Routers e performance298~299Hono offre cinque router con trade-off diversi. Il default e lo `SmartRouter`, che a startup misura quale router serve meglio le tue route e si fissa su quello.300~301| Router | Punti di forza | Quando usarlo |302|--------|----------------|---------------|303| `RegExpRouter` | Massima velocita, regex compilata | Default per la maggior parte delle API |304| `TrieRouter` | Supporta tutti i pattern | Pattern complessi non gestiti dal RegExp |305| `SmartRouter` | Sceglie il migliore in automatico | Default consigliato |306| `LinearRouter` | Registrazione velocissima | Worker one-shot, cold start critici |307| `PatternRouter` | Bundle minimo (<15KB) | Vincoli di size estremi |308~309Per worker stateless con cold start frequenti, `LinearRouter` evita il costo di compilazione iniziale ed e 33x piu veloce di `find-my-way` quando si misura registrazione + match.310~311```typescript312import { Hono } from 'hono'313import { LinearRouter } from 'hono/router/linear-router'314~315const app = new Hono({ router: new LinearRouter() })316```317~318## Deploy multi-runtime319~320Lo stesso `export default app` cambia destinazione cambiando solo il file di entry.321~322### Cloudflare Workers323~324```typescript325// src/index.ts326import { Hono } from 'hono'327~328type Bindings = { MY_KV: KVNamespace; DB: D1Database }329const app = new Hono<{ Bindings: Bindings }>()330~331app.get('/cache/:key', async (c) => {332 const value = await c.env.MY_KV.get(c.req.param('key'))333 return c.json({ value })334})335~336export default app337```338~339Deploy: `npx wrangler deploy`. Bindings KV/D1/R2/Queues sono nativi.340~341### Bun342~343```typescript344import { Hono } from 'hono'345const app = new Hono()346app.get('/', (c) => c.text('Bun + Hono'))347~348Bun.serve({ fetch: app.fetch, port: 3000 })349```350~351### Node.js352~353```typescript354import { serve } from '@hono/node-server'355import { Hono } from 'hono'356~357const app = new Hono()358app.get('/', (c) => c.text('Node + Hono'))359~360serve({ fetch: app.fetch, port: 3000 })361```362~363### Deno364~365```typescript366import { Hono } from 'jsr:@hono/hono'367const app = new Hono()368app.get('/', (c) => c.text('Deno + Hono'))369Deno.serve(app.fetch)370```371~372### Vercel373~374```typescript375// api/[[...route]].ts376import { Hono } from 'hono'377import { handle } from 'hono/vercel'378~379const app = new Hono().basePath('/api')380app.get('/hello', (c) => c.json({ msg: 'Hello from Vercel' }))381~382export const GET = handle(app)383export const POST = handle(app)384```385~386Lo stesso codice business, lo stesso routing, lo stesso middleware. Cambia solo l'adattatore.387~388## Esempio pratico: REST API con auth e DB389~390Mettiamo insieme i pezzi. Una API per un blog su Cloudflare Workers + D1, con JWT auth, validazione e RPC.391~392```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'399~400type Bindings = { DB: D1Database; JWT_SECRET: string }401type Variables = { jwtPayload: { sub: string } }402~403const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()404~405app.use('*', logger())406app.use('/api/*', cors({ origin: 'https://spinny.dev', credentials: true }))407~408const auth = (c: any, next: any) =>409 jwt({ secret: c.env.JWT_SECRET })(c, next)410~411const api = app.basePath('/api')412~413api.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})419~420api.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)437~438api.onError((err, c) => {439 console.error(err)440 return c.json({ error: 'Internal error' }, 500)441})442~443export type AppType = typeof api444export default app445```446~447Il client React lo consuma con type safety totale:448~449```typescript450import { hc } from 'hono/client'451import type { AppType } from '../api/src/index'452~453const api = hc<AppType>(import.meta.env.VITE_API_URL)454~455const res = await api.posts.$post({456 json: { title: 'Hono in 2026', body: '...' },457}, {458 headers: { Authorization: `Bearer ${token}` },459})460```461~462## Test463~464`app.request()` permette di testare le route senza far partire un server HTTP. E lo stesso path che useresti in produzione, eseguito in-memory.465~466```typescript467import { describe, it, expect } from 'vitest'468import app from '../src/index'469~470describe('GET /api/posts', () => {471 it('restituisce la lista dei post', 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```479~480Su Cloudflare Workers, `@cloudflare/vitest-pool-workers` fa girare gli stessi test dentro un Worker reale con bindings mock - massimo realismo, zero deploy.481~482## Best practice483~484### 1. Concatenare le definizioni di route485~486```typescript487const app = new Hono()488 .get('/posts', handler1)489 .post('/posts', handler2)490 .get('/posts/:id', handler3)491```492~493Il chaining mantiene il tipo di `app` aggiornato a ogni route, fondamentale per il client RPC. Definirle separatamente con `app.get(...)` su righe distinte rompe l'inferenza.494~495### 2. Esporta il tipo, non l'implementazione496~497Il client deve importare `AppType`, non l'app. `import type` garantisce che la build del frontend non includa il codice del backend.498~499### 3. Mantieni un router per dominio500~501Una sotto-app per `posts`, una per `users`, una per `webhooks`. Le componi con `app.route()` e ognuna ha i propri middleware. La struttura scala senza mega-file.502~503### 4. Validazione al bordo, sempre504~505Ogni input esterno (body, query, header) deve passare per `zValidator`. Non fidarti dei dati a valle: anche un cast TypeScript senza validazione runtime e un bug in attesa.506~507### 5. Sfrutta i bindings, non i client globali508~509Su Cloudflare, accedi a KV/D1/R2 via `c.env`. Niente singleton globali, niente connessioni che persistono tra Worker. Il modello stateless e una feature, non una limitazione.510~511### 6. Misura prima di ottimizzare il router512~513Lo `SmartRouter` di default va bene per il 95% dei casi. Cambia router solo dopo aver profilato e visto un collo di bottiglia reale.514~515## Conclusione516~517Hono e diventato nel 2026 lo standard de facto per costruire API edge-ready in TypeScript. La combinazione di Web Standards, performance, type safety e portabilita risolve esattamente i problemi che bloccavano i framework tradizionali: lock-in al runtime, bundle pesanti, type system fragile tra client e server.518~519Lo userai per microservizi su Cloudflare Workers, per sostituire Express in un monolito Node, per una function su Vercel o per una API in Bun. La stessa knowledge si trasferisce ovunque - ed e questo che lo rende un investimento solido per i prossimi anni.520~521> **Checklist per Iniziare:**522>523> - [x] `npm create hono@latest` e scegli il template del runtime524> - [x] Definisci le route con il chaining (`.get(...).post(...)`)525> - [x] Aggiungi `logger`, `cors`, `secureHeaders` come middleware globali526> - [x] Valida ogni input con `@hono/zod-validator`527> - [x] Esporta `AppType` e consuma l'API col client `hc` type-safe528> - [x] Scrivi test con `app.request()` senza HTTP server529> - [x] Deploy con `wrangler deploy` (CF), `vercel deploy` o il bundler del tuo runtime530~
NORMAL · hono-framework-guide.md [readonly]530 lines · :q to close