Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
20 minLesson 28 of 40
Next.js 15 App Router

API Routes & Route Handlers

API Routes in Next.js

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.

Creating Your First Route

// 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"}.

HTTP Methods

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 });
}

Dynamic API Routes

// 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 });
}

Reading Request Data

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;
}

A Complete REST API

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 });
}

Authentication Check

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);
}

Middleware for Auth

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*"],
};

CORS Headers

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 });
}

Handling File Uploads

// 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.

Error Handling Pattern

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.

📱

Get this course's notes on Telegram!

Free cheat sheets, summaries & practice exercises

Get Notes Free →
!