Hono Implementation Guide — Zod Validation, OpenAPI & JWT Auth for Production-Grade APIs [2026]
A comprehensive guide to building production-grade APIs with Hono using Zod validation, JWT authentication, and auto-generated OpenAPI specs. Covers CORS, rate limiting, error handling, logging, and Swagger UI integration.
What Makes a Production-Grade API? Integrating Zod, JWT & OpenAPI in Hono
A production-grade API requires six essentials: validation, authentication, OpenAPI spec, error handling, logging, and rate limiting. Hono lets you implement all of them in a lightweight, type-safe way. This guide covers each component with complete code examples.
Production API Architecture Overview
Zod Validation with @hono/zod-validator
The `@hono/zod-validator` package provides type-safe request validation for JSON bodies, query parameters, headers, and cookies. On validation failure it automatically returns a 400 response; on success, `c.req.valid()` returns fully-typed data.
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
const userSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150)
})
app.post('/users', zValidator('json', userSchema), (c) => {
const user = c.req.valid('json') // typed: { name: string; email: string; age: number }
return c.json({ id: 1, ...user }, 201)
})Validation Targets Reference
Hono supports validation across all major request parts:
| Target | First Arg | Access Method |
|---|---|---|
| JSON body | `'json'` | `c.req.valid('json')` |
| Form data | `'form'` | `c.req.valid('form')` |
| Query params | `'query'` | `c.req.valid('query')` |
| Path params | `'param'` | `c.req.valid('param')` |
| HTTP headers | `'header'` | `c.req.valid('header')` |
| Cookies | `'cookie'` | `c.req.valid('cookie')` |
Custom Error Response on Validation Failure
app.post(
'/users',
zValidator('json', userSchema, (result, c) => {
if (!result.success) {
return c.json(
{
error: 'Validation failed',
details: result.error.flatten()
},
422
)
}
}),
(c) => {
const user = c.req.valid('json')
return c.json({ id: 1, ...user }, 201)
}
)JWT Authentication Middleware
Hono includes `hono/jwt` out of the box — no extra packages needed. Apply `jwt()` middleware to protected routes and retrieve the verified payload via `c.get('jwtPayload')`.
import { Hono } from 'hono'
import { jwt } from 'hono/jwt'
const app = new Hono()
// Protect all routes under /auth/*
app.use('/auth/*', jwt({
secret: process.env.JWT_SECRET!,
alg: 'HS256'
}))
app.get('/auth/me', (c) => {
const payload = c.get('jwtPayload')
return c.json(payload)
})Issuing JWT Tokens — Login Endpoint
import { sign } from 'hono/jwt'
app.post('/login', zValidator('json', z.object({
email: z.string().email(),
password: z.string().min(8)
})), async (c) => {
const { email, password } = c.req.valid('json')
const user = await db.findUserByEmail(email)
if (!user || !(await verifyPassword(password, user.passwordHash))) {
return c.json({ error: 'Invalid credentials' }, 401)
}
const token = await sign(
{
sub: user.id,
email: user.email,
exp: Math.floor(Date.now() / 1000) + 60 * 60 // 1 hour
},
process.env.JWT_SECRET!,
'HS256'
)
return c.json({ token })
})Supported JWT Algorithms
Hono supports the following JWT signing algorithms:
| Algorithm | Type | Recommended Use |
|---|---|---|
| HS256 | HMAC | Internal APIs, simple auth |
| HS384 | HMAC | Higher security than HS256 |
| HS512 | HMAC | Maximum HMAC strength |
| RS256 | RSA | When public key distribution is needed |
| RS384 | RSA | Higher security than RS256 |
| RS512 | RSA | Maximum RSA strength |
| ES256 | ECDSA | High security with small key size |
| EdDSA | Ed25519 | Modern recommendation, fast and compact |
Other Auth Methods — Bearer Auth & Basic Auth
In addition to JWT, Hono ships with Bearer Auth and Basic Auth middleware:
| Method | Import | Use Case |
|---|---|---|
| JWT | `hono/jwt` | User auth, SPA/mobile apps |
| Bearer Auth | `hono/bearer-auth` | API key-based access control |
| Basic Auth | `hono/basic-auth` | Admin panels, dev environments |
import { bearerAuth } from 'hono/bearer-auth'
import { basicAuth } from 'hono/basic-auth'
// Bearer Auth (API Key)
app.use('/api/*', bearerAuth({ token: process.env.API_KEY! }))
// Basic Auth
app.use('/admin/*', basicAuth({
username: 'admin',
password: process.env.ADMIN_PASSWORD!
}))@hono/zod-openapi — Auto-generate OpenAPI Specs
With `@hono/zod-openapi`, your Zod schemas become the single source of truth for both runtime validation and OpenAPI 3.0 documentation. Use `OpenAPIHono` and `createRoute()` to keep code and docs always in sync.
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
const app = new OpenAPIHono()
const UserCreateSchema = z.object({
name: z.string().min(1).openapi({ example: 'Jane Doe' }),
email: z.string().email().openapi({ example: 'jane@example.com' })
})
const UserResponseSchema = z.object({
id: z.number().openapi({ example: 1 }),
name: z.string(),
email: z.string()
})
const createUserRoute = createRoute({
method: 'post',
path: '/users',
tags: ['Users'],
request: {
body: {
content: { 'application/json': { schema: UserCreateSchema } }
}
},
responses: {
201: {
content: { 'application/json': { schema: UserResponseSchema } },
description: 'User created successfully'
},
422: { description: 'Validation error' }
}
})
app.openapi(createUserRoute, (c) => {
const { name, email } = c.req.valid('json')
return c.json({ id: 1, name, email }, 201)
})
app.doc('/openapi.json', {
openapi: '3.0.0',
info: { title: 'My Production API', version: '1.0.0' }
})Swagger UI Integration
import { swaggerUI } from '@hono/swagger-ui'
// Serve Swagger UI at /ui
app.get('/ui', swaggerUI({ url: '/openapi.json' }))
// Disable UI in production
if (process.env.NODE_ENV !== 'production') {
app.get('/ui', swaggerUI({ url: '/openapi.json' }))
}CORS, Rate Limiting & Logging
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { rateLimiter } from 'hono-rate-limiter'
const app = new Hono()
// Request logging
app.use('*', logger())
// CORS — restrict to production origins
app.use('/api/*', cors({
origin: ['https://example.com', 'https://app.example.com'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Authorization', 'Content-Type'],
maxAge: 86400
}))
// Rate limit: 60 requests per minute
app.use('/api/*', rateLimiter({
windowMs: 60 * 1000,
limit: 60,
keyGenerator: (c) => c.req.header('x-forwarded-for') ?? 'unknown'
}))Global Error Handling with HTTPException
import { HTTPException } from 'hono/http-exception'
app.get('/resource/:id', async (c) => {
const resource = await db.find(c.req.param('id'))
if (!resource) {
throw new HTTPException(404, { message: 'Resource not found' })
}
return c.json(resource)
})
// Global error handler
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json(
{ error: err.message, status: err.status },
err.status
)
}
console.error('Unexpected error:', err)
return c.json({ error: 'Internal Server Error' }, 500)
})Pre-deployment Checklist
Verify all items below before deploying to production:
| Category | Checklist Item | Priority |
|---|---|---|
| Security | JWT_SECRET is at least 32 characters | Required |
| Security | CORS origin restricted to production URLs only | Required |
| Security | All secrets managed via environment variables | Required |
| Validation | zValidator applied to every endpoint | Required |
| Error Handling | onError handler is configured | Required |
| Error Handling | Stack traces excluded from production responses | Required |
| Performance | Rate limiting configured appropriately | Recommended |
| Performance | Unused middleware disabled | Recommended |
| Documentation | OpenAPI spec reflects latest changes | Recommended |
| Documentation | Swagger UI disabled in production | Recommended |
| Testing | Integration tests passing for all key endpoints | Required |
| Logging | Structured logs enabled | Recommended |
FAQ
Q1. What is the difference between @hono/zod-validator and @hono/zod-openapi? `@hono/zod-validator` handles validation only and is the lighter choice. `@hono/zod-openapi` is a superset that adds OpenAPI 3.0 spec generation on top of validation. Use the former when you don't need API docs to keep bundle size minimal. Q2. How do I implement JWT refresh tokens? Issue separate access tokens (15–60 min expiry) and refresh tokens (7–30 day expiry). Store refresh tokens in a database. At `/auth/refresh`, verify the stored refresh token and issue a new access token. This is the standard pattern for stateless auth with sliding sessions. Q3. Can I apply different JWT configs to different route groups? Yes. Use `app.use('/admin/*', jwt({ secret: ADMIN_SECRET }))` and `app.use('/api/*', jwt({ secret: API_SECRET }))` independently — each path segment gets its own middleware instance. Q4. How do I validate nested objects with Zod? Nest `z.object()` calls. For example: `z.object({ address: z.object({ city: z.string(), zip: z.string().regex(/^\d{5}$/) }) })`. Q5. Can rate limiting be applied per user ID? Yes. Customize `keyGenerator` to return a user-specific key: `keyGenerator: (c) => c.get('jwtPayload')?.sub ?? c.req.header('x-forwarded-for') ?? 'anonymous'`. Q6. Can I use the OpenAPI spec for TypeScript type generation? Absolutely. `@hono/zod-openapi` infers types from `createRoute()`. You can also run `openapi-typescript` against the generated `/openapi.json` to produce type definitions for frontend clients. Q7. How do I write tests for Hono handlers? Use `testClient()` from `hono/testing` for a type-safe test client that invokes handlers directly without starting a real HTTP server. Combine with Vitest or Jest for fast, reliable integration tests.
How Oflight Can Help
Oflight provides end-to-end support for production API development with Hono — from validation design and authentication architecture to OpenAPI spec management and deployment optimization on Cloudflare Workers or Vercel Edge. Learn more at our Software Development Service.
Feel free to contact us
Contact Us