You built your FastAPI backend. You built your React frontend. You run both locally and make your first fetch() call from React to your APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses.. The browser blocks it with a CORSWhat is cors?Cross-Origin Resource Sharing - a browser security rule that blocks web pages from making requests to a different domain unless that domain explicitly allows it. error. This is the moment every web developer discovers that browsers enforce security rules that development tools do not.
CORS is one of those topics that AI generates configuration for confidently and incorrectly. Understanding how it actually works saves you from debugging phantom errors that are really just configuration mistakes.
How CORSWhat is cors?Cross-Origin Resource Sharing - a browser security rule that blocks web pages from making requests to a different domain unless that domain explicitly allows it. works
When your React app at http://localhost:5173 makes a request to your FastAPI server at http://localhost:8000, the browser considers these different origins (different ports). By default, browsers block JavaScript from reading responses from a different originWhat is origin?The combination of protocol, domain, and port that defines a security boundary in the browser, like https://example.com:443.. This is the Same-Origin PolicyWhat is same-origin policy?A browser security rule that prevents JavaScript on one origin from reading responses from a different origin..
CORS is the mechanism that relaxes this policy. Your server tells the browser: "I trust requests from these specific origins." The browser checks the server's CORS headers and decides whether to allow the request.
There are two types of CORS requests:
Simple requests
A request is "simple" if it uses GET, HEADWhat is head?A special pointer in Git that indicates the commit you are currently working on - usually the tip of the active branch., or POST with standard content types (text/plain, multipart/form-data, application/x-www-form-urlencoded). The browser sends the request directly and checks the Access-Control-Allow-Origin header in the response.
Browser → Server: GET /api/users (Origin: http://localhost:5173)
Server → Browser: 200 OK (Access-Control-Allow-Origin: http://localhost:5173)
Browser: Origin matches → allow JavaScript to read the responsePreflight requests
For anything more complex, PUT, DELETE, PATCH, custom headers like Authorization, or Content-Type: application/json, the browser first sends an OPTIONS request called a preflight. This asks the server: "Would you accept this request?"
Browser → Server: OPTIONS /api/users (Origin, Access-Control-Request-Method: POST,
Access-Control-Request-Headers: Content-Type, Authorization)
Server → Browser: 204 No Content (Access-Control-Allow-Origin: http://localhost:5173,
Access-Control-Allow-Methods: POST,
Access-Control-Allow-Headers: Content-Type, Authorization)
Browser: Preflight passed → now send the actual POST requestIf the preflight fails (wrong origin, missing headers, server returns 401), the browser never sends the actual request. This is why auth middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it. before CORS middleware breaks everything, the preflight has no credentials, auth rejects it, and the browser sees a CORS failure.
Configuring CORSMiddleware
FastAPI uses Starlette's CORSMiddleware. Here is how to configure it properly.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
# Development configuration
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173", "http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)Each parameter controls a specific aspect of the CORSWhat is cors?Cross-Origin Resource Sharing - a browser security rule that blocks web pages from making requests to a different domain unless that domain explicitly allows it. policy:
| Parameter | What it controls | Example |
|---|---|---|
allow_origins | Which origins can make requests | ["https://myapp.com"] |
allow_credentials | Whether cookies and auth headers are sent | True or False |
allow_methods | Which HTTP methods are permitted | ["GET", "POST"] or ["*"] |
allow_headers | Which custom headers are accepted | ["Authorization", "Content-Type"] or ["*"] |
expose_headers | Which response headers JavaScript can read | ["X-Request-ID"] |
max_age | How long browsers cache preflight results (seconds) | 600 |
allow_methods=["*"] and allow_headers=["*"] are fine in most cases. The security boundary is allow_origins, not methods or headers. Restrict origins tightly; be permissive with methods and headers.The wildcard credentials trap
This is the single most common CORSWhat is cors?Cross-Origin Resource Sharing - a browser security rule that blocks web pages from making requests to a different domain unless that domain explicitly allows it. mistake, and AI generates it constantly.
# THIS DOES NOT WORK - browsers reject it
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Wildcard origin
allow_credentials=True, # With credentials
allow_methods=["*"],
allow_headers=["*"],
)The CORS specification explicitly forbids Access-Control-Allow-Origin: * when Access-Control-Allow-Credentials: true. The browser silently ignores the response and blocks the request. You see a CORS error in the console, but the server logs show a successful response, making it look like a browser bug.
Why does this rule exist? If wildcard origins and credentials were allowed together, any website could make authenticated requests to your APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. using your user's cookies. It would be a universal CSRFWhat is csrf?Cross-Site Request Forgery - an attack where a malicious website tricks your browser into sending a request to another site where you're logged in. vulnerability.
allow_origins=["*"] with allow_credentials=True. This compiles, starts without errors, and fails silently at runtime. The fix is simple: list your actual origins explicitly.The correct approach for production:
import os
ALLOWED_ORIGINS = os.getenv(
"ALLOWED_ORIGINS",
"http://localhost:5173"
).split(",")
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)This reads allowed origins from an environment variableWhat is environment variable?A value stored outside your code that configures behavior per deployment, commonly used for secrets like API keys and database URLs., defaulting to the local dev server. In production, you set ALLOWED_ORIGINS=https://myapp.com,https://app.myapp.com.
What happens without CORSWhat is cors?Cross-Origin Resource Sharing - a browser security rule that blocks web pages from making requests to a different domain unless that domain explicitly allows it. middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it.
If you add no CORS middleware at all, here is what happens:
- Browser requests from a different originWhat is origin?The combination of protocol, domain, and port that defines a security boundary in the browser, like https://example.com:443.: blocked. The browser sends the request, receives the response, sees no
Access-Control-Allow-Originheader, and refuses to let JavaScript read the response. Your APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. works but the frontend cannot use it. curland Postman: work perfectly. These tools do not enforce CORS. This is why developers are confused when "the API works in Postman but not in the browser."- Same-origin requests: work normally. If your frontend and backend are served from the same domain and port, CORS does not apply.
- Server-to-server calls: unaffected. CORS is a browser-only mechanism.
Security headers
CORSWhat is cors?Cross-Origin Resource Sharing - a browser security rule that blocks web pages from making requests to a different domain unless that domain explicitly allows it. is about who can call your APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses.. Security headers are about how browsers should behave when displaying your content. Even though FastAPI serves JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. APIs (not HTML pages), security headers still matter, they protect against edge cases and are often required by security audits.
Essential security headers for APIs
@app.middleware("http")
async def add_security_headers(request: Request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
response.headers["Cache-Control"] = "no-store"
response.headers["Content-Security-Policy"] = "default-src 'none'; frame-ancestors 'none'"
return responseWhat each header does:
| Header | Purpose |
|---|---|
X-Content-Type-Options: nosniff | Prevents browsers from guessing content types, stops MIME-type attacks |
X-Frame-Options: DENY | Prevents your API responses from being loaded in iframes (clickjacking protection) |
Strict-Transport-Security | Forces HTTPS for all future requests to your domain (HSTS) |
Cache-Control: no-store | Prevents caching of API responses that may contain sensitive data |
Content-Security-Policy | Restricts what resources can be loaded, default-src 'none' is the strictest policy |
Rate limitingWhat is rate limiting?Restricting how many requests a client can make within a time window. Prevents brute-force attacks and protects your API from being overwhelmed.
FastAPI does not include rate limiting out of the box. You have three main approaches, each with different tradeoffs.
In-memory rate limiting
The simplest approach. Works for single-server deployments but does not scale to multiple servers.
from collections import defaultdict
import time
# Simple in-memory rate limiter
request_counts: dict[str, list[float]] = defaultdict(list)
@app.middleware("http")
async def rate_limit(request: Request, call_next):
client_ip = request.client.host
now = time.time()
window = 60 # 1-minute window
max_requests = 100
# Remove old timestamps
request_counts[client_ip] = [
ts for ts in request_counts[client_ip]
if now - ts < window
]
if len(request_counts[client_ip]) >= max_requests:
return JSONResponse(
status_code=429,
content={"error": "Rate limit exceeded. Try again later."},
headers={"Retry-After": "60"},
)
request_counts[client_ip].append(now)
response = await call_next(request)
return responseThis sliding-window approach tracks timestamps for each client IP. It works but has two weaknesses: memory grows unboundedly if you do not clean up expired entries, and multiple server instances each have their own counter (so the effective limit is multiplied by the number of servers).
DependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself.-based rate limiting
Instead of middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it., you can use a FastAPI dependency. This lets you apply different limits to different routes.
from fastapi import Depends
def rate_limit(max_requests: int = 100, window: int = 60):
counts: dict[str, list[float]] = defaultdict(list)
async def check(request: Request):
client_ip = request.client.host
now = time.time()
counts[client_ip] = [
ts for ts in counts[client_ip] if now - ts < window
]
if len(counts[client_ip]) >= max_requests:
raise HTTPException(
status_code=429,
detail="Rate limit exceeded",
)
counts[client_ip].append(now)
return check
# Different limits for different routes
@app.get("/api/search", dependencies=[Depends(rate_limit(30, 60))])
async def search():
...
@app.post("/api/upload", dependencies=[Depends(rate_limit(5, 60))])
async def upload():
...Redis-based rate limiting
For multi-server deployments, you need a shared counter. Redis is the standard choice because it supports atomic increment-and-expire operations.
import redis.asyncio as redis
redis_client = redis.from_url("redis://localhost:6379")
@app.middleware("http")
async def rate_limit_redis(request: Request, call_next):
client_ip = request.client.host
key = f"rate_limit:{client_ip}"
count = await redis_client.incr(key)
if count == 1:
await redis_client.expire(key, 60)
if count > 100:
return JSONResponse(
status_code=429,
content={"error": "Rate limit exceeded"},
headers={"Retry-After": "60"},
)
response = await call_next(request)
response.headers["X-RateLimit-Remaining"] = str(max(0, 100 - count))
return responseThe Redis approach uses atomic INCR to count requests and EXPIRE to automatically reset the counter after the window. Since Redis is shared across all server instances, the rate limit is global.
MiddlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it. vs dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. for cross-cutting concerns
When should you use middleware, and when should you use a dependency?
| Use middleware when | Use a dependency when |
|---|---|
| The logic applies to every request | The logic applies to some routes |
| You need to modify the response after the handler runs | You only need to check something before the handler runs |
| Ordering relative to other middleware matters | The check is independent and self-contained |
| Examples: CORS, timing, logging, security headers | Examples: authentication, rate limiting per route, feature flags |
In practice, most production FastAPI apps use middleware for infrastructure concerns (CORSWhat is cors?Cross-Origin Resource Sharing - a browser security rule that blocks web pages from making requests to a different domain unless that domain explicitly allows it., logging, security headers) and dependencies for business logic concerns (auth, permissions, rate limitingWhat is rate limiting?Restricting how many requests a client can make within a time window. Prevents brute-force attacks and protects your API from being overwhelmed.). AI tends to put everything in middleware, which works but makes the application harder to test and configure per-route.