CORS (after Django + uv)

Prerequisite: django-uv.md. Prefer django-environment.md so allowed origins live in .env.

End state: a browser page on another origin (e.g. http://localhost:3000) can call your API without the browser blocking the response. You verify with a real cross-origin request, not only curl.

Suggested order: after environment settings. Independent of Postgres. Do this before cookie/session auth if the frontend is on another origin — session auth needs CORS credentials configured carefully. Token/API-key auth is simpler with CORS (no cookies).

How to use this: only add CORS when you actually have a separate frontend origin. Same-origin Django templates do not need this.


0. Why you are here

Browsers enforce the same-origin policy. curl does not. So “it works in curl / Thunder Client” does not prove CORS is fine.

You need CORS when:

  • Next.js/Vite/etc. on http://localhost:3000 calls http://127.0.0.1:8000
  • Production frontend and API are on different hosts

You do not need CORS for server-to-server calls or for curl.


1. Add django-cors-headers

uv add django-cors-headers
  • [ ] Run that.

2. Register the app and middleware

In config/settings.pyINSTALLED_APPS, add:

"corsheaders",

In MIDDLEWARE, put CorsMiddleware near the top, before CommonMiddleware (typically first, or right after security):

MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.security.SecurityMiddleware",
    # …rest unchanged…
]
  • [ ] Save.

Why order matters: the middleware must add CORS headers on responses (including error responses) early enough for the browser to accept them.


3. Allow specific origins

In .env:

CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000

In settings.py (with django-environ from the environment runbook):

CORS_ALLOWED_ORIGINS = env.list("CORS_ALLOWED_ORIGINS", default=[])

Without environ:

CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",
    "http://127.0.0.1:3000",
]
  • [ ] Save. Prefer the env form.

Do not set CORS_ALLOW_ALL_ORIGINS = True except as a brief local experiment you will delete. It teaches the wrong habit for anything shared or deployed.

Add a placeholder line to .env.example if you use one.


4. Credentials (only if you need cookies)

Default for API-key / Bearer token frontends:

CORS_ALLOW_CREDENTIALS = False

If you will use cookie session auth from another origin (see django-ninja-auth.md), you will need:

CORS_ALLOW_CREDENTIALS = True

…and the frontend must call fetch with credentials: "include". Origins must be explicit (you cannot combine “allow all origins” with credentials).

  • [ ] Leave credentials False unless you know you need cookies cross-origin.

5. Prove it

Start the API:

uv run python manage.py runserver

Check 1 — preflight-ish header on a normal GET (from another terminal):

curl -i -H "Origin: http://localhost:3000" \
  http://127.0.0.1:8000/api/health
  • [ ] Response includes Access-Control-Allow-Origin: http://localhost:3000 (or your configured origin).

Check 2 — browser (best proof): from a page or Vite app on http://localhost:3000, run:

fetch("http://127.0.0.1:8000/api/health")
  .then((r) => r.json())
  .then(console.log)
  .catch(console.error);
  • [ ] Console shows {"status":"ok"}, not a CORS error.

If you do not have a frontend yet, check 1 is enough to finish this runbook; re-verify when the UI exists.


Checkpoint — you are done when

  • [ ] corsheaders is installed, in INSTALLED_APPS, and middleware is ordered correctly
  • [ ] Allowed origins are explicit (preferably from env)
  • [ ] A request with Origin: … gets the matching ACAO header

Stop. Do not open every header/method “just in case.” Defaults cover typical JSON APIs; widen only when the browser error names a missing header/method.


If you get stuck

Symptom Likely cause
No Access-Control-Allow-Origin Middleware missing/wrong order, or Origin not in allow list
Works in curl, fails in browser You never sent an Origin in curl, or used a different origin than configured
Credentials / cookie issues Need CORS_ALLOW_CREDENTIALS = True and explicit origins and credentials: "include"
null origin Opening an HTML file via file:// — serve the frontend over http://localhost:…

Next