Server Actions in Next.js
Learn how to use Server Actions in Next.js, including setting up your project, understanding the App Router and Route Handlers, handling multiple HTTP methods, implementing dynamic routing, creating reusable middleware logic, and deciding when to spin up a dedicated API layer.
What are Server Actions?
Server Actions are asynchronous functions that run on the server. They allow you to perform server-side operations like database mutations, form submissions, and API calls directly from your React components without needing to create separate API routes.
Why Use Server Actions?
- Simplified Data Mutations: Handle form submissions and data changes easily
- Enhanced Security: Code runs on the server, keeping sensitive logic private
- Progressive Enhancement: Forms work even with JavaScript disabled
- Type Safety: Full TypeScript support for better development experience
Setting Up Server Actions
First, ensure you have Next.js 14 or later installed. Enable Server Actions in your next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true,
},
};
module.exports = nextConfig;Creating Your First Server Action
Let's create a simple Server Action to handle a form submission:
// app/actions/todo-actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
// Define types for our data
interface Todo {
id: number;
text: string;
completed: boolean;
}
// Create a Server Action
export async function addTodo(prevState: any, formData: FormData) {
const newTodo = formData.get("todo") as string;
// Validate input
if (!newTodo || newTodo.trim().length === 0) {
return {
message: "Please enter a valid todo",
success: false,
};
}
try {
// Here you would typically save to a database
console.log("Adding todo:", newTodo);
// Revalidate the cache for the todos page
revalidatePath("/todos");
return {
message: "Todo added successfully!",
success: true,
};
} catch (error) {
return {
message: "Failed to add todo",
success: false,
};
}
}
// Another Server Action to delete a todo
export async function deleteTodo(id: number) {
try {
// Delete logic would go here
console.log("Deleting todo with id:", id);
revalidatePath("/todos");
return { success: true };
} catch (error) {
return { success: false, error: "Failed to delete todo" };
}
}Using Server Actions in Components
Now let's use our Server Actions in a React component:
// app/components/TodoForm.tsx
"use client";
import { useActionState } from "react";
import { addTodo } from "../actions/todo-actions";
// Initial state for our form
const initialState = {
message: "",
success: false,
};
export default function TodoForm() {
// useActionState hook manages the state of the Server Action
const [state, formAction, isPending] = useActionState(
addTodo,
initialState,
);
return (
<div className="mx-auto mt-8 max-w-md">
<form action={formAction} className="space-y-4">
<div>
<label
htmlFor="todo"
className="block text-sm font-medium text-gray-700"
>
Add Todo
</label>
<input
type="text"
id="todo"
name="todo"
required
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500"
disabled={isPending}
/>
</div>
<button
type="submit"
disabled={isPending}
className="flex w-full justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50"
>
{isPending ? "Adding..." : "Add Todo"}
</button>
{state.message && (
<p
className={`text-sm ${
state.success ? "text-green-600" : "text-red-600"
}`}
>
{state.message}
</p>
)}
</form>
</div>
);
}Using Server Actions with Buttons
You can also trigger Server Actions from buttons using the formAction attribute:
"use client";
import { deleteTodo } from "../actions/todo-actions";
interface TodoItemProps {
id: number;
text: string;
}
export default function TodoItem({ id, text }: TodoItemProps) {
// Create a form for the delete action
const handleDelete = async () => {
if (window.confirm("Are you sure you want to delete this todo?")) {
await deleteTodo(id);
}
};
return (
<div className="flex items-center justify-between border-b border-gray-200 p-4">
<p className="text-lg">{text}</p>
<form>
<button
formAction={handleDelete}
className="rounded bg-red-500 px-3 py-1 text-white hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
>
Delete
</button>
</form>
</div>
);
}Server Components and Server Actions
You can also use Server Actions directly in Server Components:
// app/todos/page.tsx
import TodoForm from "../components/TodoForm";
import TodoItem from "../components/TodoItem";
// This is a Server Component that fetches data
async function getTodos() {
// In a real app, you would fetch from your database
return [
{ id: 1, text: "Learn Next.js", completed: false },
{ id: 2, text: "Build an app with Server Actions", completed: false },
];
}
export default async function TodosPage() {
const todos = await getTodos();
return (
<div className="container mx-auto px-4 py-8">
<h1 className="mb-8 text-3xl font-bold">My Todos</h1>
<TodoForm />
<div className="mt-8">
<h2 className="mb-4 text-2xl font-semibold">Todo List</h2>
<div className="space-y-2">
{todos.map((todo) => (
<TodoItem key={todo.id} id={todo.id} text={todo.text} />
))}
</div>
</div>
</div>
);
}Error Handling and Validation
Proper error handling is crucial for Server Actions:
// app/actions/user-actions.ts
"use server";
import { z } from "zod";
// Define validation schema with Zod
const userSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().email("Please enter a valid email address"),
});
export async function createUser(prevState: any, formData: FormData) {
// Validate form data
const validatedFields = userSchema.safeParse({
name: formData.get("name"),
email: formData.get("email"),
});
// Return early if the form data is invalid
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: "Missing Fields. Failed to Create User.",
};
}
// Process the form data
try {
// Save user to database
// await db.user.create({ data: validatedFields.data });
revalidatePath("/users");
return {
success: true,
message: "User created successfully!",
};
} catch (error) {
console.error("Database Error:", error);
return {
success: false,
message: "Database Error: Failed to Create User.",
};
}
}Security Considerations
Server Actions run on the server, but you should still follow security best practices:
- Validate all inputs on the server side
- Use authentication to protect sensitive actions
- Implement rate limiting to prevent abuse
- Sanitize data before storing in databases
// app/actions/auth-actions.ts
"use server";
import { auth, signIn } from "@/auth";
import { redirect } from "next/navigation";
export async function loginUser(
prevState: string | undefined,
formData: FormData,
) {
try {
await signIn("credentials", {
email: formData.get("email"),
password: formData.get("password"),
redirect: false,
});
redirect("/dashboard");
} catch (error) {
return "Authentication failed. Please check your credentials.";
}
}Fix "Internal NoFallbackError" in Next.js App Router
Practical steps to resolve NoFallbackError caused by static params, notFound/redirect handling, Node fallbacks, and API responses.
The Art of Code Review
Master the art of code review with practical techniques, best practices, and communication strategies that transform good code into great software and build stronger development teams.