Zenn
๐Ÿ”ฅ

Hono OpenAPI Valibot Prisma REST API๐Ÿ”ฅ

2025/01/18ใซๅ…ฌ้–‹

ใฏใ˜ใ‚ใซ

โ€ƒHono Advent Calendar 2024ใ‚ทใƒชใƒผใ‚บ๏ผ’ใฎ๏ผ‘๏ผ˜ๆ—ฅ็›ฎใฎ่จ˜ไบ‹ใงใ™ใ€‚

โ€ƒHono OpenAPIใงใ€Valibotใ‚’ไฝฟ็”จใงใใ‚‹ใ‚ˆใ†ใซใชใฃใŸใฎใงใ€่ฉฆใ—ใฆใฟใพใ—ใŸใ€‚

ๅ‚่€ƒใƒชใƒณใ‚ฏ

ใƒ‡ใ‚ฃใƒฌใ‚ฏใƒˆใƒชๆง‹้€ 

.
โ”œโ”€โ”€ apps
โ”‚   โ””โ”€โ”€ hono-openapi-valibot
โ”‚       โ”œโ”€โ”€ package.json
โ”‚       โ”œโ”€โ”€ src
โ”‚       โ”‚   โ”œโ”€โ”€ handler
โ”‚       โ”‚   โ”‚   โ”œโ”€โ”€ comments_handler.ts
โ”‚       โ”‚   โ”‚   โ”œโ”€โ”€ hono_handler.ts
โ”‚       โ”‚   โ”‚   โ””โ”€โ”€ posts_handler.ts
โ”‚       โ”‚   โ”œโ”€โ”€ index.ts
โ”‚       โ”‚   โ””โ”€โ”€ service
โ”‚       โ”‚       โ”œโ”€โ”€ comments_service.ts
โ”‚       โ”‚       โ””โ”€โ”€ posts_service.ts
โ”‚       โ””โ”€โ”€ tsconfig.json
โ”œโ”€โ”€ biome.json
โ”œโ”€โ”€ docs
โ”‚   โ””โ”€โ”€ ER.md
โ”œโ”€โ”€ package.json
โ”œโ”€โ”€ packages
โ”‚   โ”œโ”€โ”€ prisma
โ”‚   โ”‚   โ”œโ”€โ”€ index.ts
โ”‚   โ”‚   โ”œโ”€โ”€ package.json
โ”‚   โ”‚   โ”œโ”€โ”€ schema.prisma
โ”‚   โ”‚   โ””โ”€โ”€ tsconfig.json
โ”‚   โ””โ”€โ”€ schema
โ”‚       โ”œโ”€โ”€ index.ts
โ”‚       โ”œโ”€โ”€ package.json
โ”‚       โ””โ”€โ”€ tsconfig.json
โ””โ”€โ”€ pnpm-workspace.yaml

Prisma

โ€ƒๆœ€ๅˆใซใ€schema.prismaใ‚’็”จๆ„ใ—ใพใ™ใ€‚

schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

/// @r Post.id Comment.postId one-to-many
model Post {
  /// The unique identifier for the post.
  /// @v.pipe(v.string(), v.uuid())  
  id        String    @id @default(uuid())
  /// The title of the post.
  /// @v.pipe(v.string(), v.minLength(1), v.maxLength(40))
  title     String
  /// The content of the post.
  /// @v.pipe(v.string(), v.minLength(1), v.maxLength(140))
  content   String
  /// The list of comments associated with the post.
  comments  Comment[]
  /// The date and time when the post was created.
  /// @v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/))
  createdAt DateTime  @default(now())
  /// The date and time when the post was last updated.
  /// Automatically updated to the current timestamp on each update.
  /// @v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/))
  updatedAt DateTime  @updatedAt
}

model Comment {
  /// The unique identifier for the comment.
  /// @v.pipe(v.string(), v.uuid())  
  id        String   @id @default(uuid())
  /// The unique identifier of the post this comment belongs to.
  /// @z.string().uuid()
  /// @v.pipe(v.string(), v.uuid())  
  postId    String
  /// The post that this comment is associated with.
  /// Establishes a relationship to the Post model.
  post      Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
  /// The content of the comment.
  /// @v.pipe(v.string(), v.minLength(1), v.maxLength(140))
  content   String
  /// The date and time when the comment was created.
  /// @v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/))
  createdAt DateTime @default(now())
}

โ€ƒไปฅไธ‹ใฎใ‚ˆใ†ใซ่จ˜่ฟฐใ™ใ‚‹ใจใ€ERๅ›ณใ€Valibotใ‚นใ‚ญใƒผใƒžใ€Zodใ‚นใ‚ญใƒผใƒžใŒ็”Ÿๆˆใ•ใ‚Œใ‚‹ใƒ„ใƒผใƒซใ‚’@prisma/generator-helperใ‚’็”จใ„ใฆใ€็พๅœจ้–‹็™บ้€”ไธญใงใ™ใ€‚๏ผ“ๆœˆใพใงใซใฏใ€ใƒฉใ‚คใƒ–ใƒฉใƒชใจใ—ใฆๆไพ›ใงใใ‚‹ใ‚ˆใ†ใซๆบ–ๅ‚™ใ‚’้€ฒใ‚ใฆใ„ใพใ™ใ€‚

  • @r : ใƒชใƒฌใƒผใ‚ทใƒงใƒณ้–ขไฟ‚ใ‚’่จ˜่ฟฐ

  • @v : Valibotใฎใƒใƒชใƒ‡ใƒผใ‚ทใƒงใƒณใ‚’่จ˜่ฟฐ

  • @z : Zodใฎใƒใƒชใƒ‡ใƒผใ‚ทใƒงใƒณใ‚’่จ˜่ฟฐ

โ€ƒใƒ‘ใƒƒใ‚ฑใƒผใ‚ธๅใจใ‚ขใ‚คใ‚ณใƒณใฏใ€ใพใ ๆฑบใ‚ใฆใ„ใชใ„ใฎใงใ€่‰ฏใ„ๆกˆใŒใ‚ใ‚Œใฐๅ‹Ÿ้›†ไธญใงใ™ใ€‚

