hono-v4

star 5

Hono v4 web framework reference. Auto-loads when working with Hono routes, middleware, zValidator, streamSSE, HTTPException, hono/factory, RPC client.

nklisch By nklisch schedule Updated 3/4/2026

name: hono-v4 description: > Hono v4 web framework reference. Auto-loads when working with Hono routes, middleware, zValidator, streamSSE, HTTPException, hono/factory, RPC client.

Hono Reference

Version: 4.x Docs: https://hono.dev/docs/

Imports

// Core
import { Hono } from 'hono'
import type { Context, ErrorHandler } from 'hono'

// Middleware
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'

// Validation
import { validator } from 'hono/validator'
import { zValidator } from '@hono/zod-validator'

// SSE
import { streamSSE } from 'hono/streaming'
import type { SSEMessage, SSEStreamingApi } from 'hono/streaming'

// RPC
import { hc } from 'hono/client'
import type { InferRequestType, InferResponseType } from 'hono/client'

// Error handling
import { HTTPException } from 'hono/http-exception'

// Factory pattern
import { createMiddleware } from 'hono/factory'
import { createFactory } from 'hono/factory'

// Node.js adapter
import { serve } from '@hono/node-server'
import type { HttpBindings } from '@hono/node-server'

API Quick Reference

App Creation

const app = new Hono()
const app = new Hono<{ Bindings: B; Variables: V }>()  // Typed

Route Definition

app.get(path, ...handlers)
app.post(path, ...handlers)
app.put(path, ...handlers)
app.delete(path, ...handlers)
app.patch(path, ...handlers)
app.all(path, ...handlers)
app.on(method, path, ...handlers)

// Chaining
app.get(path, handler).post(path, handler)

// Route groups
app.route(prefix, subApp)

// Base path
app.basePath(prefix)

Context Methods

// Request
c.req.param(key)           // Path param
c.req.param()              // All params
c.req.query(key)           // Query param
c.req.header(key)          // Header
c.req.json()               // Body as JSON (async)
c.req.valid(target)        // Validated data (after zValidator)
c.req.raw                  // Raw Request object

// Response
c.json(data, status?)      // JSON response
c.text(text, status?)      // Text response
c.html(html, status?)      // HTML response
c.body(body, status?)      // Raw body
c.redirect(url, status?)   // Redirect
c.notFound()               // 404

// Headers & Status
c.status(code)             // Set status
c.header(key, value)       // Set header

// Variables
c.set(key, value)          // Set context variable
c.get(key)                 // Get context variable
c.var                      // Typed access (with Variables generic)

Validation

// Manual validator
validator(target, (value, c) => {
  if (!valid) return c.text('Invalid', 400)
  return validatedData
})

// Zod validator (must be inline with handler for type inference)
zValidator(target, schema)
zValidator(target, schema, (result, c) => {
  if (!result.success) {
    return c.json({ error: result.error.issues }, 400)
  }
})

// Validation targets: 'json' | 'form' | 'query' | 'param' | 'header' | 'cookie'

Middleware

// Global middleware
app.use(middleware)
app.use('*', middleware)

// Path-specific
app.use('/api/*', middleware)

// Inline middleware
app.use(async (c, next) => {
  // before handler
  await next()
  // after handler
})

// Reusable middleware with createMiddleware
const myMiddleware = createMiddleware<{ Variables: V }>(async (c, next) => {
  c.set('key', value)
  await next()
})

Error Handling

// Global error handler
app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return err.getResponse()
  }
  return c.json({ error: 'Internal error' }, 500)
})

// Throw HTTPException
throw new HTTPException(statusCode, { message: 'Error message' })
throw new HTTPException(statusCode, { res: customResponse })
throw new HTTPException(statusCode, { message: 'Error', cause: originalError })

SSE (Server-Sent Events)

