Lesson 27 of 5024 min read

Next.js Route Handlers: Building REST APIs (route.ts)

Learn how to build REST API endpoints in Next.js using route.ts files, covering GET, POST, PUT, and DELETE handlers with real examples.

Author: CodersNexus

Next.js Route Handlers: Building REST APIs (route.ts)

Server Actions, from Lesson 4.3, are excellent for mutations tied directly to your own application's forms and UI. But sometimes you need something different: a genuine, standalone API endpoint — one that a mobile app, a third-party service, a webhook, or an entirely separate frontend could call directly over HTTP, independent of any specific form or React component in your Next.js app.

This is exactly what Route Handlers solve. By creating a route.ts (or route.js) file inside the app/ directory, you define actual REST API endpoints, exporting functions named after HTTP methods — GET, POST, PUT, DELETE, and others — each handling that specific type of request to that URL path. This lesson covers building a complete set of REST endpoints using route.ts, and clarifies exactly when reaching for a Route Handler is the better choice compared to a Server Action.

Learning Objectives

  • Create a Route Handler using a route.ts file inside the app/ directory.
  • Implement GET, POST, PUT, and DELETE handlers for a resource.
  • Read query parameters, request bodies, and dynamic route parameters inside a Route Handler.
  • Return properly formatted JSON responses with appropriate HTTP status codes.
  • Understand when to choose a Route Handler over a Server Action.

Core Definitions

  • Route Handler: A file named route.ts (or route.js) inside the app/ directory that defines API endpoint logic for a specific URL path, using functions named after HTTP methods.
  • HTTP method: A verb (GET, POST, PUT, DELETE, PATCH, etc.) indicating the type of operation a request is performing, such as retrieving, creating, updating, or deleting data.
  • Request object: The incoming HTTP request passed into a Route Handler function, providing access to headers, the request body, and the URL's query parameters.
  • NextResponse: A Next.js utility for constructing HTTP responses from a Route Handler, including setting a status code, headers, and a JSON body.
  • REST API: An architectural style for building web APIs around resources (like /posts or /users) and standard HTTP methods representing operations on those resources.

How Next.js Route Handlers Actually Works

A Route Handler is created by adding a file literally named route.ts inside any folder within the app/ directory — importantly, a route.ts file cannot coexist with a page.tsx in the exact same folder, since a single URL segment can't simultaneously be a page and an API endpoint. Inside this file, you export async functions named exactly after the HTTP method they handle: `export async function GET(request) { ... }`, `export async function POST(request) { ... }`, and so on for PUT, DELETE, PATCH, and others.

Each handler function receives a Request object (following the standard, web-native Request API), giving you access to the incoming request's headers, and, for methods like POST or PUT that typically include a body, a way to read and parse that body (commonly via `await request.json()` for JSON payloads). For a route with dynamic segments — say, app/api/posts/[id]/route.ts — handler functions also receive a second argument containing the resolved params, exactly analogous to how a dynamic page component receives params, letting a GET handler at that path know exactly which post ID was requested.

Reading query string parameters (like ?category=tech on a GET request) is done via the request's URL, typically by constructing a URL object from request.url and reading its searchParams. Returning a response uses NextResponse.json(), which handles correctly setting the appropriate Content-Type header and lets you specify an HTTP status code as a second argument — `NextResponse.json({ error: 'Not found' }, { status: 404 })` — giving you full, precise control over the API's actual HTTP-level response, exactly as any REST API consumer (a mobile app, another service, a webhook sender) would expect.

A full REST resource typically implements several of these methods together at a consistent URL structure: a GET handler at /api/posts returning a list, a POST handler at that same path creating a new item, and GET/PUT/DELETE handlers at /api/posts/[id] operating on one specific, individually identified item — this consistent structure is the essence of a REST API's design.

So when should you reach for a Route Handler instead of a Server Action from Lesson 4.3? The deciding factor is who or what needs to call this logic. If a mutation or data fetch is only ever going to be triggered from within your own Next.js application's UI — a form submitting, a button click — a Server Action is typically simpler and more direct, with less boilerplate. If the logic needs to be callable by something outside your own Next.js frontend entirely — a mobile app consuming the same backend, a third-party webhook (like a payment provider notifying you of a completed transaction), another separate service, or any client that isn't itself a React component in your app — a genuine Route Handler, following standard REST conventions, is the correct, necessary choice, since Server Actions are specifically designed to be called from within your own Next.js application's components.

Visualizing Next.js Route Handlers

{"heading":"Visualizing Next.js Route Handlers","description":"Visualize a REST resource built with Route Handlers:\n\napp/api/posts/route.ts\n ├── export async function GET(request) → returns a list of all posts\n └── export async function POST(request) → creates a new post from the request body\n\napp/api/posts/[id]/route.ts\n ├── export async function GET(request, { params }) → returns ONE specific post\n ├── export async function PUT(request, { params }) → updates that specific post\n └── export async function DELETE(request, { params }) → deletes that specific post\n\nWho calls these?\n[Mobile app] --GET /api/posts--> [Route Handler] --> [JSON response]\n[Webhook from a 3rd-party service] --POST /api/posts--> [Route Handler] --> [Creates a record]"}

