Integrations

CSRF

Django supports Cross Site Request Forgery protection.

By default we exempt all controllers from CSRF checks, unless:

  1. csrf_exempt is set to False for a specific controller

  2. Endpoints protected by DjangoSessionSyncAuth or DjangoSessionAsyncAuth will 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:

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

We don’t ship our own pagination. We (as our main design goal suggests) provide support for any existing pagination plugin for Django. Including the built-in django.core.paginator.Paginator.

To do so, we only provide metadata for the default pagination:

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_": {
        "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": {
        "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.

class dmr.pagination.Paginated(*, count: int, num_pages: int, per_page: int, page: Page[_ModelT])[source]

Helper type to serialize the default Paginator object.

Django already ships a pagination system, we don’t want to replicate it. So, we only provide metadata. See django.core.paginator.Paginator for the exact API.

class dmr.pagination.Page(*, number: int, object_list: Sequence[_ModelT])[source]

Default page model for serialization.

Can be used when using pagination with django-modern-rest.

django-filters

No special integration with django-filter is required.

Everything just works.

import django_filters
import pydantic
from dmr import Controller, Query
from dmr.plugins.pydantic import PydanticSerializer

from your_app.models import User

class UserFilter(django_filters.FilterSet):
    class Meta:
        model = User
        fields = ('is_active',)

# Create query model for better docs:
class QueryModel(pydantic.BaseModel):
    is_active: bool

class UserModel(pydantic.BaseModel):
    username: str
    email: str
    is_active: bool

class UserListController(
    Controller[PydanticSerializer],
    Query[QueryModel],
):
    def get(self) -> list[UserModel]:
        # Still pass `.GET` for API compatibility:
        user_filter = UserFilter(
             self.request.GET,
             queryset=User.objects.all(),
        )
        return [
            UserModel.model_validate(user, from_attributes=True)
            for user in user_filter.qs
        ]

CORS Headers

No special integration with django-cors-headers is required.

Everything just works.

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

Run result

$ curl http://127.0.0.1:8000/api/example/1/ -D - -X GET
HTTP/1.1 200 OK
date: Sun, 05 Apr 2026 17:51:03 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: Sun, 05 Apr 2026 17:51:03 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: Sun, 05 Apr 2026 17:51:03 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"}