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

Next.js has come a long way from being just a React framework for server-side rendering. With the introduction of the App Router (starting in Next.js 13.4), you can now build fully functional REST APIs without needing a separate backend service. Route Handlers let you define API endpoints inside your Next.js project using the route.ts file conventions.

In this tutorial, I'll walk you through everything you need to build a production-ready REST API with Next.js Route Handlers – from basic CRUD operations to validation, error handling, and deployment. I'll also compare this approach against traditional frameworks like Express.js and Fastify, so you can decide if it's the right fit for your next project.


What Are Route Handlers?

Route Handlers replace the old /pages/api/ directory from Next.js 12 and earlier. They live directly inside the app/ directory following the file-system routing convention. Instead of exporting a default function, you export named functions for each HTTP method: GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS.

Example structure:

app/

api/

users/

route.ts // handles /api/users

[id]/

route.ts // handles /api/users/123

Each route.ts file exports async functions that receive a NextRequest and return a NextResponse.


Step 1: Setting Up a Next.js Project

If you don't already have a Next.js app using the App Router, create one:

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

cd my-api

Make sure your next.config.js is using the App Router (it’s the default since Next.js 14).


Step 2: Building a Basic CRUD API

Let's create a simple resource: products. We'll use an in‑memory array for demo purposes. In production you'd connect a real database.

app/api/products/route.ts – handles GET (list) and POST (create).

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

// Fake database

const products =

{ id: 1, name: 'Wireless [Mouse', price: 29.99 },

{ id: 2, name: 'Mechanical Keyboard', price: 89.99 },

];

export async function GET() {

return NextResponse.json(products);

}

export async function POST(request: NextRequest) {

const body = await request.json();

const newProduct = { id: products.length + 1, ...body };

products.push(newProduct);

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

}

app/api/products/[id]/route.ts – handles GET (single), PUT, and DELETE.

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

export async function GET(

request: NextRequest,

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

) {

const product = products.find(p => p.id === Number(params.id));

if (!product) {

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

}

return NextResponse.json(product);

}

export async function PUT(

request: NextRequest,

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

) {

const index = products.findIndex(p => p.id === Number(params.id));

if (index === -1) {

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

}

const body = await request.json();

products[index] = { ...products[index], ...body };

return NextResponse.json(products[index]);

}

export async function DELETE(

request: NextRequest,

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

) {

const index = products.findIndex(p => p.id === Number(params.id));

if (index === -1) {

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

}

products.splice(index, 1);

return NextResponse.json({ message: 'Deleted' });

}

That's it. Start the dev server with npm run dev and test endpoints with curl or Postman.


Step 3: Adding Input Validation (with Zod)

Raw body parsing can lead to bad data. Let's add validation using Zod. Install it:

npm install zod

Update the POST handler in app/api/products/route.ts:

import { z } from 'zod';

const productSchema = z.object({

name: z.string().min(1, 'Name is required'),

price: z.number().positive('Price must be positive'),

});

export async function POST(request: NextRequest) {

try {

const body = await request.json();

const parsed = productSchema.parse(body);

const newProduct = { id: products.length + 1, ...parsed };

products.push(newProduct);

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

} catch (error) {

if (error instanceof z.ZodError) {

return NextResponse.json({ errors: error.errors }, { status: 400 });

}

return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 });

}

}

Now invalid requests return clear error messages with proper status codes.


Step 4: Adding Middleware for Authentication

Next.js allows you to run middleware at the edge or on the server. Middleware files (middleware.ts) sit in the root of your project and can intercept all requests.

middleware.ts:

import { NextResponse } from 'next/server';

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

export function middleware(request: NextRequest) {

const token = request.headers.get('authorization')?.split(' ')[1];

if (!token) {

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

}

// Validate token (e.g., JWT verification)

return NextResponse.next();

}

export const config = {

matcher: '/api/:path*',

};

