コンテンツにスキップ

UI for Admin — セットアップ

管理者 UI は Orval を用いて管理者 API の OpenAPI 3.1 スペック(/api/v1/doc)から以下を自動生成する:

  • TypeScript 型(リクエスト / レスポンス / コンポーネント)
  • TanStack Query フック(useGetAdmins、useCreateAdmin など)
  • Zod スキーマ(フォームバリデーションに利用)

Orval 設定

// orval.config.ts
export default {
  adminApi: {
    input: 'http://localhost:8080/api/v1/doc',
    output: {
      target: 'src/api/generated/admin.ts',
      client: 'react-query',
      schemas: 'src/api/generated/model',
      httpClient: 'fetch',
      override: {
        mutator: { path: 'src/api/client.ts', name: 'adminFetcher' },
      },
    },
  },
  adminApiZod: {
    input: 'http://localhost:8080/api/v1/doc',
    output: {
      target: 'src/api/generated/admin.zod.ts',
      client: 'zod',
    },
  },
}

フェッチャー(src/api/client.ts)

  • 同一オリジン の /api/proxy/<path> に向ける(管理者 API に直接アクセスしない)
  • credentials: 'same-origin' で httpOnly Cookie を自動送信
  • レスポンスエンベロープ正規化: 管理者 API は { success, data } と旧トップレベル形式が混在中(API for Admin 参照)。フェッチャーでラップを吸収しフックには常に unwrap 済みペイロードを渡す
  • 401 リトライ: /api/proxy/<path> が 401 を返したとき、単一の in-flight refresh Promise を作成し並列リクエストをコアレッシング。成功したらオリジナルリクエストをリプレイ
// src/api/client.ts
let refreshPromise: Promise<void> | null = null

export async function adminFetcher<T>(config: AdminFetchConfig): Promise<T> {
  const res = await fetchWithCookies(config)
  if (res.status !== 401) return normalizeEnvelope(res)

  refreshPromise ??= refreshSession().finally(() => { refreshPromise = null })
  await refreshPromise

  const retried = await fetchWithCookies(config)
  return normalizeEnvelope(retried)
}

API クライアント再生成

コマンド 用途
bun run generate:api ローカル開発中。管理者 API を bun run dev-admin で立ち上げてから実行
CI チェック 生成物が OpenAPI と一致するかを比較。不一致ならビルド失敗

OpenAPI スペック入手方法(リポジトリ間連携)

管理者 API は別リポジトリ(GenAI-Guinness-backend)にあるため、OpenAPI スペックの取得方法を決めておく必要がある。

方法 タイミング メリット / デメリット
ローカルバックエンドを起動して取得 開発中 最新仕様を即反映。開発者にバックエンドのセットアップが必要
バックエンド CI が OpenAPI JSON をアーティファクトとして公開し、UI 側でバージョンをピン留め ステージング以降 再現性。バージョンずれのリスク

デフォルト方針: 開発中は方法 1。バックエンドが v1 で安定したら方法 2 に移行。