Understanding CORS
🌐 Understanding CORS, Same-Origin Policy, and How to Handle It in Dev & Production
Every web developer has at some point seen the dreaded red error:
“Access to fetch at ‘http://localhost:8081’ from origin ‘http://localhost:5173’ has been blocked by CORS policy…”
Let’s unpack why this happens, why the browser enforces it, and how to handle it cleanly in both development and production.
🧩 What Is an Origin?
An origin is defined as the combination of:
scheme + domain + port
Examples:
https://example.com → Origin A
https://api.example.com → Origin B (different subdomain)
http://localhost:5173 → Origin C (different port)
If any of these differ, you’re dealing with cross-origin access.
🚫 Why Cross-Origin Requests Are Restricted
This restriction is called the Same-Origin Policy (SOP) — it’s the browser’s built-in security firewall.
Its job: protect you from malicious sites reading or sending requests to others on your behalf.
Without it, the web would be chaos.
The Classic Bank Example 🏦
Imagine you’re logged into your bank:
https://mybank.com
Your session cookie is stored in your browser.
Now you open another site:
https://funnycatvideos.com
That site could quietly run:
fetch('https://mybank.com/transfer?to=hacker&amount=10000');
If browsers didn’t enforce SOP:
- Your bank cookies would be attached automatically.
- The bank would think you made the request.
- Money gone — no warning, no prompt.
This is the CSRF (Cross-Site Request Forgery) problem, and SOP blocks it.
Same-origin policy is your browser’s way of saying:
“Nope. You can’t talk to other domains unless they explicitly allow it.”
🔐 How Browsers Enforce It
When your frontend (say, React on localhost:5173) tries to call your backend (localhost:8081), the browser checks:
- “Are they the same origin?”
- ✅ Yes → Allowed.
- ❌ No → Blocked unless the server opts in via CORS.
CORS (Cross-Origin Resource Sharing) is that opt-in system.
It’s like the server saying:
“Yes, I trust requests coming from http://localhost:5173.”
🏭 The Production Solution: Reverse Proxy
In production, the best approach is to make everything appear under the same origin.
Typical setup
Browser → https://app.example.com
↓
├── / → serves React build (frontend)
└── /api/* → proxies internally to backend service
Nginx configuration example
server {
listen 80;
server_name app.example.com;
# Serve frontend
location / {
root /var/www/html;
try_files $uri /index.html;
}
# Proxy API
location /api/ {
proxy_pass http://localhost:8081/;
}
}
Now, from the browser’s point of view:
https://app.example.com → everything happens in the same origin.
Result: no CORS required at all.
The reverse proxy (like Nginx) routes requests behind the scenes.
🔄 Visual Summary
Without proxy (dev mode)
Browser (localhost:5173)
│
├── Request → localhost:8081 (different port)
│ ⛔ Blocked by CORS
With reverse proxy (prod mode)
Browser (app.example.com)
│
├── Request → app.example.com/api → (internally) → backend
│ ✅ Same origin, no CORS
🧑💻 Handling It During Development
When developing locally, your frontend and backend typically run on different ports, triggering cross-origin checks.
You have two clean options.
Option A: Use a Dev Proxy (Recommended)
Tools like Vite, Webpack Dev Server, or Create React App include a built-in proxy feature.
Example: Vite config
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8081',
changeOrigin: true,
secure: false
}
}
}
});
Now your frontend makes requests like:
fetch('/api/v1/journal');
Vite intercepts and forwards that to http://localhost:8081/api/v1/journal,
so to the browser it’s all localhost:5173 → same origin → no CORS.
✅ No configuration on backend required
✅ Fastest dev loop
✅ Mirrors production behavior
Option B: Configure CORS in the Backend
If you need to expose your backend to external clients (Postman, mobile, Swagger UI),
you can enable CORS safely with origin patterns, not hard-coded ports.
Spring Boot example
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("http://localhost:*", "http://127.0.0.1:*")
.allowedMethods("GET","POST","PUT","DELETE","OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
This way:
- Any localhost port (5173, 3000, etc.) is allowed.
- Safer than
"*"when using credentials.
⚙️ Recommended Setup
| Environment | Best Approach | Why |
|---|---|---|
| Development | Use Vite’s proxy (/api → backend) |
No CORS, matches production flow |
| Production | Serve frontend & backend from same domain (via Nginx or gateway) | Eliminates CORS entirely |
| Fallback | Backend CORS with allowedOriginPatterns |
For direct API consumers or tools |
⚠️ Common Mistakes to Avoid
-
Using
Access-Control-Allow-Origin: *with credentials- Browsers reject when cookies or Authorization headers are used.
-
Forgetting the preflight (OPTIONS) request
- Your backend must respond properly to OPTIONS for non-simple requests (e.g., JSON POST).
-
Testing with hardcoded origins
-
Using fixed localhost ports leads to issues when teammates use different setups.
→ use
allowedOriginPatterns("http://localhost:*").
-
-
Relying on CORS in production
- Don’t. Instead, serve frontend + backend under the same origin through a reverse proxy.
🧠 Final Thoughts
CORS isn’t a bug — it’s a safety mechanism.
It exists to protect users, not to punish developers.
Once you understand the “why,” the “how” becomes simple:
- Keep origins unified in production.
- Proxy requests in dev.
- Configure CORS only when absolutely necessary.
Your browser is your first line of defense — learn its rules, and it’ll stop feeling like an enemy.
Would you like me to format this post for a specific medium — for example, a LinkedIn article (slightly shorter, conversational tone) or a technical blog (with markdown headings, code syntax, and diagrams intact)?