[FastAPI Series 16] Configuring CORS and a Basic Security Layer

한국어 버전

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

  1. 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.
  2. Origin: The protocol + domain + port tuple that becomes the unit of your CORS allowlist.
  3. CORSMiddleware: Provided by FastAPI/Starlette to declare allowed origins, methods, and headers, while taking care of preflight responses.
  4. Security headers: Short response headers such as X-Frame-Options and X-Content-Type-Options that proactively block clickjacking and MIME sniffing.
  5. Rate limiting: Policies that cap how many times the same user or IP can call an endpoint per time window. Libraries such as slowapi or 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 present

Seeing 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

BrowserCDNAPI GatewayFastAPIDB cached static

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 CORSMiddleware and SecurityMiddleware (or the custom header middleware) with the correct domains and headers.
  • Extend: from the optional section, add slowapi or 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-headers to 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.

💬 댓글

이 글에 대한 의견을 남겨주세요