ๅ‚่€ƒใƒชใƒณใ‚ฏ

โ€ƒPrismaใฎใ‚ธใ‚งใƒใƒฌใƒผใ‚ฟใƒผใ‚’ๅฎŸ่กŒใ—ใพใ™ใ€‚

ER

Valibot

โ€ƒv.date()ใ‚’ไฝฟ็”จใ—ใฆใ„ใชใ„็†็”ฑใฏใ€describeRouteใงใ€ๅฏพๅฟœใ•ใ‚Œใฆใ„ใชใ„ใ‹ใ‚‰ใงใ™ใ€‚

import * as v from 'valibot'

/**
 * Post model
 *
 * Relationships:
 *
 * - Post.id - Comment.postId (one-to-many)
 */
export const postSchema = v.object({
  /** The unique identifier for the post. */
  id: v.pipe(v.string(), v.uuid()),
  /** The title of the post. */
  title: v.pipe(v.string(), v.minLength(1), v.maxLength(40)),
  /** The content of the post. */
  content: v.pipe(v.string(), v.minLength(1), v.maxLength(140)),
  /** The date and time when the post was created. */
  createdAt: v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/)),
  /** The date and time when the post was last updated. */
  updatedAt: v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/)),
})

export const commentSchema = v.object({
  /** The unique identifier for the comment. */
  id: v.pipe(v.string(), v.uuid()),
  /** The unique identifier of the post this comment belongs to. */
  postId: v.pipe(v.string(), v.uuid()),
  /** The content of the comment. */
  content: v.pipe(v.string(), v.minLength(1), v.maxLength(140)),
  /** The date and time when the comment was created. */
  createdAt: v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/)),
})

export type Post = v.InferInput<typeof postSchema>

export type Comment = v.InferInput<typeof commentSchema>

export const postSchemaWithRelationsSchema = v.object({
  postSchema,
  commentToPost: v.array(commentSchema),
})

export const commentSchemaWithRelationsSchema = v.object({
  commentSchema,
  commentToPost: postSchema,
})

export type postSchemaWithRelations = v.InferInput<typeof postSchemaWithRelationsSchema>

export type commentSchemaWithRelations = v.InferInput<typeof commentSchemaWithRelationsSchema>

Hono OpenAPI

โ€ƒapps/hono-openapi-valibot/src/index.ts

โ€ƒscalarใจswaggerใฉใกใ‚‰ใ‚‚่กจ็คบใ•ใ›ใพใ™ใ€‚

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'
import commentsHandler from './handler/comments_handler.js'

const app = new Hono()

const port = 3000

console.log(`Server is running on http://localhost:${port}`)

app.use('*', async (c, next) => {
  try {
    await next()
  } catch (e) {
    return c.json({ error: (e as Error).message }, 500)
  }
})

