HttpFixerBlogCors → Fix CORS on Next.js Deployed to Vercel — The Complete 2026 Guide
CORS

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

LayerFileUse forHandles OPTIONS?
Static headersvercel.jsonSimple global CORS — same origin for all routesNo — need route handler too
Middlewaremiddleware.tsDynamic origin logic, complex conditionsYes — return early for OPTIONS
Per-route handlerapp/api/*/route.tsRoute-specific CORS, different headers per endpointYes — 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