HonoX Prisma
HonoXのプロジェクトを立ち上げる。
sh
yarn create hono
APIを作成し、UIに表示させる。
Directory Structure
.
├── app
│ ├── client.ts
│ ├── global.d.ts
│ ├── islands
│ │ └── index.tsx
│ ├── routes
│ │ ├── _404.tsx
│ │ ├── api.ts
│ │ ├── _error.tsx
│ │ ├── index.tsx
│ │ └── _renderer.tsx
│ └── server.ts
├── package.json
├── public
│ └── favicon.ico
├── tsconfig.json
├── vite.config.ts
└── wrangler.toml
HonoX RPC
簡単なAPIを作成する。
ts
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import { z } from '@hono/zod-openapi'
import { hc } from 'hono/client'
export const honoSchema = z.object({
message: z.string().openapi({
example: 'HonoX🔥',
}),
})
const app = new OpenAPIHono()
export const route = app.openapi(
createRoute({
method: 'get',
path: '/honox',
responses: {
200: {
content: {
'application/json': {
schema: honoSchema,
},
},
description: 'HonoX🔥',
},
},
}),
(c) => {
return c.json({ message: 'HonoX🔥' })
},
)
export type AddType = typeof route
export const client = hc<AddType>('/api')
export default app
RPCを使用。
tsx
import { useState } from 'hono/jsx'
import { client } from '../routes/api'
const HonoXIslands = () => {
const [message, setMessage] = useState('')
const onSubmit = async () => {
const res = await client.honox.$get()
const data = await res.json()
setMessage(data.message)
}
return (
<>
<h1>RPC</h1>
<button onClick={onSubmit}>Get Message</button>
<h1>{message}</h1>
</>
)
}
export default HonoXIslands
Prisma
sh
yarn add prisma --dev
sh
yarn add @prisma/client
Zod Prisma
sh
yarn add -D zod-prisma
prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
// schemaファイルから、Zodスキーマを生成する
generator zod {
provider = "zod-prisma"
output = "../app/zod/prisma/"
relationModel = true
modelCase = "camelCase"
modelSuffix = "Schema"
useDecimalJs = true
prismaJsonNullability = true
}
model Post {
/// ポストの一意のID
/// @default {Generated by database}
/// @zod.uuid()
id String @id @default(uuid())
/// ポストの内容
/// @zod.min(1)
/// @zod.max(140)
post String
/// ポストが作成された日時
createdAt DateTime @default(now())
/// ポストが最後に更新された日時
updatedAt DateTime @updatedAt
}
Migrate
sh
yarn prisma migrate dev --name init
Generate
sh
yarn prisma generate
Directory Structure
.
├── app
│ ├── client.ts
│ ├── global.d.ts
│ ├── islands
│ │ └── index.tsx
│ ├── routes
│ │ ├── _404.tsx
│ │ ├── api.ts
│ │ ├── _error.tsx
│ │ ├── index.tsx
│ │ └── _renderer.tsx
│ ├── server.ts
│ └── zod
│ ├── honox
│ │ └── index.ts
│ └── prisma
│ ├── index.ts
│ └── post.ts
├── Makefile
├── package.json
├── prisma
│ ├── dev.db
│ ├── dev.db-journal
│ ├── migrations
│ │ ├── *_init
│ │ │ └── migration.sql
│ │ └── migration_lock.toml
│ └── schema.prisma
├── public
│ └── favicon.ico
├── tsconfig.json
├── vite.config.ts
└── wrangler.toml
Zodが自動で生成されます。
ts
import * as z from 'zod'
export const postSchema = z.object({
/**
* ポストの一意のID
* @default {Generated by database}
*/
id: z.string().uuid(),
/**
* ポストの内容
*/
post: z.string().min(1).max(140),
/**
* ポストが作成された日時
*/
createdAt: z.date(),
/**
* ポストが最後に更新された日時
*/
updatedAt: z.date(),
})
Zod OpenAPI
Zod OpenAPIと組み合わせます。
Directory Structure
├── app
│ ├── client.ts
│ ├── components
│ │ ├── Button
│ │ │ └── index.tsx
│ │ ├── Error
│ │ │ └── index.tsx
│ │ ├── Post
│ │ │ ├── PostForm.tsx
│ │ │ └── PostView.tsx
│ │ └── Title
│ │ └── index.tsx
│ ├── core
│ │ ├── domain
│ │ │ └── posts_domain.ts
│ │ ├── handler
│ │ │ ├── honox_handler.ts
│ │ │ ├── posts_handler.ts
│ │ │ └── swagger_handler.ts
│ │ ├── index.ts
│ │ ├── infra
│ │ │ └── prisma.ts
│ │ ├── openapi
│ │ │ ├── honox
│ │ │ │ └── index.ts
│ │ │ ├── index.ts
│ │ │ └── posts
│ │ │ └── index.ts
│ │ └── service
│ │ └── posts_service.ts
│ ├── global.d.ts
│ ├── islands
│ │ ├── hooks
│ │ │ ├── index.ts
│ │ │ └── post_hooks.ts
│ │ ├── index.tsx
│ │ └── post
│ │ ├── index.tsx
│ │ └── PostFormIslands.tsx
│ ├── routes
│ │ ├── _404.tsx
│ │ ├── api.ts
│ │ ├── _error.tsx
│ │ ├── index.tsx
│ │ ├── _renderer.tsx
│ │ └── tweet
│ │ └── index.tsx
│ ├── server.ts
│ ├── styles
│ │ └── index.ts
│ ├── types
│ │ ├── props.ts
│ │ └── state.ts
│ ├── utils
│ │ └── post_utils.ts
│ └── zod
│ ├── honox
│ │ └── index.ts
│ └── prisma
│ ├── index.ts
│ └── post.ts
├── package.json
├── prisma
│ ├── dev.db
│ ├── migrations
│ │ ├── *_init
│ │ │ └── migration.sql
│ │ └── migration_lock.toml
│ └── schema.prisma
├── public
│ └── favicon.ico
├── tsconfig.json
├── vite.config.ts
└── wrangler.toml
ts
import { honox } from './honox'
import { posts } from './posts'
export const routes = {
...honox,
...posts,
}
honox
ts
import { createRoute } from '@hono/zod-openapi'
import { honoxSchema } from '../../../zod/honox'
export const honox = {
HonoX: createRoute({
tags: ['HonoX'],
method: 'get',
path: '/honox',
responses: {
200: {
description: 'HonoX🔥',
content: {
'application/json': {
schema: honoxSchema,
},
},
},
},
}),
}
posts
ts
import { createRoute, z } from '@hono/zod-openapi'
import { postSchema } from '../../../zod/prisma'
// 201 Created
const createdResponse = {
201: {
description: 'Created',
content: {
'application/json': {
schema: z.object({
message: z.string().openapi({
example: 'Created',
}),
}),
},
},
},
}
// 204 No Content
const NoContentResponse = {
204: {
description: 'No Content',
},
}
// 400 Bad Request
const badRequestResponse = {
400: {
description: 'Bad Request',
content: {
'application/json': {
schema: z.object({
message: z.string().openapi({
example: 'Bad Request',
}),
}),
},
},
},
}
// 500 Internal Server Error
const internalServerErrorResponse = {
500: {
description: 'Internal Server Error',
content: {
'application/json': {
schema: z.object({
message: z.string().openapi({
example: 'Internal Server Error',
}),
}),
},
},
},
}
export const posts = {
postPosts: createRoute({
tags: ['Post'],
method: 'post',
path: '/posts',
description: 'Create a new post',
request: {
body: {
required: true,
content: {
'application/json': {
schema: postSchema.pick({
post: true,
}),
},
},
},
},
responses: {
...createdResponse,
...badRequestResponse,
...internalServerErrorResponse,
},
}),
getPosts: createRoute({
tags: ['Post'],
method: 'get',
path: '/posts',
description: 'get PostList posts with optional pagination',
request: {
query: z.object({
page: z.string(),
limit: z.string(),
}),
},
responses: {
200: {
description: 'OK',
content: {
'application/json': {
schema: z.array(postSchema),
},
},
},
...badRequestResponse,
...internalServerErrorResponse,
},
}),
putPosts: createRoute({
tags: ['Post'],
method: 'put',
path: '/posts/{id}',
description: 'update Post',
request: {
params: z.object({
id: z.string().uuid(),
}),
body: {
required: true,
content: {
'application/json': {
schema: postSchema.pick({ post: true }),
},
},
},
},
responses: {
...NoContentResponse,
...badRequestResponse,
...internalServerErrorResponse,
},
}),
deletePosts: createRoute({
tags: ['Post'],
method: 'delete',
path: '/posts/{id}',
description: 'delete post',
request: {
params: z.object({
id: z.string().uuid(),
}),
},
responses: {
...NoContentResponse,
...badRequestResponse,
...internalServerErrorResponse,
},
}),
}
Grouping routes for RPC
ts
import { OpenAPIHono } from '@hono/zod-openapi'
import { basicAuth } from 'hono/basic-auth'
import { HonoXHandler } from './handler/honox_handler'
import { PostsHandler } from './handler/posts_handler'
import { SwaggerHandler } from './handler/swagger_handler'
export class App {
private static username = process.env.SWAGGER_USER
private static password = process.env.SWAGGER_PASSWORD
static init() {
const app = new OpenAPIHono()
if (this.username && this.password) {
app.use('/auth/*', basicAuth({ username: this.username, password: this.password }))
}
return this.applyRoutes(app)
}
static applyRoutes(app: OpenAPIHono) {
return app
.route('/', HonoXHandler.apply(app))
.route('/', PostsHandler.apply(app))
.route('/', SwaggerHandler.apply(app))
}
}
Swagger UI
ts
import { swaggerUI } from '@hono/swagger-ui'
import { OpenAPIHono } from '@hono/zod-openapi'
export class SwaggerHandler {
static apply(app: OpenAPIHono) {
return app
.doc('/auth/doc', {
info: {
title: 'HonoX🔥 API',
version: 'v1',
},
openapi: '3.1.0',
servers: [
{
url: '/api',
description: 'API base path',
},
],
tags: [
{
name: 'HonoX🔥',
description: 'HonoX🔥 API',
},
{
name: 'Post',
description: 'Post API',
},
],
})
.get('/auth/ui', swaggerUI({ url: '/api/auth/doc' }))
}
}
Access Browser
http://localhost:5173/api/auth/ui
ts
import { OpenAPIHono } from '@hono/zod-openapi'
import { routes } from '../openapi'
export class HonoXHandler {
static apply(app: OpenAPIHono) {
return app.openapi(routes.HonoX, async (c) => {
return c.json({ message: 'HonoX🔥' })
})
}
}
ts
import { OpenAPIHono } from '@hono/zod-openapi'
import { Post } from '@prisma/client'
import { routes } from '../openapi'
import { PostService } from '../service/posts_service'
export class PostsHandler {
static apply(app: OpenAPIHono) {
return app
.openapi(routes.postPosts, async (c) => {
try {
const { post } = c.req.valid('json')
await PostService.createPost(post)
return c.json({ message: 'Created' }, 201)
} catch {
return c.json({ message: 'Internal Server Error' }, 500)
}
})
.openapi(routes.getPosts, async (c) => {
try {
const { page, limit } = c.req.valid('query')
const pageNumber = parseInt(page)
const perPage = parseInt(limit)
if (isNaN(pageNumber) || isNaN(perPage) || pageNumber < 1 || perPage < 1) {
return c.json({ message: 'Bad Request' }, 400)
}
const posts: Post[] = await PostService.getPosts(pageNumber, perPage)
return c.json(posts, 200)
} catch {
return c.json({ message: 'Internal Server Error' }, 500)
}
})
.openapi(routes.putPosts, async (c) => {
try {
const { id } = c.req.valid('param')
const json_valid = c.req.valid('json')
const { post } = json_valid
await PostService.putPost(id, post)
return new Response(null, { status: 204 })
} catch {
return c.json({ message: 'Internal Server Error' }, 500)
}
})
.openapi(routes.deletePosts, async (c) => {
try {
const { id } = c.req.valid('param')
await PostService.deletePost(id)
return new Response(null, { status: 204 })
} catch {
return c.json({ message: 'Internal Server Error' }, 500)
}
})
}
}
Hono RPC
ts
import { hc } from 'hono/client'
import { App } from '../core'
const app = App.init()
export type AddType = typeof app
export const client = hc<AddType>('/api')
export default app