Για χρόνια, η επιλογή web framework JavaScript σήμαινε αποδοχή ενός trade-off: το Express ήταν παγκόσμιο αλλά αργό και δεμένο στο Node, το Fastify γρήγορο αλλά μόνο για Node, το Next.js πλήρες αλλά βαρύ. Όταν ήρθαν οι edge runtimes — Cloudflare Workers, Deno Deploy, Bun, Vercel Edge — αυτά τα frameworks έδειξαν τα όριά τους.
Το Hono (ιαπωνικά για "φλόγα" 🔥) είναι η σύγχρονη απάντηση. Ένα framework κάτω των 14KB, χτισμένο εξ ολοκλήρου πάνω στα Web Standards (Request, Response, fetch), που τρέχει οπουδήποτε υπάρχει JavaScript runtime. Ο ίδιος κώδικας κάνει deploy σε Cloudflare Workers, Bun, Deno, Node.js, Vercel, Netlify και AWS Lambda — χωρίς αλλαγές.
Γιατί Hono
- Επιδόσεις. Ο
RegExpRouterσυμπιέζει όλα τα route patterns σε ένα regex. Τα benchmarks ξεπερνούν τα 400.000 ops/s. - Φορητότητα. Web Standards σημαίνει μηδενικές εξαρτήσεις από Node.
- TypeScript-first DX. Path parameters συνάγονται ως literal types, end-to-end type-safe RPC client.
Ξεκινώντας
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
Στο Cloudflare Workers αυτό φτάνει. Στο Bun: Bun.serve({ fetch: app.fetch, port: 3000 }). Στο Node: serve({ fetch: app.fetch }) από το @hono/node-server.
Routing
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) })
Ομαδοποίηση routes
// 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)
Το αντικείμενο 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: το μοντέλο κρεμμυδιού
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`) })
Ενσωματωμένα middleware
| Middleware | Σκοπός |
|---|---|
logger | Δομημένα logs μεθόδου, διαδρομής, κατάστασης, διάρκειας |
cors | CORS με ρύθμιση ανά origin, μεθόδους, headers |
csrf | Προστασία CSRF βάσει origin |
secureHeaders | Ορίζει CSP, HSTS, X-Frame-Options |
bearerAuth / basicAuth | Έτοιμη Bearer/Basic auth |
jwt | Επιβεβαίωση/υπογραφή JWT με jose |
etag | Παράγει ETag και χειρίζεται 304 |
cache | Cache μέσω Web Cache API |
compress | gzip/deflate απάντησης |
bodyLimit | Απορρίπτει bodies πάνω από όριο |
timing | Header Server-Timing για profiling |
Type-safe custom 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 }) })
Validation με 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 client 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: 'Γεια', body: 'Το Hono είναι φλόγα' }, })
Διάκριση κωδικών κατάστασης
.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() }
Routers και επιδόσεις
| Router | Δυνατά σημεία | Πότε να χρησιμοποιηθεί |
|---|---|---|
RegExpRouter | Μέγιστη ταχύτητα, μεταγλωττισμένο regex | Default για τα περισσότερα APIs |
TrieRouter | Υποστηρίζει όλα τα patterns | Σύνθετα patterns που το RegExp δεν χειρίζεται |
SmartRouter | Επιλέγει αυτόματα το καλύτερο | Συνιστώμενο default |
LinearRouter | Υπερταχεία εγγραφή | One-shot workers, κρίσιμο cold start |
PatternRouter | Ελάχιστο bundle (<15KB) | Ακραίοι περιορισμοί μεγέθους |
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)
Πρακτικό παράδειγμα: REST API με auth και 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
Δοκιμές
import { describe, it, expect } from 'vitest' import app from '../src/index' describe('GET /api/posts', () => { it('επιστρέφει τη λίστα των posts', async () => { const res = await app.request('/api/posts') expect(res.status).toBe(200) const body = await res.json() expect(body.posts).toBeInstanceOf(Array) }) })
Βέλτιστες πρακτικές
1. Αλυσιδώστε τους ορισμούς routes
const app = new Hono() .get('/posts', handler1) .post('/posts', handler2) .get('/posts/:id', handler3)
2. Εξάγετε τον τύπο, όχι την υλοποίηση
Ο client πρέπει να εισάγει AppType.
3. Ένα router ανά τομέα
Sub-app για posts, users, webhooks.
4. Validation στα όρια, πάντα
Κάθε εξωτερικό input πρέπει να περάσει από zValidator.
5. Στηριχθείτε στα bindings, όχι σε global clients
Στο Cloudflare πρόσβαση σε KV/D1/R2 μέσω c.env.
6. Μετρήστε πριν βελτιστοποιήσετε το router
Ο default SmartRouter ταιριάζει στο 95% των περιπτώσεων.
Συμπέρασμα
Το Hono έγινε το 2026 το de facto στάνταρ για την κατασκευή edge-ready APIs σε TypeScript.
Λίστα εκκίνησης:
npm create hono@latestκαι επιλέξτε το template του runtime σας- Ορίστε routes με αλυσίδωση (
.get(...).post(...))- Προσθέστε
logger,cors,secureHeadersως global middleware- Επικυρώστε κάθε input με
@hono/zod-validator- Εξάγετε
AppTypeκαι καταναλώστε το API με τον type-safehcclient- Γράψτε tests με
app.request()— χωρίς HTTP server- Κάντε deploy με
wrangler deploy(CF),vercel deployή τον bundler του runtime σας