spinny:~/writing $ vim hono-framework-guide.md
1~2Durante años, elegir un framework web en JavaScript significaba aceptar un compromiso: Express era universal pero lento y atado a Node, Fastify era rápido pero solo para Node, Next.js era completo pero pesado. Cuando llegaron los runtimes edge - Cloudflare Workers, Deno Deploy, Bun, Vercel Edge - esos frameworks mostraron sus límites: dependencias incompatibles, bundles enormes, APIs ligadas a `req`/`res` de Node.3~4[Hono](https://hono.dev) (japonés para "llama" 🔥) es la respuesta moderna. Un framework de menos de 14KB, construido enteramente sobre Web Standards (`Request`, `Response`, `fetch`), que corre dondequiera que exista un runtime JavaScript. El mismo código se despliega en Cloudflare Workers, Bun, Deno, Node.js, Vercel, Netlify y AWS Lambda - sin cambios.5~6## Por qué Hono7~8Hono hace tres cosas mejor que cualquier otro:9~101. **Performance.** El `RegExpRouter` compila todos los patrones de ruta en una única regex, evitando los bucles lineales de los routers tradicionales. Los benchmarks superan las 400.000 ops/s, situando a Hono entre los routers más rápidos del ecosistema JavaScript.112. **Portabilidad.** Web Standards significa cero dependencias de Node. El mismo `app.fetch` se exporta como default en un Cloudflare Worker, se pasa a `Bun.serve`, se monta en un servidor Deno o se adapta con `@hono/node-server`.123. **DX TypeScript-first.** Path parameters tipados como literales, cliente RPC type-safe end-to-end, validadores que infieren tipos de entrada y salida. El autocompletado es casi telepático.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## Empezar31~32La forma más rápida es el starter oficial, que andamia el proyecto para el runtime elegido.33~34```bash35npm create hono@latest my-api36cd my-api37npm install38npm run dev39```40~41El starter pregunta qué template usar: `cloudflare-workers`, `bun`, `deno`, `nodejs`, `vercel`, `aws-lambda`, `nextjs` y más. Elige las configs y scripts de despliegue adecuados. Para probar al vuelo, también puedes empezar desde un solo archivo: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~54En Cloudflare Workers esto basta. En Bun: `Bun.serve({ fetch: app.fetch, port: 3000 })`. En Node: `serve({ fetch: app.fetch })` desde `@hono/node-server`. La superficie es idéntica.55~56## Routing57~58Las rutas se declaran con métodos de verbos HTTP y soportan parámetros, wildcards y 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, tipado68 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~81Los parámetros se infieren como tipos literales: TypeScript sabe que `c.req.param('id')` retorna `string` solo si declaraste `:id` en el patrón. Equivocarse en el nombre es un error de compilación.82~83### Agrupar rutas84~85`app.route()` permite componer subaplicaciones como módulos, cada una con su prefijo.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~104Las rutas anidadas heredan el base path y el tipo de la app raíz, por lo que el cliente RPC ve toda la estructura.105~106## El objeto Context107~108Cada handler recibe un `c` - el Context para la solicitud actual. Es la única API que necesitas aprender para leer entradas y producir salidas.109~110```typescript111app.post('/echo', async (c) => {112 // Lectura113 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 en Cloudflare)117~118 // Variables compartidas entre middleware y handler119 c.set('requestId', crypto.randomUUID())120 const id = c.get('requestId')121~122 // Respuesta123 c.header('X-Request-Id', id)124 c.status(200)125 return c.json({ userAgent, page, body, id })126})127```128~129Los métodos de respuesta principales son `c.text`, `c.json`, `c.html`, `c.body` (raw) y `c.redirect`. Todos aceptan un código de estado como segundo argumento.130~131## Middleware: el modelo en cebolla132~133Los middleware son funciones `(c, next) => ...` que pueden ejecutar código antes y después del handler. Compuestos forman el clásico modelo en cebolla: el primer middleware registrado es el primero en empezar y el último en terminar.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 el control al siguiente middleware. Todo lo posterior corre en la fase de respuesta, después de que el handler haya producido un resultado. Devolver una `Response` antes de `next()` corta la cadena.155~156### Middleware integrados157~158Hono incluye un set rico de middleware listos para producción, importables desde `hono/...`:159~160| Middleware | Para qué sirve |161|-----------|---------|162| `logger` | Logs estructurados de método, path, status, duración |163| `cors` | CORS configurable por origin, métodos, headers |164| `csrf` | Protección CSRF basada en origin |165| `secureHeaders` | Establece CSP, HSTS, X-Frame-Options |166| `bearerAuth` / `basicAuth` | Auth Bearer/Basic out of the box |167| `jwt` | Verifica y firma JWT con `jose` |168| `etag` | Genera ETag y maneja 304 |169| `cache` | Cache vía Web Cache API |170| `compress` | gzip/deflate sobre la respuesta |171| `bodyLimit` | Rechaza bodies por encima de un umbral |172| `timing` | Header Server-Timing para profiling |173~174### Middleware custom type-safe175~176Para extender el `Context` con variables tipadas, 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, tipado198 return c.json({ userId })199})200```201~202Aguas abajo del middleware, `c.var.userId` está tipado sin ningún cast. Esto se propaga por toda la cadena.203~204## Validación con Zod205~206`@hono/zod-validator` integra Zod en el ciclo de la solicitud. Defines un schema, lo aplicas a la ruta y obtienes entradas ya validadas y tipadas.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') // tipado según el schema224 return c.json({ ok: true, post: data }, 201)225 }226)227```228~229Si el body no valida, Hono responde 400 con el error de Zod antes de llamar al handler. También puedes validar `query`, `param`, `header`, `cookie` y `form`.230~231## RPC: cliente type-safe end-to-end232~233La feature que realmente diferencia a Hono es el **modo RPC**. Exportas el tipo de tu app desde el servidor, el cliente `hc` lo importa y obtienes autocompletado completo - paths, query, body, headers y respuesta - sin codegen ni 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: 'Hola', body: 'Hono es una llama' }, // validado270})271```272~273Renombras una ruta en el servidor y el TypeScript del cliente rompe inmediatamente en CI. Mismo beneficio que tRPC, pero sobre HTTP estándar, sin middleware específico y con un bundle minúsculo.274~275### Status codes discriminantes276~277Si retornas estados distintos, el cliente los discrimina automáticamente.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 y performance298~299Hono ofrece cinco routers con distintos trade-offs. El default es el `SmartRouter`, que al arranque mide cuál sirve mejor tus rutas y se fija en él.300~301| Router | Fortalezas | Cuándo usar |302|--------|-----------|-------------|303| `RegExpRouter` | Velocidad máxima, regex compilada | Default para la mayoría de APIs |304| `TrieRouter` | Soporta todos los patrones | Patrones complejos no cubiertos por RegExp |305| `SmartRouter` | Elige el mejor automáticamente | Default recomendado |306| `LinearRouter` | Registro ultra rápido | Workers one-shot, cold start crítico |307| `PatternRouter` | Bundle mínimo (<15KB) | Restricciones de tamaño extremas |308~309Para workers stateless con cold starts frecuentes, `LinearRouter` evita el coste de compilación inicial y es 33x más rápido que `find-my-way` cuando se mide registro más matching.310~311```typescript312import { Hono } from 'hono'313import { LinearRouter } from 'hono/router/linear-router'314~315const app = new Hono({ router: new LinearRouter() })316```317~318## Despliegue multi-runtime319~320El mismo `export default app` cambia de destino con solo intercambiar el archivo de entrada.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~339Despliegue: `npx wrangler deploy`. Bindings KV/D1/R2/Queues son nativos.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~386Mismo código de negocio, mismo routing, mismo middleware. Solo cambia el adaptador.387~388## Ejemplo práctico: API REST con auth y BD389~390Juntando las piezas. Una API de blog en Cloudflare Workers + D1, con auth JWT, validación y 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~447Un cliente React la consume con type safety total: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 en 2026', body: '...' },457}, {458 headers: { Authorization: `Bearer ${token}` },459})460```461~462## Testing463~464`app.request()` permite testear rutas sin levantar un servidor HTTP. Es el mismo path que correrías en producción, ejecutado en memoria.465~466```typescript467import { describe, it, expect } from 'vitest'468import app from '../src/index'469~470describe('GET /api/posts', () => {471 it('retorna la lista de 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```479~480En Cloudflare Workers, `@cloudflare/vitest-pool-workers` corre los mismos tests dentro de un Worker real con bindings mock - máximo realismo, cero deploy.481~482## Mejores prácticas483~484### 1. Encadenar definiciones de ruta485~486```typescript487const app = new Hono()488 .get('/posts', handler1)489 .post('/posts', handler2)490 .get('/posts/:id', handler3)491```492~493El chaining mantiene el tipo de `app` actualizado con cada ruta, esencial para el cliente RPC. Definirlas separadas con `app.get(...)` en líneas distintas rompe la inferencia.494~495### 2. Exporta el tipo, no la implementación496~497El cliente debe importar `AppType`, no la app. `import type` garantiza que el build del frontend no incluya código de backend.498~499### 3. Un router por dominio500~501Una sub-app para `posts`, una para `users`, una para `webhooks`. Compón con `app.route()` y cada una posee su middleware. La estructura escala sin mega-archivos.502~503### 4. Validación en el borde, siempre504~505Toda entrada externa (body, query, header) debe pasar por `zValidator`. No confíes en datos aguas abajo: incluso un cast TypeScript sin validación runtime es un bug en espera.506~507### 5. Apóyate en bindings, no en clientes globales508~509En Cloudflare, accede a KV/D1/R2 vía `c.env`. Sin singletons globales, sin conexiones persistiendo entre Workers. El modelo stateless es una feature, no una limitación.510~511### 6. Mide antes de optimizar el router512~513El `SmartRouter` por defecto sirve para el 95% de los casos. Cambia de router solo después de haber profilado y visto un cuello de botella real.514~515## Conclusión516~517Hono se ha vuelto en 2026 el estándar de facto para construir APIs listas para el edge en TypeScript. La combinación de Web Standards, performance, type safety y portabilidad resuelve precisamente los problemas que frenaban a los frameworks tradicionales: lock-in al runtime, bundles pesados, type system frágil entre cliente y servidor.518~519Lo usarás para microservicios en Cloudflare Workers, para reemplazar Express en un monolito Node, para una function en Vercel o para una API en Bun. El mismo conocimiento se transfiere a todas partes - y eso es lo que lo hace una inversión sólida para los próximos años.520~521> **Checklist para empezar:**522>523> - [x] `npm create hono@latest` y elige tu template de runtime524> - [x] Define rutas con chaining (`.get(...).post(...)`)525> - [x] Añade `logger`, `cors`, `secureHeaders` como middleware globales526> - [x] Valida toda entrada con `@hono/zod-validator`527> - [x] Exporta `AppType` y consume la API con el cliente `hc` type-safe528> - [x] Escribe tests con `app.request()` - sin servidor HTTP529> - [x] Despliega con `wrangler deploy` (CF), `vercel deploy` o el bundler de tu runtime530~
NORMAL · hono-framework-guide.md [readonly]530 lines · :q to close