Hono OpenAPI Zod
Installation
sh
pnpm add hono-openapi zod zod-openapi
Scalar for Hono
sh
pnpm add -D @scalar/hono-api-reference
Swagger UI
sh
pnpm add -D @hono/swagger-ui
Hono API
ts
import { Hono } from 'hono'
import { serve } from '@hono/node-server'
import { openAPISpecs } from 'hono-openapi'
import { apiReference } from '@scalar/hono-api-reference'
import { SwaggerUI } from '@hono/swagger-ui'
import honoHandler from './handler/hono_handler.js'
import postsHandler from './handler/posts_handler.js'
const app = new Hono()
const port = 3000
console.log(`Server is running on http://localhost:${port}`)
app.get(
'/openapi',
openAPISpecs(app, {
documentation: {
info: { title: 'Hono API', version: '1.0.0', description: 'Greeting API' },
servers: [{ url: 'http://localhost:3000', description: 'Local Server' }],
},
}),
)
// scalar
app.get(
'/docs',
apiReference({
theme: 'saturn',
spec: {
url: '/openapi',
},
}),
)
// swagger
app.get('/ui', (c) => {
return c.html(`
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Custom Swagger" />
<title>Custom Swagger</title>
<script>
// custom script
</script>
<style>
/* custom style */
</style>
</head>
${SwaggerUI({ url: '/openapi' })}
</html>
`)
})
serve({
fetch: app.fetch,
port,
})
const api = app.route('/', honoHandler).route('/posts', postsHandler)
api.use('*', async (c, next) => {
try {
await next()
} catch (e) {
return c.json({ error: (e as Error).message }, 500)
}
})
export default api
Handler
honoHandler
ts
import { Hono } from 'hono'
import { describeRoute } from 'hono-openapi'
import { resolver } from 'hono-openapi/zod'
import { z } from 'zod'
import 'zod-openapi/extend'
const responseSchema = z.string().openapi({ example: 'Hono OpenAPI🔥' })
const honoHandler = new Hono().get(
'/',
describeRoute({
description: 'Hono OpenAPI🔥',
responses: {
200: {
description: 'Hono OpenAPI🔥',
content: {
'application/json': {
schema: resolver(responseSchema),
},
},
},
},
}),
(c) => {
return c.json({ message: 'Hono OpenAPI🔥' })
},
)
export default honoHandler
postsHandler
ts
import { Hono } from 'hono'
import { describeRoute } from 'hono-openapi'
import { resolver, validator as zValidator } from 'hono-openapi/zod'
import { z } from 'zod'
import { deletePostsId, getPosts, postPosts, putPostsId } from '@packages/service'
const postSchema = z.object({
id: z.string().uuid(),
post: z.string().min(1).max(140),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
})
const errorResponseSchema = z.object({ message: z.string() })
const postsHandler = new Hono()
.post(
'/',
describeRoute({
tags: ['posts'],
description: 'Submit a new post with a maximum length of 140 characters.',
responses: {
201: {
description: 'Post successfully created.',
content: { 'application/json': { schema: resolver(postSchema) } },
},
400: {
description: 'Invalid request due to bad input.',
content: { 'application/json': { schema: resolver(errorResponseSchema) } },
},
500: {
description: 'Internal server error.',
content: { 'application/json': { schema: resolver(errorResponseSchema) } },
},
},
}),
zValidator('json', z.object({ post: z.string().min(1).max(140) })),
async (c) => {
const { post } = c.req.valid('json')
await postPosts(post)
return c.json({ message: 'Created' }, 201)
},
)
.get(
'/',
describeRoute({
tags: ['posts'],
description:
'Retrieve a paginated list of posts. Specify the page and number of posts per page.',
responses: {
200: {
description: 'Successfully retrieved a list of posts.',
content: { 'application/json': { schema: resolver(z.array(postSchema)) } },
},
400: {
description: 'Invalid request due to bad input.',
content: { 'application/json': { schema: resolver(errorResponseSchema) } },
},
500: {
description: 'Internal server error.',
content: { 'application/json': { schema: resolver(errorResponseSchema) } },
},
},
}),
zValidator(
'query',
z.object({
page: z.string().pipe(z.coerce.number().int().min(0)),
rows: z.string().pipe(z.coerce.number().int().min(0)),
}),
),
async (c) => {
const { page, rows } = c.req.valid('query')
const limit = rows
const offset = (page - 1) * rows
const posts = await getPosts(limit, offset)
return c.json(posts, 200)
},
)
.put(
'/:id',
describeRoute({
tags: ['posts'],
description: 'Update a post by its ID.',
responses: {
204: { description: 'Post successfully updated.' },
400: {
description: 'Invalid input.',
content: { 'application/json': { schema: resolver(errorResponseSchema) } },
},
500: {
description: 'Internal server error.',
content: { 'application/json': { schema: resolver(errorResponseSchema) } },
},
},
}),
zValidator('param', z.object({ id: z.string().uuid() })),
zValidator('json', z.object({ post: z.string().min(1).max(140) })),
async (c) => {
const { id } = c.req.valid('param')
const { post } = c.req.valid('json')
await putPostsId(id, post)
return new Response(null, { status: 204 })
},
)
.delete(
'/:id',
describeRoute({
tags: ['posts'],
description: 'elete an existing post identified by its unique ID.',
responses: {
204: { description: 'Post successfully deleted.' },
},
}),
zValidator('param', z.object({ id: z.string().uuid() })),
async (c) => {
const { id } = c.req.valid('param')
await deletePostsId(id)
return new Response(null, { status: 204 })
},
)
export default postsHandler