Next.js Practical Example

// app/api/posts/route.ts — collection-level handlers (list + create)
import { NextResponse } from 'next/server';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const category = searchParams.get('category'); // e.g. ?category=tech

  const posts = await db.posts.findMany({
    where: category ? { category } : undefined,
  });

  return NextResponse.json(posts);
}

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

  if (!body.title) {
    return NextResponse.json({ error: 'Title is required' }, { status: 400 });
  }

  const newPost = await db.posts.create({ data: body });
  return NextResponse.json(newPost, { status: 201 });
}

// app/api/posts/[id]/route.ts — item-level handlers (read, update, delete one post)
import { NextResponse } from 'next/server';

export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const post = await db.posts.findUnique({ where: { id } });

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

export async function DELETE(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  await db.posts.delete({ where: { id } });
  return NextResponse.json({ success: true });
}

GET at app/api/posts/route.ts reads an optional category query parameter directly from the request's URL and returns a filtered or full list of posts as JSON. POST on that same path parses the request's JSON body, validates that a title was provided (returning a 400 Bad Request status with a clear error message if not), and returns the newly created post with a 201 Created status, following standard REST conventions. The nested app/api/posts/[id]/route.ts file handles operations on one specific post: GET returns that post or a 404 Not Found if it doesn't exist, while DELETE removes it and confirms success — both handlers receive the dynamic id directly through the params argument, exactly mirroring how dynamic page components receive params, just in the context of an API route rather than a rendered page.

Real-World Examples: How Companies Use Next.js Route Handlers

  • Mobile apps built alongside a Next.js web application commonly consume the exact same Route Handlers as a backend API, letting both the website and a native iOS/Android app share one consistent data layer.
  • Payment providers like Stripe send webhook notifications (e.g., 'payment succeeded') as POST requests to a dedicated Route Handler endpoint, which then updates an order's status in the database accordingly.
  • Headless CMS integrations use a Route Handler as a webhook receiver, triggering on-demand revalidation (from Lesson 2.4) whenever content is published or updated in the external CMS.
  • Public developer APIs, where a company exposes certain data or functionality for third-party developers to build on, are commonly implemented directly as Route Handlers within a Next.js application.
  • Browser extensions or separate internal tools that need to read or write data from a company's core Next.js application typically do so via a set of well-defined Route Handlers, rather than any mechanism tied to the main app's own UI.

Common Mistakes to Avoid

  • Attempting to place both a route.ts and a page.tsx in the exact same folder, which isn't supported since a URL segment can't simultaneously be a page and an API endpoint.
  • Forgetting to await request.json() before trying to use the parsed body, since it returns a Promise.
  • Returning plain JavaScript objects directly instead of wrapping them with NextResponse.json(), missing correct headers and status code handling.
  • Building a Route Handler for a mutation that's only ever called from your own app's UI, when a simpler Server Action would have been more direct and required less boilerplate.
  • Not validating and sanitizing incoming request data inside a Route Handler, applying less rigor than would correctly be applied to a Server Action or any other server-side entry point.

Interview Notes

  • A Route Handler is created via a route.ts file, exporting async functions named after HTTP methods (GET, POST, PUT, DELETE, etc.).
  • A route.ts file cannot coexist with a page.tsx in the exact same folder.
  • Request bodies are read via await request.json(); dynamic route params are received via a second { params } argument.
  • NextResponse.json() constructs a JSON response with a specific HTTP status code and correct headers.
  • Route Handlers suit external callers (mobile apps, webhooks, third-party services); Server Actions suit mutations tied directly to your own app's UI.

Key Takeaways

  • Route Handlers extend Next.js beyond a page-rendering framework into a full backend capable of exposing genuine, standalone REST API endpoints.
  • The GET/POST/PUT/DELETE function-per-method convention in route.ts closely mirrors standard REST API design principles many developers already know.
  • Choosing between a Server Action and a Route Handler comes down to a clear, simple question: is this logic only ever called from your own app's UI, or does something external need to call it too?
  • Route Handlers require the same validation, sanitization, and security rigor as any other server-side entry point, since they're genuinely public-facing endpoints once deployed.

Summary

Route Handlers let you build genuine, standalone REST API endpoints directly within a Next.js application by creating a route.ts file inside the app/ directory and exporting async functions named after HTTP methods — GET, POST, PUT, DELETE, and others. Each handler receives a standard Request object, giving access to headers, query parameters (via the request's URL), and, for methods like POST, a JSON body (via await request.json()); dynamic route segments provide their values through a second params argument, just like dynamic pages. Responses are constructed using NextResponse.json(), allowing precise control over the returned data, HTTP status code, and headers. While Server Actions (from Lesson 4.3) are ideal for mutations tied directly to your own application's forms and components, Route Handlers are the correct choice when an endpoint needs to be callable by something outside your own Next.js frontend entirely — a mobile app, a third-party webhook, or any other external service — following the standard REST conventions those external clients expect.