← Articles

Building a REST API with Next.js Route Handlers

By Mark · 29 June 20260 views

Building a REST API with Next.js Route Handlers

Next.js 13 introduced the App Router alongside a new way to build API endpoints: Route Handlers. These replace the older pages/api approach and integrate seamlessly with React Server Components, Edge runtime, streaming responses, and the Web Fetch API. This guide covers everything you need to build production-ready REST APIs with Route Handlers.

What Are Route Handlers?

Route Handlers are files named route.ts (or route.js) placed inside the app directory. They export named functions corresponding to HTTP methods — GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS.

app/
  api/
    users/
      route.ts          → /api/users
      [id]/
        route.ts        → /api/users/:id

Your First Route Handler

// app/api/users/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
  const users = await db.query('SELECT id, name, email FROM users');
  return NextResponse.json(users);
}

export async function POST(request: Request) {
  const body = await request.json();
  const user = await db.createUser(body);
  return NextResponse.json(user, { status: 201 });
}

The Request object is the standard Web API Request — no more proprietary req/res objects from Express. Responses are standard Response objects, typically created with NextResponse.json().

Reading URL Parameters

Dynamic segments are accessed through the params argument:

// app/api/users/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const user = await db.findUser(params.id);
  if (!user) {
    return NextResponse.json({ error: 'Not found' }, { status: 404 });
  }
  return NextResponse.json(user);
}

Reading Query Strings

Use the NextRequest type (which extends Request) to access the URL:

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

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const page = Number(searchParams.get('page') ?? '1');
  const limit = Number(searchParams.get('limit') ?? '20');

  const results = await db.paginate({ page, limit });
  return NextResponse.json(results);
}

Authentication with Middleware

Rather than repeating auth logic in every handler, use Next.js Middleware to protect routes:

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyJwt } from '@/lib/auth';

export function middleware(request: NextRequest) {
  const token = request.headers.get('authorization')?.replace('Bearer ', '');
  if (!token || !verifyJwt(token)) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }
  return NextResponse.next();
}

export const config = {
  matcher: ['/api/users/:path*', '/api/posts/:path*'],
};

Error Handling

Centralize error handling with a wrapper:

// lib/api-handler.ts
import { NextResponse } from 'next/server';

type Handler = (req: Request, ctx: any) => Promise<Response>;

export function withErrorHandling(handler: Handler): Handler {
  return async (req, ctx) => {
    try {
      return await handler(req, ctx);
    } catch (error) {
      console.error(error);
      const message = error instanceof Error ? error.message : 'Internal error';
      return NextResponse.json({ error: message }, { status: 500 });
    }
  };
}

Then wrap your handlers:

export const GET = withErrorHandling(async (request) => {
  const data = await riskyOperation();
  return NextResponse.json(data);
});

Edge Runtime

Route Handlers can run on the Edge runtime for ultra-low latency:

export const runtime = 'edge';

export async function GET() {
  return new Response('Hello from the edge!');
}

Edge handlers cannot use Node.js APIs (fs, crypto, etc.) — they are limited to the Web Platform API. For most database-connected endpoints, stick with the default Node.js runtime.

Streaming Responses

For long-running operations or LLM integrations, stream the response:

export async function POST(request: Request) {
  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      for await (const chunk of generateText()) {
        controller.enqueue(encoder.encode(chunk));
      }
      controller.close();
    },
  });
  return new Response(stream, {
    headers: { 'Content-Type': 'text/plain; charset=utf-8' },
  });
}

Caching

GET handlers are cached by default in production. To opt out:

export const dynamic = 'force-dynamic';

export async function GET() {
  // Always fetches fresh data
  const data = await fetchLiveData();
  return NextResponse.json(data);
}

Conclusion

Next.js Route Handlers offer a clean, standards-based approach to building REST APIs that co-locates your backend and frontend code. The Web API alignment means skills are transferable, and features like Edge runtime and streaming are first-class citizens. For full-stack Next.js applications, Route Handlers are the natural choice over running a separate API server.

Sign in to like, dislike, or report.

Comments

No comments yet. Be the first!

Sign in to leave a comment.