Next.js API Routes - Complete Guide with Examples

Mahesh Mahesh Waghmare
7 min read

Next.js API routes allow you to create backend API endpoints within your Next.js application. They’re perfect for building RESTful APIs, handling form submissions, and creating serverless functions.

This comprehensive guide covers everything from basic API routes to advanced patterns and best practices.

Introduction to API Routes

API routes in Next.js are serverless functions that handle HTTP requests. They’re located in the app/api or pages/api directory and can handle any HTTP method.

Key Features:

  • Serverless functions
  • Built-in request/response handling
  • Support for all HTTP methods
  • Dynamic routing
  • Middleware support
  • TypeScript support

Use Cases:

  • RESTful APIs
  • Form handling
  • Authentication endpoints
  • Webhook handlers
  • Database operations
  • Third-party API integration

Basic API Route

App Router (Next.js 13+)

Create app/api/hello/route.ts:

import { NextResponse } from 'next/server';

export async function GET() {
  return NextResponse.json({ message: 'Hello World' });
}

Access: http://localhost:3000/api/hello

Pages Router (Next.js 12 and earlier)

Create pages/api/hello.ts:

import type { NextApiRequest, NextApiResponse } from 'next';

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  res.status(200).json({ message: 'Hello World' });
}
Advertisement

HTTP Methods

App Router

GET Request:

export async function GET() {
  return NextResponse.json({ data: 'GET response' });
}

POST Request:

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

PUT Request:

export async function PUT(request: Request) {
  const body = await request.json();
  return NextResponse.json({ updated: body });
}

DELETE Request:

export async function DELETE() {
  return NextResponse.json({ message: 'Deleted' });
}

PATCH Request:

export async function PATCH(request: Request) {
  const body = await request.json();
  return NextResponse.json({ patched: body });
}

Pages Router

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === 'GET') {
    res.status(200).json({ method: 'GET' });
  } else if (req.method === 'POST') {
    res.status(200).json({ method: 'POST', body: req.body });
  } else {
    res.setHeader('Allow', ['GET', 'POST']);
    res.status(405).end(`Method ${req.method} Not Allowed`);
  }
}

Request Handling

Reading Request Body

App Router:

export async function POST(request: Request) {
  // JSON body
  const json = await request.json();
  
  // Form data
  const formData = await request.formData();
  
  // Text
  const text = await request.text();
  
  return NextResponse.json({ received: json });
}

Pages Router:

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const body = req.body;
  res.status(200).json({ received: body });
}

Query Parameters

App Router:

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const id = searchParams.get('id');
  
  return NextResponse.json({ id });
}

Pages Router:

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { id } = req.query;
  res.status(200).json({ id });
}

Headers

App Router:

export async function GET(request: Request) {
  const authHeader = request.headers.get('authorization');
  return NextResponse.json({ auth: authHeader });
}

Pages Router:

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const authHeader = req.headers.authorization;
  res.status(200).json({ auth: authHeader });
}

Response Handling

JSON Response

return NextResponse.json({ data: 'value' });

Custom Status Codes

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

Headers

return NextResponse.json(
  { data: 'value' },
  {
    headers: {
      'Content-Type': 'application/json',
      'Cache-Control': 'no-store',
    },
  }
);

Redirects

return NextResponse.redirect(new URL('/login', request.url));

Streaming

const stream = new ReadableStream({
  start(controller) {
    controller.enqueue(new TextEncoder().encode('data'));
    controller.close();
  },
});

return new Response(stream, {
  headers: { 'Content-Type': 'text/plain' },
});

Dynamic Routes

App Router

Single Parameter: app/api/users/[id]/route.ts

export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const { id } = params;
  return NextResponse.json({ userId: id });
}

Multiple Parameters: app/api/users/[id]/posts/[postId]/route.ts

export async function GET(
  request: Request,
  { params }: { params: { id: string; postId: string } }
) {
  const { id, postId } = params;
  return NextResponse.json({ userId: id, postId });
}

Catch-All: app/api/[...slug]/route.ts

export async function GET(
  request: Request,
  { params }: { params: { slug: string[] } }
) {
  const { slug } = params;
  return NextResponse.json({ path: slug.join('/') });
}

Pages Router

Single Parameter: pages/api/users/[id].ts

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const { id } = req.query;
  res.status(200).json({ userId: id });
}

Authentication

Basic Auth Check

export async function GET(request: Request) {
  const authHeader = request.headers.get('authorization');
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    );
  }
  
  const token = authHeader.substring(7);
  // Verify token...
  
  return NextResponse.json({ data: 'Protected data' });
}

JWT Verification

import jwt from 'jsonwebtoken';

export async function GET(request: Request) {
  const authHeader = request.headers.get('authorization');
  const token = authHeader?.replace('Bearer ', '');
  
  if (!token) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET!);
    return NextResponse.json({ data: 'Protected', user: decoded });
  } catch (error) {
    return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
  }
}
Advertisement

Error Handling

Try-Catch

export async function POST(request: Request) {
  try {
    const body = await request.json();
    // Process...
    return NextResponse.json({ success: true });
  } catch (error) {
    return NextResponse.json(
      { error: 'Invalid request' },
      { status: 400 }
    );
  }
}

Custom Error Handler

function handleError(error: unknown) {
  if (error instanceof Error) {
    return NextResponse.json(
      { error: error.message },
      { status: 500 }
    );
  }
  return NextResponse.json(
    { error: 'Unknown error' },
    { status: 500 }
  );
}

export async function GET() {
  try {
    // API logic
  } catch (error) {
    return handleError(error);
  }
}

Best Practices

1. Validate Input

import { z } from 'zod';

const schema = z.object({
  email: z.string().email(),
  name: z.string().min(1),
});

export async function POST(request: Request) {
  const body = await request.json();
  const result = schema.safeParse(body);
  
  if (!result.success) {
    return NextResponse.json(
      { errors: result.error.errors },
      { status: 400 }
    );
  }
  
  // Use validated data
  const { email, name } = result.data;
}

2. Use TypeScript

interface RequestBody {
  email: string;
  name: string;
}

export async function POST(request: Request) {
  const body: RequestBody = await request.json();
  // Type-safe body
}

3. Environment Variables

const apiKey = process.env.API_KEY;

if (!apiKey) {
  throw new Error('API_KEY not configured');
}

4. Rate Limiting

// Use middleware or library for rate limiting

5. CORS Headers

return NextResponse.json(
  { data: 'value' },
  {
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST',
    },
  }
);

Conclusion

Next.js API routes provide:

  • Serverless functions for backend logic
  • Full HTTP method support
  • Dynamic routing capabilities
  • TypeScript support
  • Built-in request/response handling

Key Points:

  • Use App Router for new projects
  • Handle all HTTP methods
  • Validate input data
  • Implement proper error handling
  • Use TypeScript for type safety

API routes make Next.js a full-stack framework, allowing you to build complete applications without separate backend services.

Advertisement
Mahesh Waghmare

Written by Mahesh Waghmare

I bridge the gap between WordPress architecture and modern React frontends. Currently building tools for the AI era.

Follow on Twitter

Read Next