FastAPI CORS: A Practical Tutorial
Configure CORS in FastAPI without security holes: how the browser preflight works, which origins and headers to allow, credentials and cookies, and the most common misconfigurations to avoid.
What you'll learn
- ✓What CORS is and what it actually protects
- ✓The simple vs preflight request distinction
- ✓Configuring CORSMiddleware properly
- ✓Cookies, credentials, and the wildcard trap
- ✓Common misconfigurations and how to detect them
- ✓When CORS is the wrong tool entirely
Prerequisites
- •A FastAPI app served separately from your frontend
CORS is the source of more frustrated dev-tools screenshots than any other part of web development. The mechanics are not complicated; the confusion comes from where the rules are enforced.
What and Why
CORS (Cross-Origin Resource Sharing) is a browser security feature. By default, JavaScript running on https://app.example.com cannot read responses from https://api.example.com. CORS lets your API explicitly opt those origins in by sending response headers like Access-Control-Allow-Origin.
CORS protects users, not your server. Curl, mobile apps, and other backends are not affected. Authentication and authorization still belong in your API.
Mental Model
Two kinds of requests exist. Simple requests (GET, HEAD, POST with safe content types) go straight to your server; the browser only checks the response headers afterwards. Anything else triggers a preflight: the browser sends an OPTIONS request first asking what is allowed. Your server responds with the allowed origin, methods, and headers, and only then does the browser send the real request.
Most CORS pain is preflight pain.
Hands-on Example
FastAPI ships a middleware that handles both cases:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://app.example.com",
"http://localhost:5173",
],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
expose_headers=["X-Request-Id"],
max_age=600,
)
@app.get("/me")
async def me():
return {"user": "alice"}
The browser caches the preflight for max_age seconds, so subsequent calls skip step 1.
Common Pitfalls
The first pitfall is allow_origins=["*"] with allow_credentials=True. The browser silently rejects this combination; you must list explicit origins when credentials are enabled.
The second is forgetting to allow the headers your client actually sends. If your frontend adds Authorization: Bearer ..., that header must appear in allow_headers or the preflight fails.
The third is mixing up CORS with cookies. allow_credentials=True is required on the server, but the client must also set credentials: "include" in fetch, and the cookie must have SameSite=None; Secure to cross origins.
The fourth is forgetting that errors thrown before the middleware runs do not have CORS headers. A 500 from a misconfigured database connection appears in the browser as a CORS error, not a 500. Check your server logs.
The fifth is putting CORS behind a redirect. A request to http://api.example.com redirected to https://api.example.com drops CORS headers on the redirect itself. Serve the canonical URL directly.
Practical Tips
Pin specific origins in production and use a different config in development. Never enable allow_origins=["*"] for an API that handles user data, even without credentials. Test with the actual production frontend, because tools like curl and Postman skip CORS entirely and give false confidence.
If your API and frontend share an origin (same domain, same port), you do not need CORS at all. A reverse proxy in front of both is often simpler than configuring CORS.
Wrap-up
CORS is a small set of response headers backed by a browser check. Allow only the origins, methods, and headers you actually use, get credentials right once, and the topic will stop occupying your debugging time.
Related articles
- FastAPI FastAPI OpenAPI Customization: A Practical Tutorial
Tailor FastAPI's auto-generated OpenAPI schema: tags, summaries, examples, response models, custom operation IDs, security schemes, and a custom Swagger UI your team will actually use.
- FastAPI FastAPI Rate Limiting: A Practical Tutorial
Add rate limiting to FastAPI using slowapi and Redis: token buckets vs fixed windows, per-user and per-IP limits, returning proper headers, and avoiding the most common production mistakes.
- FastAPI FastAPI: Async Routes and Dependency Injection
A practical guide to async path operations and Depends() in FastAPI — when async actually helps, per-request DB sessions, auth dependencies, and how sub-dependencies compose.
- FastAPI FastAPI + SQLAlchemy: Your First Database-Backed API
A practical guide to FastAPI with SQLAlchemy 2.0 — typed models with Mapped and mapped_column, sessionmaker, get_db dependency, CRUD endpoints, and where Alembic fits.