Topic 6 of 47
Server Actions
Overview
Server Actions are async functions marked with 'use server' that run on the server but can be called from client components — eliminating the need for API routes for form submissions and mutations. They integrate deeply with React and Next.js caching.
Syntax
typescript
// In a Server Component or separate file
'use server';
async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// Runs on server — direct DB access!
await db.posts.create({ title, content });
revalidatePath('/blog'); // update cached pages
redirect('/blog'); // redirect after success
}
// Used directly in form (no API needed!)
<form action={createPost}>
<input name="title" required />
<textarea name="content" required />
<button type="submit">Publish</button>
</form>
// Or called from client component
'use client';
function LikeButton({ postId }) {
return <button onClick={async () => await likePost(postId)}>Like</button>;
}Common Pitfalls
- Server Actions marked 'use server' are publicly callable HTTP endpoints — validate and authenticate inputs server-side.
- Server Actions automatically handle CSRF protection — one of their key security benefits over custom API routes.
- Interview tip: Server Actions replace the pattern of: form → client handler → API route → DB. They enable form → DB directly.
Real-World Example
A complete form with Server Action and optimistic updates:
example
typescript
// actions/todo.ts
'use server';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
const AddTodoSchema = z.object({ text: z.string().min(1).max(200) });
export async function addTodo(prevState: any, formData: FormData) {
const validated = AddTodoSchema.safeParse({ text: formData.get('text') });
if (!validated.success) return { error: 'Invalid input' };
await db.todos.create({ text: validated.data.text, userId: getSessionUserId() });
revalidatePath('/todos');
return { success: true };
}
// components/AddTodoForm.tsx
'use client';
import { useFormState, useFormStatus } from 'react-dom';
import { addTodo } from '../actions/todo';
function SubmitButton() {
const { pending } = useFormStatus(); // auto tracks form submission
return <button disabled={pending}>{pending ? 'Adding...' : 'Add Todo'}</button>;
}
export function AddTodoForm() {
const [state, action] = useFormState(addTodo, null);
return (
<form action={action}>
<input name="text" placeholder="Add a task..." required />
<SubmitButton />
{state?.error && <p className="error">{state.error}</p>}
</form>
);
}