If you build APIs with FastAPI, you’re going to touch CORS almost immediately. Usually right after your frontend starts throwing blocked by CORS policy in DevTools and everyone suddenly becomes a browser networking expert.

FastAPI makes CORS easy enough to turn on, but the hard part is choosing the right setup. There’s a big difference between “make the error go away” and “configure cross-origin access without creating a mess.”

Here’s the practical comparison guide I wish more teams used.

The baseline: FastAPI’s built-in CORSMiddleware

FastAPI relies on Starlette’s CORSMiddleware, and for most apps, that’s the right starting point.

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

app = FastAPI()

origins = [
    "https://app.example.com",
    "https://admin.example.com",
]

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

This handles:

  • Access-Control-Allow-Origin
  • preflight OPTIONS requests
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Headers
  • Access-Control-Allow-Credentials
  • Access-Control-Expose-Headers

For a lot of teams, this is enough.

Option 1: Allow specific origins

This is the sane default for production.

app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "https://app.example.com",
        "https://staging.example.com",
    ],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PATCH", "DELETE"],
    allow_headers=["Authorization", "Content-Type"],
)

Pros

  • Tight control over who can call your API from a browser
  • Works well with cookie auth and session-based flows
  • Easy to reason about during audits
  • Lower chance of accidentally exposing authenticated endpoints cross-origin

Cons

  • Annoying in multi-environment setups
  • Easy to forget preview URLs, local dev ports, or staging domains
  • Can become config sprawl if your frontend deployment model changes often

My take

If your API is used by one or two known browser apps, do this and move on. It’s boring, and boring is good in security config.

Option 2: Wildcard origin with *

This is common for public APIs, and sometimes exactly the right choice.

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=False,
    allow_methods=["GET"],
    allow_headers=["*"],
)

GitHub is a useful real-world example here. api.github.com returns:

access-control-allow-origin: *
access-control-expose-headers: ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset

That makes sense for a broadly consumable API. It’s public, heavily documented, and intentionally usable from many browser clients.

Pros

  • Dead simple
  • Great for public, read-only APIs
  • No need to maintain origin lists
  • Fewer deployment headaches

Cons

  • Wrong choice for cookie-authenticated APIs
  • Sends a broad signal that any site can make browser-based requests
  • Can hide bad API boundary decisions
  • You cannot combine allow_credentials=True with wildcard origins in the browser model

My take

If your API is public and doesn’t rely on browser credentials, wildcard CORS is fine. I’d still keep methods and headers tighter than * unless I have a strong reason not to.

Option 3: Regex-based origins

FastAPI also supports allow_origin_regex, which is handy when you have many subdomains.

app.add_middleware(
    CORSMiddleware,
    allow_origin_regex=r"https://.*\.example\.com",
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Pros

  • Cleaner than maintaining a giant allowlist
  • Good for tenant subdomains
  • Useful for ephemeral preview deployments if your naming is predictable

Cons

  • Easy to get wrong
  • Regex mistakes become security bugs
  • Harder for other developers to verify at a glance
  • Broad patterns age badly

My take

I use regex sparingly. It feels elegant until someone adds foo.example.com.evil-site.net to a poorly anchored pattern or broadens it without understanding the fallout. If you do this, keep the regex strict and tested.

A safer pattern would be something like:

allow_origin_regex=r"^https://([a-z0-9-]+\.)?example\.com$"

Even then, I prefer explicit allowlists when the number of origins is manageable.

Option 4: Dynamic origin reflection

Some teams want to check the Origin header at runtime against a database, config service, or tenant registry. FastAPI’s stock middleware doesn’t really give you a rich policy engine for that, so people write custom middleware.

Example:

from fastapi import FastAPI, Request
from starlette.responses import Response

app = FastAPI()

ALLOWED_ORIGINS = {
    "https://app.example.com",
    "https://tenant1.example.com",
    "https://tenant2.example.com",
}

@app.middleware("http")
async def custom_cors(request: Request, call_next):
    response = await call_next(request)
    origin = request.headers.get("origin")

    if origin in ALLOWED_ORIGINS:
        response.headers["Access-Control-Allow-Origin"] = origin
        response.headers["Vary"] = "Origin"
        response.headers["Access-Control-Allow-Credentials"] = "true"
        response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS"
        response.headers["Access-Control-Allow-Headers"] = "Authorization, Content-Type"

    if request.method == "OPTIONS" and origin in ALLOWED_ORIGINS:
        return Response(
            status_code=204,
            headers={
                "Access-Control-Allow-Origin": origin,
                "Vary": "Origin",
                "Access-Control-Allow-Credentials": "true",
                "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
                "Access-Control-Allow-Headers": "Authorization, Content-Type",
            },
        )

    return response

Pros

  • Maximum flexibility
  • Works for tenant-aware SaaS cases
  • Lets you integrate policy with your app’s business logic

Cons

  • Easy to break preflight handling
  • Easy to forget Vary: Origin
  • More code, more tests, more edge cases
  • You’re now maintaining security middleware yourself

My take

I only reach for custom CORS logic when there’s a real multi-tenant need. Otherwise it’s self-inflicted complexity. The built-in middleware is less glamorous and much harder to screw up.

Credentials: where most CORS mistakes happen

If you use cookies or browser-managed auth, CORS gets stricter fast.

This combination is invalid for browsers:

allow_origins=["*"]
allow_credentials=True

If credentials are allowed, the server must return a specific origin, not *.

That means this is the pattern you want:

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com"],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["Authorization", "Content-Type", "X-CSRF-Token"],
)

