Fix: Next.js CORS Error on API Routes
Quick Answer
How to fix CORS errors in Next.js API routes — adding Access-Control headers, handling preflight OPTIONS requests, configuring next.config.js headers, and avoiding common proxy mistakes.
The Error
A browser fetch or axios request to a Next.js API route fails with a CORS error:
Access to fetch at 'https://api.example.com/api/users' from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on
the requested resource.Or the preflight OPTIONS request fails:
Access to XMLHttpRequest at 'https://api.example.com/api/data' from origin
'https://app.example.com' has been blocked by CORS policy: Response to preflight
request doesn't pass access control check: It does not have HTTP ok status.In the Network tab, the failed request shows:
- Method:
OPTIONS(preflight), Status:405 Method Not Allowed - Or Method:
GET/POST, Status:200but the response is blocked because of missing CORS headers
Why This Happens
CORS (Cross-Origin Resource Sharing) is a browser security mechanism. When your frontend (on app.example.com) makes a request to an API (on api.example.com), the browser first checks whether the server allows cross-origin requests by looking for Access-Control-Allow-Origin in the response headers.
Next.js API routes don’t set CORS headers by default. Common causes:
- Missing CORS headers — the API route returns a response without
Access-Control-Allow-Origin. - Preflight not handled — for requests with custom headers or non-simple methods (PUT, DELETE, PATCH), browsers send an OPTIONS preflight request first. If your route doesn’t handle OPTIONS, Next.js returns 405.
- Wildcard origin with credentials —
Access-Control-Allow-Origin: *doesn’t work whencredentials: 'include'is used. You must specify the exact origin. - Frontend and API on different domains — during development, the frontend runs on
localhost:3000and calls an API onlocalhost:4000— different ports = different origins. - Missing headers in next.config.js — if you configure CORS in
next.config.jsheaders but the pattern doesn’t match your API routes, the headers won’t be added.
Fix 1: Add CORS Headers Directly in the API Route
The most straightforward fix: set the headers in each route handler. For the App Router:
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
const corsHeaders = {
'Access-Control-Allow-Origin': 'https://app.example.com',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
};
// Handle preflight
export async function OPTIONS() {
return NextResponse.json({}, { headers: corsHeaders });
}
export async function GET(request: NextRequest) {
const users = await getUsers();
return NextResponse.json(users, { headers: corsHeaders });
}
export async function POST(request: NextRequest) {
const body = await request.json();
const user = await createUser(body);
return NextResponse.json(user, { status: 201, headers: corsHeaders });
}For development, allow any origin:
const allowedOrigins = ['https://app.example.com', 'http://localhost:3000'];
function getCorsHeaders(request: NextRequest) {
const origin = request.headers.get('origin') ?? '';
const isAllowed = allowedOrigins.includes(origin);
return {
'Access-Control-Allow-Origin': isAllowed ? origin : allowedOrigins[0],
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
};
}For the Pages Router (pages/api/):
// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Handle preflight
if (req.method === 'OPTIONS') {
res.status(200).end();
return;
}
if (req.method === 'GET') {
res.json({ users: [] });
} else {
res.status(405).json({ error: 'Method not allowed' });
}
}Fix 2: Create a CORS Middleware Helper
To avoid repeating CORS headers in every route, create a reusable helper:
For the App Router:
// lib/cors.ts
import { NextRequest, NextResponse } from 'next/server';
type CorsOptions = {
origin?: string | string[];
methods?: string[];
headers?: string[];
};
export function withCors(
handler: (req: NextRequest) => Promise<NextResponse>,
options: CorsOptions = {}
) {
const {
origin = '*',
methods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
headers = ['Content-Type', 'Authorization'],
} = options;
return async (request: NextRequest): Promise<NextResponse> => {
const requestOrigin = request.headers.get('origin');
const allowOrigin = Array.isArray(origin)
? (origin.includes(requestOrigin ?? '') ? requestOrigin : origin[0]) ?? '*'
: origin;
const corsHeaders = {
'Access-Control-Allow-Origin': allowOrigin,
'Access-Control-Allow-Methods': methods.join(', '),
'Access-Control-Allow-Headers': headers.join(', '),
};
if (request.method === 'OPTIONS') {
return NextResponse.json({}, { headers: corsHeaders });
}
const response = await handler(request);
Object.entries(corsHeaders).forEach(([key, value]) => {
response.headers.set(key, value);
});
return response;
};
}Use it in your routes:
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { withCors } from '@/lib/cors';
async function getUsers(request: NextRequest) {
const users = await db.user.findMany();
return NextResponse.json(users);
}
export const GET = withCors(getUsers, {
origin: ['https://app.example.com', 'http://localhost:3000'],
});Fix 3: Configure CORS in next.config.js
For a centralized approach that applies to all API routes without modifying each one, use headers() in your Next.js config:
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
// Apply to all API routes
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Origin', value: 'https://app.example.com' },
{ key: 'Access-Control-Allow-Methods', value: 'GET, POST, PUT, DELETE, OPTIONS' },
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type, Authorization' },
],
},
];
},
};
module.exports = nextConfig;Limitation: The headers() config doesn’t handle preflight OPTIONS requests — Next.js returns 405 for OPTIONS by default. You still need to add OPTIONS handlers in each route, or use middleware.
Fix 4: Use Next.js Middleware for Global CORS
Middleware runs before every request, making it the cleanest place for global CORS handling:
// middleware.ts (in project root, not src/)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const allowedOrigins = [
'https://app.example.com',
'http://localhost:3000',
];
export function middleware(request: NextRequest) {
const origin = request.headers.get('origin') ?? '';
const isAllowed = allowedOrigins.includes(origin);
// Handle preflight OPTIONS
if (request.method === 'OPTIONS') {
const response = new NextResponse(null, { status: 200 });
if (isAllowed) {
response.headers.set('Access-Control-Allow-Origin', origin);
}
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
response.headers.set('Access-Control-Max-Age', '86400');
return response;
}
const response = NextResponse.next();
if (isAllowed) {
response.headers.set('Access-Control-Allow-Origin', origin);
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
}
return response;
}
export const config = {
matcher: '/api/:path*', // Only apply to API routes
};This handles both regular requests and OPTIONS preflight in one place.
Fix 5: Fix CORS with Credentials
If you send cookies or Authorization headers with credentials: 'include', the wildcard origin * won’t work — the browser requires a specific origin:
// Client-side fetch
fetch('https://api.example.com/api/profile', {
credentials: 'include', // Sends cookies
headers: { Authorization: `Bearer ${token}` },
});Server must respond with the specific origin and credentials flag:
// app/api/profile/route.ts
export async function GET(request: NextRequest) {
const origin = request.headers.get('origin') ?? '';
return NextResponse.json(data, {
headers: {
'Access-Control-Allow-Origin': origin, // Specific origin, not '*'
'Access-Control-Allow-Credentials': 'true', // Required for credentials
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}Warning: Setting
Access-Control-Allow-Originto the request’soriginheader without validation allows any origin. Always validate against an allowlist before reflecting the origin back.
Fix 6: Use a Proxy for Same-Origin Requests
The cleanest solution for a Next.js frontend calling its own backend is to route through Next.js itself — no CORS at all because both frontend and API are on the same origin:
// next.config.js — proxy /api/external to another server
const nextConfig = {
async rewrites() {
return [
{
source: '/api/external/:path*',
destination: 'https://api.external-service.com/:path*',
},
];
},
};Your frontend calls /api/external/users (same origin), Next.js proxies it to the external API server-side — no browser CORS check.
Still Not Working?
Confirm the CORS headers are actually in the response — open DevTools → Network → click the failed request → check Response Headers. If Access-Control-Allow-Origin is missing, the headers aren’t being set by your code.
Check for multiple CORS headers. Some proxy configurations (nginx, Cloudflare) add CORS headers, and if your Next.js code also adds them, you get duplicate headers like:
Access-Control-Allow-Origin: https://app.example.com, *Multiple values for Access-Control-Allow-Origin are invalid. Remove CORS headers from one layer.
Check the OPTIONS preflight response specifically. In DevTools Network tab, filter by “OPTIONS” method. If it returns 405, your route isn’t handling OPTIONS. If it returns 200 but the main request still fails, check that the Access-Control-Allow-Headers includes every custom header your request sends.
Verify the origin header is present. Browsers only send the Origin header for cross-origin requests. If you’re testing with curl or Postman, add -H "Origin: http://localhost:3000" to trigger CORS header logic.
For related Next.js issues, see Fix: Next.js API Route Not Working and Fix: Next.js Environment Variables Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Fastify Not Working — 404, Plugin Encapsulation, and Schema Validation Errors
How to fix Fastify issues — route 404 from plugin encapsulation, reply already sent, FST_ERR_VALIDATION, request.body undefined, @fastify/cors, hooks not running, and TypeScript type inference.
Fix: next-safe-action Not Working — Action Not Executing, Validation Errors Missing, or Type Errors
How to fix next-safe-action issues — action client setup, Zod schema validation, useAction and useOptimisticAction hooks, middleware, error handling, and authorization patterns.
Fix: pdf-lib Not Working — PDF Not Generating, Fonts Not Embedding, or Pages Blank
How to fix pdf-lib issues — creating PDFs from scratch, modifying existing PDFs, embedding fonts and images, form filling, merging documents, and browser and Node.js usage.
Fix: Pusher Not Working — Events Not Received, Channel Auth Failing, or Connection Dropping
How to fix Pusher real-time issues — client and server setup, channel types, presence channels, authentication endpoints, event binding, connection management, and React integration.