Fix CORS on Next.js Deployed to Vercel — The Complete 2026 Guide
Updated April 2026
Reading this? Verify your fix in real-time. Test your Next.js CORS config → CORSFixer
Next.js on Vercel gives you three places to configure CORS. Using the wrong one, or using more than one, creates bugs that are hard to trace. Here is the definitive guide to which layer does what and when to use each.
The three CORS layers in Next.js on Vercel
| Layer | File | Use for | Handles OPTIONS? |
|---|---|---|---|
| Static headers | vercel.json | Simple global CORS — same origin for all routes | No — need route handler too |
| Middleware | middleware.ts | Dynamic origin logic, complex conditions | Yes — return early for OPTIONS |
| Per-route handler | app/api/*/route.ts | Route-specific CORS, different headers per endpoint | Yes — export OPTIONS function |
Option 1 — vercel.json (simplest, global)
// vercel.json
{ "headers": [ { "source": "/api/(.*)", "headers": [ { "key": "Access-Control-Allow-Origin", "value": "https://yourapp.com" }, { "key": "Access-Control-Allow-Methods", "value": "GET,POST,PUT,DELETE,OPTIONS" }, { "key": "Access-Control-Allow-Headers", "value": "Content-Type,Authorization" } ] } ]
}
Add a route handler for OPTIONS in your API routes — vercel.json adds headers but does not intercept the request method:
// app/api/data/route.ts
export async function OPTIONS() { return new Response(null, { status: 204, headers: { "Access-Control-Allow-Origin": "https://yourapp.com", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", } });
}
export async function GET() { return Response.json({ data: "ok" });
}
Option 2 — middleware.ts (recommended for complex cases)
// middleware.ts — handles CORS for all API routes
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
const ALLOWED_ORIGINS = [ "https://yourapp.com", "https://staging.yourapp.com", // Allow Vercel preview deployments during development: /https:\/\/your-project-.*\.vercel\.app/,
];
function getAllowedOrigin(origin: string | null): string { if (!origin) return ""; return ALLOWED_ORIGINS.some(allowed => typeof allowed === "string" ? allowed === origin : allowed.test(origin) ) ? origin : "";
}
export function middleware(request: NextRequest) { const origin = request.headers.get("origin"); const allowedOrigin = getAllowedOrigin(origin); // Handle OPTIONS preflight if (request.method === "OPTIONS") { return new NextResponse(null, { status: 204, headers: { "Access-Control-Allow-Origin": allowedOrigin, "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", "Access-Control-Max-Age": "86400", "Vary": "Origin", }, }); } // Add CORS to actual request response const response = NextResponse.next(); if (allowedOrigin) { response.headers.set("Access-Control-Allow-Origin", allowedOrigin); response.headers.set("Vary", "Origin"); } return response;
}
export const config = { matcher: "/api/:path*",
};
Option 3 — per-route (App Router)
// app/api/users/route.ts
const CORS_HEADERS = { "Access-Control-Allow-Origin": "https://yourapp.com", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization",
};
export async function OPTIONS() { return new Response(null, { status: 204, headers: CORS_HEADERS });
}
export async function GET() { return Response.json( { users: [] }, { headers: CORS_HEADERS } );
}
Common mistake — duplicate headers from multiple layers
# If vercel.json AND middleware.ts both add CORS headers: # Browser sees: Access-Control-Allow-Origin: https://yourapp.com, https://yourapp.com # → Browser rejects the duplicate # → CORS error # Solution: use ONE layer only # Either vercel.json OR middleware.ts OR per-route — not all three
Vercel preview deployment origins
// Allow production + preview deployments
const ALLOWED_ORIGINS = [ "https://yourapp.com", // Regex for Vercel preview URLs
];
function isAllowed(origin: string | null): boolean { if (!origin) return false; if (origin === "https://yourapp.com") return true; // Allow Vercel preview: https://your-project-*.vercel.app if (/^https:\/\/your-project-[a-z0-9-]+\.vercel\.app$/.test(origin)) return true; return false;
} Test your Next.js CORS config → CORSFixer