Next.js API Routes - Complete Guide with Examples
Mahesh Waghmare 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' });
}
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 });
}
}
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.
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 →