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.ts
2"use server";
3
4import { revalidatePath } from "next/cache";
5import { redirect } from "next/navigation";
6import { z } from "zod";
7
8// Validation schema
9const 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 type
18type ActionResult<T> =
19 | { success: true; data: T }
20 | { success: false; error: string; fieldErrors?: Record<string, string[]> };
21
22// Create user action
23export async function createUser(
24 formData: FormData
25): 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 action
66export async function updateUser(
67 id: number,
68 formData: FormData
69): 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 action
97export 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 action
111import { 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