Mathew K Analytics

Lesson 10 · FastAPI deep dive

FastAPI Tutorial #10: Middleware & CORS Setup

Video ten of the eighteen-part series: code that wraps around every single request. Custom middleware, execution order, CORSMiddleware, and testing…

⬇ Download notebookOpen in Colab ↗

What you'll learn

Data

No separate download needed — the notebook creates or downloads everything it uses.

📓 Full notebook

Download .ipynb

FastAPI Deep-Dive, Video 10: Middleware and CORS#

  • Video ten of the eighteen-part series: code that wraps around every single request.
  • Custom middleware, execution order, CORSMiddleware, and testing cross-origin behavior.
  • Let's get into it.

Part 1: What Middleware Is#

from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.middleware('http')
async def log_requests(request, call_next):
    print(f'before: {request.url.path}')
    response = await call_next(request)
    print(f'after: {response.status_code}')
    return response
@app.get('/hello')
def hello():
    return {'message': 'hi'}
client = TestClient(app)
print(client.get('/hello').json())
before: /hello
after: 200
{'message': 'hi'}

Part 2: Middleware Modifying the Response#

import time
timing_app = FastAPI()
@timing_app.middleware('http')
async def add_timing_header(request, call_next):
    start = time.perf_counter()
    response = await call_next(request)
    duration = time.perf_counter() - start
    response.headers['X-Process-Time'] = str(round(duration, 6))
    return response
@timing_app.get('/hello')
def timing_hello():
    return {'message': 'hi'}
timing_client = TestClient(timing_app)
response = timing_client.get('/hello')
print('X-Process-Time' in response.headers)
True

Part 3: Middleware Short-Circuiting a Request#

from starlette.responses import JSONResponse
blocked_app = FastAPI()
@blocked_app.middleware('http')
async def block_bad_agent(request, call_next):
    if request.headers.get('user-agent') == 'blocked-bot':
        return JSONResponse(status_code=403, content={'detail': 'blocked'})
    return await call_next(request)
@blocked_app.get('/data')
def get_data():
    return {'data': 'secret'}
blocked_client = TestClient(blocked_app)
print(blocked_client.get('/data').status_code)
print(blocked_client.get('/data', headers={'user-agent': 'blocked-bot'}).status_code)
200
403

Part 4: Multiple Middleware - Execution Order#

events = []
order_app = FastAPI()
@order_app.middleware('http')
async def first_registered(request, call_next):
    events.append('first-before')
    response = await call_next(request)
    events.append('first-after')
    return response
@order_app.middleware('http')
async def second_registered(request, call_next):
    events.append('second-before')
    response = await call_next(request)
    events.append('second-after')
    return response
@order_app.get('/ping')
def ping():
    events.append('endpoint')
    return {'status': 'ok'}
order_client = TestClient(order_app)
order_client.get('/ping')
print(events)
['second-before', 'first-before', 'endpoint', 'first-after', 'second-after']

Part 5: CORS - the Problem It Solves#

cors_app = FastAPI()
@cors_app.get('/data')
def cors_data():
    return {'ok': True}
cors_client = TestClient(cors_app)
response = cors_client.get('/data', headers={'Origin': 'https://example.com'})
print('access-control-allow-origin' in response.headers)
False

Part 6: Adding CORSMiddleware#

from fastapi.middleware.cors import CORSMiddleware
cors_app2 = FastAPI()
@cors_app2.get('/data')
def cors_data2():
    return {'ok': True}
cors_app2.add_middleware(
    CORSMiddleware,
    allow_origins=['https://example.com'],
    allow_methods=['GET'],
    allow_headers=['*'],
)
cors_client2 = TestClient(cors_app2)
response = cors_client2.get('/data', headers={'Origin': 'https://example.com'})
print(response.headers['access-control-allow-origin'])
https://example.com

Part 7: A Disallowed Origin#

response = cors_client2.get('/data', headers={'Origin': 'https://evil.com'})
print(response.status_code)
print('access-control-allow-origin' in response.headers)
200
False

Part 8: Preflight Requests - OPTIONS#

preflight_ok = cors_client2.options(
    '/data',
    headers={'Origin': 'https://example.com', 'Access-Control-Request-Method': 'GET'},
)
print(preflight_ok.status_code)
preflight_blocked = cors_client2.options(
    '/data',
    headers={'Origin': 'https://evil.com', 'Access-Control-Request-Method': 'GET'},
)
print(preflight_blocked.status_code)
200
400

Part 9: allow_credentials#

creds_app = FastAPI()
@creds_app.get('/me')
def whoami():
    return {'user': 'alex'}
creds_app.add_middleware(
    CORSMiddleware,
    allow_origins=['https://example.com'],
    allow_credentials=True,
    allow_methods=['GET'],
)
creds_client = TestClient(creds_app)
response = creds_client.get('/me', headers={'Origin': 'https://example.com'})
print(response.headers.get('access-control-allow-credentials'))
true

Part 10: A Real Pattern - CORS Plus a Custom Logging Middleware#

production_log = []
prod_app = FastAPI()
prod_app.add_middleware(
    CORSMiddleware,
    allow_origins=['https://myfrontend.com'],
    allow_methods=['GET', 'POST'],
    allow_headers=['*'],
)
@prod_app.middleware('http')
async def request_logger(request, call_next):
    response = await call_next(request)
    production_log.append((request.method, request.url.path, response.status_code))
    return response
@prod_app.get('/api/status')
def api_status():
    return {'status': 'healthy'}
prod_client = TestClient(prod_app)
print(prod_client.get('/api/status', headers={'Origin': 'https://myfrontend.com'}).json())
print(production_log)
{'status': 'healthy'}
[('GET', '/api/status', 200)]

Wrap-Up: What You Learned#

  • Middleware wraps around every request, running code both before and after the endpoint.
  • Middleware can modify the response, like adding a custom header, without touching the endpoint.
  • Returning a response directly, without calling call_next, short-circuits the request entirely.
  • Multiple middleware stack like onion layers: last registered runs its before-code first.
  • CORS headers are what let a browser permit JavaScript to read a cross-origin response.
  • CORSMiddleware with allow_origins attaches the headers a browser checks automatically.
  • A disallowed origin still gets a 200 on a simple GET; the browser enforces the missing header.
  • A browser sends an OPTIONS preflight for complex requests, which CORSMiddleware answers directly.
  • allow_credentials permits cookies and auth headers cross-origin, and requires an explicit origin.
  • That wraps up middleware and CORS. Next up: Error Handling.

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.