Next.js is a full-stack framework: alongside pages, you can ship real backend endpoints. Route handlers let you build REST-style APIs for webhooks, mobile clients, form submissions, or third-party integrations - all inside the same project, deployed together. This lesson covers creating handlers, reading requests, returning responses, and knowing when you do not need an API at all.
A route handler is a route.js file that exports functions named after HTTP methods:
// app/api/hello/route.js
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({ message: 'Hello from Next.js!' });
}Visit /api/hello and you get JSON. The supported exports are GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS. Any method you do not export returns 405 Method Not Allowed automatically.
One rule to remember: route.js and page.js cannot live in the same folder - a URL is either a page or an endpoint. The api folder prefix is a convention, not a requirement, but it keeps endpoints easy to find.
Handlers receive a standard Web Request object:
// app/api/subscribe/route.js
import { NextResponse } from 'next/server';
export async function POST(request) {
const body = await request.json();
if (!body.email || !body.email.includes('@')) {
return NextResponse.json(
{ error: 'A valid email is required' },
{ status: 400 }
);
}
await saveSubscriber(body.email);
return NextResponse.json({ success: true }, { status: 201 });
}Always validate input and return meaningful status codes: 400 for bad input, 401 for missing auth, 404 for missing resources, 201 for created.
Dynamic segments work exactly as they do for pages:
// app/api/posts/[id]/route.js
import { NextResponse } from 'next/server';
export async function GET(request, { params }) {
const { id } = await params;
const post = await getPost(id);
if (!post) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json(post);
}export async function GET(request) {
const { searchParams } = new URL(request.url);
const q = searchParams.get('q'); // ?q=term
const auth = request.headers.get('authorization');
const theme = request.cookies.get('theme'); // { name, value }
return NextResponse.json({ q, hasAuth: Boolean(auth) });
}Everything is standard Web APIs - the same Request and Response you would use in any modern JavaScript runtime, with NextResponse adding conveniences like json() with status options and cookie helpers.
GET handlers can be statically cached if they use no dynamic input, but any use of the request object, headers, or cookies makes them dynamic - and POST, PUT, PATCH, and DELETE are always dynamic. When in doubt, be explicit:
export const dynamic = 'force-dynamic';If a Server Component needs data, query the database directly - do not create an API route just to call it from your own server. Route handlers earn their place when the caller is outside your Next.js rendering: browser-side JavaScript, a mobile app, a webhook from Stripe or GitHub, or another service. For form mutations, Server Actions are often a simpler alternative worth exploring after this course.
Next up: Lesson 10 turns to the front of the frontend - styling with CSS Modules, Tailwind, and global CSS.