Back to articles
Next.js

Building Robust API Routes in Next.js

Comprehensive guide to creating scalable API routes in Next.js. Learn about middleware, authentication, error handling, and API best practices.

14 mins read
Next.jsAPI RoutesBackendREST API

Next.js API routes provide a powerful way to build full-stack applications with both frontend and backend in a single codebase. This comprehensive guide covers everything from basic route handlers to advanced patterns for building robust, scalable APIs.

App Router API Routes

With Next.js 13+ App Router, API routes are defined using route.ts files with HTTP method exports:

TypeScriptapp/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';export async function GET(request: NextRequest) {  try {    const { searchParams } = new URL(request.url);    const page = parseInt(searchParams.get('page') || '1');    const limit = parseInt(searchParams.get('limit') || '10');        // Simulate database query    const users = await getUsersFromDB({ page, limit });        return NextResponse.json({      users,      pagination: {        page,        limit,        total: users.length      }    });  } catch (error) {    return NextResponse.json(      { error: 'Failed to fetch users' },      { status: 500 }    );  }}export async function POST(request: NextRequest) {  try {    const body = await request.json();        // Validate request body    const validatedData = validateUserData(body);        // Create user in database    const newUser = await createUser(validatedData);        return NextResponse.json(newUser, { status: 201 });  } catch (error) {    if (error instanceof ValidationError) {      return NextResponse.json(        { error: error.message },        { status: 400 }      );    }        return NextResponse.json(      { error: 'Failed to create user' },      { status: 500 }    );  }}

Dynamic Routes

TypeScriptapp/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';export async function GET(  request: NextRequest,  { params }: { params: Promise<{ id: string }> }) {  try {    const { id } = await params;        // Validate ID    if (!id || isNaN(Number(id))) {      return NextResponse.json(        { error: 'Invalid user ID' },        { status: 400 }      );    }        const user = await getUserById(Number(id));        if (!user) {      return NextResponse.json(        { error: 'User not found' },        { status: 404 }      );    }        return NextResponse.json(user);  } catch (error) {    return NextResponse.json(      { error: 'Failed to fetch user' },      { status: 500 }    );  }}export async function PUT(  request: NextRequest,  { params }: { params: Promise<{ id: string }> }) {  try {    const { id } = await params;    const body = await request.json();        const updatedUser = await updateUser(Number(id), body);        return NextResponse.json(updatedUser);  } catch (error) {    return NextResponse.json(      { error: 'Failed to update user' },      { status: 500 }    );  }}export async function DELETE(  request: NextRequest,  { params }: { params: Promise<{ id: string }> }) {  try {    const { id } = await params;        await deleteUser(Number(id));        return NextResponse.json(      { message: 'User deleted successfully' },      { status: 200 }    );  } catch (error) {    return NextResponse.json(      { error: 'Failed to delete user' },      { status: 500 }    );  }}

Middleware and Authentication

