コンテンツにスキップ

UI for Admin — コーディングルール


バリデーション

真実の源

バックエンド Zod 4 スキーマが唯一の真実の源。UI 側の Zod は以下の経路で生成される:

バックエンド Zod 4
     ↓
@hono/zod-openapi が OpenAPI 3.1 を出力
     ↓
Orval が UI 側の Zod を生成

これにより、管理者 API のリクエストスキーマが変わればフェッチャー型とフォーム Zod の両方が同時に更新される。

OpenAPI 経由で保持される制約

  • プリミティブ型(string, number, boolean, integer)
  • required / optional
  • enum 値
  • min / max、minLength / maxLength
  • 文字列フォーマット(email, uuid, date-time, ...)
  • 正規表現(pattern)
  • ネストしたオブジェクト形状

OpenAPI 経由で 保持されない 制約

  • .refine() / .superRefine()(クロスフィールド検証、条件付きルール)
  • .transform() による出力形状変換
  • ブランド型
  • バックエンドでオーサリングされたカスタムエラーメッセージ

これらは UI 側で補う必要がある。

ハンドオーサリング refinement

src/api/validators/<resource>.ts に生成 Zod をラップした派生スキーマを配置する:

// src/api/validators/admin.ts
import { z } from 'zod'
import { adminCreateBodySchema as generated } from '@/api/generated/admin.zod'

export const adminCreateBodySchema = generated.refine(
  (v) => v.password === v.confirmPassword,
  {
    path: ['confirmPassword'],
    message: 'validation.password.confirm.mismatch', // next-intl のキー
  },
)

規約:

  • ファイル名はリソース単位(admin.ts, organization.ts, ...)
  • エクスポート名は生成スキーマと同じで可。「UI 用に拡張されたもの」である点はファイルロケーションで示す
  • バックエンドが同じ refinement を追加したときは、本ファイル側の refinement を削除できる。Orval 生成物のみで済む場合は常にそちらを優先

React Hook Form との統合

import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { adminCreateBodySchema } from '@/api/validators/admin'

const form = useForm<z.infer<typeof adminCreateBodySchema>>({
  resolver: zodResolver(adminCreateBodySchema),
  defaultValues: { /* ... */ },
})

エラーメッセージと i18n

  • エラーメッセージの文字列値は i18n キー にする(日本語・英語の文字列を Zod に直接書かない)
  • レンダリング時に next-intl の t(field.error.message) でローカライズ
  • バックエンドから返る 400 エラーは error.issues をフォーム上の setError() で対応するフィールドに割り当てる

クライアント側バリデーションのスコープ

  • UX のみ — レイテンシ削減と即時フィードバックのため
  • セキュリティ境界ではない — 管理者 API が同じ Zod スキーマで再検証する。サーバーが権威
  • スキップしない — 生成済み Zod を投入するだけでコストはほぼゼロ

API クライアント利用規約

TanStack Query との統合

Orval が生成するフックは TanStack Query v5 ベース。カスタマイズは queryOptions 引数で行う:

const { data, isLoading } = useGetAdmins(
  { page, limit, sort },
  { query: { staleTime: 30_000, placeholderData: keepPreviousData } },
)

ミューテーションの成功後は queryClient.invalidateQueries({ queryKey: getGetAdminsQueryKey() }) でリストを再取得。

エラーハンドリング

  • 401 → フェッチャーのリフレッシュ機構に委譲(自動リトライ、詳細は セットアップ)
  • 403 → 権限不足。TanStack Query の onError でトーストを出し、必要に応じて /login にリダイレクト
  • 5xx → サーキットブレイカー的挙動は入れない(管理者 API 側にレート制限あり)。ユーザーに再試行を促す UI を提示

生成ファイルの扱い

生成されたファイル(src/api/generated/)は コミットする。人間が手動編集した場合、次の generate:api で上書きされる。追加のロジックは src/api/validators/ か UI 側で記述する。