Route Handlers and Server Actions
Build API endpoints and handle forms without writing fetch code.
What you will learn
- Write a route handler
- Use a server action
- Revalidate cached data
Next.js can be your backend too. Two tools cover most needs: route handlers for classic HTTP endpoints, and server actions for form submissions and mutations from your own UI.
Route handlers
A route.ts file exports functions named after HTTP methods.
// app/api/tasks/route.ts
import { NextResponse } from "next/server";
const tasks = [{ id: 1, title: "Learn Next.js" }];
export async function GET() {
return NextResponse.json(tasks);
}
export async function POST(request: Request) {
const body = await request.json();
if (!body.title) {
return NextResponse.json({ error: "title required" }, { status: 400 });
}
const task = { id: tasks.length + 1, title: String(body.title) };
tasks.push(task);
return NextResponse.json(task, { status: 201 });
}Dynamic segments work here too: app/api/tasks/[id]/route.ts. Use route handlers when other clients (a mobile app, a webhook) need an HTTP API.
Server actions
A server action is an async function marked "use server" that runs on the server but can be called straight from a form. No manual fetch, no API route, and forms work even before JavaScript loads.
// app/tasks/actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function addTask(formData: FormData) {
const title = String(formData.get("title") ?? "").trim();
if (!title) return;
await db.task.create({ data: { title } }); // your database call
revalidatePath("/tasks"); // refresh the list
}
// app/tasks/page.tsx
import { addTask } from "./actions";
export default async function Tasks() {
const tasks = await db.task.findMany();
return (
<>
<form action={addTask}>
<input name="title" placeholder="New task" required />
<button type="submit">Add</button>
</form>
<ul>{tasks.map((t) => <li key={t.id}>{t.title}</li>)}</ul>
</>
);
}"use client";
import { useFormStatus } from "react-dom";
export function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "Saving..." : "Add"}</button>;
}Server actions are public HTTP endpoints under the hood. Always validate input (for example with zod) and check that the current user is allowed to perform the action. Never trust that only your form calls it.
Middleware
A middleware.ts file at the project root runs before requests complete, which is ideal for redirects and auth checks.
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(req: NextRequest) {
const loggedIn = req.cookies.has("session");
if (!loggedIn && req.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", req.url));
}
}
export const config = { matcher: ["/dashboard/:path*"] };Try it yourself
Add GET /api/health returning { status: "ok" }, then a contact form using a server action that logs the submitted email on the server.
Show solution
// app/api/health/route.ts
import { NextResponse } from "next/server";
export async function GET() { return NextResponse.json({ status: "ok" }); }
// app/contact/actions.ts
"use server";
export async function submit(formData: FormData) {
console.log("email:", formData.get("email"));
}