You've set up your Next.js application, organized your components beautifully, but find yourself writing
'use client'
at the top of nearly every file. Your console is cluttered with React Query or SWR setup code, and you're wondering if you're actually taking advantage of Next.js at all. If this sounds familiar, you might be missing out on one of Next.js's most powerful features: Server Actions.
What Are Server Actions and Why Should You Care?
Server Actions, introduced in Next.js 13.4, are asynchronous functions that execute on the server rather than the client. They provide a direct bridge between your UI and server-side operations without creating separate API endpoints.
If you're feeling like this developer on Reddit:
"I feel like 80-90% of my application relies on 'use client' to function. I'm unsure if I'm fully leveraging Next.js's server-side capabilities."
Then it's time to reconsider your approach to data fetching and form handling.
The Power of Server-Side Execution
When you use Server Actions, you're moving computation away from your users' devices and onto your server. This brings several immediate advantages:
1. Direct Database Access Without API Endpoints
Before Server Actions, you needed to create API routes for every data operation. Now, you can directly access your database or external services from your components:
async function createTodo(formData) {
'use server';
const title = formData.get('title');
// Direct database access
await db.todo.create({ data: { title } });
revalidatePath('/todos');
}
2. Automatic Form Handling with Progressive Enhancement
Server Actions shine when handling forms. They work even if JavaScript fails to load, ensuring your application remains functional:
export default function TodoForm() {
return (
<form action={createTodo}>
<input name="title" type="text" />
<button type="submit">Add Todo</button>
</form>
);
}
3. Seamless Integration with Next.js Caching
Server Actions automatically work with Next.js's cache system, triggering revalidation when data changes:
async function deleteTodo(id) {
'use server';
await db.todo.delete({ where: { id } });
// Automatically update cached data
revalidatePath('/todos');
}
Server Actions vs. Client-Side Fetching: The Real Comparison
Many developers wonder if Server Actions are truly better than client-side fetching libraries. Let's break down the key differences:
Type Safety Out of the Box
With Server Actions, you get TypeScript integration without extra setup. As one developer noted:
"The main benefit for me is type safety, server actions have it out of the box while API routes don't."
Reduced Bundle Size
Client-side data fetching libraries add to your JavaScript bundle, slowing down initial page loads. Server Actions move this code to the server, resulting in faster page rendering.
POST-Only Limitation
It's true that Server Actions only support POST requests, which has led some developers to question their utility:
"Server actions can't do GET, only POST. Even though you can use server actions to fetch data, Next.js discourages it because it can only send a POST request."
This limitation exists for security reasons. For read operations, Next.js recommends using regular server components with async/await patterns instead.
Enhanced Security Posture
The POST-only approach actually enhances security by reducing vulnerability to Cross-Site Request Forgery (CSRF) attacks. Next.js automatically handles CSRF protection for Server Actions, giving you peace of mind.
Best Practices for Implementing Server Actions
To get the most out of Server Actions, follow these battle-tested practices from experienced Next.js developers:
1. Organize by Feature, Not by Type
Rather than creating a single "actions.js" file, organize your Server Actions alongside the components that use them:
/features
/todos
/components
TodoList.tsx
TodoForm.tsx
/actions
createTodo.js
deleteTodo.js
page.tsx
This approach makes your codebase more maintainable as it scales.
2. Add Validation with Zod or other Schema Libraries
Never trust client input. Use libraries like Zod to validate data before it hits your database:
import { z } from 'zod';
const todoSchema = z.object({
title: z.string().min(1).max(100)
});
async function createTodo(formData) {
'use server';
// Validate the input
const validatedFields = todoSchema.safeParse({
title: formData.get('title')
});
if (!validatedFields.success) {
return { error: validatedFields.error.flatten().fieldErrors };
}
// Proceed with database operation...
}
This pattern is especially useful with libraries like drizzle-zod
that integrate with your ORM.
3. Implement Error Handling
Proper error handling improves user experience by providing meaningful feedback:
async function createTodo(formData) {
'use server';
try {
// Database operations...
return { success: true };
} catch (error) {
console.error(error);
return {
success: false,
message: 'Failed to create todo'
};
}
}
4. Consider Using Helper Libraries
For complex scenarios, libraries like next-safe-action
can provide additional type safety and error handling:
import { createSafeActionClient } from 'next-safe-action';
const action = createSafeActionClient();
export const createTodo = action(
todoSchema,
async ({ title }) => {
// Type-safe, validated action
await db.todo.create({ data: { title } });
return { success: true };
}
);
Real-World Use Cases for Server Actions
Server Actions excel in several scenarios:
Form Submissions with Instant Feedback
Combine Server Actions with the useFormStatus
hook for loading states:
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button disabled={pending}>
{pending ? 'Saving...' : 'Save'}
</button>
);
}
Data Mutations with Optimistic Updates
For a smoother UX, implement optimistic updates while Server Actions process in the background:
'use client';
import { useOptimistic } from 'react';
import { deleteTodo } from './actions';
export function TodoList({ initialTodos }) {
const [todos, setTodos] = useOptimistic(initialTodos);
function handleDelete(id) {
// Optimistically update UI
setTodos(todos.filter(todo => todo.id !== id));
// Then perform the actual deletion
deleteTodo(id);
}
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.title}
<button onClick={() => handleDelete(todo.id)}>Delete</button>
</li>
))}
</ul>
);
}
Authentication and Session Management
Server Actions provide a secure way to handle authentication:
async function login(formData) {
'use server';
const email = formData.get('email');
const password = formData.get('password');
const user = await authenticateUser(email, password);
if (user) {
// Set secure HTTP-only cookies
cookies().set('session', generateSessionToken(user), {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
});
redirect('/dashboard');
}
return { error: 'Invalid credentials' };
}
Integrating Server Actions with the Next.js Ecosystem
Server Actions don't exist in isolation—they work best when integrated with other Next.js features:
Partial Page Rendering (PPR)
When Server Actions trigger revalidation, they work seamlessly with PPR to update only the parts of the page that changed, resulting in faster updates without full refreshes.
Integration with tanstack/react-query
While Server Actions reduce the need for client-side data fetching, they can still work alongside libraries like TanStack Query for more complex scenarios:
'use client';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createTodo } from './actions';
export function TodoForm() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: createTodo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
}
});
return (
<form onSubmit={(e) => {
e.preventDefault();
mutation.mutate(new FormData(e.target));
}}>
<input name="title" type="text" />
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? 'Adding...' : 'Add Todo'}
</button>
</form>
);
}
Working in a Monorepo Structure
In larger projects, especially those using monorepo architectures, you can share Server Action implementations across multiple applications:
// packages/actions/src/todos.js
'use server';
export async function createTodo(formData) {
// Implementation
}
// apps/web/app/todos/page.tsx
import { createTodo } from '@myorg/actions';
export default function TodoPage() {
return (
<form action={createTodo}>
{/* Form fields */}
</form>
);
}
Common Concerns Addressed
"Server Actions Are Too Limited"
While Server Actions do have some constraints, they're designed this way for good reasons:
POST-only: This ensures security and prevents accidental exposure of sensitive data in URLs.
No direct Response control: This encourages proper separation of concerns between data operations and presentation.
For cases where you need more control, you can always combine Server Actions with API Routes or create a dedicated API Gateway.
"I Already Have a Working Setup with React Query"
If you're already comfortable with React Query or SWR, you don't need to rewrite everything. Instead, gradually adopt Server Actions for new features or during refactoring. The two approaches can coexist in the same application.
Conclusion
Server Actions represent a paradigm shift in how we approach data fetching and mutations in Next.js applications. By moving these operations to the server, we can build faster, more secure, and more maintainable applications.
If you've been feeling like you're not fully leveraging Next.js's capabilities, or if your codebase is overwhelmed with client components and state management, it's time to give Server Actions a serious look.
As one developer put it:
"Server actions are amazing once you wrap your head around how they fit into the whole new app folder structure."
The transition might require some mental adjustment, particularly if you're accustomed to traditional API routes or client-side data fetching. But the benefits—reduced complexity, better performance, enhanced security, and tighter integration with Next.js's other features—make it well worth the effort.
Start by identifying a simple form or data mutation in your application and refactor it to use Server Actions. You'll quickly see how they can simplify your code and improve your application's architecture.
Ready to dive deeper? Check out the official Next.js documentation on Server Actions and start implementing them in your projects today.