This protects all /api/* routes. You can also apply different rules per route.


Step 5: Handling CORS

If your frontend is on a different domain, you need CORS headers. You can set them in each route handler or create a shared middleware. Here's a quick middleware approach:

export function middleware(request: NextRequest) {

if (request.method === 'OPTIONS') {

return new NextResponse(null, {

headers: {

'Access-Control-Allow-Origin': '*',

'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',

'Access-Control-Allow-Headers': 'Content-Type, Authorization',

},

});

}

// ... authentication logic

}

For a deeper CORS solution, use the cors package or wrap your responses.


Step 6: Caching & Revalidation

Route Handlers in Next.js are serverless functions by default. You can cache GET responses using revalidate:

export const revalidate = 60; // revalidate every 60 seconds

export async function GET() {

// fetch data from DB

return NextResponse.json(products);

}

If you want to opt out of caching entirely (for real‑time data), set export const dynamic = 'force-dynamic' at the file level.


Comparison: Next.js Route Handlers vs. Other Backend Frameworks

Choosing the right tool depends on your deployment target, team size, and cost constraints. Here's a fair comparison of building a REST API with Next.js Route Handlers versus traditional frameworks.

Feature / MetricNext.js Route Handlers (on Vercel)Express.js (on Railway)Fastify (on Fly.io)
Setup time~5 min (if Next.js already set)~15 min~20 min
Performance (req/s, simple)~1,200 (cold start ~300ms)~2,500 (no cold start)~3,000+
Built‑in TypeScript supportFullOptionalOptional
File‑based routingYesNoNo
Hosting cost (1M req/month)~$20 (Vercel Pro)~$10 (Railway Starter)~$15 (Fly.io)
Cold startsYes (serverless)No (long‑running)No (long‑running)
Learning curveLow (if known Next.js)LowMedium

Notes:

  • Vercel charges per function invocation and edge requests. For 1M requests, the Pro plan ($20/mo) includes 1M edge requests.
  • Railway offers a free tier with $5 credit; typical Express app with 1M requests stays under $10.
  • Fly.io charges per VM and bandwidth; a small Fastify server costs about $15/mo.
  • Performance numbers are rough averages from my tests. Cold starts on Vercel depend on function size and region.

Pros of Using Next.js Route Handlers for REST APIs

  • Unified codebase – frontend and backend in one project, no separate server to maintain.
  • Type safety – share types between client and server easily.
  • File‑based routing – no manual route registration.
  • Edge deployment – deploy globally with zero configuration (Vercel, Netlify).
  • Middleware support – authentication, logging, redirects run at the edge.
  • Automatic caching – ISR and static generation for GET endpoints.

Cons

  • Cold starts – serverless means occasional latency spikes.
  • No persistent connections – WebSockets or SSE require workarounds.
  • Limited request body size – depends on hosting provider (Vercel caps at 4.5MB).
  • Long‑running tasks – serverless timeout (Vercel: 60s on Pro, 10s on Hobby).
  • Vendor lock‑in – you’re tied to Next.js and the hosting platform for optimal performance.

When Should You Use Next.js Route Handlers?

  • Full‑stack side projects – you want to ship quickly without managing two repos.
  • Internal tool APIs – low traffic, simple CRUD.
  • Server‑driven components – you already use Next.js for SSR/SSG.
  • Prototypes – iterate fast without separate backend setup.

Avoid when you need:

  • Real‑time features (WebSockets).
  • Heavy computation or background jobs that exceed time limits.
  • High‑performance microservices at scale (choose Fastify or Express with dedicated servers).

Deploying Your API

I recommend Vercel for seamless Next.js deployment. They provide a generous free tier (100GB bandwidth, 100k edge requests). Check Vercel ->

If you prefer a DIY approach, you can self‑host with Node on a VPS or use Railway (works well with Docker). Check Railway ->


FAQ

Q: Can I use Route Handlers with the Pages Router?

A: No. Route Handlers are exclusive to the App Router. If you're on Next.js 12 or 13 with Pages Router, use the existing pages/api/ directory.

Q: How do I handle file uploads?

A: Route Handlers don't natively support multipart/form-data parsing. Use the formidable or busboy package and read the request stream manually.

Q: Can I connect to a database like PostgreSQL?

A: Absolutely. Use any database client (Prisma, Drizzle, Knex) inside your route handlers. Just be aware of cold starts and connection pooling.

Q: How do I test my API locally?

A: Next.js provides next dev. Use a tool like Postman, Insomnia, or curl. For automated tests, I recommend using Vitest with Supertest.

Q: Is there a way to avoid cold starts on Vercel?

A: You can use the "Pro" plan and set functions to "always warm" (via cron jobs), but it adds cost. Alternatively, deploy on a platform without cold starts (e.g., Railway, Fly.io).


Final Verdict

Next.js Route Handlers are a solid choice for small to medium‑sized APIs when you're already invested in the Next.js ecosystem. They excel in developer experience: no extra server code, shared types, and instant deployment.

For high‑traffic production APIs or anything requiring persistent connections, stick with Express or Fastify on a long‑running server. The cost and performance trade‑offs become noticeable after a few hundred thousand requests per month.

Try Next.js Route Handlers for your next prototype or internal tool – you'll be surprised how quickly you can ship. If you outgrow them, migrating to a dedicated backend is straightforward since you're already using standard REST patterns.

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

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