Hono OpenAPI Valibot
Installation
sh
pnpm add hono-openapi valibot
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/valibot'
import * as v from 'valibot'
const responseSchema = v.string()
const honoHandler = new Hono().get(
'/',
describeRoute({
description: 'Hono Valibot🔥',
responses: {
200: {
description: 'Hono Valibot🔥',
content: {
'application/json': {
schema: resolver(responseSchema),
},
},
},
},
}),
(c) => {
return c.json({ message: 'Hono Valibot🔥' }, 200)
},
)
export default honoHandler
postsHandler
ts
import { Hono } from 'hono'
import { describeRoute } from 'hono-openapi'
import { resolver, validator as vValidator } from 'hono-openapi/valibot'
import { deletePostsId, getPosts, postPosts, putPostsId } from '@packages/service'
import * as v from 'valibot'
const postSchema = v.object({
id: v.pipe(v.string(), v.uuid()),
post: v.pipe(v.string(), v.minLength(1), v.maxLength(140)),
createdAt: v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/)),
updatedAt: v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/)),
})
const errorResponseSchema = v.object({ message: v.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) } },
},
},
}),
vValidator('json', v.object({ post: v.pipe(v.string(), v.minLength(1), v.maxLength(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(v.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) } },
},
},
}),
vValidator(
'query',
v.object({
page: v.string(),
rows: v.string(),
}),
),
async (c) => {
const { page, rows } = c.req.valid('query')
const pageNumber = parseInt(page)
const rowsPerPage = parseInt(rows)
if (isNaN(pageNumber) || isNaN(rowsPerPage) || pageNumber < 1 || rowsPerPage < 1) {
return c.json({ message: 'Bad Request' }, 400)
}
const limit = rowsPerPage
const offset = (pageNumber - 1) * rowsPerPage
const posts = await getPosts(limit, offset)
return c.json(posts, 200)
},
)
.put(
'/:id',
describeRoute({
tags: ['posts'],
description: 'Update a post by ID.',
responses: {
204: {
description: 'Post successfully updated.',
},
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) } },
},
},
}),
vValidator('param', v.object({ id: v.pipe(v.string(), v.uuid()) })),
vValidator('json', v.object({ post: v.pipe(v.string(), v.minLength(1), v.maxLength(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: 'Delete a post by ID.',
responses: {
204: {
description: 'Post successfully deleted.',
},
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) } },
},
},
}),
vValidator('param', v.object({ id: v.pipe(v.string(), v.uuid()) })),
async (c) => {
const { id } = c.req.valid('param')
await deletePostsId(id)
return new Response(null, { status: 204 })
},
)
export default postsHandler