Integrations¶
Big list of Django integrations: https://github.com/wsvincent/awesome-django
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.
CSRF¶
Django supports Cross Site Request Forgery protection.
By default we exempt all controllers from CSRF checks, unless:
csrf_exemptis set toFalsefor a specific controllerEndpoints protected by
DjangoSessionSyncAuthorDjangoSessionAsyncAuthwill require CSRF as well. Because using Django sessions without CSRF is not secure
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¶
Limit Offset pagination¶
We support built-in django.core.paginator.Paginator.
To do so, we only provide metadata for the default pagination:
1from typing import Final, NotRequired, TypedDict
2
3import pydantic
4from django.core.paginator import Paginator
5
6from dmr import Controller, Query
7from dmr.pagination import Page, Paginated
8from dmr.plugins.pydantic import PydanticSerializer
9
10
11class _User(pydantic.BaseModel):
12 email: str
13
14
15class _PageQuery(TypedDict):
16 page_size: NotRequired[int]
17 page: NotRequired[int]
18
19
20_USERS: Final = (
21 _User(email='one@example.com'),
22 _User(email='two@example.com'),
23 _User(email='three@example.com'),
24)
25
26
27class UsersController(Controller[PydanticSerializer]):
28 def get(self, parsed_query: Query[_PageQuery]) -> Paginated[_User]:
29 page = parsed_query.get('page', 1)
30 page_size = parsed_query.get('page_size', 2)
31
32 paginator = Paginator(_USERS, page_size)
33 return Paginated(
34 count=paginator.count,
35 num_pages=paginator.num_pages,
36 per_page=paginator.per_page,
37 page=Page(
38 number=page,
39 object_list=list(paginator.page(page).object_list),
40 ),
41 )
42
Run result
$ curl http://127.0.0.1:8000/api/users/ -X GET
{"count":3,"num_pages":2,"per_page":2,"page":{"number":1,"object_list":[{"email":"one@example.com"},{"email":"two@example.com"}]}}
$ curl 'http://127.0.0.1:8000/api/users/?page=2' -X GET
{"count":3,"num_pages":2,"per_page":2,"page":{"number":2,"object_list":[{"email":"three@example.com"}]}}
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"
},
"Page__User_": {
"description": "Default page model for serialization.\n\nCan be used when using pagination with ``django-modern-rest``.",
"properties": {
"number": {
"title": "Number",
"type": "integer"
},
"object_list": {
"items": {
"$ref": "#/components/schemas/_User"
},
"title": "Object List",
"type": "array"
}
},
"required": [
"number",
"object_list"
],
"title": "Page",
"type": "object"
},
"Paginated": {
"description": "Helper type to serialize the default ``Paginator`` object.\n\nDjango already ships a pagination system, we don't want to replicate it.\nSo, we only provide metadata.\nSee :class:`django.core.paginator.Paginator` for the exact API.",
"properties": {
"count": {
"title": "Count",
"type": "integer"
},
"num_pages": {
"title": "Num Pages",
"type": "integer"
},
"page": {
"$ref": "#/components/schemas/Page__User_"
},
"per_page": {
"title": "Per Page",
"type": "integer"
}
},
"required": [
"count",
"num_pages",
"per_page",
"page"
],
"title": "Paginated",
"type": "object"
},
"_User": {
"properties": {
"email": {
"title": "Email",
"type": "string"
}
},
"required": [
"email"
],
"title": "_User",
"type": "object"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/userscontroller/": {
"get": {
"deprecated": false,
"operationId": "getUserscontrollerApiUserscontroller",
"parameters": [
{
"deprecated": false,
"in": "query",
"name": "page_size",
"schema": {
"title": "Page Size",
"type": "integer"
}
},
{
"deprecated": false,
"in": "query",
"name": "page",
"schema": {
"title": "Page",
"type": "integer"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Paginated"
}
}
},
"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"
}
}
}
}
}
}
If you are using a different pagination system, you can define your own metadata / models and use them with our framework.
Cursor pagination¶
We also support any other pagination library.
Like django-cursor-pagination or even your custom implementation.
Any Django-compatible tool should work out of the box.
Interface¶
- class dmr.pagination.Paginated(*, count: int, num_pages: int, per_page: int, page: Page[_ModelT])[source]¶
Helper type to serialize the default
Paginatorobject.Django already ships a pagination system, we don’t want to replicate it. So, we only provide metadata. See
django.core.paginator.Paginatorfor the exact API.
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 PydanticSerializer
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[PydanticSerializer]):
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"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/userscontroller/": {
"get": {
"deprecated": false,
"operationId": "getUserscontrollerApiUserscontroller",
"parameters": [
{
"deprecated": false,
"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: Thu, 07 May 2026 12:58:00 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: Thu, 07 May 2026 12:58:00 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: Thu, 07 May 2026 12:58:00 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"
}
},
"securitySchemes": {}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/example/{user_id}/": {
"get": {
"deprecated": false,
"operationId": "getConditionaletagcontrollerApiExampleUserId",
"parameters": [
{
"deprecated": false,
"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": {
"content": {
"application/json": {
"schema": {
"type": "null"
}
}
},
"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.