AiTechWorlds
AiTechWorlds
University learning companion
Web Development Bootcamp 2026 · API Routes & Route Handlers
A feature looks correct on a developer’s large monitor but fails on a slow connection and a small screen. Use this topic to inspect structure, state, loading/error behaviour, accessibility and the user journey—not just the happy path.
Use this course-level map to place “API Routes & Route Handlers” in the larger system. It complements—not replaces—a topic-specific worked diagram.
Next.js lets you build your backend API right inside your app no separate server needed. Any file named route.ts inside src/app/api/ becomes an HTTP endpoint. This is perfect for handling form submissions, authenticating users, talking to databases, or building a full REST API.
// src/app/api/hello/route.ts
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({ message: "Hello World" });
}Visit /api/hello you get {"message": "Hello World"}.
Export a function for each HTTP method you want to handle:
// src/app/api/courses/route.ts
import { NextRequest, NextResponse } from "next/server";
// GET /api/courses
export async function GET(request: NextRequest) {
const courses = await db.course.findMany();
return NextResponse.json(courses);
}
// POST /api/courses
export async function POST(request: NextRequest) {
const body = await request.json();
const course = await db.course.create({
data: {
title: body.title,
description: body.description,
},
});
return NextResponse.json(course, { status: 201 });
}// src/app/api/courses/[id]/route.ts
interface Context {
params: { id: string };
}
// GET /api/courses/123
export async function GET(request: NextRequest, { params }: Context) {
const course = await db.course.findUnique({
where: { id: params.id },
});
if (!course) {
return NextResponse.json({ error: "Course not found" }, { status: 404 });
}
return NextResponse.json(course);
}
// PATCH /api/courses/123
export async function PATCH(request: NextRequest, { params }: Context) {
const body = await request.json();
const course = await db.course.update({
where: { id: params.id },
data: body,
});
return NextResponse.json(course);
}
// DELETE /api/courses/123
export async function DELETE(request: NextRequest, { params }: Context) {
await db.course.delete({ where: { id: params.id } });
return new Response(null, { status: 204 });
}export async function POST(request: NextRequest) {
// JSON body
const body = await request.json();
// Form data
const formData = await request.formData();
const email = formData.get("email") as string;
// Query parameters
const url = new URL(request.url);
const page = url.searchParams.get("page") ?? "1";
const limit = url.searchParams.get("limit") ?? "10";
// Headers
const authHeader = request.headers.get("authorization");
// Cookies
const token = request.cookies.get("session")?.value;
}Here's a realistic user management API:
// src/app/api/users/route.ts
// GET /api/users?page=1&search=alice
export async function GET(request: NextRequest) {
const url = new URL(request.url);
const page = parseInt(url.searchParams.get("page") ?? "1");
const search = url.searchParams.get("search") ?? "";
const limit = 20;
const skip = (page - 1) * limit;
const where = search
? { OR: [{ name: { contains: search } }, { email: { contains: search } }] }
: {};
const [users, total] = await Promise.all([
db.user.findMany({ where, skip, take: limit, orderBy: { createdAt: "desc" } }),
db.user.count({ where }),
]);
return NextResponse.json({
users,
pagination: { page, limit, total, pages: Math.ceil(total / limit) },
});
}
// POST /api/users
export async function POST(request: NextRequest) {
const body = await request.json();
// Validate
if (!body.email || !body.name) {
return NextResponse.json(
{ error: "email and name are required" },
{ status: 400 }
);
}
// Check for existing user
const existing = await db.user.findUnique({ where: { email: body.email } });
if (existing) {
return NextResponse.json(
{ error: "Email already registered" },
{ status: 409 }
);
}
const user = await db.user.create({
data: { name: body.name, email: body.email },
select: { id: true, name: true, email: true, createdAt: true },
});
return NextResponse.json(user, { status: 201 });
}Protect routes by checking for a valid session:
// src/lib/auth.ts
import { cookies } from "next/headers";
import { verifyToken } from "./jwt";
export async function requireAuth() {
const cookieStore = cookies();
const token = cookieStore.get("session")?.value;
if (!token) return null;
try {
return verifyToken(token); // returns user payload
} catch {
return null;
}
}// src/app/api/dashboard/route.ts
import { requireAuth } from "@/lib/auth";
export async function GET(request: NextRequest) {
const user = await requireAuth();
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const stats = await getDashboardStats(user.id);
return NextResponse.json(stats);
}For routes that all require auth, use Next.js middleware instead of checking in every route:
// src/middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { verifyToken } from "./lib/jwt";
export function middleware(request: NextRequest) {
const token = request.cookies.get("session")?.value;
if (!token) {
// Redirect to login
return NextResponse.redirect(new URL("/login", request.url));
}
try {
verifyToken(token);
return NextResponse.next();
} catch {
return NextResponse.redirect(new URL("/login", request.url));
}
}
// Run middleware on these paths
export const config = {
matcher: ["/dashboard/:path*", "/api/dashboard/:path*"],
};For routes called from external origins:
// src/app/api/public/route.ts
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
export async function OPTIONS() {
return new Response(null, { status: 204, headers: corsHeaders });
}
export async function GET() {
const data = await getPublicData();
return NextResponse.json(data, { headers: corsHeaders });
}// src/app/api/upload/route.ts
import { writeFile } from "fs/promises";
import path from "path";
export async function POST(request: NextRequest) {
const formData = await request.formData();
const file = formData.get("file") as File;
if (!file) {
return NextResponse.json({ error: "No file provided" }, { status: 400 });
}
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
const filename = `${Date.now()}-${file.name}`;
const filepath = path.join(process.cwd(), "public/uploads", filename);
await writeFile(filepath, buffer);
return NextResponse.json({ url: `/uploads/${filename}` });
}For production, use a cloud storage service (AWS S3, Cloudflare R2, Vercel Blob) instead of writing to the filesystem.
Wrap route handlers to catch unexpected errors:
function withErrorHandling(handler: Function) {
return async function(request: NextRequest, context: any) {
try {
return await handler(request, context);
} catch (error) {
console.error("API Error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
};
}
export const GET = withErrorHandling(async (request: NextRequest) => {
const users = await db.user.findMany();
return NextResponse.json(users);
});Next lesson: Node.js and Express building backend servers with JavaScript.
University learning companion
Web Development Bootcamp 2026 · API Routes & Route Handlers
Complete the steps without looking at the answer first; record evidence rather than only a final result.
These are primary or established learning references for this subject. Check the relevant chapter or specification for the exact topic and version.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises