beginner Step 5 of 15

API Routes

Next.js Development

API Routes

Next.js allows you to build API endpoints directly within your application using Route Handlers (App Router) or API Routes (Pages Router). These server-side endpoints run on Node.js and can handle any HTTP method — GET, POST, PUT, DELETE, and more. API routes are perfect for handling form submissions, proxying requests to external APIs, webhook endpoints, and building complete backends without a separate server. They have access to the request and response objects and can interact with databases, file systems, and external services.

Route Handlers (App Router)

In the App Router, API endpoints are defined in route.ts files. Export functions named after HTTP methods to handle requests.

// app/api/hello/route.ts
import { NextResponse } from "next/server";

export async function GET() {
  return NextResponse.json({
    message: "Hello from Next.js API!",
    timestamp: new Date().toISOString(),
  });
}

// Handles POST requests
export async function POST(request: Request) {
  const body = await request.json();

  return NextResponse.json(
    { message: `Received: ${body.name}`, data: body },
    { status: 201 }
  );
}

CRUD API Example

// app/api/posts/route.ts
import { NextResponse } from "next/server";

// In-memory store (use a database in production)
let posts = [
  { id: 1, title: "First Post", body: "Hello world", createdAt: new Date().toISOString() },
  { id: 2, title: "Second Post", body: "Another post", createdAt: new Date().toISOString() },
];
let nextId = 3;

// GET /api/posts — list all posts
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const query = searchParams.get("q");

  let results = posts;
  if (query) {
    results = posts.filter((p) =>
      p.title.toLowerCase().includes(query.toLowerCase())
    );
  }

  return NextResponse.json({ data: results, total: results.length });
}

// POST /api/posts — create a new post
export async function POST(request: Request) {
  try {
    const { title, body } = await request.json();

    if (!title || !body) {
      return NextResponse.json(
        { error: "Title and body are required" },
        { status: 400 }
      );
    }

    const newPost = {
      id: nextId++,
      title,
      body,
      createdAt: new Date().toISOString(),
    };
    posts.push(newPost);

    return NextResponse.json(newPost, { status: 201 });
  } catch (error) {
    return NextResponse.json(
      { error: "Invalid JSON body" },
      { status: 400 }
    );
  }
}

Dynamic Route Handlers

// app/api/posts/[id]/route.ts
import { NextResponse } from "next/server";

interface RouteParams {
  params: Promise<{ id: string }>;
}

// GET /api/posts/:id
export async function GET(request: Request, { params }: RouteParams) {
  const { id } = await params;
  const post = posts.find((p) => p.id === parseInt(id));

  if (!post) {
    return NextResponse.json(
      { error: "Post not found" },
      { status: 404 }
    );
  }

  return NextResponse.json(post);
}

// PUT /api/posts/:id
export async function PUT(request: Request, { params }: RouteParams) {
  const { id } = await params;
  const postIndex = posts.findIndex((p) => p.id === parseInt(id));

  if (postIndex === -1) {
    return NextResponse.json({ error: "Post not found" }, { status: 404 });
  }

  const updates = await request.json();
  posts[postIndex] = { ...posts[postIndex], ...updates };

  return NextResponse.json(posts[postIndex]);
}

// DELETE /api/posts/:id
export async function DELETE(request: Request, { params }: RouteParams) {
  const { id } = await params;
  const postIndex = posts.findIndex((p) => p.id === parseInt(id));

  if (postIndex === -1) {
    return NextResponse.json({ error: "Post not found" }, { status: 404 });
  }

  posts.splice(postIndex, 1);
  return new NextResponse(null, { status: 204 });
}

Request and Response Helpers

// Working with headers, cookies, and status codes
import { NextResponse } from "next/server";
import { cookies, headers } from "next/headers";

export async function GET(request: Request) {
  // Read request headers
  const headersList = await headers();
  const authHeader = headersList.get("authorization");

  // Read cookies
  const cookieStore = await cookies();
  const token = cookieStore.get("session-token");

  // Read query parameters
  const { searchParams } = new URL(request.url);
  const page = searchParams.get("page") || "1";

  // Set response headers and cookies
  const response = NextResponse.json({ page, authenticated: !!token });
  response.headers.set("X-Custom-Header", "my-value");
  response.cookies.set("visited", "true", {
    httpOnly: true,
    maxAge: 60 * 60 * 24,
  });

  return response;
}
Tip: API routes run server-side and have access to environment variables, databases, and file systems. Never expose secrets in client-side code — keep sensitive operations in API routes. Use environment variables prefixed with NEXT_PUBLIC_ only for values safe to expose to the browser.

Key Takeaways

  • Route Handlers in route.ts files export functions named after HTTP methods (GET, POST, PUT, DELETE).
  • Dynamic route handlers use [param] folders to capture URL segments.
  • Use NextResponse.json() to return JSON responses with status codes.
  • Access headers, cookies, and query parameters from the request object.
  • API routes run server-side and can safely access databases and secrets.