Skip to content

UI for Admin — Coding Rules


Validation

Single Source of Truth

The backend Zod 4 schema is the single source of truth. The UI-side Zod is generated through the following pipeline:

Backend Zod 4
     ↓
@hono/zod-openapi outputs OpenAPI 3.1
     ↓
Orval generates UI-side Zod

This ensures that whenever the Admin API request schema changes, both the fetcher types and the form Zod schemas are updated simultaneously.

Constraints Preserved via OpenAPI

  • Primitive types (string, number, boolean, integer)
  • required / optional
  • enum values
  • min / max, minLength / maxLength
  • String formats (email, uuid, date-time, ...)
  • Regular expressions (pattern)
  • Nested object shapes

Constraints Not Preserved via OpenAPI

  • .refine() / .superRefine() (cross-field validation, conditional rules)
  • Output shape transformations via .transform()
  • Branded types
  • Custom error messages authored on the backend

These must be supplemented on the UI side.

Hand-Authored Refinements

Place derived schemas that wrap the generated Zod in src/api/validators/<resource>.ts:

// 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 key
  },
)

Conventions:

  • File names are per resource (admin.ts, organization.ts, ...)
  • Export names may match the generated schema name. The fact that they are "extended for UI use" is conveyed by the file location.
  • When the backend adds the same refinement, you can remove the refinement from this file. Always prefer the Orval-generated output when it is sufficient on its own.

Integration with 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: { /* ... */ },
})

Error Messages and i18n

  • Error message string values should be i18n keys (do not write Japanese or English strings directly in Zod)
  • Localize at render time using t(field.error.message) from next-intl
  • For 400 errors returned from the backend, assign error.issues to the corresponding form fields via setError()

Scope of Client-Side Validation

  • UX only — for reducing latency and providing immediate feedback
  • Not a security boundary — the Admin API re-validates using the same Zod schema; the server is authoritative
  • Do not skip — the cost is nearly zero since you only need to feed in the generated Zod

API Client Usage Guidelines

Integration with TanStack Query

Hooks generated by Orval are based on TanStack Query v5. Customize them via the queryOptions argument:

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

After a successful mutation, refetch the list with queryClient.invalidateQueries({ queryKey: getGetAdminsQueryKey() }).

Error Handling

  • 401 → Delegate to the fetcher's refresh mechanism (automatic retry; see Setup)
  • 403 → Insufficient permissions. Show a toast via TanStack Query's onError and redirect to /login if necessary
  • 5xx → Do not implement circuit-breaker behavior (the Admin API has its own rate limiting). Present a UI that prompts the user to retry

Handling Generated Files

Generated files (src/api/generated/) should be committed. Any manual edits made by humans will be overwritten on the next generate:api run. Write any additional logic in src/api/validators/ or on the UI side.