{"id":50428156,"url":"https://github.com/martincastroalvarez/django-admin-rest-api","last_synced_at":"2026-06-03T15:00:33.581Z","repository":{"id":360939096,"uuid":"1252300130","full_name":"MartinCastroAlvarez/django-admin-rest-api","owner":"MartinCastroAlvarez","description":"Make your Django admin headless in two lines: add it to INSTALLED_APPS, include the URLs. Every ModelAdmin you've already registered becomes a JSON REST endpoint — same permissions, same forms, same audit log. No parallel auth, no parallel validation, no rewrites. A drop-in API surface for React frontends, MCP servers, and any other JSON client.","archived":false,"fork":false,"pushed_at":"2026-06-02T00:04:41.000Z","size":1808,"stargazers_count":5,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-02T14:27:39.763Z","etag":null,"topics":["admin","api","django","django-admin","headless","json-api","open-source","plug-and-play","python","rest-api"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/django-admin-rest-api/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MartinCastroAlvarez.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-28T11:38:27.000Z","updated_at":"2026-06-02T00:05:13.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/MartinCastroAlvarez/django-admin-rest-api","commit_stats":null,"previous_names":["martincastroalvarez/django-admin-rest-api"],"tags_count":22,"template":false,"template_full_name":null,"purl":"pkg:github/MartinCastroAlvarez/django-admin-rest-api","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MartinCastroAlvarez%2Fdjango-admin-rest-api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MartinCastroAlvarez%2Fdjango-admin-rest-api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MartinCastroAlvarez%2Fdjango-admin-rest-api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MartinCastroAlvarez%2Fdjango-admin-rest-api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MartinCastroAlvarez","download_url":"https://codeload.github.com/MartinCastroAlvarez/django-admin-rest-api/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MartinCastroAlvarez%2Fdjango-admin-rest-api/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33870026,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-03T02:00:06.370Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["admin","api","django","django-admin","headless","json-api","open-source","plug-and-play","python","rest-api"],"created_at":"2026-05-31T12:00:42.366Z","updated_at":"2026-06-03T15:00:33.531Z","avatar_url":"https://github.com/MartinCastroAlvarez.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# django-admin-rest-api\n\n\u003e A JSON REST API for the Django admin — **same permissions, same `ModelAdmin`, no new features.**\n\n[![PyPI version](https://img.shields.io/pypi/v/django-admin-rest-api.svg)](https://pypi.org/project/django-admin-rest-api/)\n[![Python versions](https://img.shields.io/pypi/pyversions/django-admin-rest-api.svg)](https://pypi.org/project/django-admin-rest-api/)\n[![Django versions](https://img.shields.io/badge/Django-5.0%20%7C%205.1%20%7C%205.2%20%7C%206.0-44b78b.svg)](https://www.djangoproject.com/)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![Wire contract: stable](https://img.shields.io/badge/wire%20contract-stable-44b78b.svg)](CHANGELOG.md)\n[![Latest on Django Packages](https://img.shields.io/badge/PyPI-django--admin--rest--api--tags-8c3c26.svg)](https://djangopackages.org/packages/p/django-admin-rest-api/)\n\n`django-admin-rest-api` exposes every `ModelAdmin` you've already\nregistered on `django.contrib.admin.site` (or your own `AdminSite`)\nthrough a JSON REST API — **without** introducing a parallel\npermission system, a parallel form layer, or any features the Django\nadmin itself doesn't have.\n\nIt is the wire surface that lets these projects drive your admin:\n\n| Project | Role | PyPI |\n| --- | --- | --- |\n| 🟦 [`django-admin-react`](https://github.com/MartinCastroAlvarez/django-admin-react) | React single-page admin frontend | [`django-admin-react`](https://pypi.org/project/django-admin-react/) |\n| 🟩 **`django-admin-rest-api`** *(this repo)* | JSON REST API over `ModelAdmin` | [`django-admin-rest-api`](https://pypi.org/project/django-admin-rest-api/) |\n| 🟪 [`django-admin-mcp`](https://github.com/MartinCastroAlvarez/django-admin-mcp) | MCP server exposing the same API to LLMs | *(coming soon)* |\n\n---\n\n## ✨ The one design principle\n\n**This package adds no new behavior. It is a JSON wrapper.**\n\nThat means every one of these is owned by your existing Django setup —\nnot by this library:\n\n- 🔐 **Authentication** — Django's session + login. The API enforces the\n  same `is_active` + `is_staff` + `AdminSite.has_permission` gate the\n  HTML admin uses. No tokens, no custom auth backends, no JWTs.\n- 🛡️ **Authorization / permissions** — every endpoint calls the\n  matching `ModelAdmin.has_view_permission` / `has_add_permission` /\n  `has_change_permission` / `has_delete_permission`. If your admin\n  says no, the API says 403.\n- 📋 **Field validation** — `POST` / `PATCH` route the payload through\n  the same `ModelForm` Django would render in the HTML admin\n  (`ModelAdmin.get_form(request, obj)`), so every clean method, every\n  `unique_together` constraint, every custom widget validator runs\n  exactly once and exactly the same way.\n- ⚙️ **Actions** — the action registry comes from\n  `ModelAdmin.get_actions(request)`. Your custom action functions run\n  unmodified. **One declaration, two surfaces:** the signature of each\n  action's third parameter chooses where it shows up in the SPA — a\n  `queryset` (or `QuerySet`-annotated) param surfaces it on the\n  changelist; an `obj_id` / `pk` / `id` param (or a `str`/`int`/`Model`\n  annotation) surfaces it on the single-object detail page. No\n  third-party dependency, no separate declaration list. See\n  [⚙️ Configuration](#%EF%B8%8F-configuration) below.\n- 🔎 **Search \u0026 filters** — search uses\n  `ModelAdmin.get_search_results(request, queryset, term)`; filters\n  use `ModelAdmin.list_filter`. No parallel implementation.\n- 📜 **Audit log** — writes go through Django's `LogEntry` so your\n  history page (and every other consumer of `LogEntry`) keeps working.\n- 🌐 **CSRF \u0026 sessions** — Django's middleware. Nothing is\n  `@csrf_exempt`.\n\nIf a behavior isn't in the HTML admin, it isn't here. If it is in the\nHTML admin, this library exposes it over JSON.\n\n---\n\n## 📓 Example consumer project\n\nA copy-pasteable Django project that consumes the package lives at\n[`examples/minimal_project/`](examples/minimal_project/). Use it to\nverify the install before adding `django-admin-rest-api` to a real\nproject, or as a reference for the two-line wiring + a custom\n`ModelAdmin` with both `batch` and `detail` actions.\n\n## 🚀 Plug-and-play install\n\n```bash\npip install django-admin-rest-api\n```\n\nTwo changes to your project:\n\n```python\n# settings.py\nINSTALLED_APPS = [\n    # ... your existing apps ...\n    \"django.contrib.admin\",\n    \"django_admin_rest_api\",          # ← add\n]\n```\n\n```python\n# urls.py\nfrom django.contrib import admin\nfrom django.urls import include, path\n\nurlpatterns = [\n    path(\"admin/\", admin.site.urls),\n    path(\"admin-api/\", include(\"django_admin_rest_api.urls\")),  # ← add\n]\n```\n\nThat's it. Your admin is now also a JSON API at `/admin-api/api/v1/...`.\n\n---\n\n## 📡 The endpoints\n\n| Method  | Path                                           | What it returns                                          |\n| ------- | ---------------------------------------------- | -------------------------------------------------------- |\n| `GET`   | `/api/v1/registry/`                            | The same app/model tree Django renders in the admin index |\n| `GET`   | `/api/v1/schema/`                              | OpenAPI 3.1 schema of every endpoint below                |\n| `GET`   | `/api/v1/\u003capp\u003e/\u003cmodel\u003e/`                       | List + pagination + filters + search                      |\n| `POST`  | `/api/v1/\u003capp\u003e/\u003cmodel\u003e/`                       | Create (runs the same `ModelForm`)                        |\n| `GET`   | `/api/v1/\u003capp\u003e/\u003cmodel\u003e/\u003cpk\u003e/`                  | Detail (read view as the HTML admin renders it)           |\n| `PATCH` | `/api/v1/\u003capp\u003e/\u003cmodel\u003e/\u003cpk\u003e/`                  | Update                                                    |\n| `DELETE`| `/api/v1/\u003capp\u003e/\u003cmodel\u003e/\u003cpk\u003e/`                  | Destroy (with `LogEntry`)                                 |\n| `POST`  | `/api/v1/\u003capp\u003e/\u003cmodel\u003e/bulk-update/`           | Bulk patch                                                |\n| `POST`  | `/api/v1/\u003capp\u003e/\u003cmodel\u003e/delete-preview/`        | Cascade preview (like the HTML admin's confirm page)      |\n| `GET`   | `/api/v1/\u003capp\u003e/\u003cmodel\u003e/autocomplete/?q=…`      | `ModelAdmin.autocomplete_fields` source                   |\n| `POST`  | `/api/v1/\u003capp\u003e/\u003cmodel\u003e/actions/\u003cname\u003e/`        | Run a `ModelAdmin` action; one endpoint serves both shapes (batch / detail) — the runner inspects the callable's signature and either passes the user-narrowed `QuerySet` or `str(pk)` for the single selected row |\n| `GET`   | `/api/v1/\u003capp\u003e/\u003cmodel\u003e/\u003cpk\u003e/history/`          | The `LogEntry` history for one object                     |\n| `GET`   | `/api/v1/recent-actions/`                      | The dashboard's \"Recent Actions\" feed                     |\n| `POST`  | `/api/v1/login/`                               | Same `authenticate` + `login` as the HTML admin           |\n| `POST`  | `/api/v1/logout/`                              | Same `logout`                                             |\n| `POST`  | `/api/v1/\u003capp\u003e/\u003cmodel\u003e/\u003cpk\u003e/password/`         | JSON mirror of `UserAdmin`'s password-change page (`AdminPasswordChangeForm` + `AUTH_PASSWORD_VALIDATORS` + `set_password`); 404 unless the model's admin declares `change_password_form`; gated by `has_change_permission` |\n\nEvery endpoint enforces the same permission gates as the HTML admin.\n\n---\n\n## 📸 Screenshots\n\nThe JSON `registry` endpoint — the source-of-truth for any consumer\nfrontend:\n\n![Registry endpoint JSON response](docs/screenshots/06-registry-api-json.png)\n\nAnd here is the same admin rendered by\n[`django-admin-react`](https://github.com/MartinCastroAlvarez/django-admin-react)\non top of this API, to give you an idea of what a consumer can build:\n\n| | |\n|:-:|:-:|\n| ![SPA login](docs/screenshots/01-spa-login.png) | ![SPA registry](docs/screenshots/02-spa-registry.png) |\n| ![SPA list](docs/screenshots/03-spa-list.png) | ![SPA detail](docs/screenshots/05-spa-detail.png) |\n\n---\n\n## ⚙️ Configuration\n\nAll settings live under a single optional dict — defaults are sane,\nso most projects need no entry at all.\n\n```python\n# settings.py (all keys optional)\nDJANGO_ADMIN_REST_API = {\n    # Dotted path to the AdminSite whose ModelAdmin registry the API\n    # mirrors. Default exposes django.contrib.admin.site.\n    \"ADMIN_SITE\": \"django.contrib.admin.site\",\n\n    # Pagination. List endpoints use ModelAdmin.list_per_page as the\n    # source of truth; DEFAULT_PAGE_SIZE is the fallback. MAX_PAGE_SIZE\n    # caps ?page_size from the client (DoS guard).\n    \"DEFAULT_PAGE_SIZE\": 25,\n    \"MAX_PAGE_SIZE\": 200,\n\n    # Cap on the number of pks per `actions/\u003cname\u003e/` POST. Mirrors\n    # MAX_PAGE_SIZE's DoS-guard posture for the changelist. Set to 0\n    # (or any non-positive value) to disable the cap entirely.\n    \"MAX_ACTION_PKS\": 5000,\n\n    # When True, list responses include per-query timing in a debug\n    # block. Off by default — only enable in development.\n    \"ENABLE_PROFILING\": False,\n}\n```\n\n### Startup-time validation\n\nThe AppConfig registers three Django system checks that surface\ncommon install mistakes at `manage.py check` / `manage.py runserver`\ntime rather than as a 500 on the first request:\n\n| ID | Severity | Catches |\n|----|----------|---------|\n| `django_admin_rest_api.W001` | warning | `DJANGO_ADMIN_REST_API_*` attribute typos (the canonical dict has exactly that name; any other prefix is silently ignored otherwise). |\n| `django_admin_rest_api.E001` | error   | `ADMIN_SITE` doesn't resolve to an `AdminSite` instance. |\n| `django_admin_rest_api.W002` | warning | `CsrfViewMiddleware` / `SessionMiddleware` / `AuthenticationMiddleware` missing from `settings.MIDDLEWARE`. |\n\nYou don't have to enable them — they fire automatically on the next\n`manage.py` invocation after install.\n\n---\n\n## ⚡ Actions: one declaration, two surfaces\n\nDeclare your actions exactly the way Django docs tell you to —\n`@admin.action(description=\"…\")` plus `actions = [...]` on your\n`ModelAdmin`. The API surfaces each one in the registry, list, and\ndetail responses with a `target` field the SPA reads to decide\nwhich surface to render it on:\n\n```python\nfrom django.contrib import admin\nfrom django.db.models import QuerySet\n\n\n@admin.register(MyModel)\nclass MyAdmin(admin.ModelAdmin):\n    actions = [\"reprocess_batch\", \"reprocess_one\"]\n\n    @admin.action(description=\"Reprocess selected\")\n    def reprocess_batch(self, request, queryset: QuerySet):\n        # Shows up on the CHANGELIST (multi-select).  target=batch\n        # The runner passes the user-narrowed queryset.\n        ...\n\n    @admin.action(description=\"Reprocess this one\")\n    def reprocess_one(self, request, obj_id: str):\n        # Shows up on the DETAIL page only.            target=detail\n        # The runner passes str(pk) for the row in view.\n        ...\n```\n\nBoth actions reach the same endpoint\n(`POST /api/v1/\u003capp\u003e/\u003cmodel\u003e/actions/\u003cname\u003e/`). The runner inspects the\ncallable's third parameter — its **name** (`queryset` / `obj_id` / `pk`\n/ `id` / …) and its **type annotation** (`QuerySet` / `str` / `int` /\n`Model` subclass) — and dispatches to the right shape.\n\nPermissions stay the same (`has_change_permission` per object). No\n`django-object-actions`, no parallel declaration list, no new\nconfiguration.\n\n---\n\n## 🔒 Security\n\n- The API is **not** a parallel auth surface. It refuses any caller\n  the HTML admin would refuse, with the same gate\n  (`AdminSite.has_permission`, plus the per-model `ModelAdmin.has_*_permission`).\n- Anonymous → `403` for every data endpoint.\n- Authenticated but non-staff → `403`. Cookie present but resolved\n  user is anonymous → `403 not_authenticated`.\n- Writes always go through `ModelForm.is_valid()` —\n  `unique_together`, `clean()`, field validators all run.\n- Per-object guards run **before** the form does anything. The\n  `delete-preview` and `delete` endpoints both check `has_delete_permission(obj)`.\n- CSRF is enforced everywhere. No view in this package is\n  `@csrf_exempt`. The login endpoint requires the CSRF cookie set\n  by the consumer's shell.\n- **DoS guard on the actions runner.** `MAX_ACTION_PKS` (default\n  `5000`) caps the selection size of one action POST. Crafted\n  large-selection requests return `400` instead of pinning a\n  worker on an expensive action.\n- **Audit-log field-name redaction.** The history endpoint's\n  `change_message_structured` strips field names matching the\n  sensitive-name denylist (`password`, `token`, `secret`,\n  `api_key`, …) so the audit log can't be used as an oracle for\n  which sensitive fields were touched.\n\nSee [`SECURITY.md`](SECURITY.md) for the full threat model and the\nupstream\n[`django-admin-react` SECURITY.md](https://github.com/MartinCastroAlvarez/django-admin-react/blob/main/SECURITY.md)\nfor the React-side surface — the API surface is identical and the\nguarantees transfer 1:1.\n\n### Recommended: rate-limit the auth + password endpoints\n\nThe login and password endpoints are deliberately **not** rate-limited\nby this package — the HTML admin isn't either, and we don't want to\nduplicate behavior. **But you still need rate limiting in production.**\nA typical Django shop already has `django-axes` or `django-ratelimit`\ndeployed against `/admin/login/`; the parallel JSON endpoint needs the\nsame protection.\n\n**Option A: `django-axes`** (account-lockout-on-failed-attempts):\n\n```python\n# settings.py\nINSTALLED_APPS += [\"axes\"]\nMIDDLEWARE += [\n    # Must come AFTER AuthenticationMiddleware:\n    \"axes.middleware.AxesMiddleware\",\n]\nAUTHENTICATION_BACKENDS = [\n    \"axes.backends.AxesStandaloneBackend\",\n    \"django.contrib.auth.backends.ModelBackend\",\n]\nAXES_FAILURE_LIMIT = 5\nAXES_COOLOFF_TIME = 1  # hours\n```\n\naxes works without any package-specific config — it gates Django's\n`authenticate()` call, which is exactly the path\n`/api/v1/login/` runs through.\n\n**Option B: `django-ratelimit`** (request-per-window):\n\nWrap the package's URL include with a ratelimited dispatcher in your\nproject's `urls.py`:\n\n```python\n# your_project/urls.py\nfrom django.urls import include, path\nfrom django_ratelimit.decorators import ratelimit\nfrom django.views.decorators.csrf import csrf_protect\nfrom django.views.generic import View\n\n# 5 login attempts per minute per IP\nclass RateLimitedAuthView(View):\n    @ratelimit(key=\"ip\", rate=\"5/m\", block=True)\n    def dispatch(self, request, *args, **kwargs):\n        from django_admin_rest_api.api.views.auth import LoginView\n        return LoginView.as_view()(request, *args, **kwargs)\n\nurlpatterns = [\n    path(\"admin/\", admin.site.urls),\n    path(\"admin-api/api/v1/login/\", RateLimitedAuthView.as_view()),\n    path(\"admin-api/\", include(\"django_admin_rest_api.urls\")),\n]\n```\n\n(The literal `login/` path must come BEFORE the package include so\nDjango's URL resolver hits the ratelimited dispatcher first.)\n\nWhichever you pick, deploy it on day one — there is no reason to wait\nfor the first brute-force attempt.\n\n---\n\n## 🧪 Local development\n\n```bash\ngit clone https://github.com/MartinCastroAlvarez/django-admin-api\ncd django-admin-api\npoetry install\npoetry run pytest\npoetry run ruff check .\npoetry run black --check .\npoetry run mypy django_admin_rest_api\npoetry run bandit -c pyproject.toml -r django_admin_rest_api\n```\n\nThe test suite uses `pytest-django` + an in-memory SQLite database, so\nno setup beyond `poetry install`.\n\n### Smoke-test the install on a real project\n\nAfter dropping the package into your own Django project, run:\n\n```bash\npython manage.py admin_rest_api_check\n```\n\nIt validates the configured `ADMIN_SITE`, the required middleware,\nand lists every registered `ModelAdmin` with its action count\n(`batch` / `detail` breakdown). Exits non-zero on any problem — also\nuseful as a CI / deploy preflight.\n\n### Wire-contract reference\n\nThe JSON shape of every endpoint is documented in\n[`docs/api-contract.md`](docs/api-contract.md). It is stable under\nsemver: any rename, removal, or type change of a documented field\nrequires a major version bump.\n\n---\n\n## 🤝 Contributing\n\nIssues, PRs, and Discussions are welcome on GitHub:\n\u003chttps://github.com/MartinCastroAlvarez/django-admin-api\u003e.\n\nThe lint + security gate is the same set the upstream\n`django-admin-react` repo uses: **ruff, black, isort, flake8,\npylint, mypy, bandit, pip-audit, gitleaks.** Every change must pass\nall of them before merge.\n\n---\n\n## 📜 License\n\nMIT. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmartincastroalvarez%2Fdjango-admin-rest-api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmartincastroalvarez%2Fdjango-admin-rest-api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmartincastroalvarez%2Fdjango-admin-rest-api/lists"}