01One way to write a mutation
Every change in the CMS goes through a server action, and every server action is wrapped by the same small function. It validates the input against a Zod schema before anything else runs, so the handler only ever sees parsed, typed data, and it always returns one of three shapes: field errors, an error, or the data. A matching React hook gives every form the same loading state and error handling.
The field-error keys come from the input type, so renaming a form field is a compile error rather than a silent miss at runtime. All 57 server actions, across 12 areas of the CMS, use the wrapper, have a schema and check the session first, with no exceptions. With only one way to write a mutation, nobody has to remember the checks.
export type FieldErrors<T> = { [K in keyof T]?: string[] }; export const createSafeAction = <TInput, TOutput>( schema: z.Schema<TInput>, handler: (data: TInput) => Promise<ActionState<TInput, TOutput>>) => { return async (data: TInput): Promise<ActionState<TInput, TOutput>> => { const result = schema.safeParse(data); if (!result.success) { return { fieldErrors: result.error.flatten().fieldErrors as FieldErrors<TInput> }; } return handler(result.data); };};A hand-written wrapper instead of a library: about 30 lines to own, without another dependency's opinions.
