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:
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/optionalenumvaluesmin/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)fromnext-intl - For 400 errors returned from the backend, assign
error.issuesto the corresponding form fields viasetError()
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
onErrorand redirect to/loginif 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.