When your API finally meets a real web frontend the first wall is almost always CORS (Cross-Origin Resource Sharing). Browsers still send the HTTP request to your server, but they refuse to hand the response to JavaScript unless the response includes the right headers. Basic security headers look like tiny strings on every response, yet they stop clickjacking, MIME sniffing, and other attacks. In this lesson we set up safe browser communication, add baseline headers, and introduce the idea of rate limiting.
Key terms
- CORS: Browser rules that decide whether a different origin is allowed to call your API. The server still runs, but the browser discards the response if the origin is not permitted.
- Origin: The
protocol + domain + porttuple that becomes the unit of your CORS allowlist. - CORSMiddleware: Provided by FastAPI/Starlette to declare allowed origins, methods, and headers, while taking care of preflight responses.
- Security headers: Short response headers such as
X-Frame-OptionsandX-Content-Type-Optionsthat proactively block clickjacking and MIME sniffing. - Rate limiting: Policies that cap how many times the same user or IP can call an endpoint per time window. Libraries such as
slowapior an API Gateway usually implement it.
Practice card
- Estimated time: 50 minutes (core) / +25 minutes (optional)
- Prereqs: Episode 15 test project, browser DevTools
- Goal: Allow only the required domains through CORS, attach baseline security headers, and—time permitting—experiment with rate limiting or proxy settings
Hands-on: configuring CORS and security headers
We keep the required section tight so it fits inside an hour. Everything else moves to the “Optional expansion” section.
CORS middleware
FastAPI reuses Starlette’s CORSMiddleware as-is.
from fastapi.middleware.cors import CORSMiddleware
origins = [
"https://app.example.com",
"https://admin.example.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["Authorization", "Content-Type"],
allow_credentials=True,
expose_headers=["X-RateLimit-Remaining"],
max_age=3600,
)
allow_credentials=True is required whenever the client sends cookies or other credentials (for example, an Authorization header). When this flag is on the allow_origins value must be an explicit list—browsers reject responses that combine credentials with a wildcard (*). Wildcards are still valid for allow_methods or allow_headers. Because allowlists change between environments, pull them from APP_ALLOWED_ORIGINS instead of hardcoding.
Choose one middleware style
app.add_middleware(SomeMiddleware, ...)— easiest way to layer built-in middleware after creating the app.FastAPI(middleware=[Middleware(...)])— passes a list up front and keeps initialization in one place.@app.middleware("http")— for custom logic that must run for every request.
The order matters: middleware declared earlier wraps later ones. Pick a single style per concern so the flow stays predictable.
🔎 When CORS breaks:Browser console example
Access to fetch at 'https://api.example.com' from origin 'https://app.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is presentSeeing this error means the server responded, but the browser threw the body away.
Understand preflight
Before sending certain requests (custom headers, non-simple verbs like PUT/DELETE, or Content-Type other than JSON/form), browsers automatically send an OPTIONS request to ask for permission. This is the preflight. The server must answer with Access-Control-Allow-* headers listing the permitted methods and headers. CORSMiddleware replies for you and lets you control how long the browser caches the answer via max_age (in seconds). Use the DevTools Network tab when you want to inspect the preflight exchange.
Baseline security headers
Security headers shrink the attack surface. You can rely on Starlette’s SecurityMiddleware or add your own middleware to inject headers.
from starlette.middleware import Middleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
middleware = [
Middleware(TrustedHostMiddleware, allowed_hosts=["example.com", "*.example.com"]),
]
app = FastAPI(middleware=middleware)
app.add_middleware(HTTPSRedirectMiddleware)
@app.middleware("http")
async def add_security_headers(request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
return response
HTTPSRedirectMiddleware upgrades HTTP to HTTPS when your app terminates TLS directly. If HTTPS terminates at a reverse proxy (typical in production), configure redirects there instead so you do not get redirect loops. TrustedHostMiddleware blocks host header spoofing. The custom middleware sets:
X-Frame-Options: DENY— prevents your pages from being embedded in an<iframe>, blocking clickjacking.X-Content-Type-Options: nosniff— stops browsers from guessing MIME types for uploaded files.Referrer-Policy: strict-origin-when-cross-origin— limits the amount of URL data sent to external sites.
Optional expansion: rate limiting and proxies
Skip this section if the core practice already filled the session. Add it only when you have a spare 1–2 hours.
Rate limiting basics
Rate limiting caps how many calls the same IP or token may make within a time window. FastAPI/Starlette do not provide it out of the box, but libraries like slowapi or API Gateway layers do.
from slowapi import Limiter
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from slowapi.handlers import _rate_limit_exceeded_handler
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
@app.get("/search")
@limiter.limit("10/minute")
async def search(q: str):
return {"results": []}
10/minute means ten calls per minute. When the limit is hit the client receives HTTP 429 (Too Many Requests) along with a Retry-After header. By default get_remote_address trusts the client IP from the request directly, so it only works reliably when you pass through --proxy-headers (see below) or enforce rate limiting at the API Gateway before requests reach FastAPI.
D2: Client–proxy–API flow
In production a CDN and an API Gateway usually sit up front. They terminate TLS, apply CORS/headers, and rate-limit requests before they reach FastAPI, which then focuses on domain logic. Use the diagram as a checklist to decide which layer should own each responsibility.
uvicorn/proxy settings
HTTPS is commonly terminated by a reverse proxy (Nginx, Traefik) while FastAPI/uvicorn run on an internal network. Run uvicorn with --proxy-headers so it respects X-Forwarded-* and logs the real client IP, then set --forwarded-allow-ips to the proxy’s IP (never * in production) to prevent spoofing. Losing the client IP at the proxy layer breaks rate limiting and audit logs.
CORS and security headers are mandatory for safe frontend/API conversations. Next we will study settings and secret management strategies.
Exercises
- Follow along: apply
CORSMiddlewareandSecurityMiddleware(or the custom header middleware) with the correct domains and headers. - Extend: from the optional section, add
slowapior an API Gateway simulation to reproduce a 429 response. - Debug: use DevTools or
curl -H "Origin"to confirm both allowed and rejected cases, then turn on--proxy-headersto verify the client IP behind a load balancer. - Definition of done: allowed origins work, blocked origins show a CORS error, and you have inspected at least one rate-limiting or proxy configuration.
Wrap-up
Declaring explicit allowed domains and attaching security headers as defaults keep browser integration issues down from day one. Rate limiting and proxy settings are optional today, but keep the concepts handy so you can scale them out later.
💬 댓글
이 글에 대한 의견을 남겨주세요