株式会社オブライト
Software Dev2026-04-08

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

Loading diagram...

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.

ts
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:

TargetFirst ArgAccess 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

ts
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')`.

ts
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

ts
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:

AlgorithmTypeRecommended Use
HS256HMACInternal APIs, simple auth
HS384HMACHigher security than HS256
HS512HMACMaximum HMAC strength
RS256RSAWhen public key distribution is needed
RS384RSAHigher security than RS256
RS512RSAMaximum RSA strength
ES256ECDSAHigh security with small key size
EdDSAEd25519Modern recommendation, fast and compact

Other Auth Methods — Bearer Auth & Basic Auth

In addition to JWT, Hono ships with Bearer Auth and Basic Auth middleware:

MethodImportUse 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
ts
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.

ts
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

ts
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

Loading diagram...
ts
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

ts
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:

CategoryChecklist ItemPriority
SecurityJWT_SECRET is at least 32 charactersRequired
SecurityCORS origin restricted to production URLs onlyRequired
SecurityAll secrets managed via environment variablesRequired
ValidationzValidator applied to every endpointRequired
Error HandlingonError handler is configuredRequired
Error HandlingStack traces excluded from production responsesRequired
PerformanceRate limiting configured appropriatelyRecommended
PerformanceUnused middleware disabledRecommended
DocumentationOpenAPI spec reflects latest changesRecommended
DocumentationSwagger UI disabled in productionRecommended
TestingIntegration tests passing for all key endpointsRequired
LoggingStructured logs enabledRecommended

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