Cross-Origin Resource Sharing (CORS) is an HTTPâheader mechanism that lets a browser ask another origin for permission to read its responses. It softens the browserâs sameâorigin policy so frontend code at https://app.example can fetch from https://api.example or any other domain when the server explicitly allows it.
- Access-Control-Allow-Origin: which origins may read the response. Use
*for public APIs or a specific origin such ashttps://app.examplefor stricter security. - Access-Control-Allow-Methods: HTTP verbs the client may use (e.g.
GET, POST, PUT, PATCH, DELETE, OPTIONS). - Access-Control-Allow-Headers: custom request headers the browser may send, such as
Authorization,Content-Type, orX-CSRF-Token. - Access-Control-Allow-Credentials: whether the browser may send cookies or HTTPâauth headers. Must be
trueand used together with an explicit (nonâ*) origin. - Access-Control-Max-Age: how long the browser can cache the preâflight response, in seconds (e.g.
86400for 24 h).
Browsers issue an OPTIONS preâflight request whenever a request is ânonâsimpleâ (has custom headers, a nonâGET/POST verb, etc.). The preâflight must return all of the headers above or the real request is never sent.
Vercel Functions, when used standalone or through frameworks, do not add CORS headers automatically. If you are seeing CORS errors, here's how you can fix it.
constALLOWED_ORIGIN= process.env.NODE_ENV==='production'?'https://app.example':'*';exportasyncfunctionOPTIONS(){returnnewResponse(null,{status:200,headers:{'Access-Control-Allow-Origin':ALLOWED_ORIGIN,'Access-Control-Allow-Methods':'GET, POST, OPTIONS','Access-Control-Allow-Headers':'Content-Type, Authorization','Access-Control-Allow-Credentials':'true',},});}exportasyncfunctionGET(){returnResponse.json({ok:true},{headers:{'Access-Control-Allow-Origin':ALLOWED_ORIGIN},});}This same code example works for standalone Vercel Functions without a framework, placed at api/hello.ts.
You can also apply headers through different configuration patterns in frameworks:
- Next.js : add a
headers()async function innext.config.tsthat matches/api/:path*and sets the five headers - SvelteKit : set headers in the global
handlehook - Remix: return
json(data, { headers })from loaders or actions - Nuxt: use
routeRulesor set headers inserver/api/*
Each method ends up setting the same headers as the examples above.
{"headers":[{"source":"/api/(.*)","headers":[{"key":"Access-Control-Allow-Origin","value":"https://app.example"},{"key":"Access-Control-Allow-Methods","value":"GET, POST, OPTIONS"},{"key":"Access-Control-Allow-Headers","value":"Content-Type, Authorization"},{"key":"Access-Control-Allow-Credentials","value":"true"}]}]}Placing headers in vercel.json pushes them to Vercelâs CDN so they apply before your function runs.
For more advanced CORS scenarios where you need dynamic header values based on request properties (like origin, user agent, or geolocation), you can use Vercelâs Routing Middleware. This approach is particularly useful when you need to:
- Set different CORS policies based on the requesting origin
- Apply CORS headers conditionally based on request properties
- Implement more complex CORS logic that goes beyond static configuration
Create a middleware.ts file at the root of your project:
import{NextResponse}from'next/server';import type {NextRequest}from'next/server';exportconst config ={matcher:'/api/:path*',// Apply only to API routes};exportdefaultfunctionmiddleware(request:NextRequest){const origin = request.headers.get('origin');// Define allowed origins dynamicallyconst allowedOrigins = process.env.NODE_ENV==='production'?['https://app.example.com','https://admin.example.com']:['http://localhost:3000','http://localhost:3001'];const isAllowedOrigin = origin && allowedOrigins.includes(origin);// Handle preflight requestsif(request.method==='OPTIONS'){returnnewResponse(null,{status:200,headers:{'Access-Control-Allow-Origin': isAllowedOrigin ? origin :'null','Access-Control-Allow-Methods':'GET, POST, PUT, DELETE, OPTIONS','Access-Control-Allow-Headers':'Content-Type, Authorization','Access-Control-Allow-Credentials':'true','Access-Control-Max-Age':'86400',},});}// Continue with the request and add CORS headers to the responseconst response =NextResponse.next();if(isAllowedOrigin){ response.headers.set('Access-Control-Allow-Origin', origin); response.headers.set('Access-Control-Allow-Credentials','true');}return response;}You can also use Vercelâs geolocation data to implement region-specific CORS policies:
import{NextResponse}from'next/server';import type {NextRequest}from'next/server';exportconst config ={matcher:'/api/:path*',};exportdefaultfunctionmiddleware(request:NextRequest){const origin = request.headers.get('origin');const country = request.geo?.country ||'US';// Different CORS policies based on countryconstgetCorsPolicy=(country: string)=>{switch(country){case'US':case'CA':return{allowedOrigins:['https://us.example.com','https://ca.example.com'],allowCredentials:true,};case'GB':case'DE':return{allowedOrigins:['https://eu.example.com'],allowCredentials:true,};default:return{allowedOrigins:['https://global.example.com'],allowCredentials:false,};}};const corsPolicy =getCorsPolicy(country);const isAllowedOrigin = origin && corsPolicy.allowedOrigins.includes(origin);if(request.method==='OPTIONS'){returnnewResponse(null,{status:200,headers:{'Access-Control-Allow-Origin': isAllowedOrigin ? origin :'null','Access-Control-Allow-Methods':'GET, POST, PUT, DELETE, OPTIONS','Access-Control-Allow-Headers':'Content-Type, Authorization','Access-Control-Allow-Credentials': corsPolicy.allowCredentials.toString(),'Access-Control-Max-Age':'86400',},});}const response =NextResponse.next();if(isAllowedOrigin){ response.headers.set('Access-Control-Allow-Origin', origin); response.headers.set('Access-Control-Allow-Credentials', corsPolicy.allowCredentials.toString());}return response;}When using middleware for CORS, your API route handlers become simpler since the CORS headers are handled at the middleware level:
exportasyncfunctionGET(){// No need to set CORS headers here - middleware handles itreturnResponse.json({users:[]});}exportasyncfunctionPOST(request:Request){const data =await request.json();// Process the datareturnResponse.json({success:true});}If you have Deployment Protection turned on your preview or production Vercel deployments, you can use OPTIONS Allowlist to allow CORS to work on a list of paths that you define.
- When Vercel Authentication, Password Protection, or Trusted IPs is active, unauthenticated preâflight requests would normally be blocked.
- OPTIONS Allowlist lets you exempt specific paths from Deployment Protection only for
OPTIONSrequests./api/*is on the allowlist by default for new projects.
Here's an example of the typical flow:
- Keep Vercel Authentication enabled for
/api/*. - Ensure
/apiis in the OPTIONS Allowlist (default). - Browser preâflight succeeds; the real
POST /api/...request still requires auth.
# Preâflightcurl-i-X OPTIONS https://your-domain.vercel.app/api/hello \-H"Origin: https://app.example"\-H"Access-Control-Request-Method: POST"# Simple requestcurl-i https://your-domain.vercel.app/api/hello \-H"Origin: https://app.example"Look for a 200 status on the OPTIONS call and the correct CORS headers in both responses.
- Forgetting to add headers on error paths: wrap all return points (including 4xx/5xx) in a helper that sets CORS, or apply rules globally through configuration
- Using
*withAccess-Control-Allow-Credentials: true: the spec forbids this; send a specific origin instead - Excessive preâflight traffic: raise
Access-Control-Max-Age(up to 86400 s = 24 h) so browsers cache the response