app.get('/sse', (c) => {
  return streamSSE(c, async (stream) => {
    let running = true

    stream.onAbort(() => {
      running = false
    })

    // Send messages
    await stream.writeSSE({
      data: JSON.stringify(payload),
      event: 'event-name',  // Optional
      id: String(id),        // Optional
    })

    // Keep connection alive (REQUIRED)
    while (running) {
      await stream.sleep(30000)
      if (running) {
        await stream.write(': heartbeat\n\n')
      }
    }
  })
})

// SSEMessage type
interface SSEMessage {
  data: string | Promise<string>
  event?: string
  id?: string
  retry?: number
}

// SSEStreamingApi methods
stream.writeSSE(message: SSEMessage): Promise<void>
stream.sleep(ms: number): Promise<void>
stream.onAbort(callback: () => void | Promise<void>): void
stream.write(data: string): Promise<void>
stream.abort(): void

Type Export (for RPC)

export default app
export type AppType = typeof app

// For modular apps
const routes = app.route('/api', apiRoutes)
export type AppType = typeof routes

Node.js Server

import { serve } from '@hono/node-server'

serve({
  fetch: app.fetch,
  port: 8080,
})

Gotchas & Version Caveats

Validators must be inline - Type inference breaks if zValidator is in app.use(). Always inline with handler.

SSE requires await - Callback must await something (sleep, loop) or connection closes immediately.

Avoid c.notFound() with RPC - Response types can't be inferred. Use explicit c.json({ error }, 404).

Status codes must be literals - c.json(data, 200) not c.json(data, status) for proper RPC typing.

Context variables need typing - Use Hono<{ Variables: V }> for typed c.get() / c.var.

Headers are auto-set for SSE - streamSSE sets Content-Type, Cache-Control, Connection automatically.

Middleware execution is stack-based - Each middleware's post-await next() code runs in reverse order.

TypeScript strict mode required - Set "strict": true in tsconfig for proper RPC type inference.

Path params regex validation - Use /:param{regex} syntax for validation (e.g., /:id{[0-9]+}).

EventEmitter doesn't keep SSE alive - Must use infinite loop with heartbeats even with event-driven patterns.

Anti-Patterns

Don't use app.use() for validators:

// Bad - types break
app.use(zValidator('json', schema))
app.post('/posts', (c) => {
  const data = c.req.valid('json')  // Type error
})

// Good - inline with handler
app.post('/posts', zValidator('json', schema), (c) => {
  const data = c.req.valid('json')  // Typed
})

Don't return c.notFound() with RPC:

// Bad - RPC can't infer types
if (!post) return c.notFound()

// Good - explicit JSON response
if (!post) return c.json({ error: 'not found' }, 404)

Don't forget await in SSE callback:

// Bad - connection closes immediately
return streamSSE(c, async (stream) => {
  stream.writeSSE({ data: 'hello' })
})

// Good - keep connection alive
return streamSSE(c, async (stream) => {
  await stream.writeSSE({ data: 'hello' })
  while (running) await stream.sleep(30000)
})

Don't separate route handlers into controllers:

// Bad - path param types lost
export const getPost = (c: Context) => { /* ... */ }
app.get('/posts/:id', getPost)

// Good - inline handler or factory
app.get('/posts/:id', (c) => {
  const id = c.req.param('id')  // Typed
})

Don't use dynamic status codes with RPC:

// Bad - breaks RPC type inference
const status = success ? 200 : 400
return c.json(data, status)

// Good - explicit literal status codes
if (!success) return c.json(data, 400)
return c.json(data, 200)

Don't skip middleware for wildcard paths:

// Bad - wildcard paths don't inherit
app.use('/api/jobs', dbMiddleware)
app.route('/api/jobs', jobsRoutes)  // /api/jobs/:id won't have middleware

// Good - apply to both root and wildcard
app.use('/api/jobs', dbMiddleware)
app.use('/api/jobs/*', dbMiddleware)
app.route('/api/jobs', jobsRoutes)
Install via CLI
npx skills add https://github.com/nklisch/skills --skill hono-v4
Repository Details
star Stars 5
call_split Forks 3
navigation Branch main
article Path SKILL.md
More from Creator