← Back
Edit Post
Title
Description
Server Functions with Zod validation, typed results, granular cache invalidation.
Content
Markdown supported
# Server Functions Form submissions are classic in-between state challenges. The user clicks submit, data travels to the server, validation runs, the database updates, and finally the UI reflects the change. Without proper coordination, users see frozen buttons, lose their input on errors, or wonder if anything happened at all. Server Functions (`"use server"`) handle the entire mutation lifecycle: validation, database writes, cache invalidation, and error recovery. They run on the server but can be called directly from client components—no API routes needed. ## The Full Pattern A well-structured Server Function handles validation, persistence, cache invalidation, and returns typed results: ```tsx 'use server'; import { refresh, revalidateTag } from 'next/cache'; const postSchema = z.object({ title: z.string().min(1, 'Title is required'), content: z.string().min(1, 'Content is required'), published: z.boolean(), }); export type ActionResult = | { success: true } | { success: false; error: string; formData?: FormValues }; export async function createPost(formData: FormData): Promise<ActionResult> { const rawData = { title: formData.get('title') as string, content: formData.get('content') as string, published: formData.get('published') === 'on', }; const result = postSchema.safeParse(rawData); if (!result.success) { return { success: false, error: result.error.issues[0].message, formData: rawData }; } await prisma.post.create({ data: result.data }); revalidateTag('posts', 'max'); refresh(); return { success: true }; } ``` ## Preserving Input on Errors The key insight is returning submitted data when validation fails. This lets forms repopulate—users don't lose their work: ```tsx type ActionResult = | { success: true } | { success: false; error: string; formData?: FormValues }; ``` When used with `useActionState`, the form automatically repopulates with the returned `formData`. ## Cache Invalidation Strategy After mutations, two things need to happen: background cache invalidation and immediate UI update. ```tsx revalidateTag('posts', 'max'); // Mark cache stale, revalidate in background refresh(); // Force immediate re-render for current user ``` The `'max'` profile tells Next.js to serve stale content while revalidating in the background. Combined with `refresh()`, the current user sees the update immediately while other users get fresh data on their next request. ## Granular Invalidation For updates and deletes, invalidate both the list and the specific item: ```tsx revalidateTag('posts', 'max'); revalidateTag(`post-${slug}`, 'max'); refresh(); ``` This ensures the post list updates and any cached detail pages for that specific post are also refreshed.
Published
Save Changes
Cancel