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.


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.

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

  1. Using Access-Control-Allow-Origin: * with credentials

    • Browsers reject when cookies or Authorization headers are used.
  2. Forgetting the preflight (OPTIONS) request

    • Your backend must respond properly to OPTIONS for non-simple requests (e.g., JSON POST).
  3. Testing with hardcoded origins

    • Using fixed localhost ports leads to issues when teammates use different setups.

      → use allowedOriginPatterns("http://localhost:*").

  4. 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)?