TypeScriptlib/auth.ts
import { NextRequest } from 'next/server';import jwt from 'jsonwebtoken';export interface AuthenticatedRequest extends NextRequest {  user?: {    id: number;    email: string;    role: string;  };}export async function authenticateToken(request: NextRequest) {  const authHeader = request.headers.get('authorization');  const token = authHeader && authHeader.split(' ')[1];  if (!token) {    throw new Error('Access token required');  }  try {    const user = jwt.verify(token, process.env.JWT_SECRET!) as any;    return user;  } catch (error) {    throw new Error('Invalid token');  }}export function withAuth(handler: Function) {  return async (request: NextRequest, context: any) => {    try {      const user = await authenticateToken(request);      (request as AuthenticatedRequest).user = user;      return handler(request, context);    } catch (error) {      return NextResponse.json(        { error: 'Unauthorized' },        { status: 401 }      );    }  };}
TypeScriptapp/api/protected/route.ts
import { NextRequest, NextResponse } from 'next/server';import { withAuth, AuthenticatedRequest } from '@/lib/auth';async function handler(request: AuthenticatedRequest) {  const user = request.user!;    // User is authenticated, proceed with business logic  return NextResponse.json({    message: 'Protected data',    user: user  });}export const GET = withAuth(handler);

Input Validation

TypeScriptlib/validation.ts
import { z } from 'zod';export const UserSchema = z.object({  name: z.string().min(2).max(50),  email: z.string().email(),  age: z.number().min(18).max(120).optional(),  role: z.enum(['admin', 'user']).default('user'),});export const UpdateUserSchema = UserSchema.partial();export class ValidationError extends Error {  constructor(message: string) {    super(message);    this.name = 'ValidationError';  }}export function validateUserData(data: unknown) {  try {    return UserSchema.parse(data);  } catch (error) {    if (error instanceof z.ZodError) {      const errorMessages = error.errors.map(err =>         `${err.path.join('.')}: ${err.message}`      ).join(', ');      throw new ValidationError(errorMessages);    }    throw new ValidationError('Invalid data format');  }}

Database Integration

TypeScriptlib/database.ts
import { PrismaClient } from '@prisma/client';const globalForPrisma = globalThis as unknown as {  prisma: PrismaClient | undefined;};export const prisma = globalForPrisma.prisma ?? new PrismaClient();if (process.env.NODE_ENV !== 'production') {  globalForPrisma.prisma = prisma;}// User operationsexport async function getUsersFromDB({ page = 1, limit = 10 }) {  const skip = (page - 1) * limit;    return prisma.user.findMany({    skip,    take: limit,    select: {      id: true,      name: true,      email: true,      role: true,      createdAt: true,    },  });}export async function getUserById(id: number) {  return prisma.user.findUnique({    where: { id },    select: {      id: true,      name: true,      email: true,      role: true,      createdAt: true,    },  });}export async function createUser(userData: {  name: string;  email: string;  role?: string;}) {  return prisma.user.create({    data: userData,    select: {      id: true,      name: true,      email: true,      role: true,      createdAt: true,    },  });}export async function updateUser(id: number, updates: Partial<{  name: string;  email: string;  role: string;}>) {  return prisma.user.update({    where: { id },    data: updates,    select: {      id: true,      name: true,      email: true,      role: true,      updatedAt: true,    },  });}export async function deleteUser(id: number) {  return prisma.user.delete({    where: { id },  });}

Error Handling and Logging

TypeScriptlib/logger.ts
interface LogContext {  method: string;  url: string;  userAgent?: string;  userId?: number;  [key: string]: any;}export function logError(error: Error, context: LogContext) {  console.error({    timestamp: new Date().toISOString(),    level: 'error',    message: error.message,    stack: error.stack,    context,  });    // In production, send to logging service  if (process.env.NODE_ENV === 'production') {    // Send to Sentry, LogRocket, etc.  }}export function logInfo(message: string, context: LogContext) {  console.log({    timestamp: new Date().toISOString(),    level: 'info',    message,    context,  });}
TypeScriptapp/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';import { logError, logInfo } from '@/lib/logger';export async function GET(request: NextRequest) {  const context = {    method: 'GET',    url: request.url,    userAgent: request.headers.get('user-agent') || undefined,  };  try {    logInfo('Fetching users', context);        const users = await getUsersFromDB({});        logInfo(`Successfully fetched ${users.length} users`, context);        return NextResponse.json({ users });  } catch (error) {    logError(error as Error, context);        return NextResponse.json(      { error: 'Internal server error' },      { status: 500 }    );  }}

Rate Limiting

TypeScriptlib/rateLimit.ts
interface RateLimitOptions {  windowMs: number;  maxRequests: number;}const requestCounts = new Map<string, { count: number; resetTime: number }>();export function rateLimit(options: RateLimitOptions) {  return async (request: NextRequest) => {    const ip = request.ip || 'unknown';    const now = Date.now();    const windowStart = now - options.windowMs;    // Clean up old entries    for (const [key, data] of requestCounts.entries()) {      if (data.resetTime < now) {        requestCounts.delete(key);      }    }    const currentData = requestCounts.get(ip);    if (!currentData) {      requestCounts.set(ip, {        count: 1,        resetTime: now + options.windowMs,      });      return null; // Allow request    }    if (currentData.count >= options.maxRequests) {      return NextResponse.json(        { error: 'Too many requests' },        {           status: 429,          headers: {            'Retry-After': Math.ceil(options.windowMs / 1000).toString(),          },        }      );    }    currentData.count++;    return null; // Allow request  };}// Usage in API routeexport async function GET(request: NextRequest) {  const rateLimitResponse = await rateLimit({    windowMs: 15 * 60 * 1000, // 15 minutes    maxRequests: 100,  })(request);  if (rateLimitResponse) {    return rateLimitResponse;  }  // Continue with normal request handling  return NextResponse.json({ message: 'Success' });}
Type Safety

Full TypeScript support with request/response typing and parameter validation.

Built-in Security

CORS, CSRF protection, and security headers built into Next.js by default.

Edge Runtime

Deploy API routes to the edge for faster response times globally.

Serverless Ready

Automatic serverless deployment with Vercel or other platforms.