Po leta vyber JavaScript webového frameworku znamenal prijmout kompromis: Express byl univerzalni, ale pomaly a vazany na Node, Fastify rychly ale jen Node, Next.js plnohodnotny ale tezky. Kdyz prisly edge runtime — Cloudflare Workers, Deno Deploy, Bun, Vercel Edge — tyto frameworky ukazaly sve limity.
Hono (japonsky "plamen" 🔥) je moderni odpoved. Framework pod 14KB, plne postaveny na Web Standards (Request, Response, fetch), bezi vsude, kde existuje JavaScript runtime. Stejny kod se deployuje na Cloudflare Workers, Bun, Deno, Node.js, Vercel, Netlify a AWS Lambda — bez zmen.
Proc Hono
- Vykon.
RegExpRouterkompiluje vsechny route patterny do jednoho regexu. Benchmarky presahuji 400 000 ops/s. - Prenositelnost. Web Standards znamena nulove zavislosti na Node.
- TypeScript-first DX. Path parametry odvozeny jako literal typy, end-to-end type-safe RPC klient.
Zacatek
npm create hono@latest my-api cd my-api npm install npm run dev
// src/index.ts import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Hello Hono!')) export default app
Na Cloudflare Workers tohle staci. Na Bun: Bun.serve({ fetch: app.fetch, port: 3000 }). Na Node: serve({ fetch: app.fetch }) z @hono/node-server.
Routovani
import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Home')) app.get('/posts/:id', (c) => { const id = c.req.param('id') return c.json({ id }) }) app.get('/posts/:id/comments/:commentId', (c) => { const { id, commentId } = c.req.param() return c.json({ id, commentId }) }) app.get('/files/*', (c) => c.text('Wildcard')) app.post('/posts', async (c) => { const body = await c.req.json() return c.json({ created: body }, 201) })
Skupiny rout
// routes/posts.ts import { Hono } from 'hono' const posts = new Hono() posts.get('/', (c) => c.json({ posts: [] })) posts.get('/:id', (c) => c.json({ id: c.req.param('id') })) export default posts // src/index.ts import { Hono } from 'hono' import posts from './routes/posts' const app = new Hono() app.route('/posts', posts)
Objekt Context
app.post('/echo', async (c) => { const userAgent = c.req.header('User-Agent') const page = c.req.query('page') const body = await c.req.json() const env = c.env c.set('requestId', crypto.randomUUID()) const id = c.get('requestId') c.header('X-Request-Id', id) c.status(200) return c.json({ userAgent, page, body, id }) })
Middleware: cibulovy model
import { Hono } from 'hono' import { logger } from 'hono/logger' import { cors } from 'hono/cors' import { secureHeaders } from 'hono/secure-headers' const app = new Hono() app.use('*', logger()) app.use('*', secureHeaders()) app.use('/api/*', cors({ origin: 'https://spinny.dev' })) app.use('*', async (c, next) => { const start = performance.now() await next() c.header('X-Response-Time', `${performance.now() - start}ms`) })
Vestavene middleware
| Middleware | Ucel |
|---|---|
logger | Strukturovane logy metody, cesty, statusu, doby |
cors | CORS konfigurovatelny dle origin, metod, headeru |
csrf | Origin-zalozena ochrana CSRF |
secureHeaders | Nastavuje CSP, HSTS, X-Frame-Options |
bearerAuth / basicAuth | Hotova Bearer/Basic auth |
jwt | JWT verifikace/podpis pres jose |
etag | Generuje ETag a obsluhuje 304 |
cache | Cache pres Web Cache API |
compress | gzip/deflate odpovedi |
bodyLimit | Odmita body nad limit |
timing | Header Server-Timing pro profilovani |
Type-safe vlastni middleware
import { createMiddleware } from 'hono/factory' type AuthVars = { userId: string; role: 'user' | 'admin' } export const requireAuth = createMiddleware<{ Variables: AuthVars }>( async (c, next) => { const token = c.req.header('Authorization')?.replace('Bearer ', '') if (!token) return c.json({ error: 'Unauthorized' }, 401) const payload = await verifyJwt(token) c.set('userId', payload.sub) c.set('role', payload.role) await next() } ) app.get('/me', requireAuth, (c) => { const userId = c.var.userId return c.json({ userId }) })
Validace s Zod
import { Hono } from 'hono' import { zValidator } from '@hono/zod-validator' import { z } from 'zod' const createPost = z.object({ title: z.string().min(1).max(200), body: z.string().min(1), tags: z.array(z.string()).default([]), }) app.post( '/posts', zValidator('json', createPost), (c) => { const data = c.req.valid('json') return c.json({ ok: true, post: data }, 201) } )
RPC: type-safe klient end-to-end
// server.ts import { Hono } from 'hono' import { zValidator } from '@hono/zod-validator' import { z } from 'zod' const app = new Hono() .get('/posts/:id', (c) => c.json({ id: c.req.param('id'), title: 'Hello' }) ) .post( '/posts', zValidator('json', z.object({ title: z.string(), body: z.string() })), (c) => c.json({ ok: true }, 201) ) export type AppType = typeof app export default app
// client.ts import { hc } from 'hono/client' import type { AppType } from './server' const client = hc<AppType>('https://api.spinny.dev') const res = await client.posts[':id'].$get({ param: { id: '42' } }) if (res.ok) { const data = await res.json() console.log(data.title) } const created = await client.posts.$post({ json: { title: 'Ahoj', body: 'Hono je plamen' }, })
Diskriminace status kodu
.get('/posts/:id', (c) => { const post = findPost(c.req.param('id')) if (!post) return c.json({ error: 'not found' }, 404) return c.json({ post }, 200) })
const res = await client.posts[':id'].$get({ param: { id } }) if (res.status === 404) { const { error } = await res.json() } if (res.status === 200) { const { post } = await res.json() }
Routery a vykon
| Router | Silne stranky | Kdy pouzit |
|---|---|---|
RegExpRouter | Maximalni rychlost, kompilovany regex | Default pro vetsinu API |
TrieRouter | Podporuje vsechny patterny | Slozite patterny, ktere RegExp nezvlada |
SmartRouter | Voli nejlepsi automaticky | Doporuceny default |
LinearRouter | Ultrarychla registrace | One-shot workery, kriticky cold start |
PatternRouter | Minimalni bundle (<15KB) | Extremni omezeni velikosti |
import { Hono } from 'hono' import { LinearRouter } from 'hono/router/linear-router' const app = new Hono({ router: new LinearRouter() })
Multi-runtime deploy
Cloudflare Workers
import { Hono } from 'hono' type Bindings = { MY_KV: KVNamespace; DB: D1Database } const app = new Hono<{ Bindings: Bindings }>() app.get('/cache/:key', async (c) => { const value = await c.env.MY_KV.get(c.req.param('key')) return c.json({ value }) }) export default app
Deploy: npx wrangler deploy.
Bun
import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Bun + Hono')) Bun.serve({ fetch: app.fetch, port: 3000 })
Node.js
import { serve } from '@hono/node-server' import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => c.text('Node + Hono')) serve({ fetch: app.fetch, port: 3000 })
Deno
import { Hono } from 'jsr:@hono/hono' const app = new Hono() app.get('/', (c) => c.text('Deno + Hono')) Deno.serve(app.fetch)
Vercel
// api/[[...route]].ts import { Hono } from 'hono' import { handle } from 'hono/vercel' const app = new Hono().basePath('/api') app.get('/hello', (c) => c.json({ msg: 'Hello from Vercel' })) export const GET = handle(app) export const POST = handle(app)
Prakticky priklad: REST API s auth a DB
import { Hono } from 'hono' import { jwt } from 'hono/jwt' import { logger } from 'hono/logger' import { cors } from 'hono/cors' import { zValidator } from '@hono/zod-validator' import { z } from 'zod' type Bindings = { DB: D1Database; JWT_SECRET: string } type Variables = { jwtPayload: { sub: string } } const app = new Hono<{ Bindings: Bindings; Variables: Variables }>() app.use('*', logger()) app.use('/api/*', cors({ origin: 'https://spinny.dev', credentials: true })) const auth = (c: any, next: any) => jwt({ secret: c.env.JWT_SECRET })(c, next) const api = app.basePath('/api') api.get('/posts', async (c) => { const { results } = await c.env.DB .prepare('SELECT id, title, created_at FROM posts ORDER BY created_at DESC LIMIT 50') .all() return c.json({ posts: results }) }) api.post( '/posts', auth, zValidator('json', z.object({ title: z.string().min(1).max(200), body: z.string().min(1), })), async (c) => { const { title, body } = c.req.valid('json') const userId = c.var.jwtPayload.sub const result = await c.env.DB .prepare('INSERT INTO posts (title, body, author_id) VALUES (?, ?, ?) RETURNING id') .bind(title, body, userId) .first<{ id: number }>() return c.json({ id: result?.id }, 201) } ) api.onError((err, c) => { console.error(err) return c.json({ error: 'Internal error' }, 500) }) export type AppType = typeof api export default app
Testovani
import { describe, it, expect } from 'vitest' import app from '../src/index' describe('GET /api/posts', () => { it('vraci seznam prispevku', async () => { const res = await app.request('/api/posts') expect(res.status).toBe(200) const body = await res.json() expect(body.posts).toBeInstanceOf(Array) }) })
Nejlepsi praktiky
1. Retezte definice rout
const app = new Hono() .get('/posts', handler1) .post('/posts', handler2) .get('/posts/:id', handler3)
2. Exportuj typ, ne implementaci
Klient musi importovat AppType.
3. Jeden router na domenu
Sub-app pro posts, users, webhooks.
4. Validace na hranici, vzdy
Kazdy externi input musi projit pres zValidator.
5. Spolehni se na bindings, ne na globalni klienty
Na Cloudflare pristupuj ke KV/D1/R2 pres c.env.
6. Mer pred optimalizaci routeru
Defaultni SmartRouter vyhovuje v 95% pripadu.
Zaver
Hono se v roce 2026 stalo de facto standardem pro stavbu edge-ready API v TypeScriptu.
Checklist na start:
npm create hono@latesta vyber sablonu runtime- Definuj routy retezenim (
.get(...).post(...))- Pridej
logger,cors,secureHeadersjako globalni middleware- Validuj kazdy input pres
@hono/zod-validator- Exportuj
AppTypea konzumuj API pres type-safehcklienta- Pis testy pres
app.request()— bez HTTP serveru- Deployuj pres
wrangler deploy(CF),vercel deploynebo bundler tveho runtime