We use cookies to enhance your experience on the site
CodeWorlds
Back to collections
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.