Guide/guides

API を構築して公開する

このガイドでは、Guren で JSON API を構築して公開するまでの手順を説明します。API 専用プロジェクトの作成、データベーススキーマの定義、バリデーション付きコントローラーの作成、エンドポイントのテストまでをカバーします。

API を構築して公開する

このガイドでは、Guren で JSON API を構築して公開するまでの手順を説明します。API 専用プロジェクトの作成、データベーススキーマの定義、バリデーション付きコントローラーの作成、エンドポイントのテストまでをカバーします。

note

コントローラー、バリデーション、ミドルウェアの詳細はコントローラーバリデーションを参照してください。

前提条件

  • Bun 1.1 以降
  • Docker Desktop (Compose v2) — Postgres 用

1. API プロジェクトを作成する

api ブループリントを指定すると、Inertia やフロントエンドツールを省いた軽量な API スターターが生成されます:

bunx create-guren-app my-api --blueprint api --db postgres
cd my-api
bun install

2. データベースを起動する

bun run db:up

3. スキーマを定義する

db/schema.ts を開いてテーブルを追加します。以下はシンプルな tasks テーブルの例です:

import { pgTable, serial, text, boolean, timestamp } from '@guren/orm/drizzle/pg'

export const tasks = pgTable('tasks', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
  completed: boolean('completed').notNull().default(false),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
})

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

bunx guren db:migrate:generate create_tasks
bunx guren db:migrate

4. モデルを作成する

bunx guren make:model Task

スキーマと関連付けます:

import { defineModel } from '@guren/core'
import { tasks } from '@/db/schema'

export class Task extends defineModel(tasks) {}

5. コントローラーを作成する

bunx guren make:controller TaskController

Zod バリデーション付きの CRUD アクションを追加します:

import { Controller } from '@guren/core'
import { z } from 'zod'
import { Task } from '@/app/Models/Task'

const CreateTaskSchema = z.object({
  title: z.string().min(1).max(255),
})

const UpdateTaskSchema = z.object({
  title: z.string().min(1).max(255).optional(),
  completed: z.boolean().optional(),
})

const TaskIdSchema = z.object({
  id: z.coerce.number().int().positive(),
})

export class TaskController extends Controller {
  async index() {
    const tasks = await Task.all()
    return this.json({ tasks })
  }

  async show() {
    const { id } = this.validateParams(TaskIdSchema)
    const task = await Task.findOrFail(id)
    return this.json({ task })
  }

  async store() {
    const data = await this.validateBody(CreateTaskSchema)
    const task = await Task.create(data)
    return this.json({ task }, 201)
  }

  async update() {
    const { id } = this.validateParams(TaskIdSchema)
    const data = await this.validateBody(UpdateTaskSchema)
    const task = await Task.findOrFail(id)
    await task.update(data)
    return this.json({ task })
  }

  async destroy() {
    const { id } = this.validateParams(TaskIdSchema)
    const task = await Task.findOrFail(id)
    await task.delete()
    return this.json({ message: 'Deleted' })
  }
}

6. ルートを登録する

routes/web.ts(API 専用プロジェクトでは routes/api.ts)を開いて追加します:

import { Router } from '@guren/core'
import { TaskController } from '@/app/Http/Controllers/TaskController'

export function registerApiRoutes(router: Router): void {
  router.get('/api/tasks', [TaskController, 'index']).name('tasks.index')
  router.get('/api/tasks/:id', [TaskController, 'show']).name('tasks.show')
  router.post('/api/tasks', [TaskController, 'store']).name('tasks.store')
  router.put('/api/tasks/:id', [TaskController, 'update']).name('tasks.update')
  router.delete('/api/tasks/:id', [TaskController, 'destroy']).name('tasks.destroy')
}

7. 型マニフェストを生成する

bun run codegen

ここでは .guren/pages.gen.ts は生成されません。このマニフェストは @guren/inertia-client を import しますが、API 専用アプリはそのパッケージを インストールしていない一方で、tsconfig.json.guren/ 配下をすべて型検査 します。生成してしまうと bun run typecheck が 1 行目で落ちます。

この判断はスキャフォルダーではなく codegen が持っています。resources/js/pages にページコンポーネントが現れたとき — 手でコピーした場合でも、チェックアウトで 入ってきた場合でも — codegen はマニフェストを書かず、その理由を出力します:

[warn] 1 page component under resources/js/pages, but this app has no
@guren/inertia-client dependency and no routes/web.ts, so codegen writes no
.guren/pages.gen.ts

guren checkguren doctor も同じ状態を報告します。アプリがこの形になる前に 生成された .guren/pages.gen.ts がディスクに残っている場合はより強く警告します。 tsc を落とすのはこの残骸なので、原因となったページコンポーネントを削除した後でも 報告され、guren check --ci はこの状態で失敗します(未使用のページコンポーネント だけでは CI は失敗しません)。codegen はそのファイルを削除しません。本当に必要な ファイルを消してしまうと、型エラーが原因不明の不具合に変わるからです。不要なら 自分で削除し、Inertia のページを描画するアプリであれば @guren/inertia-client の 依存と routes/web.ts を追加してください。

8. エンドポイントをテストする

開発サーバーを起動します:

bun run dev

curl や任意の HTTP クライアントでリクエストを送ります:

# タスクを作成
curl -X POST http://localhost:3333/api/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "ドキュメントを書く"}'

# タスク一覧を取得
curl http://localhost:3333/api/tasks

# タスクを取得
curl http://localhost:3333/api/tasks/1

# タスクを更新
curl -X PUT http://localhost:3333/api/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"completed": true}'

# タスクを削除
curl -X DELETE http://localhost:3333/api/tasks/1

9. API トークン認証を追加する

認証が必要なルートにはAPIトークンを配線します。これにスキャフォールドはありません — guren add auth は Inertia のサインイン画面を生成するため、API 専用アプリでは 実行を拒否します。ミドルウェアは自分で用意してください:

import { createBearerTokenMiddleware, DatabaseApiTokenStore } from '@guren/core'
import { apiTokens } from '@/db/schema'

const store = new DatabaseApiTokenStore(apiTokens)

export const requireApiToken = createBearerTokenMiddleware({ store })

これで変更系のルートを保護します:

router.middleware(requireApiToken).group((auth) => {
  auth.post('/api/tasks', [TaskController, 'store']).name('tasks.store')
  auth.put('/api/tasks/:id', [TaskController, 'update']).name('tasks.update')
  auth.delete('/api/tasks/:id', [TaskController, 'destroy']).name('tasks.destroy')
})

api_tokens テーブル、createApiToken でのトークン発行、abilities によるスコープ 制限についてはAPIトークンガイドを参照してください。

クライアントは Authorization ヘッダーにトークンを含めます:

curl -X POST http://localhost:3333/api/tasks \
  -H "Authorization: Bearer your-api-token" \
  -H "Content-Type: application/json" \
  -d '{"title": "認証済みタスク"}'

次のステップ