# Django Admin Rest Api

> 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.

- **Type:** MCP server
- **Install:** `agentstack add mcp-martincastroalvarez-django-admin-rest-api`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [MartinCastroAlvarez](https://agentstack.voostack.com/s/martincastroalvarez)
- **Installs:** 0
- **Category:** [Integrations](https://agentstack.voostack.com/c/integrations)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [MartinCastroAlvarez](https://github.com/MartinCastroAlvarez)
- **Source:** https://github.com/MartinCastroAlvarez/django-admin-rest-api
- **Website:** https://pypi.org/project/django-admin-rest-api/

## Install

```sh
agentstack add mcp-martincastroalvarez-django-admin-rest-api
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# django-admin-rest-api

> A JSON REST API for the Django admin — **same permissions, same `ModelAdmin`, no new features.**

[](https://pypi.org/project/django-admin-rest-api/)
[](https://pypi.org/project/django-admin-rest-api/)
[](https://www.djangoproject.com/)
[](LICENSE)
[](CHANGELOG.md)
[](https://djangopackages.org/packages/p/django-admin-rest-api/)

`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`](https://github.com/MartinCastroAlvarez/django-admin-react) | React single-page admin frontend | [`django-admin-react`](https://pypi.org/project/django-admin-react/) |
| 🟩 **`django-admin-rest-api`** *(this repo)* | JSON REST API over `ModelAdmin` | [`django-admin-rest-api`](https://pypi.org/project/django-admin-rest-api/) |
| 🟪 [`django-admin-mcp`](https://github.com/MartinCastroAlvarez/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_permission` gate 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` / `PATCH` route the payload through
  the same `ModelForm` Django would render in the HTML admin
  (`ModelAdmin.get_form(request, obj)`), so every clean method, every
  `unique_together` constraint, 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 — a
  `queryset` (or `QuerySet`-annotated) param surfaces it on the
  changelist; an `obj_id` / `pk` / `id` param (or a `str`/`int`/`Model`
  annotation) surfaces it on the single-object detail page. No
  third-party dependency, no separate declaration list. See
  [⚙️ Configuration](#%EF%B8%8F-configuration) below.
- 🔎 **Search & filters** — search uses
  `ModelAdmin.get_search_results(request, queryset, term)`; filters
  use `ModelAdmin.list_filter`. No parallel implementation.
- 📜 **Audit log** — writes go through Django's `LogEntry` so your
  history page (and every other consumer of `LogEntry`) 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/`](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

```bash
pip install django-admin-rest-api
```

Two changes to your project:

```python
# settings.py
INSTALLED_APPS = [
    # ... your existing apps ...
    "django.contrib.admin",
    "django_admin_rest_api",          # ← add
]
```

```python
# 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///`                       | List + pagination + filters + search                      |
| `POST`  | `/api/v1///`                       | Create (runs the same `ModelForm`)                        |
| `GET`   | `/api/v1////`                  | Detail (read view as the HTML admin renders it)           |
| `PATCH` | `/api/v1////`                  | Update                                                    |
| `DELETE`| `/api/v1////`                  | Destroy (with `LogEntry`)                                 |
| `PATCH` | `/api/v1///bulk/`                  | Bulk patch                                                |
| `GET`   | `/api/v1////delete-preview/`   | Cascade preview (like the HTML admin's confirm page)      |
| `GET`   | `/api/v1///autocomplete/?q=…`      | `ModelAdmin.autocomplete_fields` source                   |
| `POST`  | `/api/v1///actions//`        | 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////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////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.

---

## 🧩 ModelAdmin carry-through status

The package duck-types `ModelAdmin` and surfaces a wide slice of its
configuration on the wire. This table is the honest at-a-glance answer to
"will my gnarly admin just work?" — **Honored** (carried through and
tested), **Partial** (surfaced with a documented caveat), **Not yet**
(no signal emitted today).

| ModelAdmin hook | Status | Notes |
| --------------- | ------ | ----- |
| `list_display` (+ `@admin.display` callables) | Honored | Methods resolve via `lookup_field`. |
| `list_display_links` | Honored | Top-level `list_display_links` array on the changelist; `None` → `[]`. |
| `list_filter` (Simple / boolean / choice / FK / date / related-path) | Honored | FK filters carry `autocomplete:true`. |
| `search_fields` / `search_help_text` / `get_search_results` | Honored | |
| `get_ordering` / `get_sortable_by` / `ordering` | Honored | |
| `date_hierarchy` | Honored | |
| `list_editable` (bulk save) | Honored | Via `PATCH .../bulk/`. |
| `list_select_related` / `get_queryset` | Honored | N+1 guard on list **and** inlines. |
| `actions` (batch + detail) | Honored | One runner serves both shapes. |
| `fieldsets` / `get_fieldsets` (+ classes / description) | Honored | |
| `get_readonly_fields` | Honored | |
| `inlines` (Stacked / Tabular) — read | Honored | FK/M2M columns select_related / prefetch_related. |
| `raw_id_fields` / `radio_fields` / `filter_horizontal` / `filter_vertical` | Honored | Emitted as `widget` hints. |
| `formfield_overrides` | Honored | Reconciled into `widget` / `type`. |
| `save_as` / `save_on_top` / save-flow buttons | Honored | |
| `empty_value_display` / `message_user` / `view_on_site` | Honored | |
| `show_full_result_count` / `list_max_show_all` | Honored | |
| custom `AdminSite` / `get_app_list` | Honored | Via `DJANGO_ADMIN_REST_API["ADMIN_SITE"]`. |
| `change_password_form` (UserAdmin) | Honored | `…//password/`. |
| `prepopulated_fields` | Honored | `{target:[sources]}` on the add form-spec / `/add/` schema. |
| `autocomplete_fields` | Partial | Endpoint exists; a `widget:"autocomplete"` hint is emitted **only** when the target admin declares `search_fields`. Authorization is target-`has_view_permission` based (slightly broader than Django's source-relation check). |
| `change_form_template` / `add_form_template`, overridden `change_view` / `add_view` | Honored | Rendered server-side and returned as an `html-fragment` (admin chrome stripped; inline ``/`` preserved) for the SPA to inject in place — no iframe. POST round-trips through `…//change/` (redirect / re-render + captured messages). Overrides must stay GET-idempotent (see SECURITY.md). |
| date `list_filter` range UX | Partial | Surfaced as `{type:"date"}` with exact-match; range UI deferred. |
| `get_urls` custom views | Not yet | No generic passthrough (by design — use the SPA's own routes or an iframe). |
| Generic inlines (`GenericTabularInline` / `GenericStackedInline`) | Not yet | Not specifically handled. |

---

## 📸 Screenshots

The JSON `registry` endpoint — the source-of-truth for any consumer
frontend:

And here is the same admin rendered by
[`django-admin-react`](https://github.com/MartinCastroAlvarez/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.

```python
# 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//` 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:

```python
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///actions//`). 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-model `ModelAdmin.has_*_permission`).
- Anonymous → `403` for 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-preview` and `delete` endpoints both check `has_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` (default
  `5000`) caps the selection size of one action POST. Crafted
  large-selection requests return `400` instead of pinning a
  worker on an expensive action.
- **Audit-log field-name redaction.** The history endpoint's
  `change_message_structured` strips 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`](SECURITY.md) for the full threat model and the
upstream
[`django-admin-react` SECURITY.md](https://github.com/MartinCastroAlvarez/django-admin-react/blob/main/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):

```python
# 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`:

```python
# your_project/urls.py
from django.urls import include,

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [MartinCastroAlvarez](https://github.com/MartinCastroAlvarez)
- **Source:** [MartinCastroAlvarez/django-admin-rest-api](https://github.com/MartinCastroAlvarez/django-admin-rest-api)
- **License:** MIT
- **Homepage:** https://pypi.org/project/django-admin-rest-api/

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-martincastroalvarez-django-admin-rest-api
- Seller: https://agentstack.voostack.com/s/martincastroalvarez
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
