A JSON REST API for the Django admin — same permissions, same ModelAdmin, no new features. Powers django-admin-react and django-admin-mcp.
Project description
django-admin-rest-api
A JSON REST API for the Django admin — same permissions, same
ModelAdmin, no new features.
django-admin-rest-api exposes every ModelAdmin you've already
registered on django.contrib.admin.site (or your own AdminSite)
through a JSON REST API — without introducing a parallel
permission system, a parallel form layer, or any features the Django
admin itself doesn't have.
It is the wire surface that lets these projects drive your admin:
| Project | Role | PyPI |
|---|---|---|
🟦 django-admin-react |
React single-page admin frontend | django-admin-react |
🟩 django-admin-rest-api (this repo) |
JSON REST API over ModelAdmin |
django-admin-rest-api |
🟪 django-admin-mcp |
MCP server exposing the same API to LLMs | (coming soon) |
✨ The one design principle
This package adds no new behavior. It is a JSON wrapper.
That means every one of these is owned by your existing Django setup — not by this library:
- 🔐 Authentication — Django's session + login. The API enforces the
same
is_active+is_staff+AdminSite.has_permissiongate the HTML admin uses. No tokens, no custom auth backends, no JWTs. - 🛡️ Authorization / permissions — every endpoint calls the
matching
ModelAdmin.has_view_permission/has_add_permission/has_change_permission/has_delete_permission. If your admin says no, the API says 403. - 📋 Field validation —
POST/PATCHroute the payload through the sameModelFormDjango would render in the HTML admin (ModelAdmin.get_form(request, obj)), so every clean method, everyunique_togetherconstraint, every custom widget validator runs exactly once and exactly the same way. - ⚙️ Actions — the action registry comes from
ModelAdmin.get_actions(request). Your custom action functions run unmodified. One declaration, two surfaces: the signature of each action's third parameter chooses where it shows up in the SPA — aqueryset(orQuerySet-annotated) param surfaces it on the changelist; anobj_id/pk/idparam (or astr/int/Modelannotation) surfaces it on the single-object detail page. No third-party dependency, no separate declaration list. See ⚙️ Configuration below. - 🔎 Search & filters — search uses
ModelAdmin.get_search_results(request, queryset, term); filters useModelAdmin.list_filter. No parallel implementation. - 📜 Audit log — writes go through Django's
LogEntryso your history page (and every other consumer ofLogEntry) keeps working. - 🌐 CSRF & sessions — Django's middleware. Nothing is
@csrf_exempt.
If a behavior isn't in the HTML admin, it isn't here. If it is in the HTML admin, this library exposes it over JSON.
📓 Example consumer project
A copy-pasteable Django project that consumes the package lives at
examples/minimal_project/. Use it to
verify the install before adding django-admin-rest-api to a real
project, or as a reference for the two-line wiring + a custom
ModelAdmin with both batch and detail actions.
🚀 Plug-and-play install
pip install django-admin-rest-api
Two changes to your project:
# settings.py
INSTALLED_APPS = [
# ... your existing apps ...
"django.contrib.admin",
"django_admin_rest_api", # ← add
]
# urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("admin-api/", include("django_admin_rest_api.urls")), # ← add
]
That's it. Your admin is now also a JSON API at /admin-api/api/v1/....
📡 The endpoints
| Method | Path | What it returns |
|---|---|---|
GET |
/api/v1/registry/ |
The same app/model tree Django renders in the admin index |
GET |
/api/v1/schema/ |
OpenAPI 3.1 schema of every endpoint below |
GET |
/api/v1/<app>/<model>/ |
List + pagination + filters + search |
POST |
/api/v1/<app>/<model>/ |
Create (runs the same ModelForm) |
GET |
/api/v1/<app>/<model>/<pk>/ |
Detail (read view as the HTML admin renders it) |
PATCH |
/api/v1/<app>/<model>/<pk>/ |
Update |
DELETE |
/api/v1/<app>/<model>/<pk>/ |
Destroy (with LogEntry) |
POST |
/api/v1/<app>/<model>/bulk-update/ |
Bulk patch |
POST |
/api/v1/<app>/<model>/delete-preview/ |
Cascade preview (like the HTML admin's confirm page) |
GET |
/api/v1/<app>/<model>/autocomplete/?q=… |
ModelAdmin.autocomplete_fields source |
POST |
/api/v1/<app>/<model>/actions/<name>/ |
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 |
GET |
/api/v1/<app>/<model>/<pk>/history/ |
The LogEntry history for one object |
GET |
/api/v1/recent-actions/ |
The dashboard's "Recent Actions" feed |
POST |
/api/v1/login/ |
Same authenticate + login as the HTML admin |
POST |
/api/v1/logout/ |
Same logout |
POST |
/api/v1/<app>/<model>/<pk>/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 |
Every endpoint enforces the same permission gates as the HTML admin.
📸 Screenshots
The JSON registry endpoint — the source-of-truth for any consumer
frontend:
And here is the same admin rendered by
django-admin-react
on top of this API, to give you an idea of what a consumer can build:
⚙️ Configuration
All settings live under a single optional dict — defaults are sane, so most projects need no entry at all.
# settings.py (all keys optional)
DJANGO_ADMIN_REST_API = {
# Dotted path to the AdminSite whose ModelAdmin registry the API
# mirrors. Default exposes django.contrib.admin.site.
"ADMIN_SITE": "django.contrib.admin.site",
# Pagination. List endpoints use ModelAdmin.list_per_page as the
# source of truth; DEFAULT_PAGE_SIZE is the fallback. MAX_PAGE_SIZE
# caps ?page_size from the client (DoS guard).
"DEFAULT_PAGE_SIZE": 25,
"MAX_PAGE_SIZE": 200,
# Cap on the number of pks per `actions/<name>/` POST. Mirrors
# MAX_PAGE_SIZE's DoS-guard posture for the changelist. Set to 0
# (or any non-positive value) to disable the cap entirely.
"MAX_ACTION_PKS": 5000,
# When True, list responses include per-query timing in a debug
# block. Off by default — only enable in development.
"ENABLE_PROFILING": False,
}
Startup-time validation
The AppConfig registers three Django system checks that surface
common install mistakes at manage.py check / manage.py runserver
time rather than as a 500 on the first request:
| ID | Severity | Catches |
|---|---|---|
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). |
django_admin_rest_api.E001 |
error | ADMIN_SITE doesn't resolve to an AdminSite instance. |
django_admin_rest_api.W002 |
warning | CsrfViewMiddleware / SessionMiddleware / AuthenticationMiddleware missing from settings.MIDDLEWARE. |
You don't have to enable them — they fire automatically on the next
manage.py invocation after install.
⚡ Actions: one declaration, two surfaces
Declare your actions exactly the way Django docs tell you to —
@admin.action(description="…") plus actions = [...] on your
ModelAdmin. The API surfaces each one in the registry, list, and
detail responses with a target field the SPA reads to decide
which surface to render it on:
from django.contrib import admin
from django.db.models import QuerySet
@admin.register(MyModel)
class MyAdmin(admin.ModelAdmin):
actions = ["reprocess_batch", "reprocess_one"]
@admin.action(description="Reprocess selected")
def reprocess_batch(self, request, queryset: QuerySet):
# Shows up on the CHANGELIST (multi-select). target=batch
# The runner passes the user-narrowed queryset.
...
@admin.action(description="Reprocess this one")
def reprocess_one(self, request, obj_id: str):
# Shows up on the DETAIL page only. target=detail
# The runner passes str(pk) for the row in view.
...
Both actions reach the same endpoint
(POST /api/v1/<app>/<model>/actions/<name>/). The runner inspects the
callable's third parameter — its name (queryset / obj_id / pk
/ id / …) and its type annotation (QuerySet / str / int /
Model subclass) — and dispatches to the right shape.
Permissions stay the same (has_change_permission per object). No
django-object-actions, no parallel declaration list, no new
configuration.
🔒 Security
- The API is not a parallel auth surface. It refuses any caller
the HTML admin would refuse, with the same gate
(
AdminSite.has_permission, plus the per-modelModelAdmin.has_*_permission). - Anonymous →
403for every data endpoint. - Authenticated but non-staff →
403. Cookie present but resolved user is anonymous →403 not_authenticated. - Writes always go through
ModelForm.is_valid()—unique_together,clean(), field validators all run. - Per-object guards run before the form does anything. The
delete-previewanddeleteendpoints both checkhas_delete_permission(obj). - CSRF is enforced everywhere. No view in this package is
@csrf_exempt. The login endpoint requires the CSRF cookie set by the consumer's shell. - DoS guard on the actions runner.
MAX_ACTION_PKS(default5000) caps the selection size of one action POST. Crafted large-selection requests return400instead of pinning a worker on an expensive action. - Audit-log field-name redaction. The history endpoint's
change_message_structuredstrips field names matching the sensitive-name denylist (password,token,secret,api_key, …) so the audit log can't be used as an oracle for which sensitive fields were touched.
See SECURITY.md for the full threat model and the
upstream
django-admin-react SECURITY.md
for the React-side surface — the API surface is identical and the
guarantees transfer 1:1.
Recommended: rate-limit the auth + password endpoints
The login and password endpoints are deliberately not rate-limited
by this package — the HTML admin isn't either, and we don't want to
duplicate behavior. But you still need rate limiting in production.
A typical Django shop already has django-axes or django-ratelimit
deployed against /admin/login/; the parallel JSON endpoint needs the
same protection.
Option A: django-axes (account-lockout-on-failed-attempts):
# settings.py
INSTALLED_APPS += ["axes"]
MIDDLEWARE += [
# Must come AFTER AuthenticationMiddleware:
"axes.middleware.AxesMiddleware",
]
AUTHENTICATION_BACKENDS = [
"axes.backends.AxesStandaloneBackend",
"django.contrib.auth.backends.ModelBackend",
]
AXES_FAILURE_LIMIT = 5
AXES_COOLOFF_TIME = 1 # hours
axes works without any package-specific config — it gates Django's
authenticate() call, which is exactly the path
/api/v1/login/ runs through.
Option B: django-ratelimit (request-per-window):
Wrap the package's URL include with a ratelimited dispatcher in your
project's urls.py:
# your_project/urls.py
from django.urls import include, path
from django_ratelimit.decorators import ratelimit
from django.views.decorators.csrf import csrf_protect
from django.views.generic import View
# 5 login attempts per minute per IP
class RateLimitedAuthView(View):
@ratelimit(key="ip", rate="5/m", block=True)
def dispatch(self, request, *args, **kwargs):
from django_admin_rest_api.api.views.auth import LoginView
return LoginView.as_view()(request, *args, **kwargs)
urlpatterns = [
path("admin/", admin.site.urls),
path("admin-api/api/v1/login/", RateLimitedAuthView.as_view()),
path("admin-api/", include("django_admin_rest_api.urls")),
]
(The literal login/ path must come BEFORE the package include so
Django's URL resolver hits the ratelimited dispatcher first.)
Whichever you pick, deploy it on day one — there is no reason to wait for the first brute-force attempt.
🧪 Local development
git clone https://github.com/MartinCastroAlvarez/django-admin-api
cd django-admin-api
poetry install
poetry run pytest
poetry run ruff check .
poetry run black --check .
poetry run mypy django_admin_rest_api
poetry run bandit -c pyproject.toml -r django_admin_rest_api
The test suite uses pytest-django + an in-memory SQLite database, so
no setup beyond poetry install.
Smoke-test the install on a real project
After dropping the package into your own Django project, run:
python manage.py admin_rest_api_check
It validates the configured ADMIN_SITE, the required middleware,
and lists every registered ModelAdmin with its action count
(batch / detail breakdown). Exits non-zero on any problem — also
useful as a CI / deploy preflight.
Wire-contract reference
The JSON shape of every endpoint is documented in
docs/api-contract.md. It is stable under
semver: any rename, removal, or type change of a documented field
requires a major version bump.
🤝 Contributing
Issues, PRs, and Discussions are welcome on GitHub: https://github.com/MartinCastroAlvarez/django-admin-api.
The lint + security gate is the same set the upstream
django-admin-react repo uses: ruff, black, isort, flake8,
pylint, mypy, bandit, pip-audit, gitleaks. Every change must pass
all of them before merge.
📜 License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_admin_rest_api-1.1.1.tar.gz.
File metadata
- Download URL: django_admin_rest_api-1.1.1.tar.gz
- Upload date:
- Size: 101.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1780f59ed648262cf67aab98b7bb8a3d7551472c03e89a319a220ed6c089bfd7
|
|
| MD5 |
e8556527e93e70f9a0e1df012bbdc795
|
|
| BLAKE2b-256 |
63d6f060f7dbd34a238dc4af72ace2f81763533b175cacda7f376c3d3de2972c
|
Provenance
The following attestation bundles were made for django_admin_rest_api-1.1.1.tar.gz:
Publisher:
publish.yml on MartinCastroAlvarez/django-admin-rest-api
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_admin_rest_api-1.1.1.tar.gz -
Subject digest:
1780f59ed648262cf67aab98b7bb8a3d7551472c03e89a319a220ed6c089bfd7 - Sigstore transparency entry: 1681280925
- Sigstore integration time:
-
Permalink:
MartinCastroAlvarez/django-admin-rest-api@353f04e558b1d80b9e66d3391bfbc20afe3c3686 -
Branch / Tag:
refs/tags/v1.1.1 - Owner: https://github.com/MartinCastroAlvarez
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@353f04e558b1d80b9e66d3391bfbc20afe3c3686 -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_admin_rest_api-1.1.1-py3-none-any.whl.
File metadata
- Download URL: django_admin_rest_api-1.1.1-py3-none-any.whl
- Upload date:
- Size: 126.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dacc9103d6a0a6c40cfa5e9e3f687681f3ce8bfe019050833eb848a4da2385d5
|
|
| MD5 |
d4e55aa6d49dcc72ba146c92907a53e4
|
|
| BLAKE2b-256 |
76b996e1d0d4b1922d08023e7200d1a9046eaab79b445c474e6cac8a7e169107
|
Provenance
The following attestation bundles were made for django_admin_rest_api-1.1.1-py3-none-any.whl:
Publisher:
publish.yml on MartinCastroAlvarez/django-admin-rest-api
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_admin_rest_api-1.1.1-py3-none-any.whl -
Subject digest:
dacc9103d6a0a6c40cfa5e9e3f687681f3ce8bfe019050833eb848a4da2385d5 - Sigstore transparency entry: 1681281003
- Sigstore integration time:
-
Permalink:
MartinCastroAlvarez/django-admin-rest-api@353f04e558b1d80b9e66d3391bfbc20afe3c3686 -
Branch / Tag:
refs/tags/v1.1.1 - Owner: https://github.com/MartinCastroAlvarez
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@353f04e558b1d80b9e66d3391bfbc20afe3c3686 -
Trigger Event:
push
-
Statement type: