How to Enable CORS in Express, Flask, Spring & Nginx
Your frontend runs on http://localhost:3000, your API on http://localhost:5000, and the browser refuses to read the response. That is Cross-Origin Resource Sharing (CORS) doing exactly what it should: a different port is a different origin, so the API must explicitly opt in to being read by your app. Enabling CORS means sending the right Access-Control-* response headers from the server. This guide shows the real configuration for four common stacks, with a working localhost:3000 to localhost:5000 dev example for each, then the two mistakes that cause most of the lost hours.
Express (Node.js)
The simplest path is the official cors middleware. For a single known frontend, pass an options object rather than enabling a blanket wildcard:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors({
origin: 'http://localhost:3000',
credentials: true
}));
When you have several allowed origins (local dev, staging, production), pass a function so you can validate dynamically and echo back the matching origin:
const allowed = ['http://localhost:3000', 'https://app.example.com'];
app.use(cors({
origin: (origin, cb) => {
if (!origin || allowed.includes(origin)) return cb(null, true);
cb(new Error('Not allowed by CORS'));
},
credentials: true
}));
The middleware automatically answers the preflight OPTIONS request and reflects the allowed origin, so you do not set Access-Control-Allow-Origin by hand.
Flask (Python)
Use the flask-cors extension. Installing it (pip install flask-cors) and wrapping the app covers the common case:
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
CORS(app, origins=['http://localhost:3000'], supports_credentials=True)
If you would rather not add a dependency, set the headers manually in an after_request hook. Note that you must still handle the preflight by returning early on OPTIONS:
@app.after_request
def add_cors(resp):
resp.headers['Access-Control-Allow-Origin'] = 'http://localhost:3000'
resp.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
resp.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
resp.headers['Access-Control-Allow-Credentials'] = 'true'
return resp
Spring Boot (Java)
For a single controller or method, the @CrossOrigin annotation is the quickest switch:
@CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true")
@RestController
public class ApiController { /* ... */ }
For an application-wide policy, register a global configuration instead of annotating every controller. This keeps the rule in one place:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true);
}
}
If your app uses Spring Security, CORS must also be enabled in the security filter chain (http.cors()), or the filter will reject the request before your MVC config runs.
Nginx and Apache
When a reverse proxy fronts your API, you can attach CORS headers at the proxy layer. In Nginx, add the headers inside the relevant location block and short-circuit the preflight with a 204:
location /api/ {
add_header Access-Control-Allow-Origin "http://localhost:3000" always;
add_header Access-Control-Allow-Credentials "true" always;
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
return 204;
}
}
In Apache, the equivalent lives in your virtual host or .htaccess using mod_headers:
Header set Access-Control-Allow-Origin "http://localhost:3000"
Header set Access-Control-Allow-Credentials "true"
Generating these blocks by hand is error-prone. The Nginx config generator and Apache .htaccess generator produce correct, copy-ready server blocks for you.
Gotcha 1: wildcard origin breaks credentials
The single most common dead end. Per the Fetch Standard (MDN), Access-Control-Allow-Origin: * cannot be combined with Access-Control-Allow-Credentials: true. If your request sends cookies, an Authorization header, or a TLS client cert, the browser discards a wildcard response and reports a CORS error even though the server returned the header. The fix is to echo the specific requesting origin instead of * — and always validate that origin against an allowlist first, never reflect any origin blindly. All four examples above do this. When you reflect a dynamic origin, also send Vary: Origin so shared caches and CDNs do not serve one origin's response to another.
Gotcha 2: double-layer CORS produces duplicate headers
If both your application (Express, Flask, Spring) and your proxy (Nginx, Apache) add CORS headers, the response carries Access-Control-Allow-Origin twice. Browsers require a single value for that header and reject anything with multiple or comma-joined origins, so the request fails with a confusing "multiple values" error. Pick one layer to own CORS and remove it from the other. Generally the application layer is the better owner because it knows the route-level rules; let the proxy pass headers through untouched. If you must do it at the proxy, strip any upstream CORS headers first. When something still misbehaves after this, the CORS debugging guide walks through reading the failing request in DevTools.
Quick reference
- Same machine, different port = different origin.
localhost:3000andlocalhost:5000need CORS just like separate domains. - Sending credentials? Echo the exact origin, never
*, and addVary: Origin. - Own CORS in exactly one layer to avoid duplicate headers.
- Non-simple requests trigger a preflight. Make sure
OPTIONSreturns the allowed methods and headers.
For ready-made header sets you can paste into any of these stacks, the CORS headers generator outputs the complete block for simple and credentialed requests alike.
Frequently Asked Questions
An origin is the combination of scheme, host, and port. Because the port differs, the browser treats 3000 and 5000 as separate origins, so the API on 5000 must send Access-Control-Allow-Origin headers before your app on 3000 is allowed to read its responses, even on the same machine.
No. The Fetch Standard prohibits combining the wildcard origin with Access-Control-Allow-Credentials: true. If your request includes cookies or an Authorization header, the browser rejects a wildcard response. You must echo back the specific requesting origin instead of using *.
Both your application and your reverse proxy are adding CORS headers, so the response carries the header twice. Browsers require a single value and reject duplicates. Configure CORS in only one layer; usually the application is the better owner, with the proxy passing headers through untouched.
For non-simple requests (custom headers, PUT/DELETE, JSON bodies), the browser sends a preflight OPTIONS request first. Libraries like cors and flask-cors answer it automatically. If you set headers manually or at the proxy, you must return the allowed methods and headers and respond with a 204 to the OPTIONS call.
CORS does not weaken your API; it only tells browsers which web origins may read responses. It is not a server-side access control. Always validate authentication and authorization on the server, and list only the origins you trust rather than reflecting every origin or using a permissive wildcard.