Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds
Volver a colecciones
Guide33 min read

Hono

Hono is an ultrafast web framework for edge computing and serverless. Cloudflare Workers, Deno, Bun, Node.js.

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

  1. Ultraszybki - Jeden z najszybszych frameworków w benchmarkach
  2. Mały rozmiar - ~14KB, zero zależności
  3. Multi-runtime - Działa wszędzie: Edge, Deno, Bun, Node.js
  4. Type-safe - Pełne wsparcie TypeScript z inference
  5. Web Standards - Oparte na Fetch API, Request/Response
  6. Rich middleware - Wbudowane middleware dla typowych zadań

Instalacja

Dla różnych środowisk

Code
Bash
# 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-server

Podstawowy projekt

Code
Bash
npm create hono@latest my-api
cd my-api
npm install
npm run dev

Podstawy Hono

Hello World

Code
TypeScript
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 app

Context object (c)

Context c to główny obiekt w Hono, zawierający request, response helpers i więcej:

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
// 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

Code
TypeScript
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/:id

Bazowa ścieżka

Code
TypeScript
// 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/posts

Request handling

Body parsing

Code
TypeScript
// 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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
// 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

Code
TypeScript
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:

Code
TypeScript
// 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:

TSserver.ts
TypeScript
// 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
TSclient.ts
TypeScript
// 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

TSsrc/index.ts
TypeScript
// 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
TOML
# 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

Code
TypeScript
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 app

Node.js Adapter

TSsrc/index.ts
TypeScript
// 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

TSsrc/index.ts
TypeScript
// 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

TSmain.ts
TypeScript
// 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

Code
TypeScript
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

TSapp.ts
TypeScript
// 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

Code
TypeScript
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 app

Best practices

1. Organizacja projektu

Code
TEXT
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.ts

2. Modularyzacja routów

TSroutes/users.ts
TypeScript
// 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 app

3. Type-safe environment

Code
TypeScript
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

CechaHonoExpressFastifyElysia
Rozmiar~14KB~200KB~120KB~35KB
TypeScriptNative@typesPluginNative
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

  1. Ultrafast - One of the fastest frameworks in benchmarks
  2. Small size - ~14KB, zero dependencies
  3. Multi-runtime - Runs everywhere: Edge, Deno, Bun, Node.js
  4. Type-safe - Full TypeScript support with inference
  5. Web Standards - Built on Fetch API, Request/Response
  6. Rich middleware - Built-in middleware for common tasks

Installation

For different runtimes

Code
Bash
# 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-server

Basic project

Code
Bash
npm create hono@latest my-api
cd my-api
npm install
npm run dev

Hono basics

Hello World

Code
TypeScript
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 app

Context object (c)

The context c is the main object in Hono, containing the request, response helpers, and more:

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
// 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

Code
TypeScript
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/:id

Base path

Code
TypeScript
// 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/posts

Request handling

Body parsing

Code
TypeScript
// 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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
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

Code
TypeScript
// 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

Code
TypeScript
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:

Code
TypeScript
// 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:

TSserver.ts
TypeScript
// 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
TSclient.ts
TypeScript
// 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

TSsrc/index.ts
TypeScript
// 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
TOML
# 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

Code
TypeScript
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 app

Node.js adapter

TSsrc/index.ts
TypeScript
// 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

TSsrc/index.ts
TypeScript
// 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

TSmain.ts
TypeScript
// 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

Code
TypeScript
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

TSapp.ts
TypeScript
// 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

Code
TypeScript
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 app

Best practices

1. Project organization

Code
TEXT
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.ts

2. Route modularization

TSroutes/users.ts
TypeScript
// 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 app

3. Type-safe environment

Code
TypeScript
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

FeatureHonoExpressFastifyElysia
Size~14KB~200KB~120KB~35KB
TypeScriptNative@typesPluginNative
Edge supportYesNoNoYes
BunYesPartialPartialYes
DenoYesNoNoNo
Node.jsYesYesYesYes
RPC ClientYesNoNoYes

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.