Hono エンドポイントの追加

Hono REST テンプレート (および Full-Stack / Monorepo 派生) はユーザー CRUDを同梱しています。このレシピでは、ルートハンドラ、Drizzle による永続化、Zod バリデーション、OpenAPI ドキュメントまで、新しいエンドポイントをエンドツーエンドで追加する手順を説明します。

デフォルトで提供されるもの

生成されたプロジェクトには以下が既に組み込まれています:

  • src/Server.res — ロガー、/openapi.json/docs (Scalar UI) を備えた Hono アプリ

  • src/Routes/Users.res/users の GET/POST/PUT/DELETE

  • src/Schema.res — Drizzle の users テーブル

  • src/Db.res — libsql クライアント + 汎用クエリヘルパー

  • src/ZodOpenapi.res@hono/zod-openapi 向けの Zod バインディング

目標: 投稿一覧を返し、投稿を作成する /posts エンドポイントを追加します。

Step 1 — Schema.res にテーブルを追加する

let posts = sqliteTable("posts", {
  "id": intCol("id", {"primaryKey": true, "autoIncrement": true}),
  "title": textCol("title", {"notNull": true}),
  "body": textCol("body", {"notNull": true}),
  "authorId": intCol("author_id", {"notNull": true}),
})

マイグレーションを生成して適用します:

pnpm db:generate
pnpm db:migrate

Step 2 — ルートモジュールを書く

src/Routes/Posts.res を作成します:

let register = app => {
  app->Hono.get("/posts", async ctx => {
    let rows =
      await Db.db
      ->Db.select({
        "id": Schema.posts["id"],
        "title": Schema.posts["title"],
        "body": Schema.posts["body"],
        "authorId": Schema.posts["authorId"],
      })
      ->Db.from(Schema.posts)
      ->Db.allAsync
    ctx->Hono.json(rows)
  })

  app->Hono.post("/posts", async ctx => {
    let payload = await ctx->Hono.req->Hono.jsonBody
    let inserted =
      await Db.db
      ->Db.insert(Schema.posts)
      ->Db.values({
        "title": payload["title"],
        "body": payload["body"],
        "authorId": payload["authorId"],
      })
      ->Db.returning
    ctx->Hono.status(201)->Hono.json(inserted->Array.get(0))
  })
}

Step 3 — Server.res に登録する

Routes.Users.register(app)
Routes.Posts.register(app)

Step 4 — Zod バリデーションを追加する

ペイロードの形状を共有の Zod スキーマとして切り出します:

let createPostSchema = ZodOpenapi.object({
  "title": ZodOpenapi.string(~minLength=1, ()),
  "body": ZodOpenapi.string(),
  "authorId": ZodOpenapi.number(),
})

データベースにアクセスする前にバリデーションを実行します:

app->Hono.post("/posts", async ctx => {
  let raw = await ctx->Hono.req->Hono.jsonBody
  switch createPostSchema->ZodOpenapi.safeParse(raw) {
  | {success: true, data} =>
    /* insert data */
  | {success: false, error} =>
    ctx->Hono.status(400)->Hono.json({"error": error})
  }
})

Step 5 — OpenAPI に公開する

テンプレートには /openapi.json/docs が既に配線されています。app->Hono.get(...) で登録したルートは自動的に表示されます。より詳細なメタデータ (タグ、レスポンススキーマ) が必要な場合は @hono/zod-openapicreateRoute ヘルパーに切り替えてください。完全なパターンは Hono ドキュメントを参照してください。

Step 6 — テストする

# Terminal 1
pnpm dev

# Terminal 2
curl -X POST http://localhost:3000/posts \
  -H 'Content-Type: application/json' \
  -d '{"title":"Hello","body":"World","authorId":1}'

curl http://localhost:3000/posts

http://localhost:3000/docs を開くと、Scalar UI で新しいエンドポイントを実行できます。

よくある落とし穴

  • マイグレーションの実行漏れdb:generate が SQL ファイルを生成し、db:migrate が適用します。insert が "no such table" で失敗する場合は Step 2 を飛ばしています。

  • レコードと dict の混在 — Drizzle の行形状は dict(アクセスは レコードと Dict の混同 — Drizzle の行の形状は Dict (payload["title"] でアクセス) です。下流でより厳密な型付けが必要であれば、境界で ReScript レコードに変換してください。

  • フロントエンド開発時の CORS — Hono 側で CORS を設定する代わりに、Vite+ の開発プロキシ (Full-Stack / Monorepo テンプレートは自動設定済み) を使用してください。