Loading...
Loading...
Hono で HTTP API を設計・実装する。@hono/zod-openapi による OpenAPI 準拠、RFC 9457 の problem+json エラー、パスバージョニング、Bearer 認証と loopback サービスの Origin 検証、ヘルスチェック、Spectral リント、@hono/mcp。API エンドポイント、ルーティング、OpenAPI スキーマ、認証方式、エラーレスポンス、MCP サーバーの追加・変更・レビューを求められたときに使う。
npx skill4agent add philtzjp/skills honoimport { Hono } from 'hono'
const api = new Hono().basePath('/api/v1')import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
const UserSchema = z.object({
id: z.string().openapi({ example: '01JABC' }),
displayName: z.string().max(50),
}).openapi('User')
const ProblemSchema = z.object({
type: z.string(),
title: z.string(),
status: z.number(),
detail: z.string().optional(),
}).openapi('Problem')
const getUser = createRoute({
method: 'get',
path: '/user/{id}',
request: {
params: z.object({ id: z.string() }),
},
responses: {
200: {
content: { 'application/json': { schema: UserSchema } },
description: 'ユーザーを返す',
},
404: {
content: { 'application/problem+json': { schema: ProblemSchema } },
description: '該当するユーザーがない',
},
},
})
const app = new OpenAPIHono().basePath('/api/v1')
app.openapi(getUser, (c) => {
const { id } = c.req.valid('param')
return c.json({ id, displayName: '例' })
})
app.doc('/openapi.json', {
openapi: '3.1.0',
info: { title: 'Example API', version: '1.0.0' },
})import { HTTPException } from 'hono/http-exception'
app.onError((err, c) => {
const status = err instanceof HTTPException ? err.status : 500
const problem = {
type: `https://example.com/problems/${status === 404 ? 'not-found' : 'internal'}`,
title: status === 404 ? 'Resource not found' : 'Internal server error',
status,
instance: c.get('requestId'),
}
return c.body(JSON.stringify(problem), status, {
'Content-Type': 'application/problem+json; charset=utf-8',
})
})import { bearerAuth } from 'hono/bearer-auth'
app.use('/api/v1/*', bearerAuth({ verifyToken: async (token, c) => verify(token) }))app.use('*', async (c, next) => {
const origin = c.req.header('Origin')
const host = c.req.header('Host')
if (origin && !ALLOWED_ORIGINS.includes(origin)) return c.body(null, 403)
if (!host || !/^(127\.0\.0\.1|\[::1\]|localhost)(:\d+)?$/.test(host)) return c.body(null, 403)
await next()
})app.get('/health', (c) =>
c.body(JSON.stringify({
status: 'pass',
version: '1',
releaseId: process.env.RELEASE_ID,
checks: {
'db:responseTime': [{ componentType: 'datastore', status: 'pass' }],
},
}), 200, { 'Content-Type': 'application/health+json; charset=utf-8' }))npx --yes @stoplight/spectral-cli lint openapi.json.spectral.yamlextends: ["spectral:oas"]