Middleware & CORS

RoadmapsPython Backend

Scenario

Your frontend is on `app.example.com` and your backend is on `api.example.com`. You test the API in Postman—it works perfectly. You deploy the frontend, open the browser, and see a massive red 'Blocked by CORS policy' error. Your backend logs show the request never even reached your code.

Why did the browser block the request, and how do you configure your server to allow it without opening a massive security hole?

Mental model

Middleware is the bouncer at the club door. CORS is the VIP guest list the bouncer checks.

Middleware sits perfectly between the internet and your route handlers. Every request passes through it on the way in, and every response passes through it on the way out. CORS (Cross-Origin Resource Sharing) is a specific built-in middleware. When a browser tries to call your API from a different domain, it first asks for permission. The CORS middleware checks an allow-list and adds specific HTTP headers to the response telling the browser, 'Yes, this frontend is allowed to talk to me.'

Deep dive

Middleware wraps the entire request/response lifecycle. You write a function that takes a request, does something (like logging or timing), hands the request to the rest of the application (the `call_next` step), and then gets the response back to modify before sending it to the user.

Middleware executes both before and after your route handler runs.

The most critical middleware you'll configure is CORS. By default, browsers enforce the Same-Origin Policy: a script on `example.com` cannot fetch data from `api.com`. If it tries, the browser blocks it to prevent malicious sites from stealing user data. CORS is how your API relaxes this rule.

CORS is a browser security feature, not a backend security feature. Your API must explicitly opt-in.

When making complex requests (like sending JSON or using auth headers), the browser first sends an `OPTIONS` request called a 'preflight'. It asks the server, 'Are you okay with a POST from this domain?' If your CORS middleware isn't configured, the server rejects the preflight, and the actual POST never happens.

Browsers send an OPTIONS preflight request to check CORS permissions before the real request.

In FastAPI, you add `CORSMiddleware` to your app. The golden rule for production: never use `allow_origins=['*']` if you are dealing with authenticated users. The CORS specification strictly forbids combining wildcard origins with `allow_credentials=True`. You must maintain an explicit allow-list of your frontend domains.

Never use a wildcard `*` for CORS origins if your API accepts credentials (cookies, auth headers).

Code examples

A custom timing middleware

import time
from fastapi import FastAPI, Request

app = FastAPI()

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.time()
    
    # 1. Pass request down the chain to the router/handler
    response = await call_next(request)
    
    # 2. Modify the response on the way out
    process_time = time.time() - start_time
    response.headers["X-Process-Time"] = str(process_time)
    return response

Middleware forms an onion around your app. Code before `call_next` runs on the incoming request; code after runs on the outgoing response. The handler inside is totally unaware.

The dangerous/invalid CORS setup

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# ❌ DO NOT DO THIS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],           # Wildcard origin
    allow_credentials=True,        # + Credentials = ERROR
    allow_methods=["*"],
    allow_headers=["*"],
)

If `allow_credentials=True`, the CORS spec prohibits `allow_origins=["*"]`. Browsers will reject this setup, and FastAPI will often throw a startup error. You cannot blindly trust every domain with user credentials.

Production CORS setup (explicit allow-list)

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# ✅ Read from environment/settings in production
ALLOWED_ORIGINS = [
    "https://app.example.com",
    "https://staging.example.com",
    "http://localhost:3000",  # For local frontend dev
]

app.add_middleware(
    CORSMiddleware,
    allow_origins=ALLOWED_ORIGINS,
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["Authorization", "Content-Type"],
)

An explicit allow-list ensures only your verified frontends can make credentialed requests to your API. Always load these origins from environment variables via Pydantic settings so they can change per environment.

Common mistakes

Glossary

middleware
Code that runs before every request hits your route handler, and after every response leaves it.
CORS
Cross-Origin Resource Sharing. A browser security mechanism that restricts web pages from making requests to a different domain than the one that served the web page.
preflight request
An HTTP OPTIONS request sent by the browser before the actual request, to check if the server permits the cross-origin request.

Recall questions

Questions & answers

Your API is deployed. The frontend on a different domain makes a POST request with an Authorization header. The browser console shows a CORS error. How do you resolve this in FastAPI?

Add `CORSMiddleware` to the FastAPI app. Configure `allow_origins` to explicitly include the frontend's domain URL (no trailing slash). Set `allow_credentials=True`, and ensure `allow_methods` and `allow_headers` include 'POST' and 'Authorization'.

Approach: Identify that the browser blocked the cross-origin request because the server didn't supply the right headers. The fix is explicitly configuring the backend CORS middleware to authorize that specific origin.

How would you implement a system to measure the exact processing time of every API request and log it?

Use a middleware function. Record the start time before calling `call_next(request)`, wait for the response, record the end time, calculate the difference, log it, and optionally add it as a custom header to the outgoing response.

Approach: Middleware is the perfect pattern for cross-cutting concerns like logging or timing, as it wraps the entire request lifecycle centrally without touching individual route handlers.

Continue learning

Previous: Routers & App Structure

Next: Config & Pydantic Settings

Return to Python Backend Roadmap