Step-by-step
-
1
Understand what CORS actually enforces
When a browser makes a cross-origin request (different domain, port, or scheme), it checks the response for
Access-Control-Allow-Origin. If the header is missing or does not match the requesting origin, the browser blocks the response from reaching JavaScript — the request still hit your server and your server still responded.This means CORS is not a security mechanism for your server — it is a browser policy that protects users from malicious websites making requests on their behalf. A server-to-server request (curl, Postman, another backend) is never affected by CORS.
-
2
Never use a wildcard for credentialed requests
Setting
Access-Control-Allow-Origin: *allows any origin to read responses, but the browser will refuse to send cookies orAuthorizationheaders to a wildcard origin. If your API relies on session cookies or bearer tokens, a wildcard cannot work — and you should not want it to, since it would allow any website to make authenticated calls on your users' behalf.bash// DO NOT use this for authenticated APIs Access-Control-Allow-Origin: * // DO: specify the exact origin Access-Control-Allow-Origin: https://app.example.com -
3
Allow-list a set of trusted origins
For most APIs you have a small, known set of origins (your frontend, your admin panel, maybe a mobile web view). Validate the request's
Originheader against that list and echo back only if it matches.javascript// Node.js / Express — cors middleware const cors = require('cors'); const allowedOrigins = [ 'https://app.example.com', 'https://admin.example.com', ]; app.use(cors({ origin: (origin, callback) => { // Allow server-to-server (no Origin header) or a known origin if (!origin || allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error(`CORS: origin ${origin} not allowed`)); } }, credentials: true, })); -
4
Handle preflight OPTIONS requests
For requests that use non-simple methods (PUT, DELETE, PATCH) or custom headers, the browser sends a preflight
OPTIONSrequest first. Your server must respond with 200 or 204 and the appropriate CORS headers — otherwise the actual request is never sent.bash// Express: the cors() middleware handles OPTIONS automatically. // If you are handling CORS manually, add this: app.options('*', cors(corsOptions)); // enable pre-flight for all routes // The preflight response must include: Access-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Headers: Content-Type, Authorization Access-Control-Max-Age: 86400 // cache the preflight for 24 hours -
5
Set Allow-Credentials for cookie-based auth
If your API uses session cookies or requires the browser to send stored cookies, you must set
Access-Control-Allow-Credentials: trueand set a specific (non-wildcard) origin. The client also needs to setcredentials: 'include'in the fetch call.javascript// Response header Access-Control-Allow-Credentials: true // Client side (fetch) fetch('https://api.example.com/me', { method: 'GET', credentials: 'include', // send cookies cross-origin }); // Client side (axios) axios.get('https://api.example.com/me', { withCredentials: true }); -
6
Configure CORS in Laravel
Laravel ships with a
config/cors.phpfile and theHandleCorsmiddleware registered globally. Edit the config — do not write a custom middleware.php// config/cors.php return [ 'paths' => ['api/*'], 'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], 'allowed_origins' => ['https://app.example.com', 'https://admin.example.com'], 'allowed_origins_patterns' => [], 'allowed_headers' => ['Content-Type', 'Authorization', 'Accept'], 'exposed_headers' => [], 'max_age' => 86400, 'supports_credentials'=> true, ]; -
7
Install and configure cors in Express
Install the
corspackage and apply it as early as possible in your middleware chain — before any routes.javascriptnpm install cors // app.js const cors = require('cors'); const corsOptions = { origin: ['https://app.example.com', 'https://admin.example.com'], methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'], credentials: true, maxAge: 86400, }; app.use(cors(corsOptions)); app.options('*', cors(corsOptions)); // handle preflight for all routes -
8
Debug the "Access-Control-Allow-Origin" error in development
During development your frontend runs on
http://localhost:3000and your API onhttp://localhost:8000. This is a cross-origin pair. Addhttp://localhost:3000(or your dev port) to your allowed origins — do not addlocalhostwithout a port, as the port is part of the origin. Remove it before deploying to production.If the error persists after fixing the header, check the Network tab in browser devtools: look at the actual response headers on the failed request to confirm your server is sending the correct origin. A misconfigured reverse proxy often strips or overwrites CORS headers silently.
Tips & gotchas
- CORS errors in the browser do not mean your API is secure — they only mean the browser refused to hand the response to JavaScript. A direct HTTP client (curl, Postman, server-side code) bypasses CORS entirely.
- Keep your allowed origins list in an environment variable so it can differ between development, staging, and production without code changes.
- Do not set <code>Access-Control-Allow-Headers: *</code> when credentials are enabled — wildcard headers are not supported with credentialed requests in many browsers.
- A long <code>Access-Control-Max-Age</code> (86400 = 24 hours) reduces preflight overhead significantly for high-traffic APIs.
Wrapping up
CORS configuration is not complex once you understand that the browser is the enforcer, not your server. Keep your allowed origins list small and explicit, enable credentials only when needed, always handle preflight, and never use a wildcard on authenticated endpoints. Five minutes of correct configuration prevents hours of confused debugging.