If you've ever opened your browser console and seen a red error message about CORS policy, you're not alone. CORS errors are one of the most common and most frustrating issues in web development — and they almost always surface after deployment, not during local development.

This guide explains what CORS is, why browsers enforce it, and how to fix it in every major framework.

## What Is CORS?

CORS stands for Cross-Origin Resource Sharing. It's a security mechanism built into web browsers that controls which websites can make requests to your server.

When your frontend at `https://app.example.com` tries to fetch data from `https://api.example.com`, the browser checks whether the API server explicitly allows requests from `app.example.com`. If the server doesn't say yes, the browser blocks the response.

This isn't your server rejecting the request — the server processes it normally. The _browser_ blocks the response from reaching your JavaScript code.

## The Same-Origin Policy

CORS is built on top of the Same-Origin Policy, which defines what counts as the same origin:

```
https://app.example.com:443/page
  ↑ ↑ ↑
protocol domain port
```

Two URLs have the same origin only if all three parts match:

| URL A | URL B | Same origin? | Why |
| --- | --- | --- | --- |
| `https://app.example.com` | `https://app.example.com/api` | Yes | Same protocol, domain, port |
| `https://app.example.com` | `http://app.example.com` | No | Different protocol |
| `https://app.example.com` | `https://api.example.com` | No | Different subdomain |
| `https://app.example.com` | `https://app.example.com:8080` | No | Different port |

When origins don't match, the browser requires CORS headers from the server before it will allow the response through.

## Simple Requests vs Preflight Requests

Not all cross-origin requests are handled the same way.

### Simple Requests

The browser sends the request directly if it meets all of these conditions:

- Method is `GET`, `HEAD`, or `POST`
- Only standard headers (`Accept`, `Content-Type`, `Content-Language`)
- `Content-Type` is `application/x-www-form-urlencoded`, `multipart/form-data`, or `text/plain`

For simple requests, the browser sends the request and checks the CORS headers in the response.

### Preflight Requests

For anything else — `PUT`, `DELETE`, `PATCH`, custom headers, `Content-Type: application/json` — the browser sends a preflight `OPTIONS` request first:

```
Browser → OPTIONS /api/users (preflight)
Server → 200 OK + CORS headers
Browser → PUT /api/users (actual request)
Server → 200 OK + data
```

```
sequenceDiagram
    Browser->>Server: OPTIONS /api/users (preflight)
    Server-->>Browser: 200 OK + CORS headers
    Browser->>Server: PUT /api/users (actual request)
    Server-->>Browser: 200 OK + response data
```

The preflight asks: Am I allowed to make this type of request? If the server responds with the right headers, the browser proceeds with the actual request.

## CORS Headers Explained

### Access-Control-Allow-Origin

The most important header. It tells the browser which origins can access the response:

```
Access-Control-Allow-Origin: https://app.example.com
```

Or allow any origin (use with caution):

```
Access-Control-Allow-Origin: *
```

**Important** : You can only specify one origin or `*`. You cannot list multiple origins. To support multiple origins, your server must check the request's `Origin` header and echo it back if it's in your allowed list.

### Access-Control-Allow-Methods

Which HTTP methods are permitted:

```
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH
```

### Access-Control-Allow-Headers

Which request headers are permitted:

```
Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With
```

### Access-Control-Allow-Credentials

Whether the browser should send cookies and authentication:

```
Access-Control-Allow-Credentials: true
```

**Critical** : When this is `true`, `Access-Control-Allow-Origin` cannot be `*`. You must specify the exact origin.

### Access-Control-Max-Age

How long (in seconds) the browser caches the preflight response:

```
Access-Control-Max-Age: 86400
```

This avoids sending a preflight `OPTIONS` request before every actual request. Set it to 24 hours (86400) for production.

### Access-Control-Expose-Headers

Which response headers JavaScript can read (beyond the basic set):

```
Access-Control-Expose-Headers: X-Total-Count, X-Request-Id
```

## How to Enable CORS

### Node.js / Express

The `cors` middleware is the standard approach:

```
npm install cors
```

```
const express = require('express');
const cors = require('cors');
const app = express();

// Allow specific origin
app.use(cors({
  origin: 'https://app.example.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400
}));

// Or allow multiple origins
app.use(cors({
  origin: ['https://app.example.com', 'https://staging.example.com'],
  credentials: true
}));
```

### Python / Django

