Server Actions in Next.js: Mutations Without Writing an API
A Server Action is an async function that runs on the server but can be called from a form or a client component. Next.js creates the endpoint for you.
A form that writes to the database
// app/todos/page.tsx
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
export default async function TodosPage() {
const todos = await db.todo.findMany();
async function addTodo(formData: FormData) {
'use server';
const title = String(formData.get('title') ?? '').trim();
if (!title) return;
await db.todo.create({ data: { title } });
revalidatePath('/todos'); // refresh the list
}
return (
<>
<form action={addTodo}>
<input name="title" />
<button>Add</button>
</form>
<ul>{todos.map((t) => <li key={t.id}>{t.title}</li>)}</ul>
</>
);
}
This form works even before JavaScript loads (progressive enhancement).
Actions in their own file
// app/actions.ts
'use server';
import { z } from 'zod';
import { auth } from '@/lib/auth';
const Schema = z.object({ title: z.string().min(1).max(200) });
export async function createTodo(prevState: unknown, formData: FormData) {
const session = await auth();
if (!session) return { error: 'Please sign in.' };
const parsed = Schema.safeParse({ title: formData.get('title') });
if (!parsed.success) return { error: 'Title is required.' };
await db.todo.create({ data: { title: parsed.data.title, userId: session.user.id } });
return { ok: true };
}
Use it from a Client Component with useActionState(createTodo, {}) to show errors and pending state.
Security: actions are public endpoints
- Anyone can call an action with any arguments. Authenticate and authorise inside every action.
- Validate all input with a schema; never trust
FormData. - Don't return more data than the client needs.
- Closures over server values are encrypted, but keep secrets out of them anyway.
After a mutation
revalidatePath('/todos')- refresh a route's data.revalidateTag('todos', 'max')- refresh data tagged withcacheTag(stale-while-revalidate).redirect('/todos/' + id)- navigate after creating something.
Was this article helpful?
Comments
No comments yet. Have a question or a tip? Be the first to share it.