Hono - Ultraszybki Web Framework dla Edge
Czym jest Hono?
Hono (炎 - "płomień" po japońsku) to minimalistyczny, ultraszybki web framework zaprojektowany dla edge computing. Z rozmiarem około 14KB, działa na praktycznie każdym środowisku JavaScript: Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, Vercel i innych. Hono oferuje Express-podobne API z nowoczesnymi funkcjami i pełnym wsparciem TypeScript.
Dlaczego Hono?
Problemy z tradycyjnymi frameworkami
- Express: Duży, powolny, brak natywnego TypeScript
- Fastify: Głównie dla Node.js, skomplikowana konfiguracja
- Koa: Mały ekosystem, brak edge support
- Next.js API Routes: Tylko dla Next.js, overhead frameworka
Zalety Hono
- Ultraszybki - Jeden z najszybszych frameworków w benchmarkach
- Mały rozmiar - ~14KB, zero zależności
- Multi-runtime - Działa wszędzie: Edge, Deno, Bun, Node.js
- Type-safe - Pełne wsparcie TypeScript z inference
- Web Standards - Oparte na Fetch API, Request/Response
- Rich middleware - Wbudowane middleware dla typowych zadań
Instalacja
Dla różnych środowisk
# Cloudflare Workers
npm create hono@latest my-app
# Wybierz: cloudflare-workers
# Deno
deno add npm:hono
# Bun
bun create hono my-app
# Node.js
npm install hono @hono/node-serverPodstawowy projekt
npm create hono@latest my-api
cd my-api
npm install
npm run devPodstawy Hono
Hello World
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.text('Hello Hono!')
})
app.get('/json', (c) => {
return c.json({ message: 'Hello JSON!', timestamp: Date.now() })
})
app.get('/html', (c) => {
return c.html('<h1>Hello HTML!</h1>')
})
export default appContext object (c)
Context c to główny obiekt w Hono, zawierający request, response helpers i więcej:
app.get('/user/:id', (c) => {
// Request info
const method = c.req.method // GET, POST, etc.
const url = c.req.url // Full URL
const path = c.req.path // Path only
const headers = c.req.header() // All headers
const userAgent = c.req.header('User-Agent')
// URL params
const id = c.req.param('id')
// Query strings
const page = c.req.query('page')
const queries = c.req.queries('tags') // Multiple values
// Response helpers
return c.json({ id, page })
})Routing
Podstawowe metody HTTP
const app = new Hono()
// Podstawowe metody
app.get('/users', (c) => c.json([]))
app.post('/users', (c) => c.json({ created: true }))
app.put('/users/:id', (c) => c.json({ updated: true }))
app.patch('/users/:id', (c) => c.json({ patched: true }))
app.delete('/users/:id', (c) => c.json({ deleted: true }))
// Wszystkie metody
app.all('/webhook', (c) => c.text('Received'))
// Wiele metod
app.on(['GET', 'POST'], '/data', (c) => c.text('GET or POST'))Parametry URL
// Pojedynczy parametr
app.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ userId: id })
})
// Wiele parametrów
app.get('/users/:userId/posts/:postId', (c) => {
const { userId, postId } = c.req.param()
return c.json({ userId, postId })
})
// Opcjonalny parametr
app.get('/posts/:id?', (c) => {
const id = c.req.param('id')
if (id) {
return c.json({ postId: id })
}
return c.json({ posts: [] })
})
// Wildcard
app.get('/files/*', (c) => {
const path = c.req.param('*')
return c.text(`File path: ${path}`)
})
// Regex pattern
app.get('/user/:id{[0-9]+}', (c) => {
const id = c.req.param('id')
return c.json({ numericId: parseInt(id) })
})Grupowanie routów
const app = new Hono()
// Tworzenie sub-aplikacji
const api = new Hono()
api.get('/users', (c) => c.json([]))
api.get('/users/:id', (c) => c.json({ id: c.req.param('id') }))
api.post('/users', (c) => c.json({ created: true }))
api.get('/posts', (c) => c.json([]))
api.get('/posts/:id', (c) => c.json({ id: c.req.param('id') }))
// Montowanie pod ścieżką
app.route('/api/v1', api)
// Teraz dostępne:
// GET /api/v1/users
// GET /api/v1/users/:id
// POST /api/v1/users
// GET /api/v1/posts
// GET /api/v1/posts/:idBazowa ścieżka
// Ustawienie bazowej ścieżki dla całej aplikacji
const app = new Hono().basePath('/api')
app.get('/users', (c) => c.json([])) // GET /api/users
app.get('/posts', (c) => c.json([])) // GET /api/postsRequest handling
Body parsing
// JSON body
app.post('/users', async (c) => {
const body = await c.req.json()
return c.json({ received: body })
})
// Form data
app.post('/upload', async (c) => {
const formData = await c.req.formData()
const name = formData.get('name')
const file = formData.get('file')
return c.json({ name, fileName: file?.name })
})
// Raw text
app.post('/webhook', async (c) => {
const text = await c.req.text()
return c.text(`Received: ${text}`)
})
// Array buffer (binary)
app.post('/binary', async (c) => {
const buffer = await c.req.arrayBuffer()
return c.text(`Received ${buffer.byteLength} bytes`)
})
// Parsed body (auto-detect content type)
app.post('/auto', async (c) => {
const body = await c.req.parseBody()
return c.json(body)
})Walidacja z Zod
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
// Schema dla body
const createUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().int().min(0).optional(),
})
// Schema dla query params
const paginationSchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(10),
})
// Schema dla params
const idSchema = z.object({
id: z.string().uuid(),
})
// Walidacja body
app.post(
'/users',
zValidator('json', createUserSchema),
(c) => {
const data = c.req.valid('json')
// data jest typu { name: string, email: string, age?: number }
return c.json({ created: data })
}
)
// Walidacja query
app.get(
'/users',
zValidator('query', paginationSchema),
(c) => {
const { page, limit } = c.req.valid('query')
return c.json({ page, limit })
}
)
// Walidacja params
app.get(
'/users/:id',
zValidator('param', idSchema),
(c) => {
const { id } = c.req.valid('param')
return c.json({ userId: id })
}
)
// Wiele walidatorów
app.put(
'/users/:id',
zValidator('param', idSchema),
zValidator('json', createUserSchema.partial()),
(c) => {
const { id } = c.req.valid('param')
const data = c.req.valid('json')
return c.json({ updated: { id, ...data } })
}
)
// Custom error handling
app.post(
'/validated',
zValidator('json', createUserSchema, (result, c) => {
if (!result.success) {
return c.json(
{
error: 'Validation failed',
details: result.error.issues,
},
400
)
}
}),
(c) => {
const data = c.req.valid('json')
return c.json(data)
}
)Middleware
Wbudowane middleware
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { prettyJSON } from 'hono/pretty-json'
import { secureHeaders } from 'hono/secure-headers'
import { compress } from 'hono/compress'
import { etag } from 'hono/etag'
import { timing } from 'hono/timing'
import { basicAuth } from 'hono/basic-auth'
import { bearerAuth } from 'hono/bearer-auth'
import { jwt } from 'hono/jwt'
const app = new Hono()
// Logger - loguje requesty
app.use('*', logger())
// CORS
app.use('/api/*', cors({
origin: ['http://localhost:3000', 'https://example.com'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization'],
exposeHeaders: ['X-Total-Count'],
maxAge: 86400,
credentials: true,
}))
// Pretty JSON (dla developmentu)
app.use('*', prettyJSON())
// Secure headers
app.use('*', secureHeaders())
// Kompresja
app.use('*', compress())
// ETag
app.use('/static/*', etag())
// Timing (Server-Timing header)
app.use('*', timing())
// Basic Auth
app.use('/admin/*', basicAuth({
username: 'admin',
password: 'secret',
}))
// Bearer Auth
app.use('/api/*', bearerAuth({
token: 'my-secret-token',
}))
// JWT Auth
app.use('/protected/*', jwt({
secret: process.env.JWT_SECRET!,
}))
app.get('/protected/data', (c) => {
const payload = c.get('jwtPayload')
return c.json({ user: payload })
})Custom middleware
import { Hono, Next } from 'hono'
import type { Context } from 'hono'
const app = new Hono()
// Proste middleware
const requestId = async (c: Context, next: Next) => {
const id = crypto.randomUUID()
c.set('requestId', id)
c.header('X-Request-ID', id)
await next()
}
// Middleware z logiką przed i po
const responseTime = async (c: Context, next: Next) => {
const start = Date.now()
await next()
const ms = Date.now() - start
c.header('X-Response-Time', `${ms}ms`)
}
// Error handling middleware
const errorHandler = async (c: Context, next: Next) => {
try {
await next()
} catch (err) {
console.error('Error:', err)
if (err instanceof HTTPException) {
return err.getResponse()
}
return c.json(
{ error: 'Internal Server Error' },
500
)
}
}
// Auth middleware z własną logiką
const authMiddleware = async (c: Context, next: Next) => {
const authHeader = c.req.header('Authorization')
if (!authHeader?.startsWith('Bearer ')) {
return c.json({ error: 'Unauthorized' }, 401)
}
const token = authHeader.slice(7)
try {
const user = await verifyToken(token)
c.set('user', user)
await next()
} catch {
return c.json({ error: 'Invalid token' }, 401)
}
}
// Użycie middleware
app.use('*', requestId)
app.use('*', responseTime)
app.use('*', errorHandler)
app.use('/api/*', authMiddleware)
app.get('/api/me', (c) => {
const user = c.get('user')
return c.json({ user })
})Middleware factory
// Middleware z konfiguracją
function rateLimit(options: {
limit: number
window: number // sekundy
}) {
const requests = new Map<string, { count: number; resetAt: number }>()
return async (c: Context, next: Next) => {
const ip = c.req.header('CF-Connecting-IP') ||
c.req.header('X-Forwarded-For') ||
'unknown'
const now = Date.now()
const record = requests.get(ip)
if (!record || now > record.resetAt) {
requests.set(ip, {
count: 1,
resetAt: now + options.window * 1000,
})
} else if (record.count >= options.limit) {
const retryAfter = Math.ceil((record.resetAt - now) / 1000)
c.header('Retry-After', String(retryAfter))
return c.json(
{ error: 'Too many requests' },
429
)
} else {
record.count++
}
await next()
}
}
// Użycie
app.use('/api/*', rateLimit({ limit: 100, window: 60 }))Response helpers
Różne typy odpowiedzi
app.get('/text', (c) => c.text('Plain text'))
app.get('/json', (c) => c.json({ message: 'JSON response' }))
app.get('/html', (c) => c.html('<h1>HTML response</h1>'))
app.get('/redirect', (c) => c.redirect('/new-location'))
app.get('/redirect-permanent', (c) => c.redirect('/new-location', 301))
app.get('/not-found', (c) => c.notFound())
// Custom status
app.post('/created', (c) => {
return c.json({ id: 1, name: 'New item' }, 201)
})
// Custom headers
app.get('/custom-headers', (c) => {
c.header('X-Custom', 'value')
c.header('Cache-Control', 'max-age=3600')
return c.json({ data: 'with headers' })
})
// Streaming response
app.get('/stream', (c) => {
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 5; i++) {
controller.enqueue(`Data chunk ${i}\n`)
await new Promise(r => setTimeout(r, 1000))
}
controller.close()
},
})
return new Response(stream, {
headers: { 'Content-Type': 'text/plain' },
})
})
// File download
app.get('/download', async (c) => {
const file = await fetch('https://example.com/file.pdf')
c.header('Content-Disposition', 'attachment; filename="file.pdf"')
return new Response(file.body, {
headers: c.res.headers,
})
})JSX/TSX Support
Hono ma wbudowane wsparcie dla JSX:
// Włącz JSX w tsconfig.json
// "jsx": "react-jsx",
// "jsxImportSource": "hono/jsx"
import { Hono } from 'hono'
import type { FC } from 'hono/jsx'
const app = new Hono()
// Komponent
const Layout: FC<{ title: string; children: any }> = ({ title, children }) => (
<html>
<head>
<title>{title}</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>{children}</body>
</html>
)
const UserCard: FC<{ name: string; email: string }> = ({ name, email }) => (
<div class="user-card">
<h2>{name}</h2>
<p>{email}</p>
</div>
)
// Użycie w route
app.get('/', (c) => {
return c.html(
<Layout title="Home">
<h1>Welcome!</h1>
<UserCard name="John" email="john@example.com" />
</Layout>
)
})
// Lista użytkowników
app.get('/users', async (c) => {
const users = await getUsers()
return c.html(
<Layout title="Users">
<h1>Users</h1>
<ul>
{users.map((user) => (
<li key={user.id}>
<a href={`/users/${user.id}`}>{user.name}</a>
</li>
))}
</ul>
</Layout>
)
})RPC Client
Hono oferuje type-safe RPC client:
// server.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
.get('/users', (c) => {
return c.json([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
])
})
.post(
'/users',
zValidator('json', z.object({
name: z.string(),
email: z.string().email(),
})),
(c) => {
const data = c.req.valid('json')
return c.json({ id: 3, ...data }, 201)
}
)
.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ id: parseInt(id), name: 'John' })
})
export type AppType = typeof app
export default app// client.ts
import { hc } from 'hono/client'
import type { AppType } from './server'
const client = hc<AppType>('http://localhost:8787')
// Type-safe API calls!
async function main() {
// GET /users
const usersRes = await client.users.$get()
const users = await usersRes.json()
// users jest typu { id: number, name: string }[]
// POST /users
const createRes = await client.users.$post({
json: {
name: 'New User',
email: 'new@example.com',
},
})
const created = await createRes.json()
// created jest typu { id: number, name: string, email: string }
// GET /users/:id
const userRes = await client.users[':id'].$get({
param: { id: '1' },
})
const user = await userRes.json()
}Cloudflare Workers
Podstawowa konfiguracja
// src/index.ts
import { Hono } from 'hono'
type Bindings = {
// KV Namespace
MY_KV: KVNamespace
// D1 Database
DB: D1Database
// Environment variables
API_KEY: string
}
const app = new Hono<{ Bindings: Bindings }>()
// Dostęp do bindings przez c.env
app.get('/kv/:key', async (c) => {
const key = c.req.param('key')
const value = await c.env.MY_KV.get(key)
return c.json({ key, value })
})
app.post('/kv/:key', async (c) => {
const key = c.req.param('key')
const { value } = await c.req.json()
await c.env.MY_KV.put(key, value)
return c.json({ success: true })
})
// D1 Database
app.get('/users', async (c) => {
const { results } = await c.env.DB
.prepare('SELECT * FROM users')
.all()
return c.json(results)
})
export default app# wrangler.toml
name = "my-api"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[vars]
API_KEY = "secret"
[[kv_namespaces]]
binding = "MY_KV"
id = "xxx"
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "xxx"Durable Objects
import { Hono } from 'hono'
// Durable Object class
export class Counter {
state: DurableObjectState
app: Hono
constructor(state: DurableObjectState) {
this.state = state
this.app = new Hono()
this.app.get('/value', async (c) => {
const value = (await this.state.storage.get('count')) || 0
return c.json({ count: value })
})
this.app.post('/increment', async (c) => {
const value = ((await this.state.storage.get('count')) as number || 0) + 1
await this.state.storage.put('count', value)
return c.json({ count: value })
})
}
fetch(request: Request) {
return this.app.fetch(request)
}
}
// Main app
type Bindings = {
COUNTER: DurableObjectNamespace
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/counter/:name/*', async (c) => {
const name = c.req.param('name')
const id = c.env.COUNTER.idFromName(name)
const obj = c.env.COUNTER.get(id)
const url = new URL(c.req.url)
url.pathname = url.pathname.replace(`/counter/${name}`, '')
return obj.fetch(new Request(url, c.req.raw))
})
export default appNode.js Adapter
// src/index.ts
import { Hono } from 'hono'
import { serve } from '@hono/node-server'
import { serveStatic } from '@hono/node-server/serve-static'
const app = new Hono()
// Serve static files
app.use('/static/*', serveStatic({ root: './public' }))
app.get('/', (c) => c.text('Hello from Node.js!'))
app.get('/api/users', (c) => {
return c.json([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
])
})
// Start server
const port = 3000
console.log(`Server running on http://localhost:${port}`)
serve({
fetch: app.fetch,
port,
})Bun Adapter
// src/index.ts
import { Hono } from 'hono'
import { serveStatic } from 'hono/bun'
const app = new Hono()
// Serve static files
app.use('/static/*', serveStatic({ root: './public' }))
app.get('/', (c) => c.text('Hello from Bun!'))
// Bun-specific features
app.get('/file', async (c) => {
const file = Bun.file('./data.json')
const content = await file.json()
return c.json(content)
})
export default {
port: 3000,
fetch: app.fetch,
}Deno Adapter
// main.ts
import { Hono } from 'npm:hono'
import { serveStatic } from 'npm:hono/deno'
const app = new Hono()
app.use('/static/*', serveStatic({ root: './public' }))
app.get('/', (c) => c.text('Hello from Deno!'))
Deno.serve({ port: 8000 }, app.fetch)Error handling
import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'
const app = new Hono()
// Custom error class
class NotFoundError extends Error {
constructor(message: string) {
super(message)
this.name = 'NotFoundError'
}
}
// Global error handler
app.onError((err, c) => {
console.error('Error:', err)
if (err instanceof HTTPException) {
return err.getResponse()
}
if (err instanceof NotFoundError) {
return c.json({ error: err.message }, 404)
}
return c.json({ error: 'Internal Server Error' }, 500)
})
// 404 handler
app.notFound((c) => {
return c.json({ error: 'Not Found', path: c.req.path }, 404)
})
// Throwing errors
app.get('/users/:id', async (c) => {
const id = c.req.param('id')
const user = await findUser(id)
if (!user) {
throw new NotFoundError(`User ${id} not found`)
}
return c.json(user)
})
// HTTP Exception
app.get('/protected', (c) => {
const auth = c.req.header('Authorization')
if (!auth) {
throw new HTTPException(401, {
message: 'Authorization required',
})
}
return c.json({ data: 'secret' })
})Testing
// app.ts
import { Hono } from 'hono'
const app = new Hono()
app.get('/users', (c) => c.json([{ id: 1, name: 'John' }]))
app.post('/users', async (c) => {
const body = await c.req.json()
return c.json({ id: 2, ...body }, 201)
})
export default app
// app.test.ts
import { describe, it, expect } from 'vitest'
import app from './app'
describe('Users API', () => {
it('GET /users returns users list', async () => {
const res = await app.request('/users')
expect(res.status).toBe(200)
const data = await res.json()
expect(data).toEqual([{ id: 1, name: 'John' }])
})
it('POST /users creates user', async () => {
const res = await app.request('/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: 'Jane' }),
})
expect(res.status).toBe(201)
const data = await res.json()
expect(data).toEqual({ id: 2, name: 'Jane' })
})
it('returns 404 for unknown routes', async () => {
const res = await app.request('/unknown')
expect(res.status).toBe(404)
})
})OpenAPI/Swagger
import { Hono } from 'hono'
import { swaggerUI } from '@hono/swagger-ui'
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
const app = new OpenAPIHono()
// Define route with OpenAPI schema
const getUserRoute = createRoute({
method: 'get',
path: '/users/{id}',
request: {
params: z.object({
id: z.string().openapi({
param: { name: 'id', in: 'path' },
example: '123',
}),
}),
},
responses: {
200: {
content: {
'application/json': {
schema: z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
}),
},
},
description: 'User found',
},
404: {
content: {
'application/json': {
schema: z.object({
error: z.string(),
}),
},
},
description: 'User not found',
},
},
})
app.openapi(getUserRoute, (c) => {
const { id } = c.req.valid('param')
// ...
return c.json({ id, name: 'John', email: 'john@example.com' })
})
// Swagger UI
app.get('/docs', swaggerUI({ url: '/doc' }))
app.doc('/doc', {
openapi: '3.0.0',
info: {
title: 'My API',
version: '1.0.0',
},
})
export default appBest practices
1. Organizacja projektu
src/
├── index.ts # Entry point
├── routes/
│ ├── users.ts # /users routes
│ ├── posts.ts # /posts routes
│ └── index.ts # Route aggregator
├── middleware/
│ ├── auth.ts
│ ├── logging.ts
│ └── index.ts
├── services/
│ ├── userService.ts
│ └── postService.ts
├── types/
│ └── index.ts
└── utils/
└── helpers.ts2. Modularyzacja routów
// routes/users.ts
import { Hono } from 'hono'
const users = new Hono()
users.get('/', (c) => c.json([]))
users.get('/:id', (c) => c.json({ id: c.req.param('id') }))
users.post('/', (c) => c.json({ created: true }))
export default users
// routes/index.ts
import { Hono } from 'hono'
import users from './users'
import posts from './posts'
const routes = new Hono()
routes.route('/users', users)
routes.route('/posts', posts)
export default routes
// index.ts
import { Hono } from 'hono'
import routes from './routes'
const app = new Hono()
app.route('/api', routes)
export default app3. Type-safe environment
type Bindings = {
DATABASE_URL: string
API_KEY: string
KV: KVNamespace
}
type Variables = {
user: { id: string; name: string }
requestId: string
}
const app = new Hono<{
Bindings: Bindings
Variables: Variables
}>()
app.use('*', async (c, next) => {
c.set('requestId', crypto.randomUUID())
await next()
})
app.get('/me', (c) => {
const user = c.get('user') // Type-safe!
return c.json(user)
})Porównanie z innymi frameworkami
| Cecha | Hono | Express | Fastify | Elysia |
|---|---|---|---|---|
| Rozmiar | ~14KB | ~200KB | ~120KB | ~35KB |
| TypeScript | Native | @types | Plugin | Native |
| Edge support | ✅ | ❌ | ❌ | ✅ |
| Bun | ✅ | ⚠️ | ⚠️ | ✅ |
| Deno | ✅ | ❌ | ❌ | ❌ |
| Node.js | ✅ | ✅ | ✅ | ✅ |
| RPC Client | ✅ | ❌ | ❌ | ✅ |
Podsumowanie
Hono to nowoczesny, ultraszybki framework idealny dla edge computing i serverless. Główne zalety:
- Wydajność - Jeden z najszybszych frameworków
- Uniwersalność - Działa na każdym runtime JavaScript
- Type-safety - Pełne wsparcie TypeScript
- Mały rozmiar - ~14KB bez zależności
- Web Standards - Oparte na Fetch API
- DX - Intuicyjne API podobne do Express
Jeśli budujesz API dla Cloudflare Workers, Deno Deploy, lub potrzebujesz lekkiego frameworka dla Node.js/Bun, Hono jest doskonałym wyborem.
Hono - Ultrafast Web Framework for the Edge
What is Hono?
Hono (炎 - "flame" in Japanese) is a minimalist, ultrafast web framework designed for edge computing. At approximately 14KB in size, it runs on virtually any JavaScript runtime: Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, Vercel, and more. Hono offers an Express-like API with modern features and full TypeScript support.
Why Hono?
Problems with traditional frameworks
- Express: Large, slow, no native TypeScript
- Fastify: Primarily for Node.js, complex configuration
- Koa: Small ecosystem, no edge support
- Next.js API Routes: Next.js only, framework overhead
Advantages of Hono
- Ultrafast - One of the fastest frameworks in benchmarks
- Small size - ~14KB, zero dependencies
- Multi-runtime - Runs everywhere: Edge, Deno, Bun, Node.js
- Type-safe - Full TypeScript support with inference
- Web Standards - Built on Fetch API, Request/Response
- Rich middleware - Built-in middleware for common tasks
Installation
For different runtimes
# Cloudflare Workers
npm create hono@latest my-app
# Select: cloudflare-workers
# Deno
deno add npm:hono
# Bun
bun create hono my-app
# Node.js
npm install hono @hono/node-serverBasic project
npm create hono@latest my-api
cd my-api
npm install
npm run devHono basics
Hello World
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.text('Hello Hono!')
})
app.get('/json', (c) => {
return c.json({ message: 'Hello JSON!', timestamp: Date.now() })
})
app.get('/html', (c) => {
return c.html('<h1>Hello HTML!</h1>')
})
export default appContext object (c)
The context c is the main object in Hono, containing the request, response helpers, and more:
app.get('/user/:id', (c) => {
// Request info
const method = c.req.method // GET, POST, etc.
const url = c.req.url // Full URL
const path = c.req.path // Path only
const headers = c.req.header() // All headers
const userAgent = c.req.header('User-Agent')
// URL params
const id = c.req.param('id')
// Query strings
const page = c.req.query('page')
const queries = c.req.queries('tags') // Multiple values
// Response helpers
return c.json({ id, page })
})Routing
Basic HTTP methods
const app = new Hono()
// Basic methods
app.get('/users', (c) => c.json([]))
app.post('/users', (c) => c.json({ created: true }))
app.put('/users/:id', (c) => c.json({ updated: true }))
app.patch('/users/:id', (c) => c.json({ patched: true }))
app.delete('/users/:id', (c) => c.json({ deleted: true }))
// All methods
app.all('/webhook', (c) => c.text('Received'))
// Multiple methods
app.on(['GET', 'POST'], '/data', (c) => c.text('GET or POST'))URL parameters
// Single parameter
app.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ userId: id })
})
// Multiple parameters
app.get('/users/:userId/posts/:postId', (c) => {
const { userId, postId } = c.req.param()
return c.json({ userId, postId })
})
// Optional parameter
app.get('/posts/:id?', (c) => {
const id = c.req.param('id')
if (id) {
return c.json({ postId: id })
}
return c.json({ posts: [] })
})
// Wildcard
app.get('/files/*', (c) => {
const path = c.req.param('*')
return c.text(`File path: ${path}`)
})
// Regex pattern
app.get('/user/:id{[0-9]+}', (c) => {
const id = c.req.param('id')
return c.json({ numericId: parseInt(id) })
})Route grouping
const app = new Hono()
// Creating sub-applications
const api = new Hono()
api.get('/users', (c) => c.json([]))
api.get('/users/:id', (c) => c.json({ id: c.req.param('id') }))
api.post('/users', (c) => c.json({ created: true }))
api.get('/posts', (c) => c.json([]))
api.get('/posts/:id', (c) => c.json({ id: c.req.param('id') }))
// Mounting under a path
app.route('/api/v1', api)
// Now available:
// GET /api/v1/users
// GET /api/v1/users/:id
// POST /api/v1/users
// GET /api/v1/posts
// GET /api/v1/posts/:idBase path
// Setting a base path for the entire application
const app = new Hono().basePath('/api')
app.get('/users', (c) => c.json([])) // GET /api/users
app.get('/posts', (c) => c.json([])) // GET /api/postsRequest handling
Body parsing
// JSON body
app.post('/users', async (c) => {
const body = await c.req.json()
return c.json({ received: body })
})
// Form data
app.post('/upload', async (c) => {
const formData = await c.req.formData()
const name = formData.get('name')
const file = formData.get('file')
return c.json({ name, fileName: file?.name })
})
// Raw text
app.post('/webhook', async (c) => {
const text = await c.req.text()
return c.text(`Received: ${text}`)
})
// Array buffer (binary)
app.post('/binary', async (c) => {
const buffer = await c.req.arrayBuffer()
return c.text(`Received ${buffer.byteLength} bytes`)
})
// Parsed body (auto-detect content type)
app.post('/auto', async (c) => {
const body = await c.req.parseBody()
return c.json(body)
})Validation with Zod
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
// Body schema
const createUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().int().min(0).optional(),
})
// Query params schema
const paginationSchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(10),
})
// Params schema
const idSchema = z.object({
id: z.string().uuid(),
})
// Body validation
app.post(
'/users',
zValidator('json', createUserSchema),
(c) => {
const data = c.req.valid('json')
// data is typed as { name: string, email: string, age?: number }
return c.json({ created: data })
}
)
// Query validation
app.get(
'/users',
zValidator('query', paginationSchema),
(c) => {
const { page, limit } = c.req.valid('query')
return c.json({ page, limit })
}
)
// Params validation
app.get(
'/users/:id',
zValidator('param', idSchema),
(c) => {
const { id } = c.req.valid('param')
return c.json({ userId: id })
}
)
// Multiple validators
app.put(
'/users/:id',
zValidator('param', idSchema),
zValidator('json', createUserSchema.partial()),
(c) => {
const { id } = c.req.valid('param')
const data = c.req.valid('json')
return c.json({ updated: { id, ...data } })
}
)
// Custom error handling
app.post(
'/validated',
zValidator('json', createUserSchema, (result, c) => {
if (!result.success) {
return c.json(
{
error: 'Validation failed',
details: result.error.issues,
},
400
)
}
}),
(c) => {
const data = c.req.valid('json')
return c.json(data)
}
)Middleware
Built-in middleware
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { prettyJSON } from 'hono/pretty-json'
import { secureHeaders } from 'hono/secure-headers'
import { compress } from 'hono/compress'
import { etag } from 'hono/etag'
import { timing } from 'hono/timing'
import { basicAuth } from 'hono/basic-auth'
import { bearerAuth } from 'hono/bearer-auth'
import { jwt } from 'hono/jwt'
const app = new Hono()
// Logger - logs requests
app.use('*', logger())
// CORS
app.use('/api/*', cors({
origin: ['http://localhost:3000', 'https://example.com'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization'],
exposeHeaders: ['X-Total-Count'],
maxAge: 86400,
credentials: true,
}))
// Pretty JSON (for development)
app.use('*', prettyJSON())
// Secure headers
app.use('*', secureHeaders())
// Compression
app.use('*', compress())
// ETag
app.use('/static/*', etag())
// Timing (Server-Timing header)
app.use('*', timing())
// Basic Auth
app.use('/admin/*', basicAuth({
username: 'admin',
password: 'secret',
}))
// Bearer Auth
app.use('/api/*', bearerAuth({
token: 'my-secret-token',
}))
// JWT Auth
app.use('/protected/*', jwt({
secret: process.env.JWT_SECRET!,
}))
app.get('/protected/data', (c) => {
const payload = c.get('jwtPayload')
return c.json({ user: payload })
})Custom middleware
import { Hono, Next } from 'hono'
import type { Context } from 'hono'
const app = new Hono()
// Simple middleware
const requestId = async (c: Context, next: Next) => {
const id = crypto.randomUUID()
c.set('requestId', id)
c.header('X-Request-ID', id)
await next()
}
// Middleware with before and after logic
const responseTime = async (c: Context, next: Next) => {
const start = Date.now()
await next()
const ms = Date.now() - start
c.header('X-Response-Time', `${ms}ms`)
}
// Error handling middleware
const errorHandler = async (c: Context, next: Next) => {
try {
await next()
} catch (err) {
console.error('Error:', err)
if (err instanceof HTTPException) {
return err.getResponse()
}
return c.json(
{ error: 'Internal Server Error' },
500
)
}
}
// Auth middleware with custom logic
const authMiddleware = async (c: Context, next: Next) => {
const authHeader = c.req.header('Authorization')
if (!authHeader?.startsWith('Bearer ')) {
return c.json({ error: 'Unauthorized' }, 401)
}
const token = authHeader.slice(7)
try {
const user = await verifyToken(token)
c.set('user', user)
await next()
} catch {
return c.json({ error: 'Invalid token' }, 401)
}
}
// Using middleware
app.use('*', requestId)
app.use('*', responseTime)
app.use('*', errorHandler)
app.use('/api/*', authMiddleware)
app.get('/api/me', (c) => {
const user = c.get('user')
return c.json({ user })
})Middleware factory
// Middleware with configuration
function rateLimit(options: {
limit: number
window: number // seconds
}) {
const requests = new Map<string, { count: number; resetAt: number }>()
return async (c: Context, next: Next) => {
const ip = c.req.header('CF-Connecting-IP') ||
c.req.header('X-Forwarded-For') ||
'unknown'
const now = Date.now()
const record = requests.get(ip)
if (!record || now > record.resetAt) {
requests.set(ip, {
count: 1,
resetAt: now + options.window * 1000,
})
} else if (record.count >= options.limit) {
const retryAfter = Math.ceil((record.resetAt - now) / 1000)
c.header('Retry-After', String(retryAfter))
return c.json(
{ error: 'Too many requests' },
429
)
} else {
record.count++
}
await next()
}
}
// Usage
app.use('/api/*', rateLimit({ limit: 100, window: 60 }))Response helpers
Different response types
app.get('/text', (c) => c.text('Plain text'))
app.get('/json', (c) => c.json({ message: 'JSON response' }))
app.get('/html', (c) => c.html('<h1>HTML response</h1>'))
app.get('/redirect', (c) => c.redirect('/new-location'))
app.get('/redirect-permanent', (c) => c.redirect('/new-location', 301))
app.get('/not-found', (c) => c.notFound())
// Custom status
app.post('/created', (c) => {
return c.json({ id: 1, name: 'New item' }, 201)
})
// Custom headers
app.get('/custom-headers', (c) => {
c.header('X-Custom', 'value')
c.header('Cache-Control', 'max-age=3600')
return c.json({ data: 'with headers' })
})
// Streaming response
app.get('/stream', (c) => {
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 5; i++) {
controller.enqueue(`Data chunk ${i}\n`)
await new Promise(r => setTimeout(r, 1000))
}
controller.close()
},
})
return new Response(stream, {
headers: { 'Content-Type': 'text/plain' },
})
})
// File download
app.get('/download', async (c) => {
const file = await fetch('https://example.com/file.pdf')
c.header('Content-Disposition', 'attachment; filename="file.pdf"')
return new Response(file.body, {
headers: c.res.headers,
})
})JSX/TSX support
Hono has built-in support for JSX:
// Enable JSX in tsconfig.json
// "jsx": "react-jsx",
// "jsxImportSource": "hono/jsx"
import { Hono } from 'hono'
import type { FC } from 'hono/jsx'
const app = new Hono()
// Component
const Layout: FC<{ title: string; children: any }> = ({ title, children }) => (
<html>
<head>
<title>{title}</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>{children}</body>
</html>
)
const UserCard: FC<{ name: string; email: string }> = ({ name, email }) => (
<div class="user-card">
<h2>{name}</h2>
<p>{email}</p>
</div>
)
// Usage in a route
app.get('/', (c) => {
return c.html(
<Layout title="Home">
<h1>Welcome!</h1>
<UserCard name="John" email="john@example.com" />
</Layout>
)
})
// User list
app.get('/users', async (c) => {
const users = await getUsers()
return c.html(
<Layout title="Users">
<h1>Users</h1>
<ul>
{users.map((user) => (
<li key={user.id}>
<a href={`/users/${user.id}`}>{user.name}</a>
</li>
))}
</ul>
</Layout>
)
})RPC Client
Hono offers a type-safe RPC client:
// server.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
.get('/users', (c) => {
return c.json([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
])
})
.post(
'/users',
zValidator('json', z.object({
name: z.string(),
email: z.string().email(),
})),
(c) => {
const data = c.req.valid('json')
return c.json({ id: 3, ...data }, 201)
}
)
.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ id: parseInt(id), name: 'John' })
})
export type AppType = typeof app
export default app// client.ts
import { hc } from 'hono/client'
import type { AppType } from './server'
const client = hc<AppType>('http://localhost:8787')
// Type-safe API calls!
async function main() {
// GET /users
const usersRes = await client.users.$get()
const users = await usersRes.json()
// users is typed as { id: number, name: string }[]
// POST /users
const createRes = await client.users.$post({
json: {
name: 'New User',
email: 'new@example.com',
},
})
const created = await createRes.json()
// created is typed as { id: number, name: string, email: string }
// GET /users/:id
const userRes = await client.users[':id'].$get({
param: { id: '1' },
})
const user = await userRes.json()
}Cloudflare Workers
Basic configuration
// src/index.ts
import { Hono } from 'hono'
type Bindings = {
// KV Namespace
MY_KV: KVNamespace
// D1 Database
DB: D1Database
// Environment variables
API_KEY: string
}
const app = new Hono<{ Bindings: Bindings }>()
// Access bindings via c.env
app.get('/kv/:key', async (c) => {
const key = c.req.param('key')
const value = await c.env.MY_KV.get(key)
return c.json({ key, value })
})
app.post('/kv/:key', async (c) => {
const key = c.req.param('key')
const { value } = await c.req.json()
await c.env.MY_KV.put(key, value)
return c.json({ success: true })
})
// D1 Database
app.get('/users', async (c) => {
const { results } = await c.env.DB
.prepare('SELECT * FROM users')
.all()
return c.json(results)
})
export default app# wrangler.toml
name = "my-api"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[vars]
API_KEY = "secret"
[[kv_namespaces]]
binding = "MY_KV"
id = "xxx"
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "xxx"Durable Objects
import { Hono } from 'hono'
// Durable Object class
export class Counter {
state: DurableObjectState
app: Hono
constructor(state: DurableObjectState) {
this.state = state
this.app = new Hono()
this.app.get('/value', async (c) => {
const value = (await this.state.storage.get('count')) || 0
return c.json({ count: value })
})
this.app.post('/increment', async (c) => {
const value = ((await this.state.storage.get('count')) as number || 0) + 1
await this.state.storage.put('count', value)
return c.json({ count: value })
})
}
fetch(request: Request) {
return this.app.fetch(request)
}
}
// Main app
type Bindings = {
COUNTER: DurableObjectNamespace
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/counter/:name/*', async (c) => {
const name = c.req.param('name')
const id = c.env.COUNTER.idFromName(name)
const obj = c.env.COUNTER.get(id)
const url = new URL(c.req.url)
url.pathname = url.pathname.replace(`/counter/${name}`, '')
return obj.fetch(new Request(url, c.req.raw))
})
export default appNode.js adapter
// src/index.ts
import { Hono } from 'hono'
import { serve } from '@hono/node-server'
import { serveStatic } from '@hono/node-server/serve-static'
const app = new Hono()
// Serve static files
app.use('/static/*', serveStatic({ root: './public' }))
app.get('/', (c) => c.text('Hello from Node.js!'))
app.get('/api/users', (c) => {
return c.json([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
])
})
// Start server
const port = 3000
console.log(`Server running on http://localhost:${port}`)
serve({
fetch: app.fetch,
port,
})Bun adapter
// src/index.ts
import { Hono } from 'hono'
import { serveStatic } from 'hono/bun'
const app = new Hono()
// Serve static files
app.use('/static/*', serveStatic({ root: './public' }))
app.get('/', (c) => c.text('Hello from Bun!'))
// Bun-specific features
app.get('/file', async (c) => {
const file = Bun.file('./data.json')
const content = await file.json()
return c.json(content)
})
export default {
port: 3000,
fetch: app.fetch,
}Deno adapter
// main.ts
import { Hono } from 'npm:hono'
import { serveStatic } from 'npm:hono/deno'
const app = new Hono()
app.use('/static/*', serveStatic({ root: './public' }))
app.get('/', (c) => c.text('Hello from Deno!'))
Deno.serve({ port: 8000 }, app.fetch)Error handling
import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'
const app = new Hono()
// Custom error class
class NotFoundError extends Error {
constructor(message: string) {
super(message)
this.name = 'NotFoundError'
}
}
// Global error handler
app.onError((err, c) => {
console.error('Error:', err)
if (err instanceof HTTPException) {
return err.getResponse()
}
if (err instanceof NotFoundError) {
return c.json({ error: err.message }, 404)
}
return c.json({ error: 'Internal Server Error' }, 500)
})
// 404 handler
app.notFound((c) => {
return c.json({ error: 'Not Found', path: c.req.path }, 404)
})
// Throwing errors
app.get('/users/:id', async (c) => {
const id = c.req.param('id')
const user = await findUser(id)
if (!user) {
throw new NotFoundError(`User ${id} not found`)
}
return c.json(user)
})
// HTTP Exception
app.get('/protected', (c) => {
const auth = c.req.header('Authorization')
if (!auth) {
throw new HTTPException(401, {
message: 'Authorization required',
})
}
return c.json({ data: 'secret' })
})Testing
// app.ts
import { Hono } from 'hono'
const app = new Hono()
app.get('/users', (c) => c.json([{ id: 1, name: 'John' }]))
app.post('/users', async (c) => {
const body = await c.req.json()
return c.json({ id: 2, ...body }, 201)
})
export default app
// app.test.ts
import { describe, it, expect } from 'vitest'
import app from './app'
describe('Users API', () => {
it('GET /users returns users list', async () => {
const res = await app.request('/users')
expect(res.status).toBe(200)
const data = await res.json()
expect(data).toEqual([{ id: 1, name: 'John' }])
})
it('POST /users creates user', async () => {
const res = await app.request('/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: 'Jane' }),
})
expect(res.status).toBe(201)
const data = await res.json()
expect(data).toEqual({ id: 2, name: 'Jane' })
})
it('returns 404 for unknown routes', async () => {
const res = await app.request('/unknown')
expect(res.status).toBe(404)
})
})OpenAPI/Swagger
import { Hono } from 'hono'
import { swaggerUI } from '@hono/swagger-ui'
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
const app = new OpenAPIHono()
// Define route with OpenAPI schema
const getUserRoute = createRoute({
method: 'get',
path: '/users/{id}',
request: {
params: z.object({
id: z.string().openapi({
param: { name: 'id', in: 'path' },
example: '123',
}),
}),
},
responses: {
200: {
content: {
'application/json': {
schema: z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
}),
},
},
description: 'User found',
},
404: {
content: {
'application/json': {
schema: z.object({
error: z.string(),
}),
},
},
description: 'User not found',
},
},
})
app.openapi(getUserRoute, (c) => {
const { id } = c.req.valid('param')
// ...
return c.json({ id, name: 'John', email: 'john@example.com' })
})
// Swagger UI
app.get('/docs', swaggerUI({ url: '/doc' }))
app.doc('/doc', {
openapi: '3.0.0',
info: {
title: 'My API',
version: '1.0.0',
},
})
export default appBest practices
1. Project organization
src/
├── index.ts # Entry point
├── routes/
│ ├── users.ts # /users routes
│ ├── posts.ts # /posts routes
│ └── index.ts # Route aggregator
├── middleware/
│ ├── auth.ts
│ ├── logging.ts
│ └── index.ts
├── services/
│ ├── userService.ts
│ └── postService.ts
├── types/
│ └── index.ts
└── utils/
└── helpers.ts2. Route modularization
// routes/users.ts
import { Hono } from 'hono'
const users = new Hono()
users.get('/', (c) => c.json([]))
users.get('/:id', (c) => c.json({ id: c.req.param('id') }))
users.post('/', (c) => c.json({ created: true }))
export default users
// routes/index.ts
import { Hono } from 'hono'
import users from './users'
import posts from './posts'
const routes = new Hono()
routes.route('/users', users)
routes.route('/posts', posts)
export default routes
// index.ts
import { Hono } from 'hono'
import routes from './routes'
const app = new Hono()
app.route('/api', routes)
export default app3. Type-safe environment
type Bindings = {
DATABASE_URL: string
API_KEY: string
KV: KVNamespace
}
type Variables = {
user: { id: string; name: string }
requestId: string
}
const app = new Hono<{
Bindings: Bindings
Variables: Variables
}>()
app.use('*', async (c, next) => {
c.set('requestId', crypto.randomUUID())
await next()
})
app.get('/me', (c) => {
const user = c.get('user') // Type-safe!
return c.json(user)
})Comparison with other frameworks
| Feature | Hono | Express | Fastify | Elysia |
|---|---|---|---|---|
| Size | ~14KB | ~200KB | ~120KB | ~35KB |
| TypeScript | Native | @types | Plugin | Native |
| Edge support | Yes | No | No | Yes |
| Bun | Yes | Partial | Partial | Yes |
| Deno | Yes | No | No | No |
| Node.js | Yes | Yes | Yes | Yes |
| RPC Client | Yes | No | No | Yes |
Summary
Hono is a modern, ultrafast framework ideal for edge computing and serverless. Its main advantages:
- Performance - One of the fastest frameworks available
- Versatility - Runs on every JavaScript runtime
- Type-safety - Full TypeScript support
- Small size - ~14KB with no dependencies
- Web Standards - Built on Fetch API
- DX - Intuitive API similar to Express
If you are building APIs for Cloudflare Workers, Deno Deploy, or need a lightweight framework for Node.js/Bun, Hono is an excellent choice.