Next.js API Routes

Build and handle API endpoints directly inside your Next.js application.

TL;DR

  1. Create API routes in app/api folder with route.ts files.
  2. Export handler functions for HTTP methods (GET, POST, etc).
  3. Access request data and return typed Response objects with status codes.

Basic API Route

  • Create API routes in app/api folder.
    // app/api/hello/route.ts
    export async function GET(request: Request) {
      return Response.json({ message: 'Hello' });
    }
  • Each route.ts file becomes an API endpoint.
  • /app/api/hello/route.ts becomes /api/hello.
  • Return a plain text response using the Response constructor.
    export async function GET() {
      return new Response("Hello, world!", {
        headers: { "Content-Type": "text/plain" }
      });
    }
  • Set response status and headers with the second argument.
    export async function POST() {
      return Response.json({ created: true }, { status: 201 });
    }

HTTP Methods

  • Handle different HTTP methods with exports.
    // app/api/posts/route.ts
    export async function GET() {
      const posts = await fetchPosts();
      return Response.json(posts);
    }
    
    export async function POST(request: Request) {
      const body = await request.json();
      const post = await createPost(body);
      return Response.json(post, { status: 201 });
    }
  • Supports GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS.
  • Handle PUT requests to update an existing resource.
    export async function PUT(request: Request) {
      const body = await request.json();
      const updated = await updatePost(body.id, body);
      return Response.json(updated);
    }
  • Handle DELETE to remove a resource by ID.
    export async function DELETE(request: Request) {
      const { id } = await request.json();
      await deletePost(id);
      return Response.json({ deleted: true });
    }
  • Return 405 for unsupported methods.
    export async function PATCH() {
      return Response.json({ error: "Method not allowed" }, { status: 405 });
    }

Request Handling

  • Read JSON body from requests.
    export async function POST(request: Request) {
      const body = await request.json();
      console.log(body);
      return Response.json({ success: true });
    }
  • Access query parameters and authorization headers.
    export async function GET(request: Request) {
      const { searchParams } = new URL(request.url);
      const id = searchParams.get('id');
      const auth = request.headers.get('authorization');
      return Response.json({ id, auth });
    }
  • Parse form data from multipart requests.
    export async function POST(request: Request) {
      const formData = await request.formData();
      const name = formData.get("name") as string;
      return Response.json({ name });
    }
  • Stream a large response body using ReadableStream.
    export async function GET() {
      const stream = new ReadableStream({
        start(controller) {
          controller.enqueue(new TextEncoder().encode("chunk 1"));
          controller.close();
        }
      });
      return new Response(stream);
    }
  • Validate request body shape before processing.
    const { name, email } = await request.json();
    if (!name || !email) {
      return Response.json({ error: "name and email are required" }, { status: 400 });
    }

Cookies and Headers

  • Read cookies from incoming requests — cookies() is async in Next.js 15.
    import { cookies } from "next/headers";
    
    export async function GET() {
      const cookieStore = await cookies(); // must await in Next.js 15
      const token = cookieStore.get("token")?.value;
      return Response.json({ token });
    }
  • Set cookies in the response.
    import { cookies } from "next/headers";
    
    export async function POST() {
      const cookieStore = await cookies();
      cookieStore.set("session", "abc123", { httpOnly: true });
      return Response.json({ ok: true });
    }
  • Read request headers directly from the Request object.
    export async function GET(request: Request) {
      const auth = request.headers.get("authorization");
      const contentType = request.headers.get("content-type");
      return Response.json({ auth, contentType });
    }
  • Set custom response headers.
    export async function GET() {
      return new Response(JSON.stringify({ ok: true }), {
        headers: {
          "Content-Type": "application/json",
          "Cache-Control": "no-store"
        }
      });
    }
  • Use next/headers headers() to read request headers server-side (also async in Next.js 15).
    import { headers } from "next/headers";
    const headersList = await headers();
    const userAgent = headersList.get("user-agent");

Error Handling and Status Codes

  • Return appropriate status codes.
    export async function GET(request: Request) {
      try {
        const data = await fetchData();
        return Response.json(data);
      } catch (error) {
        return Response.json(
          { error: 'Failed to fetch data' },
          { status: 500 }
        );
      }
    }
  • Use Response.json with status and headers.
    return Response.json(data, {
      status: 201,
      headers: { 'Content-Type': 'application/json' }
    });
  • Return 400 for invalid or missing request body fields.
    export async function POST(request: Request) {
      const body = await request.json();
      if (!body.name) {
        return Response.json({ error: "Name is required" }, { status: 400 });
      }
      return Response.json({ ok: true });
    }
  • Return 401 for unauthenticated requests.
    const token = request.headers.get("authorization");
    if (!token) {
      return Response.json({ error: "Unauthorized" }, { status: 401 });
    }
  • Log errors server-side before returning a generic message.
    } catch (error) {
      console.error("API error:", error);
      return Response.json({ error: "Internal server error" }, { status: 500 });
    }

Tips

  1. Use "use server" functions in app/actions for simple mutations — API routes are for complex endpoints.

Warnings

  1. API routes are public by default — add authentication checks for sensitive endpoints.

FAQ