And no, CORS is not CSRF protection. People still blur those together. CORS controls which browser origins can read responses. CSRF is about whether a browser can be tricked into sending authenticated requests. Different problem entirely.

Exposed headers: useful and often forgotten

Browsers don’t automatically expose every response header to frontend JavaScript. If your app needs to read custom headers, add them explicitly.

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://app.example.com"],
    allow_credentials=True,
    allow_methods=["GET"],
    allow_headers=["Authorization", "Content-Type"],
    expose_headers=["X-Request-Id", "X-RateLimit-Remaining"],
)

GitHub does this well. Their exposed header list includes practical stuff like ETag, Link, Retry-After, and rate limit headers. That’s a good model: expose what clients actually need, not every header you happen to send.

Dev vs prod: don’t copy-paste the same CORS policy

A common pattern:

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    environment: str = "development"

settings = Settings()

if settings.environment == "development":
    origins = ["http://localhost:3000", "http://127.0.0.1:5173"]
else:
    origins = ["https://app.example.com"]

app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

That’s fine. What I don’t like is teams leaving dev wildcards in production because “we’ll tighten it later.” They won’t.

Which approach should you choose?

Here’s the short version:

Specific origin allowlist

Best for:

  • internal apps
  • SPAs with login
  • admin dashboards
  • anything using cookies

Tradeoff:

  • more maintenance, better security

Wildcard *

Best for:

  • public APIs
  • read-only browser access
  • anonymous endpoints

Tradeoff:

  • easiest setup, weakest origin restriction

Regex origins

Best for:

  • structured subdomain fleets
  • preview environments with predictable hostnames

Tradeoff:

  • compact config, higher risk of pattern mistakes

Custom dynamic CORS

Best for:

  • multi-tenant SaaS with per-tenant browser origins
  • advanced runtime policy needs

Tradeoff:

  • maximum flexibility, maximum foot-gun potential

A few practical rules I stick to

  1. Default to explicit origins in production.
  2. Avoid custom middleware unless the built-in one genuinely can’t model your policy.
  3. If credentials are involved, never use wildcard origins.
  4. Expose only the headers your frontend actually reads.
  5. Test preflight requests, not just normal GETs.

When I want to quickly inspect a live API’s headers or verify whether a preflight response is sane, I’ll use a header inspection tool like headertest.com. Saves time versus manually piecing everything together from browser tabs.

And if your conversation starts drifting into broader response hardening, CORS is only one piece. Headers like CSP, HSTS, and X-Content-Type-Options matter too. If you’re tuning the full header set, csp-guide.com is worth a look for the CSP side of the house.

CORS in FastAPI is not hard. Choosing the least dangerous configuration for your app is the real job. If the API is private or authenticated, keep the origin list tight. If it’s public, wildcard may be perfectly reasonable. Just make that decision on purpose, not because the browser yelled at you five minutes before deploy.