Next.js Server Actions
Server Actions are a powerful feature introduced in Next.js 13+ that allows you to write server-side code directly alongside your client components. They eliminate the need to create separate API endpoints for data mutations, making your code more cohesive and easier to maintain.
Introduction to Server Actions
Server Actions are built on React's use server directive, which enables you to define asynchronous functions that execute exclusively on the server. This feature enhances the developer experience by:
- Enabling form submissions without client-side JavaScript
- Simplifying data mutations by removing the need for separate API routes
- Providing progressive enhancement by default
- Improving security through automatic input validation and encoding
Let's start with understanding the basic concept before diving into practical examples.
How Server Actions Work
Server Actions rely on the "use server"
directive, which can be used in two ways:
- At the top of a file to mark all exported functions as Server Actions
- Directly inside an async function to mark only that function as a Server Action
Basic Syntax
Here's how you can define Server Actions:
// 1. File-level directive - all functions in this file are Server Actions
"use server";
export async function submitForm(data) {
// Server-side code here
}
// 2. Function-level directive
export async function updateProfile(formData) {
"use server";
// Server-side code here
}
Creating Your First Server Action
Let's create a simple form that uses Server Actions to handle a user signup:
// app/components/SignupForm.jsx
"use client";
import { useState } from "react";
import { signupAction } from "../actions";
export default function SignupForm() {
const [message, setMessage] = useState("");
async function handleSubmit(formData) {
const result = await signupAction(formData);
setMessage(result.message);
}
return (
<div className="max-w-md mx-auto p-4 bg-white rounded shadow">
<h2 className="text-xl font-bold mb-4">Sign Up</h2>
<form action={handleSubmit}>
<div className="mb-4">
<label htmlFor="name" className="block text-sm font-medium mb-1">
Name
</label>
<input
type="text"
id="name"
name="name"
className="w-full px-3 py-2 border rounded"
required
/>
</div>
<div className="mb-4">
<label htmlFor="email" className="block text-sm font-medium mb-1">
Email
</label>
<input
type="email"
id="email"
name="email"
className="w-full px-3 py-2 border rounded"
required
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600"
>
Sign Up
</button>
</form>
{message && <p className="mt-4 text-center">{message}</p>}
</div>
);
}
And here's the server action implementation:
// app/actions.js
"use server";
import { redirect } from "next/navigation";
export async function signupAction(formData) {
// Extract data from the FormData object
const name = formData.get("name");
const email = formData.get("email");
// Validate the data
if (!name || name.length < 3) {
return {
success: false,
message: "Name must be at least 3 characters"
};
}
if (!email || !email.includes("@")) {
return {
success: false,
message: "Please provide a valid email"
};
}
// In a real app, you would save to a database here
// For example: await db.user.create({ data: { name, email } })
console.log("Creating user:", { name, email });
// Return success response or redirect
return {
success: true,
message: `Thanks for signing up, ${name}!`
};
// Alternatively, redirect to another page:
// redirect("/dashboard");
}
Form Handling with Server Actions
Server Actions are particularly powerful for form handling. Here's how you can use them with forms:
Method 1: Using the action
Prop
You can pass a Server Action directly to a form's action
prop:
// app/components/ContactForm.jsx
export default function ContactForm() {
return (
<form action={async (formData) => {
"use server";
// Process form data
const name = formData.get("name");
const message = formData.get("message");
await saveMessage(name, message);
}}>
<input type="text" name="name" />
<textarea name="message"></textarea>
<button type="submit">Send Message</button>
</form>
);
}
Method 2: Revalidating After Submission
Server Actions can be used to revalidate data after submission:
// app/actions.js
"use server";
import { revalidatePath } from "next/cache";
export async function addComment(formData) {
const comment = formData.get("comment");
const postId = formData.get("postId");
// Save comment to database
await db.comment.create({
data: {
content: comment,
postId: postId,
},
});
// Revalidate the post page to show the new comment
revalidatePath(`/posts/${postId}`);
}
Working with Databases in Server Actions
Server Actions are perfect for database operations. Here's how you might use them with Prisma, a popular database ORM:
// app/actions.js
"use server";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export async function createPost(formData) {
const title = formData.get("title");
const content = formData.get("content");
// Validate data
if (!title || title.length < 3) {
return {
success: false,
message: "Title must be at least 3 characters"
};
}
try {
// Create post in database
const post = await prisma.post.create({
data: {
title,
content,
},
});
return {
success: true,
message: "Post created successfully",
post,
};
} catch (error) {
return {
success: false,
message: "Failed to create post: " + error.message,
};
}
}
Error Handling in Server Actions
Proper error handling is crucial in Server Actions:
// app/actions.js
"use server";
export async function updateUserProfile(formData) {
try {
// Validate and sanitize inputs
const name = formData.get("name")?.trim();
const bio = formData.get("bio")?.trim();
if (!name) {
throw new Error("Name is required");
}
// Update database
// await db.user.update({ ... })
return {
success: true,
message: "Profile updated successfully"
};
} catch (error) {
// Log the error on the server
console.error("Profile update error:", error);
// Return user-friendly error to the client
return {
success: false,
message: error.message || "Failed to update profile"
};
}
}
Real-World Example: Shopping Cart
Let's see a more complete example implementing shopping cart functionality using Server Actions:
// app/components/AddToCartButton.jsx
"use client";
import { useState } from "react";
import { addToCart } from "../actions";
export default function AddToCartButton({ productId, productName }) {
const [status, setStatus] = useState(null);
async function handleAddToCart() {
setStatus("loading");
const result = await addToCart(productId);
setStatus(result.success ? "success" : "error");
// Reset status after 2 seconds
setTimeout(() => setStatus(null), 2000);
}
return (
<button
onClick={handleAddToCart}
disabled={status === "loading"}
className="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700 disabled:opacity-50"
>
{status === "loading" ? "Adding..." :
status === "success" ? "Added!" :
status === "error" ? "Failed" :
"Add to Cart"}
</button>
);
}
// app/actions.js
"use server";
import { cookies } from "next/headers";
import { revalidatePath } from "next/cache";
// Add product to cart
export async function addToCart(productId) {
try {
// Get current cart from cookies
const cartCookie = cookies().get("cart")?.value;
const cart = cartCookie ? JSON.parse(cartCookie) : { items: [] };
// Check if product already in cart
const existingItem = cart.items.find(item => item.id === productId);
if (existingItem) {
// Increase quantity if product already in cart
existingItem.quantity += 1;
} else {
// Add new product to cart
// In a real app, you might fetch product details from a database
cart.items.push({
id: productId,
quantity: 1,
});
}
// Save updated cart to cookies
cookies().set("cart", JSON.stringify(cart), {
maxAge: 60 * 60 * 24 * 30, // 30 days
path: "/",
});
// Revalidate cart page to show updated cart
revalidatePath("/cart");
return { success: true };
} catch (error) {
console.error("Failed to add item to cart:", error);
return { success: false, error: error.message };
}
}
// Remove product from cart
export async function removeFromCart(productId) {
try {
const cartCookie = cookies().get("cart")?.value;
if (!cartCookie) return { success: true };
const cart = JSON.parse(cartCookie);
cart.items = cart.items.filter(item => item.id !== productId);
cookies().set("cart", JSON.stringify(cart), {
maxAge: 60 * 60 * 24 * 30,
path: "/",
});
revalidatePath("/cart");
return { success: true };
} catch (error) {
return { success: false, error: error.message };
}
}
Best Practices for Server Actions
When working with Server Actions, follow these best practices:
- Input Validation - Always validate user input on the server
- Error Handling - Implement proper error handling and provide meaningful feedback
- Revalidation - Use
revalidatePath
orrevalidateTag
to update cached data - Progressive Enhancement - Ensure forms work even without JavaScript by using standard form submissions
- Security - Be mindful of sensitive data and use secure practices
- Performance - Keep server actions lightweight and focused on specific tasks
Limitations and Considerations
While Server Actions are powerful, they have some limitations:
- They can only be used in Server Components or imported into Client Components
- They must be async functions
- They can't be dynamically defined
- They can't access client-specific APIs like
window
orlocalStorage
Summary
Next.js Server Actions provide a streamlined way to handle form submissions and data mutations without creating separate API endpoints. They simplify your codebase by allowing server-side logic to live alongside client-side UI components.
Key takeaways:
- Server Actions use the
"use server"
directive to mark functions that run only on the server - They can be used directly with forms via the
action
prop - They handle form submissions even when JavaScript is disabled
- They integrate well with Next.js data revalidation features
- They provide a more cohesive development experience by keeping related code together
Additional Resources
- Next.js Server Actions Documentation
- React Server Components Documentation
- Next.js Form Handling Examples
Exercises
- Create a newsletter signup form using Server Actions
- Build a comment system for a blog with real-time revalidation
- Implement a multi-step form with Server Actions for each step
- Create a file upload feature using Server Actions and Next.js API
- Build a user authentication system using Server Actions for login and registration
By mastering Server Actions, you'll be able to build more cohesive and maintainable Next.js applications with seamless server-client integration.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)