Integrations#
Big list of Django integrations: https://github.com/wsvincent/awesome-django
Big list of
django-modern-restawesome things: https://github.com/kondratevdev/awesome-django-modern-rest
Warning
In the future - some integrations from this list might be included
into the core of django-modern-rest package. Or ship as plugins.
If you are interested in something: open an issue.
django-modern-rest must be compatible with the most packages
from the django ecosystem. To do so, we support:
All the regular
Viewbased APIs like mixins, sinceControlleris a subclass ofViewMiddlewares to wrap any existing middleware
Decorators, thanks to
endpoint_decorator()anddispatch_decorator()External views to integrate any existing views into
django-modern-rest, including regulardjango,django-rest-framework, anddjango-ninja
CSRF#
Django supports Cross Site Request Forgery protection.
We support both ways of enforcing CSRF checks:
Via
django.middleware.csrf.CsrfViewMiddlewaremiddlewareVia
django.views.decorators.csrf.csrf_protect()decorator
By default we exempt all controllers from CSRF checks, unless:
csrf_exemptis set toFalsefor a specific controllerEndpoints protected by
DjangoSessionSyncAuth,DjangoSessionAsyncAuthorCookieJWTSyncAuth,CookieJWTAsyncAuthwill require CSRF as well. Because using cookies for auth with explicit CSRF is not secure
Note
Detailed CSRF failure reason on response content will be exposed only in debug mode for security reasons.
Here’s an example of how you can re-enable CSRF for a specific controller:
1from dmr import Controller
2from dmr.plugins.pydantic import PydanticFastSerializer
3
4
5class ExampleController(Controller[PydanticFastSerializer]):
6 csrf_exempt = False
7
8 def post(self) -> str:
9 return 'ok'
10
Run result
$ curl http://127.0.0.1:8000/api/example/ -D - -X POST -H 'X-CSRFToken: dmrDocsExampleCsrfTokenValue0001' --cookie csrftoken=dmrDocsExampleCsrfTokenValue0001
HTTP/1.1 201 Created
date: Fri, 16 Oct 2026 22:53:52 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 4
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
"ok"
$ curl http://127.0.0.1:8000/api/example/ -D - -X POST
HTTP/1.1 403 Forbidden
date: Fri, 16 Oct 2026 22:53:52 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 35
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"detail":[{"msg":"CSRF Failed."}]}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
}
},
"securitySchemes": {
"csrf": {
"description": "CSRF protection",
"in": "cookie",
"name": "csrftoken",
"type": "apiKey"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/examplecontroller/": {
"post": {
"operationId": "postExampleControllerApiExamplecontroller",
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"type": "string"
}
}
},
"description": "Created"
},
"403": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when CSRF check failed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
},
"security": [
{
"csrf": []
}
]
}
}
}
}
Next: see CSRF_FAILURE_VIEW
Django setting value to configure how CSRF failure view will work.
To do so, we use build_csrf_handler() function:
>>> from dmr.security.csrf import build_csrf_handler
>>> from dmr.plugins.pydantic import PydanticSerializer
>>> CSRF_FAILURE_VIEW = build_csrf_handler(
... 'api/',
... serializer=PydanticSerializer,
... )
This configuration will work similarly
to build_404_handler():
It will check the request path, if it starts with
'api/', we will negotiate the renderer and return the expected error in the default formatIf not, it will return the regular Django HTML page
We automatically inject CSRF response specs and security schemes
in endpoints for controllers with csrf_exempt = False
and unsafe HTTP methods like POST, etc.
We do so with the help
of semantic_schema_providers setting
and CSRFSemanticSchemaProvider
response spec provider.
Note
csrf security requirement spec is injected with AND logic.
So, if you have existing auth instances, you will have to satisfy
both this auth and csrf requirements. Just like in the runtime.
Customizing error model#
Since we support full error customization we also support this feature for CSRF responses.
To change the error message format you will need:
Pass needed format_error callback parameter to
build_csrf_handler()Customize
error_modelparameter toCSRFSemanticSchemaProviderinsemantic_schema_providerssetting
Bring your own DI#
We don’t have any opinions about any DI that you can potentially use.
Because django-modern-rest is compatible with any of the existing ones.
Use any DI that you already have or want to use with django.
Try any of these officially recommended tools:
https://github.com/maksimzayats/diwire with the official django-modern-rest how-to
https://github.com/reagento/dishka with the help of https://github.com/arturboyun/dmr-dishka plugin
Or any other one that suits your needs :)
Typing#
Django does not have type annotations, by default,
so mypy won’t type check Django apps by default.
But, when django-stubs
is installed, type checking starts to shine.
So, when you use mypy, you will need
to install django-stubs together with django-modern-rest
to have the best type checking experience.
This package is included in pyright by default. No actions are required.
We check django-modern-rest code with mypy and pyright
strict modes in CI, so be sure to have the best typing possible.
See our
project template
to learn how typing works, how mypy is configured,
how django-stubs is used.
Pagination#
See Pagination — we support both limit/offset and cursor-based pagination out of the box.
Filters#
No special integration with django-filter is required.
Everything just works:
1import django_filters
2import pydantic
3from django.contrib.auth.models import User
4
5from dmr import Controller, Query
6from dmr.plugins.pydantic import PydanticFastSerializer
7
8
9class UserFilter(django_filters.FilterSet):
10 class Meta:
11 model = User
12 fields = ('is_active',)
13
14
15# Create query model for validation and docs:
16class QueryModel(pydantic.BaseModel):
17 is_active: bool
18
19
20class UserModel(pydantic.BaseModel):
21 username: str
22 email: str
23 is_active: bool
24
25
26_UserList = pydantic.TypeAdapter(list[UserModel])
27
28
29class UsersController(Controller[PydanticFastSerializer]):
30 def get(self, parsed_query: Query[QueryModel]) -> list[UserModel]:
31 # Still pass `.GET` for API compatibility:
32 user_filter = UserFilter(
33 self.request.GET,
34 queryset=User.objects.all(),
35 )
36 return _UserList.validate_python(user_filter.qs, from_attributes=True)
37
Run result
$ curl 'http://127.0.0.1:8000/api/users/?is_active=1' -X GET
[{"username":"test_user","email":"test@example.com","is_active":true}]
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"UserModel": {
"properties": {
"email": {
"title": "Email",
"type": "string"
},
"is_active": {
"title": "Is Active",
"type": "boolean"
},
"username": {
"title": "Username",
"type": "string"
}
},
"required": [
"username",
"email",
"is_active"
],
"title": "UserModel",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/userscontroller/": {
"get": {
"operationId": "getUsersControllerApiUserscontroller",
"parameters": [
{
"in": "query",
"name": "is_active",
"required": true,
"schema": {
"title": "Is Active",
"type": "boolean"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/UserModel"
},
"type": "array"
}
}
},
"description": "OK"
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
Health Checks#
We recommend using django-health-check for monitoring your application’s health.
No special integration is required — the package works out-of-the-box with
django-modern-rest. Simply install it, include its URLs in your main
urlconf, and add the desired check apps to INSTALLED_APPS.
For advanced configuration, please refer to the django-health-check documentation.
CORS Headers#
No special integration with django-cors-headers is required.
Everything just works.
Content Security Policy (CSP)#
No special integration with django-csp is required.
Everything just works, but there is one important nuance:
django-modern-rest itself only controls Django templates and local
initialization files. If you use OpenAPI UI renderers, final CSP compatibility
still depends on the upstream frontend bundle you choose.
The OpenAPI UI templates shipped by django-modern-rest avoid inline
<script> blocks and pass schema data via Django’s
django.utils.html.json_script(), so DMR’s own templates work well with
stricter CSP setups.
Known caveats:
Some upstream OpenAPI bundles inject styles at runtime, so a very strict policy can still break the page.
When CSP is a hard requirement, start with local bundled assets and test the exact renderer and version you plan to deploy.
Example django-csp setup can be found in
wemake-django-template.
If you use OpenAPI UIs, see OpenAPI for renderer-specific guidance.
Conditional requests (ETag)#
Django has built-in support for conditional request processing
(If-None-Match, If-Modified-Since, 304 Not Modified):
With django-modern-rest you can integrate it via
wrap_middleware()
and django.views.decorators.http.condition().
1from datetime import UTC, datetime
2from http import HTTPStatus
3from types import MappingProxyType
4from typing import Any, Final, final
5
6import pydantic
7from django.http import HttpRequest, HttpResponse
8from django.views.decorators.http import condition
9
10from dmr import Controller, HeaderSpec, Path, ResponseSpec
11from dmr.decorators import wrap_middleware
12from dmr.errors import ErrorType
13from dmr.plugins.pydantic import PydanticSerializer
14from dmr.response import APIError
15
16
17@final
18class _UserModel(pydantic.BaseModel):
19 user_id: int
20 updated_at: datetime
21 message: str
22
23
24@final
25class _PathModel(pydantic.BaseModel):
26 user_id: int
27
28
29@final
30class _ResponseModel(pydantic.BaseModel):
31 message: str
32 updated_at: str
33
34
35# Imitate DB
36_USERS: Final = MappingProxyType({
37 1: _UserModel(
38 user_id=1,
39 updated_at=datetime(2026, 3, 23, 12, 30, tzinfo=UTC), # noqa: WPS432
40 message='Fresh content for user #1',
41 ),
42 2: _UserModel(
43 user_id=2,
44 updated_at=datetime(2026, 3, 24, 9, 15, tzinfo=UTC), # noqa: WPS432
45 message='Fresh content for user #2',
46 ),
47})
48
49
50def _build_etag(user: _UserModel) -> str:
51 updated_at = user.updated_at.isoformat()
52 return f'"user-{user.user_id}-{updated_at}"'
53
54
55def _etag(request: HttpRequest, user_id: int = 0, **kwargs: Any) -> str | None:
56 user = _USERS.get(user_id)
57 return _build_etag(user) if user else None
58
59
60@wrap_middleware(
61 condition(etag_func=_etag),
62 ResponseSpec(return_type=_ResponseModel, status_code=HTTPStatus.OK),
63 ResponseSpec(
64 return_type=None,
65 status_code=HTTPStatus.NOT_MODIFIED,
66 headers={'ETag': HeaderSpec()},
67 ),
68)
69def _condition_middleware(response: HttpResponse) -> HttpResponse:
70 """Adds Content-Type for 304 responses to satisfy strict validation."""
71 if response.status_code == HTTPStatus.NOT_MODIFIED:
72 response.headers['Content-Type'] = 'application/json'
73 return response
74
75
76@final
77@_condition_middleware
78class ConditionalETagController(Controller[PydanticSerializer]):
79 responses = _condition_middleware.responses
80
81 def get(self, parsed_path: Path[_PathModel]) -> HttpResponse:
82 user = _USERS.get(parsed_path.user_id)
83 if user is None:
84 raise APIError(
85 self.format_error(
86 f'User {parsed_path.user_id} not found',
87 error_type=ErrorType.not_found,
88 ),
89 status_code=HTTPStatus.NOT_FOUND,
90 )
91 return self.to_response(
92 _ResponseModel(
93 message=user.message,
94 updated_at=user.updated_at.isoformat(),
95 ),
96 )
97
Run result
$ curl http://127.0.0.1:8000/api/example/1/ -D - -X GET
HTTP/1.1 200 OK
date: Fri, 16 Oct 2026 22:53:53 GMT
server: uvicorn
Content-Type: application/json
ETag: "user-1-2026-03-23T12:30:00+00:00"
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 80
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"message":"Fresh content for user #1","updated_at":"2026-03-23T12:30:00+00:00"}
$ curl http://127.0.0.1:8000/api/example/1/ -D - -X GET -H 'If-None-Match: "user-1-2026-03-23T12:30:00+00:00"'
HTTP/1.1 304 Not Modified
date: Fri, 16 Oct 2026 22:53:54 GMT
server: uvicorn
ETag: "user-1-2026-03-23T12:30:00+00:00"
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 0
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
$ curl http://127.0.0.1:8000/api/example/2/ -D - -X GET
HTTP/1.1 200 OK
date: Fri, 16 Oct 2026 22:53:54 GMT
server: uvicorn
Content-Type: application/json
ETag: "user-2-2026-03-24T09:15:00+00:00"
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 80
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
{"message":"Fresh content for user #2","updated_at":"2026-03-24T09:15:00+00:00"}
OpenAPI Schema
Preview openapi.json
{
"components": {
"schemas": {
"ErrorDetail": {
"description": "Base schema for error details description.",
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "integer"
},
{
"type": "string"
}
]
},
"title": "Loc",
"type": "array"
},
"msg": {
"title": "Msg",
"type": "string"
},
"type": {
"title": "Type",
"type": "string"
}
},
"required": [
"msg"
],
"title": "ErrorDetail",
"type": "object"
},
"ErrorModel": {
"description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ErrorDetail"
},
"title": "Detail",
"type": "array"
}
},
"required": [
"detail"
],
"title": "ErrorModel",
"type": "object"
},
"_ResponseModel": {
"properties": {
"message": {
"title": "Message",
"type": "string"
},
"updated_at": {
"title": "Updated At",
"type": "string"
}
},
"required": [
"message",
"updated_at"
],
"title": "_ResponseModel",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/example/{user_id}/": {
"get": {
"operationId": "getConditionalETagControllerApiExampleUserId",
"parameters": [
{
"in": "path",
"name": "user_id",
"required": true,
"schema": {
"title": "User Id",
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/_ResponseModel"
}
}
},
"description": "OK"
},
"304": {
"description": "Not Modified",
"headers": {
"ETag": {
"required": true,
"schema": {
"type": "string"
}
}
}
},
"400": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when request components cannot be parsed"
},
"404": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when path parameters do not match"
},
"406": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when provided `Accept` header cannot be satisfied"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Raised when returned response does not match the response schema"
}
}
}
}
}
}
HTMX#
Works with django-htmx out of the box.
dj-rest-auth#
Added in version 0.15.0.
We support dj-rest-auth in two different ways:
By using it directly and integrating into
external_path()as DRF viewsBy using
django-allauthwith headless mode andexternal_path()
We also have a skill for the migration from dj-rest-auth: dj-rest-auth migration.