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:3000callshttp://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.py → INSTALLED_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
- [ ]
corsheadersis installed, inINSTALLED_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
- Protect routes →
django-ninja-auth.md(decide API key vs session; CORS credentials follow that choice) - New feature module →
django-second-app.md