You write an asynchronous fetch() or Axios request in your React, Vue, or vanilla JavaScript frontend to retrieve data from your backend API. Everything looks correct, but your browser console lights up in red: Access to fetch at 'https://api.example.com/data' from origin 'https://app.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. The network request fails, returning a status of (failed) net::ERR_FAILED.
The CORS Access-Control-Allow-Origin error is one of the most frequent hurdles encountered when decoupling frontend single-page applications from backend REST or GraphQL microservices. Under the browser Same-Origin Policy (SOP), client scripts are strictly prohibited from reading responses from a differing domain, subdomain, port, or protocol unless the destination server explicitly returns permission headers.
In this technical walkthrough, we break down preflight OPTIONS requests, explain why CORS is strictly a server-side configuration, and provide production-ready middleware snippets for Express (Node.js), Laravel (PHP), FastAPI (Python), and Nginx. If your business needs secure, scalable API architectures, check out our end-to-end API development and backend integration services.
1. How Cross-Origin Resource Sharing Actually Operates

According to the official MDN Web Docs, two URLs have the "Same Origin" only if their protocol (HTTP vs HTTPS), domain (example.com vs api.example.com), and port (3000 vs 8000) are identical. If any element differs, the browser initiates a cross-origin transaction.
- Simple Requests: Basic GET and POST requests without custom authorization headers send an
Originheader. The server must reply withAccess-Control-Allow-Originmatching the requesting domain (or wildcard*). - Preflight OPTIONS Requests: If your request includes JSON payloads (
application/json), Bearer tokens, or custom HTTP headers, the browser automatically sends an HTTPOPTIONShandshake first. If the server does not handle OPTIONS, the main request is never sent. - Credentials and Cookies: If your frontend passes
credentials: 'include', the wildcard*is rejected by browsers; you must return the explicit origin string and setAccess-Control-Allow-Credentials: true.
Common Misconception: Frontend Headers Cannot Fix CORS
Setting 'Access-Control-Allow-Origin': '*' in your client-side fetch or Axios configuration does nothing. CORS headers are response headers that must be emitted by the backend server answering the request.
2. Backend Configurations for Modern Frameworks
Add these verified middleware configurations to your backend service:
Fix A: Express.js (Node.js)
Install the official cors package and configure allowed origins and preflight handling:
const express = require('express');
const cors = require('cors');
const app = express();
const corsOptions = {
origin: ['https://app.example.com', 'http://localhost:3000'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
};
app.use(cors(corsOptions));Fix B: Laravel 10 and 11 (PHP)
In Laravel, CORS is handled natively via config/cors.php. Update your allowed paths and origins:
return [
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],
'allowed_origins' => ['https://app.example.com', 'http://localhost:3000'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 86400,
'supports_credentials' => true,
];Fix C: Nginx Reverse Proxy Header Injection
If you prefer terminating CORS at your edge web server before traffic hits your backend application, add these directives to your Nginx location block:
location /api/ {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8';
return 204;
}
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
proxy_pass http://127.0.0.1:8000;
}3. Local Development Workaround: Frontend Proxying
During local development, you can completely bypass browser CORS checks by routing requests through your development server (like Vite or Next.js) which proxies traffic server-to-server:
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
secure: false,
}
}
}
};4. CORS Response Header Reference
| HTTP Header | Purpose | Sample Value |
|---|---|---|
| Access-Control-Allow-Origin | Specifies permitted requesting domain | https://app.example.com |
| Access-Control-Allow-Methods | Allowed HTTP request verbs | GET, POST, PUT, DELETE, OPTIONS |
| Access-Control-Allow-Headers | Permitted custom request headers | Authorization, Content-Type |
| Access-Control-Allow-Credentials | Allows transmitting cookies and tokens | true |
To learn more about modern web full-stack architectures, read our technical guide on deploying modern web applications. For custom enterprise security and API design consulting, reach out through our contact page.
5. Where to Go From Here
CORS is a browser security protocol designed to protect your users, not an obstacle designed to impede developers. Follow these architectural best practices when designing web services:
- Never use
Access-Control-Allow-Origin: *on authenticated endpoints that handle user credentials or private data. - Ensure your backend framework answers preflight HTTP
OPTIONSrequests with a204 No Contentor200 OKstatus. - Keep staging and production CORS whitelist domains clearly separated across your environment variable files.