Server Actions are async functions that run on the server. They're marked with 'use server' and can be called from Client Components to perform mutations.
The Basic Pattern
tsx// actions.ts 'use server'; export async function createPost(formData: FormData) { const title = formData.get('title') as string; const content = formData.get('content') as string; await db.posts.create({ data: { title, content } }); revalidatePath('/posts'); }
tsx// PostForm.tsx 'use client'; import { createPost } from './actions'; export function PostForm() { return ( <form action={createPost}> <input name="title" placeholder="Title" required /> <textarea name="content" placeholder="Content" required /> <button type="submit">Create Post</button> </form> ); }
Why Server Actions?
Before: Manual API Routes
tsx// pages/api/posts.ts export default async function handler(req, res) { if (req.method === 'POST') { const { title, content } = req.body; await db.posts.create({ data: { title, content } }); res.status(200).json({ success: true }); } } // Component 'use client'; function PostForm() { const handleSubmit = async (e) => { e.preventDefault(); const formData = new FormData(e.target); await fetch('/api/posts', { method: 'POST', body: JSON.stringify(Object.fromEntries(formData)) }); }; return <form onSubmit={handleSubmit}>...</form>; }
After: Server Actions
tsx// actions.ts 'use server'; export async function createPost(formData: FormData) { await db.posts.create({ data: { title: formData.get('title'), content: formData.get('content') } }); } // Component 'use client'; import { createPost } from './actions'; function PostForm() { return ( <form action={createPost}> <input name="title" /> <textarea name="content" /> <button type="submit">Create</button> </form> ); }
Two Ways to Define Server Actions
1. Module-Level Directive
tsx// actions.ts 'use server'; // All exports are Server Actions export async function createUser(data) { ... } export async function deleteUser(id) { ... } export async function updateUser(id, data) { ... }
2. Inline in Server Components
tsx// ServerComponent.tsx (Server Component) async function ServerComponent() { async function handleSubmit(formData: FormData) { 'use server'; // This function runs on the server await db.items.create({ ... }); } return ( <form action={handleSubmit}> <input name="item" /> <button>Add</button> </form> ); }
Progressive Enhancement
Forms with Server Actions work even before JavaScript loads:
tsx<form action={serverAction}> {/* Works without JS! */} <input name="email" type="email" /> <button>Subscribe</button> </form>
This is because the form submits as a regular HTML form, and the server handles it.