Difficulty: Intermediate
CORS (Cross-Origin Resource Sharing) is a security mechanism enforced by web browsers that restricts web pages from making requests to a different domain than the one that served the page. When your React frontend at http://localhost:3000 tries to call your FastAPI backend at http://localhost:8000, the browser blocks the request unless the backend explicitly allows it via CORS headers.
CORS works through HTTP headers. When a browser makes a cross-origin request, it first sends a 'preflight' OPTIONS request to ask the server if the actual request is allowed. The server responds with headers like Access-Control-Allow-Origin, Access-Control-Allow-Methods, and Access-Control-Allow-Headers. If the headers permit the request, the browser proceeds with the actual request.
FastAPI provides CORSMiddleware for configuring CORS. The key settings are: allow_origins (which domains can make requests), allow_methods (which HTTP methods are allowed), allow_headers (which request headers are allowed), allow_credentials (whether cookies/auth headers are sent), and expose_headers (which response headers the browser can read).
Setting allow_origins=["*"] permits any origin, which is convenient for development but insecure for production. In production, list your specific frontend domains. Note that when allow_credentials=True, you cannot use allow_origins=["*"] - the browser requires specific origins when credentials are involved.
The preflight request (OPTIONS) is sent automatically by browsers for requests that are not 'simple' - requests with custom headers, content-type other than form-data/text/urlencoded, or methods other than GET/HEAD/POST. The CORSMiddleware handles these OPTIONS requests automatically, so you do not need to define OPTIONS handlers.
max_age controls how long the browser caches preflight responses. A higher value (like 600 seconds) reduces the number of preflight requests, improving performance. The browser will reuse the cached preflight response for subsequent requests to the same endpoint.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Development: allow all origins
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/api/data")
def get_data():
return {"data": "Hello from the API"}
# Response will include:
# Access-Control-Allow-Origin: *
The wildcard * allows any origin to make requests. This is fine for development and public APIs but should be restricted in production. All methods and headers are allowed.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Production: specific origins only
allowed_origins = [
"https://myapp.com",
"https://www.myapp.com",
"https://admin.myapp.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True, # Allow cookies and auth headers
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
allow_headers=[
"Authorization",
"Content-Type",
"X-Request-Id",
],
expose_headers=[ # Headers the browser can read
"X-Total-Count",
"X-Request-Id",
],
max_age=600, # Cache preflight for 10 minutes
)
@app.get("/api/data")
def get_data():
return {"data": "Secure API response"}
# Response for https://myapp.com will include:
# Access-Control-Allow-Origin: https://myapp.com
# Access-Control-Allow-Credentials: true
Production CORS should list specific origins, restrict methods to those actually used, and only allow necessary headers. expose_headers lets the browser read custom response headers. max_age caches preflight responses.
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Different CORS settings per environment
ENV = os.getenv("ENVIRONMENT", "development")
if ENV == "production":
origins = [
"https://myapp.com",
"https://www.myapp.com",
]
else:
origins = [
"http://localhost:3000",
"http://localhost:5173", # Vite dev server
"http://127.0.0.1:3000",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
max_age=600 if ENV == "production" else 0,
)
@app.get("/")
def root():
return {"environment": ENV}
Use environment variables to configure CORS differently for development and production. Development allows localhost origins. Production allows only your domain. max_age is higher in production to reduce preflight requests.
CORS, Cross-Origin, Preflight, Access-Control, Origins