Install [django-cors-headers](https://github.com/adamchainz/django-cors-headers):

```
pip install django-cors-headers
```

```
# settings.py
INSTALLED_APPS = [
    'corsheaders',
    # ...
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware', # Must be before CommonMiddleware
    'django.middleware.common.CommonMiddleware',
    # ...
]

CORS_ALLOWED_ORIGINS = [
    'https://app.example.com',
    'https://staging.example.com',
]

CORS_ALLOW_CREDENTIALS = True
```

### Python / Flask

```
pip install flask-cors
```

```
from flask import Flask
from flask_cors import CORS

app = Flask( __name__ )
CORS(app, origins=['https://app.example.com'], supports_credentials=True)
```

### Ruby on Rails

Add `rack-cors` to your Gemfile:

```
gem 'rack-cors'
```

```
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'https://app.example.com'
    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options],
      credentials: true,
      max_age: 86400
  end
end
```

### PHP (Manual Headers)

```
<?php
$allowed_origin = 'https://app.example.com';

if (isset($_SERVER['HTTP_ORIGIN']) && $_SERVER['HTTP_ORIGIN'] === $allowed_origin) {
    header("Access-Control-Allow-Origin: $allowed_origin");
    header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE");
    header("Access-Control-Allow-Headers: Content-Type, Authorization");
    header("Access-Control-Allow-Credentials: true");
    header("Access-Control-Max-Age: 86400");
}

// Handle preflight
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}
```

### Nginx (Server-Level)

```
server {
    listen 80;
    server_name api.example.com;

    location / {
        # Handle preflight
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE';
            add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
            add_header 'Access-Control-Max-Age' 86400;
            add_header 'Content-Length' 0;
            return 204;
        }

        add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
        add_header 'Access-Control-Allow-Credentials' 'true';

        proxy_pass http://127.0.0.1:3000;
    }
}
```

## Common CORS Errors and Fixes

### No 'Access-Control-Allow-Origin' header is present

The server isn't sending CORS headers. Add the appropriate middleware or headers for your framework.

### The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '\*' when the request's credentials mode is 'include'

You're using `credentials: true` (or `withCredentials: true` in fetch/axios) but the server is responding with `Access-Control-Allow-Origin: *`. You must specify the exact origin:

```
// Wrong
res.header('Access-Control-Allow-Origin', '*');

// Correct
res.header('Access-Control-Allow-Origin', 'https://app.example.com');
```

### Preflight Failures (OPTIONS Returns 405 or 404)

Your server doesn't handle `OPTIONS` requests. Some frameworks need explicit configuration:

```
// Express: cors middleware handles this automatically
// Without cors middleware, add:
app.options('*', (req, res) => {
  res.header('Access-Control-Allow-Origin', 'https://app.example.com');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.sendStatus(204);
});
```

### CORS Works Locally but Fails in Production

This is the most common deployment issue. Locally, your frontend and API often run on `localhost` (same origin or with a dev proxy). In production, they're on different subdomains.

Check that your production server's CORS configuration includes your production frontend URL, not just `localhost:3000`.

## CORS in Development vs Production

### Development

During development, CORS issues are often avoided with a dev proxy. Most frontend tools support this:

**Vite:**

```
// vite.config.js
export default {
  server: {
    proxy: {
      '/api': 'http://localhost:3000'
    }
  }
}
```

The proxy makes API requests appear same-origin during development, bypassing CORS entirely.

### Production

In production, you have several options:

1. **Same origin** : Serve frontend and API from the same domain (no CORS needed)
2. **CORS headers** : Configure your API to accept requests from your frontend's origin
3. **Reverse proxy** : Use Nginx to serve both frontend and API from the same domain

Option 3 is often the simplest. If your frontend and API are both deployed via [DeployHQ](https://deployhq.com/features), an Nginx reverse proxy can route `/api/*` to your backend and everything else to your frontend — making them the same origin.

## How This Relates to Deployment

CORS issues are deployment issues. They rarely appear during development (because of dev proxies or same-origin localhost) and almost always appear when code hits staging or production.

When you deploy with [DeployHQ](https://www.deployhq.com) from [GitHub](https://deployhq.com/deploy-from-github) or [GitLab](https://deployhq.com/deploy-from-gitlab):

- **Your CORS configuration deploys with your code** — it's part of your application's middleware or server config
- **Environment-specific origins** can be managed via DeployHQ's [build pipelines](https://deployhq.com/features/build-pipelines) — set `CORS_ORIGIN` as a build variable that differs between staging and production
- **Nginx config deploys alongside your app** — your reverse proxy rules are version-controlled and auto-deployed

For [agencies](https://deployhq.com/for-agencies) deploying frontend and backend separately for clients, getting CORS right from the start saves hours of debugging post-deploy.

## Security Considerations

- **Never use `*` in production** for APIs that handle authentication. It allows any website to make requests to your API
- **Validate origins** : Don't blindly echo the `Origin` header. Maintain a whitelist of allowed origins
- **Don't confuse CORS with authentication**. CORS controls which _websites_ can talk to your API. It doesn't replace API authentication (tokens, sessions)
- **Credentials require specific origins**. If you use cookies or auth headers, you must specify exact origins — `*` is not allowed

## FAQ

**Does CORS apply to server-to-server requests?** No. CORS is a browser-only mechanism. Server-to-server HTTP requests (curl, fetch from Node.js, Python requests) are not subject to CORS restrictions.

**Can I disable CORS in the browser?** Yes, for testing — but never in production. Chrome can be launched with `--disable-web-security`, but this is dangerous and only for debugging.

**Does CORS protect my API from abuse?** No. CORS is enforced by browsers, not servers. Anyone can use curl or Postman to make requests directly. Use authentication and rate limiting to protect your API.

**Why does my API work in Postman but not in the browser?** Postman doesn't enforce CORS — it's not a browser. The same request that fails in the browser due to CORS will succeed in Postman because there's no same-origin policy to enforce.

**Should I handle CORS in my application or in Nginx?** Either works. Application-level CORS (Express cors middleware, Django cors-headers) is more flexible and portable. Nginx-level CORS is useful when you can't modify the application. Don't configure CORS in both places — the headers can conflict.

* * *

CORS is confusing at first, but it's simple once you understand the flow: the browser asks the server for permission, the server responds with headers, and the browser enforces the result. Get your headers right, test on staging before production, and you'll never see that red console error again.

**[Try](https://deployhq.com/signup)[DeployHQ](https://www.deployhq.com) free** — deploy your frontend and backend together with consistent CORS configuration. See [pricing](https://deployhq.com/pricing) for team plans.

* * *

Questions? Reach out at [support@deployhq.com](mailto:support@deployhq.com) or [@deployhq](https://x.com/deployhq).

