S
Server Actions
Type-safe Server Actions for form handling and mutations in Next.js with proper error handling.
Code
typescript
1// app/actions/user.ts2"use server";3 4import { revalidatePath } from "next/cache";5import { redirect } from "next/navigation";6import { z } from "zod";7 8// Validation schema9const CreateUserSchema = z.object({10 name: z.string().min(2).max(50),11 email: z.string().email(),12 role: z.enum(["admin", "user"]).default("user"),13});14 15const UpdateUserSchema = CreateUserSchema.partial();16 17// Action result type18type ActionResult<T> =19 | { success: true; data: T }20 | { success: false; error: string; fieldErrors?: Record<string, string[]> };21 22// Create user action23export async function createUser(24 formData: FormData25): Promise<ActionResult<{ id: number }>> {26 const rawData = {27 name: formData.get("name"),28 email: formData.get("email"),29 role: formData.get("role"),30 };31 32 const validatedFields = CreateUserSchema.safeParse(rawData);33 34 if (!validatedFields.success) {35 return {36 success: false,37 error: "Validation failed",38 fieldErrors: validatedFields.error.flatten().fieldErrors as Record<string, string[]>,39 };40 }41 42 try {43 const response = await fetch("https://api.example.com/users", {44 method: "POST",45 headers: { "Content-Type": "application/json" },46 body: JSON.stringify(validatedFields.data),47 });48 49 if (!response.ok) {50 throw new Error("Failed to create user");51 }52 53 const user = await response.json();54 55 revalidatePath("/users");56 return { success: true, data: { id: user.id } };57 } catch (error) {58 return {59 success: false,60 error: error instanceof Error ? error.message : "Unknown error",61 };62 }63}64 65// Update user action66export async function updateUser(67 id: number,68 formData: FormData69): Promise<ActionResult<void>> {70 const rawData = Object.fromEntries(formData);71 const validatedFields = UpdateUserSchema.safeParse(rawData);72 73 if (!validatedFields.success) {74 return {75 success: false,76 error: "Validation failed",77 fieldErrors: validatedFields.error.flatten().fieldErrors as Record<string, string[]>,78 };79 }80 81 try {82 await fetch(`https://api.example.com/users/${id}`, {83 method: "PATCH",84 headers: { "Content-Type": "application/json" },85 body: JSON.stringify(validatedFields.data),86 });87 88 revalidatePath("/users");89 revalidatePath(`/users/${id}`);90 return { success: true, data: undefined };91 } catch (error) {92 return { success: false, error: "Failed to update user" };93 }94}95 96// Delete user action97export async function deleteUser(id: number): Promise<ActionResult<void>> {98 try {99 await fetch(`https://api.example.com/users/${id}`, {100 method: "DELETE",101 });102 103 revalidatePath("/users");104 redirect("/users");105 } catch (error) {106 return { success: false, error: "Failed to delete user" };107 }108}109 110// app/users/new/page.tsx - Form using server action111import { createUser } from "@/app/actions/user";112 113export default function NewUserPage() {114 return (115 <form action={createUser}>116 <input name="name" placeholder="Name" required />117 <input name="email" type="email" placeholder="Email" required />118 <select name="role">119 <option value="user">User</option>120 <option value="admin">Admin</option>121 </select>122 <button type="submit">Create User</button>123 </form>124 );125}Run this example locally
$ npx create-next-app@latest --typescript