Building Secure APIs with Next.js: A Practical Guide
Introduction
Next.js API routes make it easy to build full-stack applications, but the convenience can lead to security shortcuts. This guide covers the essential security measures every Next.js API should implement, with practical code examples you can use in your projects.
Authentication with NextAuth.js
Every protected endpoint should verify the user's session before processing the request:
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { NextRequest, NextResponse } from 'next/server';
export async function GET(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json(
{ error: 'Authentication required' },
{ status: 401 }
);
}
// Proceed with authenticated logic
return NextResponse.json({ data: 'protected content' });
}
Input Validation
Never trust client data. Validate and sanitize every input:
interface CreatePostInput {
title: string;
content: string;
tags: string[];
}
function validatePost(body: unknown): CreatePostInput {
if (!body || typeof body !== 'object') {
throw new Error('Invalid request body');
}
const { title, content, tags } = body as Record<string, unknown>;
if (typeof title !== 'string' || title.trim().length < 1) {
throw new Error('Title is required');
}
if (title.length > 200) {
throw new Error('Title must be under 200 characters');
}
if (typeof content !== 'string' || content.trim().length < 10) {
throw new Error('Content must be at least 10 characters');
}
if (!Array.isArray(tags) || tags.some(t => typeof t !== 'string')) {
throw new Error('Tags must be an array of strings');
}
return {
title: title.trim(),
content: content.trim(),
tags: tags.map(t => t.trim()).filter(Boolean),
};
}
Rate Limiting
Protect your APIs from abuse with rate limiting:
const rateLimitMap = new Map<string, { count: number; resetTime: number }>();
function rateLimit(
identifier: string,
limit: number = 10,
windowMs: number = 60000
): boolean {
const now = Date.now();
const entry = rateLimitMap.get(identifier);
if (!entry || now > entry.resetTime) {
rateLimitMap.set(identifier, { count: 1, resetTime: now + windowMs });
return true;
}
if (entry.count >= limit) {
return false;
}
entry.count++;
return true;
}
For production, consider using Redis-backed rate limiting with sliding windows for better accuracy and multi-instance support.
Security Headers
Add security headers via next.config.js or middleware:
// middleware.ts
export function middleware(request: NextRequest) {
const response = NextResponse.next();
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-XSS-Protection', '1; mode=block');
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
return response;
}
Error Handling
Never leak internal details in error responses:
export async function POST(req: NextRequest) {
try {
// ... handler logic
} catch (error) {
// Log the full error internally
console.error('API Error:', error);
// Return a generic message to the client
return NextResponse.json(
{ error: 'An unexpected error occurred' },
{ status: 500 }
);
}
}
File Upload Security
If your API accepts file uploads, validate thoroughly:
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
export async function POST(req: NextRequest) {
const formData = await req.formData();
const file = formData.get('file') as File;
if (!file) {
return NextResponse.json({ error: 'No file provided' }, { status: 400 });
}
if (!ALLOWED_TYPES.includes(file.type)) {
return NextResponse.json({ error: 'File type not allowed' }, { status: 400 });
}
if (file.size > MAX_SIZE) {
return NextResponse.json({ error: 'File too large' }, { status: 400 });
}
// Process the validated file...
}
Conclusion
Security isn't a feature you add at the end — it's a practice you integrate from the start. These patterns cover the fundamentals, but always consider the specific threat model for your application and conduct regular security reviews as your codebase evolves.