How to Build a REST API with Next.js Route Handlers

Tutorial · 9 min read 🔄 Affiliate Links

🔍 Want the best deal? Check current prices and availability.

Compare Prices →

When you buy through links on our site, we may earn a commission.

How to Build a REST API with Next.js Route Handlers

If you’re building a Next.js application and need an API, you used to have two choices: spin up a separate Express server, or use the Pages Router’s API routes. With the App Router (stable since Next.js 14), there’s a cleaner option: Route Handlers. They let you define API endpoints directly inside your app directory, sharing the same file-based routing as your pages. No extra server, no boilerplate – just a route.ts file and you’re done.

In this tutorial, I’ll walk you through building a complete REST API with Next.js Route Handlers. We’ll cover everything from basic GET endpoints to authentication, error handling, and deployment. By the end, you’ll have a clear picture of when Route Handlers Make sense and when you might want something else.


What Are Next.js Route Handlers?

Route Handlers are the App Router’s replacement for the old pages/api endpoints. They live in app/api/*/route.ts (or .js) and automatically handle HTTP methods based on exported functions.

// app/api/hello/route.ts

export async function GET(request: Request) {

return new Response(JSON.stringify({ message: "Hello World" }), {

status: 200,

headers: { "Content-Type": "application/json" },

});

}

That’s it – no Express setup, no apiRoutes config. The file structure mirrors the URL path: app/api/users/route.ts maps to GET /api/users.

Key features:

  • File-based routing – no need to manually define routes.
  • HTTP method support – export GET, POST, PUT, PATCH, DELETE, etc.
  • Request/Response Web API – uses the standard Request and Response objects.
  • Dynamic routes[id] folders for parameters.
  • Middleware – run logic before handlers (via middleware.ts in the project root).
  • Edge runtime – deploy to Vercel’s edge network for low latency.
  • Streaming – return ReadableStream for real-time data.

Route Handlers are designed for full-stack Next.js apps where the API logic is tightly coupled with the frontend, but they can also serve as a standalone backend for mobile apps or third-party integrations.


Step-by-Step: Building a REST API

Let’s build a simple task manager API with CRUD operations. We’ll use an in-memory store for simplicity, but the patterns work with any database (Postgres, MongoDB, Prisma, etc.).

1. Set Up the Project

Create a new Next.js App Router project:

npx create-next-app@latest task-api --typescript --app

cd task-api

The --app flag enables the App Router. You can also add --tailwind if you want a frontend later.

2. Define the Data Model

We’ll use a plain array as our “database” for now. Create a lib/data.ts file:

// lib/data.ts

export interface Task {

id: string;

title: string;

completed: boolean;

createdAt: Date;

}

let tasks: Task[] = [

{ id: "1", title: "Learn Next.js Route Handlers", completed: false, createdAt: new Date() },

];

export function getTasks(): Task[] {

return tasks;

}

export function addTask(title: string): Task {

const task: Task = {

id: String(Date.now()),

title,

completed: false,

createdAt: new Date(),

};

tasks.push(task);

return task;

}

export function updateTask(id: string, updates: Partial): Task | null {

const index = tasks.findIndex((t) => t.id === id);

if (index === -1) return null;

tasks[index] = { ...tasks[index], ...updates };

return tasks[index];

}

export function deleteTask(id: string): boolean {

const index = tasks.findIndex((t) => t.id === id);

if (index === -1) return false;

tasks.splice(index, 1);

return true;

}

3. Create the Route Handlers

We’ll put all task endpoints under app/api/tasks.

GET /api/tasks – list all tasks:

// app/api/tasks/route.ts

import { NextRequest, NextResponse } from "next/server";

import { getTasks } from "@/lib/data";

export async function GET(request: NextRequest) {

const tasks = getTasks();

return NextResponse.json(tasks);

}

NextResponse.json() is a convenience wrapper around Response.json(). It sets the Content-Type header automatically.

POST /api/tasks – create a new task:

export async function POST(request: NextRequest) {

const body = await request.json();

const { title } = body;

if (!title || typeof title !== "string") {

return NextResponse.json(

{ error: "Title is required and must be a string" },

{ status: 400 }

);

}

const task = addTask(title);

return NextResponse.json(task, { status: 201 });

}

We added basic validation. Route Handlers can read the request body via request.json() (or request.text(), request.formData()).

Dynamic route for single task – create app/api/tasks/[id]/route.ts:

import { NextRequest, NextResponse } from "next/server";

import { getTasks, updateTask, deleteTask } from "@/lib/data";

export async function GET(

request: NextRequest,

{ params }: { params: { id: string } }

) {

const tasks = getTasks();

const task = tasks.find((t) => t.id === params.id);

if (!task) {

return NextResponse.json({ error: "Task not found" }, { status: 404 });

}

return NextResponse.json(task);

}

export async function PUT(

request: NextRequest,

{ params }: { params: { id: string } }

) {

const body = await request.json();

const updated = updateTask(params.id, body);

if (!updated) {

return NextResponse.json({ error: "Task not found" }, { status: 404 });

}

return NextResponse.json(updated);

}

export async function DELETE(

request: NextRequest,

{ params }: { params: { id: string } }

) {

const deleted = deleteTask(params.id);

if (!deleted) {

return NextResponse.json({ error: "Task not found" }, { status: 404 });

}

return NextResponse.json({ message: "Task deleted" });

}

Dynamic segments work exactly like page routes: [id] captures the value and passes it to params.id.

4. Test the API

Run npm run dev and test with curl or a tool like Postman:

curl http://localhost:3000/api/tasks

curl -X POST http://localhost:3000/api/tasks -H "Content-Type: application/json" -d '{"title":"New task"}'

curl http://localhost:3000/api/tasks/1

curl -X PUT http://localhost:3000/api/tasks/1 -H "Content-Type: application/json" -d '{"completed":true}'

curl -X DELETE http://localhost:3000/api/tasks/1

Everything works. No Express, no extra config.


Adding Authentication with Middleware

Route Handlers can be protected by Next.js Middleware, which runs before every request. Here’s a simple API key check:

// middleware.ts

import { NextResponse } from "next/server";

import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {

// Only protect API routes

if (request.nextUrl.pathname.startsWith("/api/")) {

const apiKey = request.headers.get("x-api-key");

if (apiKey !== process.env.API_KEY) {

return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

}

}

return NextResponse.next();

}

export const config = {

matcher: "/api/:path*",

};

This runs on the Edge runtime, so it’s fast but has some limitations (no access to Node.js APIs like fs). For more complex auth (e.g., JWT verification), you can write logic inside each route handler or use a library like NextAuth.js.


Edge vs Node.js Runtime

By default, Route Handlers run on the Node.js runtime. You can opt into the Edge runtime by exporting a runtime config:

export const runtime = "edge"; // or "nodejs"

Edge runtime is faster (deploys to Vercel’s edge network) but has constraints: no Buffer, no process.env at runtime (use process.env at build time or NextRequest’s geo and ip), and limited Node.js APIs. For most CRUD APIs, Node.js runtime is fine. Use Edge when you need global low latency for read-heavy endpoints.


Comparison: Route Handlers vs Other API Approaches

FeatureNext.js Route HandlersExpress.js (standalone)Next.js Pages Router API
File-based routingYes (App Router)No (manual)Yes (Pages Router)
HTTP methodsExport functionsapp.get/post/...Export default handler with req.method switch
MiddlewareGlobal middleware.tsapp.use()No built-in middleware; use next-connect
Edge runtimeYes (opt-in)NoNo
StreamingYes (Response stream)Yes (with pipe)No
Typed request/responseWeb API (Request/Response)Express req/resIncomingMessage/ServerResponse
Authentication librariesNextAuth.js, Clerk, customPassport, JWTNextAuth.js, custom
Learning curveMedium (App Router)LowMedium (Pages Router)
Hosting optionsVercel, Netlify, self-hostedAny Node.js hostSame as Route Handlers
Pricing (free tier limits)Vercel: 100k requests/month, 100GB bandwidth; Netlify: 125k requests/monthSelf-hosted: server costSame as Route Handlers

Verdict on the table: Route Handlers are the most integrated option for full-stack Next.js apps. They share the same routing, middleware, and deployment as your frontend. Express.js is better if you want a separate backend that can be scaled independently. Pages Router API routes are legacy but still work – just less flexible.


Pros and Cons of Next.js Route Handlers

Pros

  • No extra server – your API lives inside your Next.js project. One package.json, one deploy.
  • File-based routing – URL structure is obvious from the folder tree.
  • Web API standardRequest and Response are modern and familiar if you’ve used fetch.
  • Edge ready – drop-in for global performance.
  • Streaming support – great for real-time data (e.g., SSE, AI responses).
  • Middleware – centralize auth, logging, rate limiting.

Cons

  • Tight coupling – API logic is mixed with frontend code. Hard to reuse if you later extract a separate backend.
  • No built-in validation – you have to manually parse and validate request bodies (though libraries like Zod help).
  • Limited Node.js APIs on Edge – if you need fs, crypto (Node.js version), or database drivers that rely on Node.js streams, Edge won’t work.
  • Not ideal for microservices – each Next.js app is a monolith. If you need independent API services, Express or Fastify are better.
  • Learning curve – developers new to the App Router may find route.ts files confusing at first.

Verdict: Should You Use Next.js Route Handlers?

Yes, if:

  • You’re already building a full-stack Next.js app and need a simple API.
  • Your API is tightly coupled with your frontend (e.g., server data fetching, form submissions).
  • You want to deploy everything on Vercel or Netlify with minimal configuration.
  • You’re building a small-to-medium project and don’t need a separate backend team.

No, if:

  • You need a standalone API that multiple frontends (mobile, web, third-party) will consume.
  • You want to scale the API independently from the frontend.
  • You rely heavily on Node.js-specific APIs (e.g., fs, child_process, or database ORMs that require Node.js streams).
  • You prefer Express.js middleware patterns and want to reuse existing Express middleware.

For most indie hackers and small teams building a Next.js app, Route Handlers are the fastest way to get an API up and running. They’re not a replacement for a dedicated backend framework, but they don’t need to be.

If you’re deploying on Vercel, the free tier handles 100k requests per month – enough for a side project. Deploy on Vercel ->


FAQ

1. Can I use Route Handlers with the Pages Router?

No. Route Handlers are exclusive to the App Router. If you’re using the Pages Router, you’ll need to stick with pages/api or migrate to the App Router.

2. How do I handle CORS?

You can set CORS headers in a middleware or in each route handler:

// middleware.ts

export function middleware(request: NextRequest) {

const response = NextResponse.next();

response.headers.set("Access-Control-Allow-Origin", "*");

response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");

return response;

}

3. Can I use Route Handlers with Prisma or other ORMs?

Yes. Prisma works in the Node.js runtime. Just import your Prisma client in route handlers and use it as usual. For Edge runtime, use Prisma Accelerate or a serverless-compatible driver.

4. How do I read query parameters?

Use `

🔍 Want the best deal? Check current prices and availability.

Compare Prices →
D

Dev Tool Rank Editorial Team

We're a team of tech enthusiasts who test and review tools so you don't have to. Our reviews are independent — we only recommend what we'd actually use ourselves.