Cross-Origin Resource Sharing (CORS) is a critical security mechanism that controls how web applications make requests across different origins. In Next.js, handling CORS properly is essential when building APIs that need to be accessed from different domains, such as during frontend-backend separation, third-party integrations, or when building APIs for mobile applications.
This comprehensive guide will walk you through understanding CORS, implementing it correctly in Next.js, and following security best practices to protect your application.
What is CORS?
CORS (Cross-Origin Resource Sharing) is a browser security mechanism that controls how web pages from one origin can request resources from a different origin. An origin consists of three parts:
- Protocol (http or https)
- Domain (example.com)
- Port (80, 443, 3000, etc.)
For example, https://example.com:3000 and https://example.com:4000 are considered different origins because they use different ports.
The Same-Origin Policy
Browsers implement a same-origin policy that restricts web pages from making requests to a different origin than the one that served the web page. This is a fundamental security feature that prevents malicious websites from accessing sensitive data from other domains.
However, there are legitimate scenarios where cross-origin requests are necessary:
- API calls from a frontend domain to a separate backend domain
- Loading resources (fonts, images, scripts) from CDNs
- Third-party integrations and webhooks
- Mobile applications communicating with web APIs
CORS provides a controlled way to enable these cross-origin requests while maintaining security boundaries.
How CORS Works: Preflight Requests
CORS operates through a preflight request mechanism for certain types of requests.
Simple vs. Preflight Requests
Simple requests meet all these criteria:
- Uses GET, HEAD, or POST method
- Uses only specific headers: Accept, Accept-Language, Content-Language, Content-Type (with specific values)
- No custom headers
Preflight requests occur when:
- Uses methods other than GET, HEAD, or POST
- Uses POST with custom Content-Type (e.g., application/json)
- Includes custom headers
- Includes certain other headers
Preflight Flow
- Preflight Request: Browser sends an OPTIONS request to check if the cross-origin request is allowed
- Server Response: Server responds with CORS headers indicating permissions
- Actual Request: If preflight succeeds, browser sends the actual request
- Server Response: Server processes the actual request and responds
Key CORS Headers
Request Headers (from client)
- Origin: Indicates where the request originates
- Access-Control-Request-Method: The method to be used in the actual request
- Access-Control-Request-Headers: Headers to be used in the actual request
Response Headers (from server)
- Access-Control-Allow-Origin: Which origins are permitted (e.g.,
https://example.comor*) - Access-Control-Allow-Methods: Allowed HTTP methods
- Access-Control-Allow-Headers: Which request headers are permitted
- Access-Control-Allow-Credentials: Whether cookies/credentials are allowed
- Access-Control-Max-Age: How long (in seconds) the preflight response can be cached
- Access-Control-Expose-Headers: Which response headers are accessible to the browser
Preflight Response Example
OPTIONS /api/data HTTP/1.1
Origin: https://frontend.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Why CORS Matters in Next.js
Next.js applications frequently encounter CORS challenges due to their architecture and common use cases:
API Routes and External Requests
When building Next.js API routes, they're served from the same domain as your frontend. However, if your frontend is deployed on a different domain (Vercel, Netlify) than your backend API, you'll encounter CORS issues.
Common Scenarios Requiring CORS in Next.js
- Frontend-Backend Separation: Frontend on
app.example.com, API routes onapi.example.comor a different subdomain - Third-Party Integrations: Consuming external APIs from your Next.js application
- Webhooks: Receiving webhook requests from third-party services
- Mobile Applications: Native mobile apps calling your Next.js API routes
- Microservices: Different services handling different parts of your application
- Testing: Running frontend and backend on different ports during development
Development vs. Production
During development with next dev, your application runs on a single port (typically 3000), so CORS isn't an issue. However, in production with next start, or when separating frontend and backend, CORS configuration becomes critical.
Security Implications
Improper CORS configuration can lead to:
- Security vulnerabilities: Allowing all origins (
*) with credentials - Data exposure: Unauthorized access to sensitive API endpoints
- CSRF attacks: Cross-Site Request Forgery vulnerabilities
Understanding and implementing CORS correctly is therefore essential for both functionality and security. Proper web development practices include secure API configuration to protect your applications.
Method 1: Manual Header Configuration
The most straightforward approach to handle CORS in Next.js is to manually set CORS headers in your API routes using the Next.js Response API.
Basic Implementation
For API routes, you can set CORS headers directly:
1// pages/api/data.ts2import type { NextApiRequest, NextApiResponse } from 'next'3 4export default function handler(5 req: NextApiRequest,6 res: NextApiResponse7) {8 // Set CORS headers9 res.setHeader('Access-Control-Allow-Origin', 'https://your-frontend-domain.com')10 res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')11 res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')12 res.setHeader('Access-Control-Allow-Credentials', 'true')13 14 // Handle preflight requests15 if (req.method === 'OPTIONS') {16 res.status(200).end()17 return18 }19 20 // Your API logic here21 res.status(200).json({ message: 'Success!' })22}Pages Router (pages/api/)
In the Pages Router, each API route file exports functions that receive req and res objects. You can set headers before sending responses:
1// pages/api/users/[id].ts2import type { NextApiRequest, NextApiResponse } from 'next'3 4export default function handler(5 req: NextApiRequest,6 res: NextApiResponse7) {8 // CORS headers9 res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com')10 res.setHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS')11 res.setHeader('Access-Control-Allow-Headers', 'Content-Type')12 13 // Handle OPTIONS14 if (req.method === 'OPTIONS') {15 res.status(200).end()16 return17 }18 19 const { id } = req.query20 21 if (req.method === 'GET') {22 // Fetch user23 res.status(200).json({ id, name: 'John Doe' })24 } else if (req.method === 'PUT') {25 // Update user26 res.status(200).json({ id, updated: true })27 } else if (req.method === 'DELETE') {28 // Delete user29 res.status(200).json({ deleted: true })30 } else {31 res.status(405).json({ error: 'Method Not Allowed' })32 }33}App Router (app/api/)
With the App Router, you create route handlers using NextRequest and NextResponse:
Simple handler:
1// app/api/data/route.ts2import { NextResponse } from 'next/server'3 4export async function OPTIONS(request: Request) {5 return NextResponse.json(6 {},7 {8 headers: {9 'Access-Control-Allow-Origin': 'https://your-frontend.com',10 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',11 'Access-Control-Allow-Headers': 'Content-Type, Authorization',12 'Access-Control-Allow-Credentials': 'true',13 },14 }15 )16}17 18export async function GET(request: Request) {19 return NextResponse.json(20 { message: 'Success!' },21 {22 headers: {23 'Access-Control-Allow-Origin': 'https://your-frontend.com',24 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',25 'Access-Control-Allow-Headers': 'Content-Type, Authorization',26 'Access-Control-Allow-Credentials': 'true',27 },28 }29 )30}31 32export async function POST(request: Request) {33 const body = await request.json()34 35 return NextResponse.json(36 { received: body },37 {38 headers: {39 'Access-Control-Allow-Origin': 'https://your-frontend.com',40 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',41 'Access-Control-Allow-Headers': 'Content-Type, Authorization',42 'Access-Control-Allow-Credentials': 'true',43 },44 }45 )46}Dynamic Origin Configuration
For more flexibility, you can read the origin from the request and validate it:
1// app/api/secure/route.ts2import { NextResponse } from 'next/server'3 4const allowedOrigins = [5 'https://app.example.com',6 'https://admin.example.com',7 'http://localhost:3000', // For development8]9 10export async function OPTIONS(request: Request) {11 const origin = request.headers.get('origin')12 const isAllowedOrigin = origin && allowedOrigins.includes(origin)13 14 return NextResponse.json(15 {},16 {17 headers: {18 'Access-Control-Allow-Origin': isAllowedOrigin ? origin : '',19 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',20 'Access-Control-Allow-Headers': 'Content-Type',21 'Access-Control-Allow-Credentials': 'true',22 },23 }24 )25}26 27export async function GET(request: Request) {28 const origin = request.headers.get('origin')29 const isAllowedOrigin = origin && allowedOrigins.includes(origin)30 31 return NextResponse.json(32 { data: 'secure data' },33 {34 headers: {35 'Access-Control-Allow-Origin': isAllowedOrigin ? origin : '',36 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',37 'Access-Control-Allow-Headers': 'Content-Type',38 'Access-Control-Allow-Credentials': 'true',39 },40 }41 )42}Method 2: Using the cors Package
The cors npm package provides a middleware that simplifies CORS handling. While it's commonly used with Express, it can also work with Next.js API routes.
Installation
npm install cors
# or
yarn add cors
# or
pnpm add cors
Pages Router Implementation
With the Pages Router, you can use cors as middleware in your API routes:
1// pages/api/products.ts2import type { NextApiRequest, NextApiResponse } from 'next'3import cors from 'cors'4 5// Initialize cors middleware6const corsMiddleware = cors({7 origin: 'https://your-frontend.com',8 methods: ['GET', 'POST'],9 allowedHeaders: ['Content-Type', 'Authorization'],10 credentials: true,11})12 13// Helper function to run middleware14function runMiddleware(15 req: NextApiRequest,16 res: NextApiResponse,17 fn: Function18) {19 return new Promise((resolve, reject) => {20 fn(req, res, (result: any) => {21 if (result instanceof Error) {22 return reject(result)23 }24 return resolve(result)25 })26 })27}28 29export default async function handler(30 req: NextApiRequest,31 res: NextApiResponse32) {33 // Run CORS middleware34 await runMiddleware(req, res, corsMiddleware)35 36 // Handle preflight37 if (req.method === 'OPTIONS') {38 res.end()39 return40 }41 42 // Your API logic43 res.status(200).json({ products: [] })44}Advanced Configuration
The cors package offers extensive configuration options:
1// pages/api/advanced.ts2import type { NextApiRequest, NextApiResponse } from 'next'3import cors from 'cors'4 5const corsOptions = {6 // Allow specific origins (array of strings)7 origin: [8 'https://app.example.com',9 'https://admin.example.com',10 'http://localhost:3000',11 ],12 13 // Or use a function for dynamic origin checking14 origin: function (origin: string | undefined, callback: Function) {15 // Allow requests with no origin (like mobile apps)16 if (!origin) return callback(null, true)17 18 const allowedOrigins = ['https://example.com']19 if (allowedOrigins.includes(origin)) {20 callback(null, true)21 } else {22 callback(new Error('Not allowed by CORS'))23 }24 },25 26 // Specify allowed methods27 methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],28 29 // Specify allowed headers30 allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],31 32 // Allow credentials33 credentials: true,34 35 // Cache preflight response for 24 hours36 maxAge: 86400,37 38 // Expose certain headers39 exposedHeaders: ['X-Total-Count'],40 41 // Additional options42 optionsSuccessStatus: 200, // Some legacy browsers (IE11) choke on 20443}44 45export default function handler(46 req: NextApiRequest,47 res: NextApiResponse48) {49 cors(corsOptions)(req, res, () => {50 if (req.method === 'OPTIONS') {51 res.end()52 return53 }54 55 // Your API logic56 res.status(200).json({ success: true })57 })58}Global CORS Configuration
For projects with multiple API routes, create a reusable configuration:
1// lib/cors.ts2import { NextApiRequest, NextApiResponse } from 'next'3import cors from 'cors'4 5export const corsOptions: cors.CorsOptions = {6 origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',7 methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],8 allowedHeaders: ['Content-Type', 'Authorization'],9 credentials: true,10}11 12export function runCors(13 req: NextApiRequest,14 res: NextApiResponse15) {16 return new Promise((resolve, reject) => {17 cors(corsOptions)(req, res, (result) => {18 if (result instanceof Error) {19 reject(result)20 } else {21 resolve(result)22 }23 })24 })25}1// pages/api/items.ts2import type { NextApiRequest, NextApiResponse } from 'next'3import { runCors } from '../../lib/cors'4 5export default async function handler(6 req: NextApiRequest,7 res: NextApiResponse8) {9 // Run CORS middleware10 await runCors(req, res)11 12 if (req.method === 'OPTIONS') {13 res.end()14 return15 }16 17 // Your API logic here18 res.status(200).json({ items: [] })19}Method 3: Next.js Middleware
Next.js middleware allows you to handle CORS at a global level, making it easier to manage CORS for multiple API routes or even the entire application.
Creating Middleware
Middleware is placed in the root of your project as middleware.ts or middleware.js:
1// middleware.ts2import { NextRequest, NextResponse } from 'next/server'3 4export function middleware(request: NextRequest) {5 // Handle CORS for API routes6 if (request.nextUrl.pathname.startsWith('/api/')) {7 // Clone the request headers and set CORS headers8 const response = NextResponse.next()9 10 response.headers.set('Access-Control-Allow-Origin', 'https://your-frontend.com')11 response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')12 response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')13 response.headers.set('Access-Control-Allow-Credentials', 'true')14 15 // Handle preflight OPTIONS requests16 if (request.method === 'OPTIONS') {17 return new NextResponse(null, {18 status: 200,19 headers: response.headers,20 })21 }22 23 return response24 }25 26 // For non-API routes, just pass through27 return NextResponse.next()28}29 30export const config = {31 matcher: ['/api/:path*'],32}Advanced Middleware with Dynamic Origins
For more sophisticated CORS handling:
1// middleware.ts2import { NextRequest, NextResponse } from 'next/server'3 4export function middleware(request: NextRequest) {5 const response = NextResponse.next()6 7 // Only handle API routes8 if (request.nextUrl.pathname.startsWith('/api/')) {9 const origin = request.headers.get('origin')10 11 // List of allowed origins12 const allowedOrigins = [13 'https://app.example.com',14 'https://admin.example.com',15 process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null,16 ].filter(Boolean) as string[]17 18 // Check if origin is allowed19 const isAllowedOrigin = origin && allowedOrigins.includes(origin)20 21 // Set CORS headers22 response.headers.set(23 'Access-Control-Allow-Origin',24 isAllowedOrigin ? origin : ''25 )26 response.headers.set(27 'Access-Control-Allow-Methods',28 'GET, POST, PUT, DELETE, PATCH, OPTIONS'29 )30 response.headers.set(31 'Access-Control-Allow-Headers',32 'Content-Type, Authorization, X-Requested-With'33 )34 response.headers.set('Access-Control-Allow-Credentials', 'true')35 response.headers.set('Access-Control-Max-Age', '86400')36 response.headers.set('Access-Control-Expose-Headers', 'X-Total-Count')37 38 // Handle preflight requests39 if (request.method === 'OPTIONS') {40 return new NextResponse(null, { status: 200, headers: response.headers })41 }42 }43 44 return response45}46 47export const config = {48 matcher: [49 '/api/:path*',50 // You can add more matchers if needed51 ],52}Using Environment Variables
For production flexibility, use environment variables:
1// middleware.ts2import { NextRequest, NextResponse } from 'next/server'3 4export function middleware(request: NextRequest) {5 const response = NextResponse.next()6 7 if (request.nextUrl.pathname.startsWith('/api/')) {8 const origin = request.headers.get('origin')9 10 // Get allowed origins from environment variable11 const allowedOrigins = [12 ...(process.env.ALLOWED_ORIGINS?.split(',') || []),13 // Add development origins14 ...(process.env.NODE_ENV === 'development' 15 ? ['http://localhost:3000', 'http://localhost:3001']16 : []17 ),18 ]19 20 // Function to validate origin21 const isAllowedOrigin = (origin: string | null): origin is string => {22 if (!origin) return false23 24 // Check exact match25 if (allowedOrigins.includes(origin)) return true26 27 // Check wildcard subdomain matching28 const hostname = new URL(origin).hostname29 const allowedDomains = ['example.com', 'localhost']30 31 return allowedDomains.some(domain => 32 hostname === domain || hostname.endsWith(`.${domain}`)33 )34 }35 36 const isAllowed = isAllowedOrigin(origin)37 38 response.headers.set(39 'Access-Control-Allow-Origin',40 isAllowed ? (origin || '') : ''41 )42 response.headers.set('Access-Control-Allow-Credentials', 'true')43 response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')44 response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')45 46 if (request.method === 'OPTIONS') {47 return new NextResponse(null, { status: 200, headers: response.headers })48 }49 }50 51 return response52}Different Patterns for Different Routes
You can have different CORS policies for different API routes:
1// middleware.ts2import { NextRequest, NextResponse } from 'next/server'3 4export function middleware(request: NextRequest) {5 const response = NextResponse.next()6 const { pathname } = request.nextUrl7 8 // Public API routes - allow all origins9 if (pathname.startsWith('/api/public/')) {10 response.headers.set('Access-Control-Allow-Origin', '*')11 response.headers.set('Access-Control-Allow-Methods', 'GET, OPTIONS')12 response.headers.set('Access-Control-Allow-Headers', 'Content-Type')13 }14 15 // Protected API routes - strict origin checking16 else if (pathname.startsWith('/api/admin/')) {17 const origin = request.headers.get('origin')18 const adminOrigins = ['https://admin.example.com']19 20 const isAllowed = origin && adminOrigins.includes(origin)21 22 response.headers.set('Access-Control-Allow-Origin', isAllowed ? origin : '')23 response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')24 response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')25 response.headers.set('Access-Control-Allow-Credentials', 'true')26 }27 28 // General API routes29 else if (pathname.startsWith('/api/')) {30 const origin = request.headers.get('origin')31 const appOrigins = ['https://app.example.com']32 33 const isAllowed = origin && appOrigins.includes(origin)34 35 response.headers.set('Access-Control-Allow-Origin', isAllowed ? origin : '')36 response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')37 response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')38 response.headers.set('Access-Control-Allow-Credentials', 'true')39 }40 41 // Handle OPTIONS requests42 if (request.method === 'OPTIONS') {43 return new NextResponse(null, { status: 200, headers: response.headers })44 }45 46 return response47}48 49export const config = {50 matcher: '/api/:path*',51}Security Best Practices
Proper CORS configuration is crucial for maintaining application security. Here are essential best practices to follow:
Never use '*' with credentials
The wildcard origin (*) cannot be used with Access-Control-Allow-Credentials: true. This is a browser security requirement.
Whitelist specific origins
Always specify exact origins rather than using wildcards. Create a list of trusted domains and validate against it.
Validate dynamic origins
When using dynamic origins (e.g., user-generated subdomains), implement strict validation to prevent security bypasses.
Limit allowed methods
Only allow the HTTP methods your API actually needs. Don't expose all methods unnecessarily.
Restrict allowed headers
Only accept headers that your API requires. Avoid accepting arbitrary headers that could be exploited.
Set appropriate max-age
Cache preflight responses to reduce overhead, but not indefinitely. 24 hours (86400 seconds) is a common practice.
Use environment variables
Store allowed origins in environment variables, not hardcoded. This allows for easier security updates and deployment flexibility.
Implement proper authentication
CORS is not authentication. Always implement proper authentication and authorization checks regardless of CORS configuration.
Secure CORS Implementation Example
Here's a comprehensive secure CORS implementation:
1// lib/secure-cors.ts2import { NextRequest, NextResponse } from 'next/server'3 4interface SecurityConfig {5 allowedOrigins: string[]6 allowedMethods: string[]7 allowedHeaders: string[]8 exposedHeaders?: string[]9 maxAge: number10}11 12const createSecureCORS = (config: SecurityConfig) => {13 return {14 setCORS: (origin: string | null) => {15 // Security checks16 if (!origin) {17 // Allow requests with no origin (mobile apps, Postman)18 return {19 'Access-Control-Allow-Origin': '',20 }21 }22 23 // Validate origin against whitelist24 if (!config.allowedOrigins.includes(origin)) {25 throw new Error(`Origin '${origin}' not allowed by CORS policy`)26 }27 28 return {29 'Access-Control-Allow-Origin': origin,30 'Access-Control-Allow-Credentials': 'true',31 'Access-Control-Allow-Methods': config.allowedMethods.join(', '),32 'Access-Control-Allow-Headers': config.allowedHeaders.join(', '),33 'Access-Control-Max-Age': config.maxAge.toString(),34 ...(config.exposedHeaders && {35 'Access-Control-Expose-Headers': config.exposedHeaders.join(', '),36 }),37 }38 },39 }40}41 42// Configuration43const corsConfig: SecurityConfig = {44 allowedOrigins: process.env.ALLOWED_ORIGINS?.split(',') || [],45 allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'],46 allowedHeaders: ['Content-Type', 'Authorization'],47 exposedHeaders: ['X-Total-Count'],48 maxAge: 86400, // 24 hours49}50 51const cors = createSecureCORS(corsConfig)52 53export function handleCORS(request: NextRequest) {54 const origin = request.headers.get('origin')55 56 try {57 const headers = cors.setCORS(origin)58 59 if (request.method === 'OPTIONS') {60 return new NextResponse(null, { status: 200, headers })61 }62 63 return { headers }64 } catch (error) {65 // Origin not allowed66 return {67 error: error instanceof Error ? error.message : 'CORS error',68 }69 }70}Performance Considerations
CORS implementation can impact your application's performance. Here's how to optimize it:
1. Preflight Response Caching
The Access-Control-Max-Age header caches preflight responses, reducing the number of OPTIONS requests:
response.headers.set('Access-Control-Max-Age', '86400') // 24 hours
This prevents browsers from sending preflight requests for the same combination of origin, method, and headers within 24 hours.
2. Reducing Preflight Requests
Preflight requests add latency. To minimize them:
Use Simple Requests When Possible:
- Stick to GET, HEAD, or POST methods
- Use standard headers (Content-Type: application/json, etc.)
- Avoid custom headers when not strictly necessary
Example of request that triggers preflight:
1// This triggers preflight (bad for performance)2fetch('https://api.example.com/data', {3 method: 'POST',4 headers: {5 'Content-Type': 'application/json',6 'X-Custom-Header': 'value', // Custom header triggers preflight7 'Authorization': 'Bearer token',8 },9 body: JSON.stringify({ data: 'value' }),10})11 12// Better: Reduce custom headers13fetch('https://api.example.com/data', {14 method: 'POST',15 headers: {16 'Content-Type': 'application/json',17 // Only essential headers18 },19 body: JSON.stringify({ data: 'value' }),20})3. Middleware vs. Route-Level
Middleware (Recommended for most cases):
- Single implementation for all API routes
- Better performance (runs once per request)
- Easier to maintain
- Consistent CORS policy
Route-level (Use for specific cases):
- Different CORS policies per route
- More granular control
- More code duplication
- Potential for inconsistency
4. Monitoring and Metrics
Track CORS-related metrics:
1// lib/cors-metrics.ts2export function trackCORSMetrics(request: Request, response: Response) {3 // Track preflight requests4 if (request.method === 'OPTIONS') {5 console.log('Preflight request:', {6 origin: request.headers.get('origin'),7 url: request.url,8 })9 }10 11 // Track CORS errors12 const corsError = response.headers.get('Access-Control-Allow-Origin') === ''13 if (corsError && request.method !== 'OPTIONS') {14 console.warn('CORS blocked request:', {15 origin: request.headers.get('origin'),16 url: request.url,17 method: request.method,18 })19 }20}5. Performance Tips Summary
- Set appropriate max-age: 86400 seconds (24 hours) is a good balance
- Minimize custom headers: Use only essential headers to avoid preflights
- Use middleware: Reduces code duplication and improves consistency
- Cache allowed origins: Don't parse array on every request
- Monitor preflight rates: High preflight rates indicate optimization opportunities
- Keep middleware lightweight: Avoid database calls or heavy computation
- Use connection pooling: For backend services your API calls
- Consider CDN: Serve static assets from CDN with proper CORS headers
Troubleshooting Common Errors
CORS errors can be frustrating. Here's how to diagnose and fix common issues:
Common CORS Errors and Solutions
Debugging Tools
Browser DevTools:
- Network tab: Look for failed requests (red entries)
- Check response headers for CORS headers
- Console tab: CORS errors are displayed here
Testing CORS:
Use curl to test preflight and actual requests:
1# Test preflight request2curl -X OPTIONS \3 -H "Origin: https://example.com" \4 -H "Access-Control-Request-Method: POST" \5 -H "Access-Control-Request-Headers: Content-Type" \6 https://your-api.com/api/endpoint7 8# Test actual request9curl -X POST \10 -H "Origin: https://example.com" \11 -H "Content-Type: application/json" \12 -d '{"test": "data"}' \13 https://your-api.com/api/endpoint14 15# Check response headers16curl -I https://your-api.com/api/endpointCORS Debugging Checklist
When facing CORS issues, verify:
- Server is receiving the request
- Origin header is present in request
- Origin is in allowed origins list
- Server is setting Access-Control-Allow-Origin header
- Access-Control-Allow-Methods includes the HTTP method
- Access-Control-Allow-Headers includes all custom headers
- OPTIONS handler exists and returns 200
- Credentials configuration is consistent (client/server)
- No conflicting middleware or duplicate headers
- Environment variables are set in production
- No typos in allowed origins or headers
- Browser isn't caching old responses (try incognito)
Summary and Recommendations
Cross-Origin Resource Sharing (CORS) is a fundamental aspect of modern web development, especially when building APIs or separating frontend and backend services. We've covered three primary methods for implementing CORS in Next.js applications.
Quick Decision Guide
Use Method 1 (Manual Headers) when:
- You have a small number of API routes
- Routes need different CORS policies
- You want complete control over CORS logic
Use Method 2 (cors Package) when:
- You're using Pages Router
- You want familiar Express-style middleware
- You need complex CORS configuration
Use Method 3 (Next.js Middleware) when:
- You're using App Router
- You want consistent CORS across all API routes
- You want to centralize CORS policy
- Performance is a priority
Best Practices Recap
- Security First
- Never use '*' with credentials
- Whitelist specific origins
- Validate dynamic origins
- Limit allowed methods and headers
- Performance Optimization
- Cache preflight responses (max-age: 86400)
- Minimize custom headers
- Use middleware for consistency
- Keep middleware lightweight
- Development Workflow
- Test CORS thoroughly in development
- Verify production environment variables
- Use browser DevTools for debugging
- Monitor CORS errors in production
- Common Pitfalls to Avoid
- Don't use '*' with credentials
- Don't forget OPTIONS handlers
- Don't set CORS headers multiple times
- Don't ignore environment variables
Recommended Approach
For most Next.js projects, we recommend using Next.js Middleware (Method 3) with the App Router. It provides:
- Centralized CORS policy management
- Better performance
- Cleaner code
- Easier maintenance
- TypeScript support
Here's a production-ready middleware template:
1// middleware.ts - Production-ready template2import { NextRequest, NextResponse } from 'next/server'3 4export function middleware(request: NextRequest) {5 const response = NextResponse.next()6 7 // Only handle API routes8 if (request.nextUrl.pathname.startsWith('/api/')) {9 const origin = request.headers.get('origin')10 11 // Configure allowed origins12 const allowedOrigins = [13 ...(process.env.NEXT_PUBLIC_ALLOWED_ORIGINS?.split(',') || []),14 // Development origins15 ...(process.env.NODE_ENV === 'development'16 ? ['http://localhost:3000', 'http://localhost:3001']17 : []18 ),19 ]20 21 const isAllowed = origin && allowedOrigins.includes(origin)22 23 // Set CORS headers24 response.headers.set(25 'Access-Control-Allow-Origin',26 isAllowed ? origin : ''27 )28 response.headers.set('Access-Control-Allow-Credentials', 'true')29 response.headers.set(30 'Access-Control-Allow-Methods',31 'GET, POST, PUT, DELETE, PATCH, OPTIONS'32 )33 response.headers.set(34 'Access-Control-Allow-Headers',35 'Content-Type, Authorization'36 )37 response.headers.set('Access-Control-Max-Age', '86400')38 39 // Handle preflight requests40 if (request.method === 'OPTIONS') {41 return new NextResponse(null, { status: 200, headers: response.headers })42 }43 }44 45 return response46}47 48export const config = {49 matcher: '/api/:path*',50}Next Steps
Now that you understand CORS in Next.js:
- Implement CORS in your application using the method that best fits your architecture
- Test thoroughly in both development and production environments
- Monitor CORS errors in your production logs
- Review security - ensure you're following the security best practices
- Optimize performance - cache preflight responses and minimize custom headers
- Document your CORS policy for your team and future reference
Our web development team specializes in building secure, performant Next.js applications with proper CORS configuration and API architecture.
Additional Resources
- MDN Web Docs: CORS
- Next.js Middleware Documentation
- Fetch API CORS Specification
- OWASP CORS Best Practices
Need Help?
If you're implementing CORS in your Next.js application and need assistance, our web development team is here to help. We specialize in building secure, performant Next.js applications with proper CORS configuration.
- Full-stack development: Frontend and backend integration with proper CORS
- API development: Secure, scalable API endpoints
- Performance optimization: Fast, efficient CORS implementation
- Security audits: Review and improve your CORS configuration