app.get(
  '/openapi',
  openAPISpecs(app, {
    documentation: {
      info: {
        title: 'Hono API',
        version: '1.0.0',
        description: 'Hono Valibot API',
      },
      tags: [
        {
          name: 'Hono',
          description: 'Hono API',
        },
        {
          name: 'Post',
          description: 'Post API',
        },
        {
          name: 'Comment',
          description: 'Comment 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)
  .route('/comments', commentsHandler)

export default api

REST APIใฎไฝœๆˆ

apps/hono-openapi-valibot/src/handler/hono_handler.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({
    tags: ['Hono'],
    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
apps/hono-openapi-valibot/src/handler/posts_handler.ts
import { Hono } from 'hono'
import { describeRoute } from 'hono-openapi'
import { resolver, validator } from 'hono-openapi/valibot'
import { postSchema } from '@packages/schema'

import * as v from 'valibot'
import {
  deletePostsId,
  getPosts,
  getPostsId,
  postPosts,
  putPostsId,
} from '../service/posts_service.js'

const errorSchema = v.object({ message: v.string() })

const postsHandler = new Hono()
  .post(
    '/',
    describeRoute({
      tags: ['Post'],
      description: 'Create a post',
      responses: {
        201: {
          description: 'Post created successfully',
          content: {
            'application/json': { schema: resolver(postSchema) },
          },
        },
        400: {
          description: 'Invalid request due to bad input.',
          content: { 'application/json': { schema: resolver(errorSchema) } },
        },
        500: {
          description: 'Internal server error.',
          content: { 'application/json': { schema: resolver(errorSchema) } },
        },
      },
    }),
    validator(
      'json',
      v.object({
        title: v.pipe(v.string(), v.minLength(1), v.maxLength(40)),
        content: v.pipe(v.string(), v.minLength(1), v.maxLength(140)),
      }),
    ),
    async (c) => {
      const { title, content } = c.req.valid('json')
      const post = await postPosts(title, content)
      return c.json(post)
    },
  )
  .get(
    '/:id',
    describeRoute({
      tags: ['Post'],
      description: 'Get a post by ID.',
      responses: {
        200: {
          description: 'Post fetched successfully',
          content: { 'application/json': { schema: resolver(postSchema) } },
        },
        400: {
          description: 'Invalid request due to bad input.',
          content: { 'application/json': { schema: resolver(errorSchema) } },
        },
        500: {
          description: 'Internal server error.',
          content: { 'application/json': { schema: resolver(errorSchema) } },
        },
      },
    }),
    validator('param', v.object({ id: v.pipe(v.string(), v.uuid()) })),
    async (c) => {
      const { id } = c.req.valid('param')
      const post = await getPostsId(id)
      return c.json(post)
    },
  )
  .get(
    '/',
    describeRoute({
      tags: ['Post'],
      description: 'Get posts',
      responses: {
        200: {
          description: 'Posts fetched successfully',
          content: {
            'application/json': { schema: resolver(postSchema) },
          },
        },
        400: {
          description: 'Invalid request due to bad input.',
          content: { 'application/json': { schema: resolver(errorSchema) } },
        },
        500: {
          description: 'Internal server error.',
          content: { 'application/json': { schema: resolver(errorSchema) } },
        },
      },
    }),
    validator(
      '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 < 0 || 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)
    },
  )
  .put(
    '/:id',
    describeRoute({
      tags: ['Post'],
      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(errorSchema) } },
        },
        500: {
          description: 'Internal server error.',
          content: { 'application/json': { schema: resolver(errorSchema) } },
        },
      },
    }),
    validator('param', v.object({ id: v.pipe(v.string(), v.uuid()) })),
    validator(
      'json',
      v.object({
        title: v.optional(v.pipe(v.string(), v.minLength(1), v.maxLength(40))),
        content: v.optional(v.pipe(v.string(), v.minLength(1), v.maxLength(140))),
      }),
    ),
    async (c) => {
      const { id } = c.req.valid('param')
      const { title, content } = c.req.valid('json')
      await putPostsId(id, title, content)
      return new Response(null, { status: 204 })
    },
  )
  .delete(
    '/:id',
    describeRoute({
      tags: ['Post'],
      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(errorSchema) } },
        },
        500: {
          description: 'Internal server error.',
          content: { 'application/json': { schema: resolver(errorSchema) } },
        },
      },
    }),
    validator('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
apps/hono-openapi-valibot/src/handler/comments_handler.ts
import { Hono } from 'hono'
import { describeRoute } from 'hono-openapi'
import {
  deleteCommentsId,
  getComments,
  postComments,
  putCommentsId,
} from '../service/comments_service.js'
import { resolver, validator } from 'hono-openapi/valibot'
import { postSchema, commentSchema } from '@packages/schema'
import * as v from 'valibot'

const errorSchema = v.object({ message: v.string() })

const commentsHandler = new Hono()
  .post(
    '/:id',
    describeRoute({
      tags: ['Comment'],
      description: 'Create a comment',
      responses: {
        201: {
          description: 'Comment created successfully',
          content: { 'application/json': { schema: resolver(commentSchema) } },
        },
        400: {
          description: 'Invalid request due to bad input.',
          content: { 'application/json': { schema: resolver(errorSchema) } },
        },
        500: {
          description: 'Internal server error.',
          content: { 'application/json': { schema: resolver(errorSchema) } },
        },
      },
    }),
    validator('param', v.pick(postSchema, ['id'])),
    validator('json', v.pick(commentSchema, ['content'])),
    async (c) => {
      const { id } = c.req.valid('param')
      const { content } = c.req.valid('json')
      const comment = await postComments(id, content)
      return c.json(comment)
    },
  )
  .get(
    '/:id',
    describeRoute({
      tags: ['Comment'],
      description: 'Get a comment by ID.',
      responses: {
        200: {
          description: 'Comment fetched successfully',
          content: { 'application/json': { schema: resolver(commentSchema) } },
        },
        400: {
          description: 'Invalid request due to bad input.',
          content: { 'application/json': { schema: resolver(errorSchema) } },
        },
        404: {
          description: 'Comment not found.',
          content: { 'application/json': { schema: resolver(errorSchema) } },
        },
        500: {
          description: 'Internal server error.',
          content: { 'application/json': { schema: resolver(errorSchema) } },
        },
      },
    }),
    validator('param', v.pick(postSchema, ['id'])),
    async (c) => {
      const { id } = c.req.valid('param')
      const comment = await getComments(id)
      if (comment.length === 0) {
        return c.json({ message: 'Comment not found' }, 404)
      }
      return c.json(comment)
    },
  )
  .put(
    '/:id',
    describeRoute({
      tags: ['Comment'],
      description: 'Update a comment by ID.',
      responses: {
        204: {
          description: 'Comment successfully updated.',
        },
        400: {
          description: 'Invalid request due to bad input.',
          content: { 'application/json': { schema: resolver(errorSchema) } },
        },
        500: {
          description: 'Internal server error.',
          content: { 'application/json': { schema: resolver(errorSchema) } },
        },
      },
    }),
    validator('param', v.pick(commentSchema, ['id'])),
    validator('json', v.pick(commentSchema, ['content'])),
    async (c) => {
      const { id } = c.req.valid('param')
      const { content } = c.req.valid('json')
      await putCommentsId(id, content)
      return new Response(null, { status: 204 })
    },
  )
  .delete(
    '/:id',
    describeRoute({
      tags: ['Comment'],
      description: 'Delete a comment by ID.',
      responses: {
        204: {
          description: 'Comment successfully deleted.',
        },
        400: {
          description: 'Invalid request due to bad input.',
          content: { 'application/json': { schema: resolver(errorSchema) } },
        },
        500: {
          description: 'Internal server error.',
          content: { 'application/json': { schema: resolver(errorSchema) } },
        },
      },
    }),
    validator('param', v.pick(commentSchema, ['id'])),
    async (c) => {
      const { id } = c.req.valid('param')
      await deleteCommentsId(id)
      return new Response(null, { status: 204 })
    },
  )

export default commentsHandler

โ€ƒHono OpenAPIใงใฏใ€describeRouteใ‚’็”จใ„ใ‚‹ใ“ใจใงใ€OpenAPIใฎใƒ‰ใ‚ญใƒฅใƒกใƒณใƒˆใ‚’็”Ÿๆˆใ™ใ‚‹ใ“ใจใŒใงใใพใ™ใ€‚

OpenAPI

Scalar

Swagger

REST Clientใงใ€APIใฎใƒฌใ‚นใƒใƒณใ‚นใ‚’็ขบ่ช

ใƒชใ‚ฏใ‚จใ‚นใƒˆ

GET http://localhost:3000/
Content-Type: application/json
Accept: application/json

ใƒฌใ‚นใƒใƒณใ‚น

HTTP/1.1 200 OK
content-type: application/json
Content-Length: 30
Date: ***, ** *** **** **:**:** GMT
Connection: keep-alive
keep-alive: timeout=5

{
  "message": "Hono Valibot๐Ÿ”ฅ"
}

ๆŠ•็จฟใƒชใ‚ฏใ‚จใ‚นใƒˆ

POST http://localhost:3000/posts HTTP/1.1
Content-Type: application/json
Accept: application/json

{
    "title": "Hono OpenAPI Valibot Prisma REST API๐Ÿ”ฅ",
    "content": "Hono OpenAPI Valibot Prisma REST API๐Ÿ”ฅ ใ‚’่ฉฆใ—ใฆใฟใพใ—ใŸใ€‚"
}

ใƒฌใ‚นใƒใƒณใ‚น

HTTP/1.1 200 OK
content-type: application/json
Content-Length: 255
Date: ***, ** *** **** **:**:** GMT
Connection: keep-alive
keep-alive: timeout=5

{
  "id": "************************************",
  "title": "Hono OpenAPI Valibot Prisma REST API๐Ÿ”ฅ",
  "content": "Hono OpenAPI Valibot Prisma REST API๐Ÿ”ฅ ใ‚’่ฉฆใ—ใฆใฟใพใ—ใŸใ€‚",
  "createdAt": "***, ** *** **** **:**:** GMT",
  "updatedAt": "***, ** *** **** **:**:** GMT"
}

ใ‚ฏใ‚จใƒชใƒ‘ใƒฉใƒกใƒผใ‚ฟใงใ€ใƒชใ‚ฏใ‚จใ‚นใƒˆ

GET http://localhost:3000/posts?page=1&rows=10 HTTP/1.1
Content-Type: application/json
Accept: application/json
ใƒฌใ‚นใƒใƒณใ‚น
HTTP/1.1 200 OK
content-type: application/json
Content-Length: 2391
Date: ***, ** *** **** **:**:** GMT
Connection: keep-alive
keep-alive: timeout=5

[
  {
    "id": "************************************",
    "title": "Prisma Drizzle",
    "content": "Prisma Drizzle ใฎๆฏ”่ผƒใ€‚",
    "createdAt": "***, ** *** **** **:**:** GMT",
    "updatedAt": "***, ** *** **** **:**:** GMT",
    "comments": []
  },
  {
    "id": "************************************",
    "title": "Hono๐Ÿ”ฅ Prisma",
    "content": "Hono๐Ÿ”ฅ Prisma ใ‚’่ฉฆใ—ใฆใฟใพใ—ใŸใ€‚",
    "createdAt": "***, ** *** **** **:**:** GMT",
    "updatedAt": "***, ** *** **** **:**:** GMT",
    "comments": []
  },
  {
    "id": "************************************",
    "title": "Hono๐Ÿ”ฅ Drizzle",
    "content": "Hono๐Ÿ”ฅ Drizzle ใ‚’่ฉฆใ—ใฆใฟใพใ—ใŸใ€‚",
    "createdAt": "***, ** *** **** **:**:** GMT",
    "updatedAt": "***, ** *** **** **:**:** GMT",
    "comments": []
  },
  {
    "id": "************************************",
    "title": "Hono๐Ÿ”ฅ ใจ Mojollicious",
    "content": "Hono๐Ÿ”ฅ ใจ Mojollicious ใซไผผใฆใ„ใพใ™ใ€‚",
    "createdAt": "***, ** *** **** **:**:** GMT",
    "updatedAt": "***, ** *** **** **:**:** GMT",
    "comments": []
  },
  {
    "id": "************************************",
    "title": "Hono๐Ÿ”ฅ ใจ Express",
    "content": "Hono๐Ÿ”ฅใฏใ€Expressใฎไปฃๆ›ฟใจใชใ‚Šใคใคใ‚ใ‚Šใพใ™ใ€‚",
    "createdAt": "***, ** *** **** **:**:** GMT",
    "updatedAt": "***, ** *** **** **:**:** GMT",
    "comments": []
  },
  {
    "id": "************************************",
    "title": "Hono Takibi๐Ÿ”ฅ",
    "content": "Hono Takibi๐Ÿ”ฅใฏใ€OpenAPIใฎๅฎš็พฉใ‹ใ‚‰ใ€Zod OpenAPI Honoใฎใ‚ณใƒผใƒ‰ใ‚’็”Ÿๆˆใ™ใ‚‹ใƒ„ใƒผใƒซใงใ™ใ€‚",
    "createdAt": "***, ** *** **** **:**:** GMT",
    "updatedAt": "***, ** *** **** **:**:** GMT",
    "comments": []
  },
  {
    "id": "************************************",
    "title": "Hono Validator๐Ÿ”ฅ",
    "content": "Hono Validator๐Ÿ”ฅ ใ‚’่ฉฆใ—ใฆใฟใพใ—ใŸใ€‚",
    "createdAt": "***, ** *** **** **:**:** GMT",
    "updatedAt": "***, ** *** **** **:**:** GMT",
    "comments": []
  },
  {
    "id": "************************************",
    "title": "HonoX๐Ÿ”ฅ ใ‚’่ฉฆใ—ใฆใฟใŸ",
    "content": "HonoX๐Ÿ”ฅ ใ‚’่ฉฆใ—ใฆใฟใพใ—ใŸใ€‚",
    "createdAt": "***, ** *** **** **:**:** GMT",
    "updatedAt": "***, ** *** **** **:**:** GMT",
    "comments": []
  },
  {
    "id": "************************************",
    "title": "Hono OpenAPI Zod Prisma REST API๐Ÿ”ฅ",
    "content": "Hono OpenAPI Zod Prisma REST API๐Ÿ”ฅ ใ‚’่ฉฆใ—ใฆใฟใพใ—ใŸใ€‚",
    "createdAt": "***, ** *** **** **:**:** GMT",
    "updatedAt": "***, ** *** **** **:**:** GMT",
    "comments": []
  },
  {
    "id": "************************************",
    "title": "Hono OpenAPI Valibot Prisma REST API๐Ÿ”ฅ",
    "content": "Hono OpenAPI Valibot Prisma REST API๐Ÿ”ฅ ใ‚’่ฉฆใ—ใฆใฟใพใ—ใŸใ€‚",
    "createdAt": "***, ** *** **** **:**:** GMT",
    "updatedAt": "***, ** *** **** **:**:** GMT",
    "comments": []
  }
]

ใ‚ฏใ‚จใƒชใƒ‘ใƒฉใƒกใƒผใ‚ฟใ‚’ๆŒ‡ๅฎšใ—ใ€๏ผ’ใƒšใƒผใ‚ธ็›ฎใ‚’ๅ–ๅพ—

GET http://localhost:3000/posts?page=2&rows=10 HTTP/1.1
Content-Type: application/json
Accept: application/json

```sh
HTTP/1.1 200 OK
content-type: application/json
Content-Length: 227
Date: ***, ** *** **** **:**:** GMT
Connection: keep-alive
keep-alive: timeout=5

[
  {
    "id": "************************************",
    "title": "Hono Validator๐Ÿ”ฅ",
    "content": "Hono Validator๐Ÿ”ฅ ใ‚’่ฉฆใ—ใฆใฟใพใ—ใŸใ€‚",
    "createdAt": "***, ** *** **** **:**:** GMT",
    "updatedAt": "***, ** *** **** **:**:** GMT",
    "comments": []
  }
]

ใ‚ณใƒกใƒณใƒˆใƒชใ‚ฏใ‚จใ‚นใƒˆ

POST http://localhost:3000/comments/************************************ HTTP/1.1
Content-Type: application/json
Accept: application/json

{
    "content": "Hono Takibi๐Ÿ”ฅ"
}

ๆŠ•็จฟIDใ‚’ใƒ‘ใ‚นใƒ‘ใƒฉใƒกใƒผใ‚ฟใซๆŒ‡ๅฎšใ—ใ€็‰นๅฎšใฎๆŠ•็จฟใจใ‚ณใƒกใƒณใƒˆใ‚’ๅ–ๅพ—ใ™ใ‚‹ใŸใ‚ใฎใƒชใ‚ฏใ‚จใ‚นใƒˆ

GET http://localhost:3000/posts/************************************ HTTP/1.1
Content-Type: application/json
Accept: application/json

ใƒฌใ‚นใƒใƒณใ‚น

HTTP/1.1 200 OK
content-type: application/json
Content-Length: 443
Date: ***, ** *** **** **:**:** GMT
Connection: keep-alive
keep-alive: timeout=5

{
  "id": "************************************",
  "title": "Hono Takibi๐Ÿ”ฅ",
  "content": "Hono Takibi๐Ÿ”ฅใฏใ€OpenAPIใฎๅฎš็พฉใ‹ใ‚‰ใ€Zod OpenAPI Honoใฎใ‚ณใƒผใƒ‰ใ‚’็”Ÿๆˆใ™ใ‚‹ใƒ„ใƒผใƒซใงใ™ใ€‚",
  "createdAt": "***, ** *** **** **:**:** GMT",
  "updatedAt": "***, ** *** **** **:**:** GMT",
  "comments": [
    {
      "id": "************************************",
      "postId": "************************************",
      "content": "Hono Takibi๐Ÿ”ฅ",
      "createdAt": "***, ** *** **** **:**:** GMT"
    }
  ]
}

้–‹็™บใ—ใชใŒใ‚‰ใ€็”Ÿๆˆใ•ใ‚ŒใŸOpenAPIๅฎš็พฉใซHono Takibiใ‚’้ฉ็”จใ—ใฆใฟใ‚‹

โ€ƒไปฅไธ‹ใฏใ€Hono OpenAPIใ‚’็”จใ„ใฆ้–‹็™บใ—ใ€็”Ÿๆˆใ•ใ‚ŒใŸOpenAPIๅฎš็พฉใงใ™ใ€‚

openapi.json
{
  "openapi": "3.1.0",
  "info": { "title": "Hono API", "description": "Hono Valibot API", "version": "1.0.0" },
  "tags": [
    { "name": "Hono", "description": "Hono API" },
    { "name": "Post", "description": "Post API" },
    { "name": "Comment", "description": "Comment API" }
  ],
  "servers": [{ "url": "http://localhost:3000", "description": "Local Server" }],
  "paths": {
    "/": {
      "get": {
        "responses": {
          "200": {
            "description": "Hono Valibot๐Ÿ”ฅ",
            "content": { "application/json": { "schema": { "type": "string" } } }
          }
        },
        "operationId": "getIndex",
        "tags": ["Hono"],
        "description": "Hono Valibot๐Ÿ”ฅ"
      }
    },
    "/posts": {
      "post": {
        "responses": {
          "201": {
            "description": "Post created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string", "format": "uuid" },
                    "title": { "type": "string", "minLength": 1, "maxLength": 40 },
                    "content": { "type": "string", "minLength": 1, "maxLength": 140 },
                    "comments": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "string", "format": "uuid" },
                          "postId": { "type": "string", "format": "uuid" },
                          "content": { "type": "string", "minLength": 1, "maxLength": 140 },
                          "createdAt": {
                            "type": "string",
                            "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z$"
                          }
                        },
                        "required": ["id", "postId", "content", "createdAt"]
                      }
                    },
                    "createdAt": {
                      "type": "string",
                      "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z$"
                    },
                    "updatedAt": {
                      "type": "string",
                      "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z$"
                    }
                  },
                  "required": ["id", "title", "content", "comments", "createdAt", "updatedAt"]
                }
              }
            }
          },
          "400": {
            "description": "Invalid request due to bad input.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "message": { "type": "string" } },
                  "required": ["message"]
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "message": { "type": "string" } },
                  "required": ["message"]
                }
              }
            }
          }
        },
        "operationId": "postPosts",
        "tags": ["Post"],
        "description": "Create a post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": { "type": "string", "minLength": 1, "maxLength": 40 },
                  "content": { "type": "string", "minLength": 1, "maxLength": 140 }
                },
                "required": ["title", "content"]
              }
            }
          }
        }
      },
      "get": {
        "responses": {
          "200": {
            "description": "Posts fetched successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string", "format": "uuid" },
                    "title": { "type": "string", "minLength": 1, "maxLength": 40 },
                    "content": { "type": "string", "minLength": 1, "maxLength": 140 },
                    "comments": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "string", "format": "uuid" },
                          "postId": { "type": "string", "format": "uuid" },
                          "content": { "type": "string", "minLength": 1, "maxLength": 140 },
                          "createdAt": {
                            "type": "string",
                            "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z$"
                          }
                        },
                        "required": ["id", "postId", "content", "createdAt"]
                      }
                    },
                    "createdAt": {
                      "type": "string",
                      "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z$"
                    },
                    "updatedAt": {
                      "type": "string",
                      "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z$"
                    }
                  },
                  "required": ["id", "title", "content", "comments", "createdAt", "updatedAt"]
                }
              }
            }
          },
          "400": {
            "description": "Invalid request due to bad input.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "message": { "type": "string" } },
                  "required": ["message"]
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "message": { "type": "string" } },
                  "required": ["message"]
                }
              }
            }
          }
        },
        "operationId": "getPosts",
        "tags": ["Post"],
        "description": "Get posts",
        "parameters": [
          { "in": "query", "name": "page", "schema": { "type": "string" }, "required": true },
          { "in": "query", "name": "rows", "schema": { "type": "string" }, "required": true }
        ]
      }
    },
    "/posts/{id}": {
      "get": {
        "responses": {
          "200": {
            "description": "Post fetched successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string", "format": "uuid" },
                    "title": { "type": "string", "minLength": 1, "maxLength": 40 },
                    "content": { "type": "string", "minLength": 1, "maxLength": 140 },
                    "comments": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "string", "format": "uuid" },
                          "postId": { "type": "string", "format": "uuid" },
                          "content": { "type": "string", "minLength": 1, "maxLength": 140 },
                          "createdAt": {
                            "type": "string",
                            "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z$"
                          }
                        },
                        "required": ["id", "postId", "content", "createdAt"]
                      }
                    },
                    "createdAt": {
                      "type": "string",
                      "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z$"
                    },
                    "updatedAt": {
                      "type": "string",
                      "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z$"
                    }
                  },
                  "required": ["id", "title", "content", "comments", "createdAt", "updatedAt"]
                }
              }
            }
          },
          "400": {
            "description": "Invalid request due to bad input.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "message": { "type": "string" } },
                  "required": ["message"]
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "message": { "type": "string" } },
                  "required": ["message"]
                }
              }
            }
          }
        },
        "operationId": "getPostsById",
        "tags": ["Post"],
        "description": "Get a post by ID.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": { "type": "string", "format": "uuid" },
            "required": true
          }
        ]
      },
      "put": {
        "responses": {
          "204": { "description": "Post successfully updated." },
          "400": {
            "description": "Invalid request due to bad input.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "message": { "type": "string" } },
                  "required": ["message"]
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "message": { "type": "string" } },
                  "required": ["message"]
                }
              }
            }
          }
        },
        "operationId": "putPostsById",
        "tags": ["Post"],
        "description": "Update a post by ID.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": { "type": "string", "format": "uuid" },
            "required": true
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": { "type": "string", "minLength": 1, "maxLength": 40 },
                  "content": { "type": "string", "minLength": 1, "maxLength": 140 }
                },
                "required": []
              }
            }
          }
        }
      },
      "delete": {
        "responses": {
          "204": { "description": "Post successfully deleted." },
          "400": {
            "description": "Invalid request due to bad input.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "message": { "type": "string" } },
                  "required": ["message"]
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "message": { "type": "string" } },
                  "required": ["message"]
                }
              }
            }
          }
        },
        "operationId": "deletePostsById",
        "tags": ["Post"],
        "description": "Delete a post by ID.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": { "type": "string", "format": "uuid" },
            "required": true
          }
        ]
      }
    },
    "/comments/{id}": {
      "post": {
        "responses": {
          "201": {
            "description": "Comment created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string", "format": "uuid" },
                    "postId": { "type": "string", "format": "uuid" },
                    "post": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "string", "format": "uuid" },
                        "title": { "type": "string", "minLength": 1, "maxLength": 40 },
                        "content": { "type": "string", "minLength": 1, "maxLength": 140 },
                        "comments": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": { "type": "string", "format": "uuid" },
                              "postId": { "type": "string", "format": "uuid" },
                              "content": { "type": "string", "minLength": 1, "maxLength": 140 },
                              "createdAt": {
                                "type": "string",
                                "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z$"
                              }
                            },
                            "required": ["id", "postId", "content", "createdAt"]
                          }
                        },
                        "createdAt": {
                          "type": "string",
                          "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z$"
                        },
                        "updatedAt": {
                          "type": "string",
                          "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z$"
                        }
                      },
                      "required": ["id", "title", "content", "comments", "createdAt", "updatedAt"]
                    },
                    "content": { "type": "string", "minLength": 1, "maxLength": 140 },
                    "createdAt": {
                      "type": "string",
                      "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z$"
                    }
                  },
                  "required": ["id", "postId", "post", "content", "createdAt"]
                }
              }
            }
          },
          "400": {
            "description": "Invalid request due to bad input.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "message": { "type": "string" } },
                  "required": ["message"]
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "message": { "type": "string" } },
                  "required": ["message"]
                }
              }
            }
          }
        },
        "operationId": "postCommentsById",
        "tags": ["Comment"],
        "description": "Create a comment",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": { "type": "string", "format": "uuid" },
            "required": true
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": { "content": { "type": "string", "minLength": 1, "maxLength": 140 } },
                "required": ["content"]
              }
            }
          }
        }
      },
      "get": {
        "responses": {
          "200": {
            "description": "Comment fetched successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string", "format": "uuid" },
                    "postId": { "type": "string", "format": "uuid" },
                    "post": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "string", "format": "uuid" },
                        "title": { "type": "string", "minLength": 1, "maxLength": 40 },
                        "content": { "type": "string", "minLength": 1, "maxLength": 140 },
                        "comments": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": { "type": "string", "format": "uuid" },
                              "postId": { "type": "string", "format": "uuid" },
                              "content": { "type": "string", "minLength": 1, "maxLength": 140 },
                              "createdAt": {
                                "type": "string",
                                "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z$"
                              }
                            },
                            "required": ["id", "postId", "content", "createdAt"]
                          }
                        },
                        "createdAt": {
                          "type": "string",
                          "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z$"
                        },
                        "updatedAt": {
                          "type": "string",
                          "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z$"
                        }
                      },
                      "required": ["id", "title", "content", "comments", "createdAt", "updatedAt"]
                    },
                    "content": { "type": "string", "minLength": 1, "maxLength": 140 },
                    "createdAt": {
                      "type": "string",
                      "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z$"
                    }
                  },
                  "required": ["id", "postId", "post", "content", "createdAt"]
                }
              }
            }
          },
          "400": {
            "description": "Invalid request due to bad input.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "message": { "type": "string" } },
                  "required": ["message"]
                }
              }
            }
          },
          "404": {
            "description": "Comment not found.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "message": { "type": "string" } },
                  "required": ["message"]
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "message": { "type": "string" } },
                  "required": ["message"]
                }
              }
            }
          }
        },
        "operationId": "getCommentsById",
        "tags": ["Comment"],
        "description": "Get a comment by ID.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": { "type": "string", "format": "uuid" },
            "required": true
          }
        ]
      },
      "put": {
        "responses": {
          "204": { "description": "Comment successfully updated." },
          "400": {
            "description": "Invalid request due to bad input.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "message": { "type": "string" } },
                  "required": ["message"]
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "message": { "type": "string" } },
                  "required": ["message"]
                }
              }
            }
          }
        },
        "operationId": "putCommentsById",
        "tags": ["Comment"],
        "description": "Update a comment by ID.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": { "type": "string", "format": "uuid" },
            "required": true
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": { "content": { "type": "string", "minLength": 1, "maxLength": 140 } },
                "required": ["content"]
              }
            }
          }
        }
      },
      "delete": {
        "responses": {
          "204": { "description": "Comment successfully deleted." },
          "400": {
            "description": "Invalid request due to bad input.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "message": { "type": "string" } },
                  "required": ["message"]
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": { "message": { "type": "string" } },
                  "required": ["message"]
                }
              }
            }
          }
        },
        "operationId": "deleteCommentsById",
        "tags": ["Comment"],
        "description": "Delete a comment by ID.",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "schema": { "type": "string", "format": "uuid" },
            "required": true
          }
        ]
      }
    }
  },
  "components": { "schemas": {} }
}

โ€ƒไปฅไธ‹ใŒ็”Ÿๆˆใ•ใ‚ŒใŸใ‚ณใƒผใƒ‰ใงใ™ใ€‚

import { createRoute, z } from '@hono/zod-openapi'

export const getRoute = createRoute({
  tags: ['Hono'],
  method: 'get',
  path: '/',
  description: 'Hono Valibot๐Ÿ”ฅ',
  responses: {
    200: { description: 'Hono Valibot๐Ÿ”ฅ', content: { 'application/json': { schema: z.string() } } },
  },
})

export const postPostsRoute = createRoute({
  tags: ['Post'],
  method: 'post',
  path: '/posts',
  description: 'Create a post',
  request: {
    body: {
      required: false,
      content: {
        'application/json': {
          schema: z.object({
            title: z.string().min(1).max(40),
            content: z.string().min(1).max(140),
          }),
        },
      },
    },
  },
  responses: {
    201: {
      description: 'Post created successfully',
      content: {
        'application/json': {
          schema: z.object({
            id: z.string().uuid(),
            title: z.string().min(1).max(40),
            content: z.string().min(1).max(140),
            comments: z.array(
              z.object({
                id: z.string().uuid(),
                postId: z.string().uuid(),
                content: z.string().min(1).max(140),
                createdAt: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/),
              }),
            ),
            createdAt: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/),
            updatedAt: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/),
          }),
        },
      },
    },
    400: {
      description: 'Invalid request due to bad input.',
      content: { 'application/json': { schema: z.object({ message: z.string() }) } },
    },
    500: {
      description: 'Internal server error.',
      content: { 'application/json': { schema: z.object({ message: z.string() }) } },
    },
  },
})

export const getPostsRoute = createRoute({
  tags: ['Post'],
  method: 'get',
  path: '/posts',
  description: 'Get posts',
  request: { query: z.object({ page: z.string(), rows: z.string() }) },
  responses: {
    200: {
      description: 'Posts fetched successfully',
      content: {
        'application/json': {
          schema: z.object({
            id: z.string().uuid(),
            title: z.string().min(1).max(40),
            content: z.string().min(1).max(140),
            comments: z.array(
              z.object({
                id: z.string().uuid(),
                postId: z.string().uuid(),
                content: z.string().min(1).max(140),
                createdAt: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/),
              }),
            ),
            createdAt: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/),
            updatedAt: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/),
          }),
        },
      },
    },
    400: {
      description: 'Invalid request due to bad input.',
      content: { 'application/json': { schema: z.object({ message: z.string() }) } },
    },
    500: {
      description: 'Internal server error.',
      content: { 'application/json': { schema: z.object({ message: z.string() }) } },
    },
  },
})

export const getPostsIdRoute = createRoute({
  tags: ['Post'],
  method: 'get',
  path: '/posts/{id}',
  description: 'Get a post by ID.',
  request: { params: z.object({ id: z.string().uuid() }) },
  responses: {
    200: {
      description: 'Post fetched successfully',
      content: {
        'application/json': {
          schema: z.object({
            id: z.string().uuid(),
            title: z.string().min(1).max(40),
            content: z.string().min(1).max(140),
            comments: z.array(
              z.object({
                id: z.string().uuid(),
                postId: z.string().uuid(),
                content: z.string().min(1).max(140),
                createdAt: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/),
              }),
            ),
            createdAt: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/),
            updatedAt: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/),
          }),
        },
      },
    },
    400: {
      description: 'Invalid request due to bad input.',
      content: { 'application/json': { schema: z.object({ message: z.string() }) } },
    },
    500: {
      description: 'Internal server error.',
      content: { 'application/json': { schema: z.object({ message: z.string() }) } },
    },
  },
})

export const putPostsIdRoute = createRoute({
  tags: ['Post'],
  method: 'put',
  path: '/posts/{id}',
  description: 'Update a post by ID.',
  request: {
    body: {
      required: false,
      content: {
        'application/json': {
          schema: z
            .object({ title: z.string().min(1).max(40), content: z.string().min(1).max(140) })
            .partial(),
        },
      },
    },
    params: z.object({ id: z.string().uuid() }),
  },
  responses: {
    204: { description: 'Post successfully updated.' },
    400: {
      description: 'Invalid request due to bad input.',
      content: { 'application/json': { schema: z.object({ message: z.string() }) } },
    },
    500: {
      description: 'Internal server error.',
      content: { 'application/json': { schema: z.object({ message: z.string() }) } },
    },
  },
})

export const deletePostsIdRoute = createRoute({
  tags: ['Post'],
  method: 'delete',
  path: '/posts/{id}',
  description: 'Delete a post by ID.',
  request: { params: z.object({ id: z.string().uuid() }) },
  responses: {
    204: { description: 'Post successfully deleted.' },
    400: {
      description: 'Invalid request due to bad input.',
      content: { 'application/json': { schema: z.object({ message: z.string() }) } },
    },
    500: {
      description: 'Internal server error.',
      content: { 'application/json': { schema: z.object({ message: z.string() }) } },
    },
  },
})

export const postCommentsIdRoute = createRoute({
  tags: ['Comment'],
  method: 'post',
  path: '/comments/{id}',
  description: 'Create a comment',
  request: {
    body: {
      required: false,
      content: {
        'application/json': { schema: z.object({ content: z.string().min(1).max(140) }) },
      },
    },
    params: z.object({ id: z.string().uuid() }),
  },
  responses: {
    201: {
      description: 'Comment created successfully',
      content: {
        'application/json': {
          schema: z.object({
            id: z.string().uuid(),
            postId: z.string().uuid(),
            post: z.object({
              id: z.string().uuid(),
              title: z.string().min(1).max(40),
              content: z.string().min(1).max(140),
              comments: z.array(
                z.object({
                  id: z.string().uuid(),
                  postId: z.string().uuid(),
                  content: z.string().min(1).max(140),
                  createdAt: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/),
                }),
              ),
              createdAt: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/),
              updatedAt: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/),
            }),
            content: z.string().min(1).max(140),
            createdAt: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/),
          }),
        },
      },
    },
    400: {
      description: 'Invalid request due to bad input.',
      content: { 'application/json': { schema: z.object({ message: z.string() }) } },
    },
    500: {
      description: 'Internal server error.',
      content: { 'application/json': { schema: z.object({ message: z.string() }) } },
    },
  },
})

export const getCommentsIdRoute = createRoute({
  tags: ['Comment'],
  method: 'get',
  path: '/comments/{id}',
  description: 'Get a comment by ID.',
  request: { params: z.object({ id: z.string().uuid() }) },
  responses: {
    200: {
      description: 'Comment fetched successfully',
      content: {
        'application/json': {
          schema: z.object({
            id: z.string().uuid(),
            postId: z.string().uuid(),
            post: z.object({
              id: z.string().uuid(),
              title: z.string().min(1).max(40),
              content: z.string().min(1).max(140),
              comments: z.array(
                z.object({
                  id: z.string().uuid(),
                  postId: z.string().uuid(),
                  content: z.string().min(1).max(140),
                  createdAt: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/),
                }),
              ),
              createdAt: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/),
              updatedAt: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/),
            }),
            content: z.string().min(1).max(140),
            createdAt: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/),
          }),
        },
      },
    },
    400: {
      description: 'Invalid request due to bad input.',
      content: { 'application/json': { schema: z.object({ message: z.string() }) } },
    },
    404: {
      description: 'Comment not found.',
      content: { 'application/json': { schema: z.object({ message: z.string() }) } },
    },
    500: {
      description: 'Internal server error.',
      content: { 'application/json': { schema: z.object({ message: z.string() }) } },
    },
  },
})

export const putCommentsIdRoute = createRoute({
  tags: ['Comment'],
  method: 'put',
  path: '/comments/{id}',
  description: 'Update a comment by ID.',
  request: {
    body: {
      required: false,
      content: {
        'application/json': { schema: z.object({ content: z.string().min(1).max(140) }) },
      },
    },
    params: z.object({ id: z.string().uuid() }),
  },
  responses: {
    204: { description: 'Comment successfully updated.' },
    400: {
      description: 'Invalid request due to bad input.',
      content: { 'application/json': { schema: z.object({ message: z.string() }) } },
    },
    500: {
      description: 'Internal server error.',
      content: { 'application/json': { schema: z.object({ message: z.string() }) } },
    },
  },
})

export const deleteCommentsIdRoute = createRoute({
  tags: ['Comment'],
  method: 'delete',
  path: '/comments/{id}',
  description: 'Delete a comment by ID.',
  request: { params: z.object({ id: z.string().uuid() }) },
  responses: {
    204: { description: 'Comment successfully deleted.' },
    400: {
      description: 'Invalid request due to bad input.',
      content: { 'application/json': { schema: z.object({ message: z.string() }) } },
    },
    500: {
      description: 'Internal server error.',
      content: { 'application/json': { schema: z.object({ message: z.string() }) } },
    },
  },
})

ใŠใ‚ใ‚Šใซ

โ€ƒHono OpenAPIใจValibotใ‚’่ฉฆใ—ใฆใฟใพใ—ใŸใ€‚ไป–ใซใ‚‚ใ€Zodใ‚„ArkTypeใ€TypeBoxใชใฉใ‚‚้ธๆŠž่‚ขใŒใ‚ใ‚‹ใฎใงใ€ใใ‚Œใ‚‰ใ‚‚่ฉฆใ—ใฆใฟใŸใ„ใงใ™ใ€‚

ๅ‚่€ƒใƒชใƒณใ‚ฏ

Discussion

ใƒญใ‚ฐใ‚คใƒณใ™ใ‚‹ใจใ‚ณใƒกใƒณใƒˆใงใใพใ™