Next.js · Lesson 4 of 5
Route Handlers and Server Actions
Build API endpoints and handle forms without writing fetch code.
- Intermediate
- 16 min read
- 3 objectives
Before this lessonLesson 3: Server and Client Components
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>
</>
);
}Pending and error states
"use client";
import { useFormStatus } from "react-dom";
export function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "Saving..." : "Add"}</button>;
}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*"] };// Write your solution here
