API Routes and Server Actions
Building an API with Route Handlers, mutating data with Server Actions, and when to use each.
Route Handlers
A Route Handler turns a folder in app/ into an API endpoint instead of a page, by adding a route.ts file that exports functions named after HTTP methods:
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
const users = [
{ id: 1, name: 'Ada Lovelace' },
{ id: 2, name: 'Grace Hopper' },
];
export async function GET() {
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
const body = await request.json();
if (!body.name) {
return NextResponse.json({ error: 'name is required' }, { status: 400 });
}
const newUser = { id: users.length + 1, name: body.name };
users.push(newUser);
return NextResponse.json(newUser, { status: 201 });
}
// app/api/users/[id]/route.ts
import { NextResponse } from 'next/server';
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
const user = users.find((u) => u.id === Number(id));
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
return NextResponse.json(user);
}
A route.ts file and a page.tsx file can't both live in the same folder — a folder is either a page or an API endpoint at a given path.
Server Actions
A Server Action is an async function, marked with 'use server', that runs on the server but can be called directly from a component — most often to handle a form submission — with no separate API endpoint and no client-side fetch call to write:
// app/posts/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title');
if (typeof title !== 'string' || title.trim() === '') {
throw new Error('Title is required');
}
await fetch('https://api.example.com/posts', {
method: 'POST',
body: JSON.stringify({ title }),
headers: { 'Content-Type': 'application/json' },
});
revalidatePath('/posts'); // tell Next this route's cached data is now stale
}
// app/posts/new/page.tsx
import { createPost } from '../actions';
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="Post title" />
<button type="submit">Create post</button>
</form>
);
}
Passing the Server Action straight to a <form>'s action prop works even with JavaScript disabled in the browser — the form submits as a normal HTML form post that Next intercepts and routes to the server function. revalidatePath (or revalidateTag) is how a Server Action tells Next that cached data elsewhere in the app (like the statically rendered /posts list from the previous page) is now out of date and should be regenerated.
Server Actions vs Route Handlers — which to reach for
| Server Action | Route Handler | |
|---|---|---|
| Typical use | A mutation triggered from your own app's UI (a form, a button) | A general-purpose API — consumed by your app, a mobile client, or a third party |
| Called from | Directly, as a function reference (<form action={fn}>, onClick) |
An HTTP request (fetch, curl, another service) |
| Needs a URL/endpoint | No | Yes |
| Good fit for | Form submissions, simple CRUD tied to one app | A public or versioned API contract other clients depend on |
Common mistakes
- Forgetting the
'use server'directive at the top of a Server Action file (or function) — without it, Next has no way to know the function should run only on the server. - Building a full Route Handler plus a client-side
fetchcall for a same-app form submission that a single Server Action would have handled more simply. - Mutating data in a Server Action but forgetting to call
revalidatePath/revalidateTagafterward — the mutation succeeds, but statically cached pages keep showing the old data until the next scheduled regeneration.