Learn / Frameworks / Next.js / Route Handlers and Server Actions

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
Up next · Lesson 5SEO, Images and DeploymentMetadata, next/image, environment variables and deploying.