| name | dmr |
| description | Write the best DMR code possible with all the recommended best practices, avoiding common mistakes. |
django-modern-rest skill
Here's a list of best practices to use for different parts of the application.
Installing django-modern-rest
Always prefer to install msgspec extra, because it provides
the fastest json parsing / loading.
Always add django-stubs[compatible-mypy] to the dev dependencies,
because django-modern-rest requires types for Django during type checking.
Defining controller
Do not use @validate, when @modify is enough
This code:
from http import HTTPStatus
import msgspec
from django.http import HttpResponse
from dmr import Body, Controller, ResponseSpec, validate
from dmr.plugins.msgspec import MsgspecSerializer
class UserModel(msgspec.Struct):
email: str
class UserController(Controller[MsgspecSerializer]):
@validate(
ResponseSpec(
UserModel,
status_code=HTTPStatus.OK,
),
)
def post(self, parsed_body: Body[UserModel]) -> HttpResponse:
return self.to_response(
parsed_body,
status_code=HTTPStatus.OK,
)
should be rewritten and simplified as:
from http import HTTPStatus
import msgspec
from dmr import Body, Controller, modify
from dmr.plugins.msgspec import MsgspecSerializer
class UserModel(msgspec.Struct):
email: str
class UserController(Controller[MsgspecSerializer]):
@modify(status_code=HTTPStatus.OK)
def post(self, parsed_body: Body[UserModel]) -> UserModel:
return parsed_body
Because it does not use any of the validate features,
like settings extra headers or cookies.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/using-controller/index.html
Prefer implicit @modify over the explicit one
A code like:
from http import HTTPStatus
from dmr import Controller, modify
from dmr.plugins.msgspec import MsgspecSerializer
class UserController(Controller[MsgspecSerializer]):
@modify(status_code=HTTPStatus.OK)
def put(self) -> UserModel: ...
Should be rewritten as:
from http import HTTPStatus
from dmr import Controller
from dmr.plugins.msgspec import MsgspecSerializer
class UserController(Controller[MsgspecSerializer]):
def put(self) -> UserModel: ...
Because no @modify features were actually used, since status_code
was matching the default inferred one.
Prefer MsgspecSerializer
When defining simple models that do not require
any complex logic that pydantic provides,
prefer msgspec plugin over pydantic one.
Because it can be 10x times faster.
This code:
from http import HTTPStatus
import pydantic
from dmr import Body, Controller
from dmr.plugins.pydantic import PydanticSerializer
class UserModel(pydantic.BaseModel):
email: str
class UserController(Controller[PydanticSerializer]):
def post(self, parsed_body: Body[UserModel]) -> UserModel:
return parsed_body
Should be rewritten as:
from http import HTTPStatus
import msgspec
from dmr import Body, Controller
from dmr.plugins.msgspec import MsgspecSerializer
class UserModel(msgspec.Struct):
email: str
class UserController(Controller[MsgspecSerializer]):
def post(self, parsed_body: Body[UserModel]) -> UserModel:
return parsed_body
Prefer PydanticFastSerializer
When no content negotiation is used, when working with json only,
and when working with pydantic, it is better to rewrite code like:
from http import HTTPStatus
import pydantic
from dmr import Body, Controller, modify
from dmr.plugins.pydantic import PydanticSerializer
class UserModel(pydantic.BaseModel):
email: str
class UserController(Controller[PydanticSerializer]):
@modify(status_code=HTTPStatus.OK)
def post(self, parsed_body: Body[UserModel]) -> UserModel:
return parsed_body
To be:
from http import HTTPStatus
import pydantic
from dmr import Body, Controller, modify
from dmr.plugins.pydantic import PydanticFastSerializer
class UserModel(pydantic.BaseModel):
email: str
class UserController(Controller[PydanticFastSerializer]):
@modify(status_code=HTTPStatus.OK)
def post(self, parsed_body: Body[UserModel]) -> UserModel:
return parsed_body
Because PydanticFastSerializer is at least 3 times faster in this case.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/deep-dive/public-api.html#dmr.plugins.pydantic.PydanticFastSerializer
Never return Django HttpResponse directly — use to_response, to_error, or APIError
Returning Django responses directly bypasses content negotiation, cookie and header management.
Wrong:
import json
from django.http import HttpResponse
from dmr import Controller
from dmr.plugins.msgspec import MsgspecSerializer
class UserController(Controller[MsgspecSerializer]):
def get(self) -> HttpResponse:
return HttpResponse(
json.dumps({'email': 'user@example.com'}),
content_type='application/json',
headers={'X-API-Token': 'some-token'},
status=200,
)
Correct:
import msgspec
from django.http import HttpResponse
from dmr import Body, Controller, validate
from dmr.plugins.msgspec import MsgspecSerializer
class UserModel(msgspec.Struct):
email: str
class UserController(Controller[MsgspecSerializer]):
def get(self) -> HttpResponse:
return self.to_response(
{'email': 'user@example.com'},
headers={'X-API-Token': 'some-token'},
)
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/using-controller/index.html
Use APIError for error responses instead of manually building HttpResponse
APIError is automatically handled by the framework, formatted through format_error, and documented in OpenAPI schema.
Wrong:
import json
from http import HTTPStatus
from django.http import HttpResponse
from dmr import Controller
from dmr.plugins.msgspec import MsgspecSerializer
class UserController(Controller[MsgspecSerializer]):
def get(self) -> HttpResponse:
return HttpResponse(
json.dumps({'detail': [{'msg': 'Not found'}]}),
content_type='application/json',
status=404,
)
Correct:
from http import HTTPStatus
from dmr import APIError, Controller
from dmr.errors import ErrorType
from dmr.plugins.msgspec import MsgspecSerializer
class UserController(Controller[MsgspecSerializer]):
def get(self) -> str:
raise APIError(
self.format_error(
'Not found',
error_type=ErrorType.user_msg,
),
status_code=HTTPStatus.NOT_FOUND,
)
You can also use self.to_error when using @validate endpoints.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/error-handling.html
Routing
Use dmr.routing.path instead of django.urls.path
dmr.routing.path is a drop-in replacement that uses prefix-based pattern matching for 9-31% faster URL routing.
Wrong:
from django.urls import include, path
urlpatterns = [
path('api/', include('myapp.urls')),
]
Correct:
from django.urls import include
from dmr.routing import path
urlpatterns = [
path('api/', include('myapp.urls')),
]
Limitations: no API changes required — it is a full drop-in replacement for django.urls.path.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/routing.html
Use build_404_handler for API-style 404 responses
build_404_handler returns JSON 404 responses for API prefixes while keeping Django HTML 404s for non-API paths.
Wrong:
from django.urls import include
from dmr.routing import Router, path
from myapp.views import UserController
router = Router(
'api/',
[
path('user/', UserController.as_view(), name='users'),
],
)
urlpatterns = [
path(router.prefix, include((router.urls, 'app'), namespace='api')),
]
Correct:
from django.urls import include
from dmr.plugins.msgspec import MsgspecSerializer
from dmr.routing import Router, build_404_handler, path
from myapp.views import UserController
router = Router(
'api/',
[
path('user/', UserController.as_view(), name='users'),
],
)
urlpatterns = [
path(router.prefix, include((router.urls, 'app'), namespace='api')),
]
handler404 = build_404_handler(router.prefix, serializer=MsgspecSerializer)
Limitations: overriding handler404 has no effect while DEBUG = True — this is Django's default behavior.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/routing.html
Use build_500_handler for API-style 500 responses
build_500_handler returns JSON 500 responses for API prefixes while keeping Django HTML 500s for non-API paths.
Wrong:
from django.urls import include
from dmr.routing import Router, path
from myapp.views import UserController
router = Router(
'api/',
[
path('user/', UserController.as_view(), name='users'),
],
)
urlpatterns = [
path(router.prefix, include((router.urls, 'app'), namespace='api')),
]
Correct:
from django.urls import include
from dmr.plugins.msgspec import MsgspecSerializer
from dmr.routing import Router, build_500_handler, path
from myapp.views import UserController
router = Router(
'api/',
[
path('user/', UserController.as_view(), name='users'),
],
)
urlpatterns = [
path(router.prefix, include((router.urls, 'app'), namespace='api')),
]
handler500 = build_500_handler(router.prefix, serializer=MsgspecSerializer)
Limitations: overriding handler500 has no effect while DEBUG = True — this is Django's default behavior.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/routing.html
Error handling
Match error handler sync/async type to the endpoint type
Async endpoints require async error handlers, and sync endpoints require sync error handlers — this is validated on endpoint creation.
Wrong:
from http import HTTPStatus
from django.http import HttpResponse
from dmr import Controller, modify
from dmr.endpoint import Endpoint
from dmr.plugins.msgspec import MsgspecSerializer
from dmr.serializer import BaseSerializer
def sync_error_handler(
endpoint: Endpoint,
controller: Controller[BaseSerializer],
exc: Exception,
) -> HttpResponse:
return controller.to_error(
controller.format_error(str(exc)),
status_code=HTTPStatus.BAD_REQUEST,
)
class MyController(Controller[MsgspecSerializer]):
@modify(error_handler=sync_error_handler)
async def get(self) -> str:
return 'hello'
Correct:
from http import HTTPStatus
from django.http import HttpResponse
from dmr import Controller, modify
from dmr.endpoint import Endpoint
from dmr.plugins.msgspec import MsgspecSerializer
from dmr.serializer import BaseSerializer
async def async_error_handler(
endpoint: Endpoint,
controller: Controller[BaseSerializer],
exc: Exception,
) -> HttpResponse:
return controller.to_error(
controller.format_error(str(exc)),
status_code=HTTPStatus.BAD_REQUEST,
)
class MyController(Controller[MsgspecSerializer]):
@modify(error_handler=async_error_handler)
async def get(self) -> str:
return 'hello'
Limitations: the same rule applies to controller-level handle_error (sync) and handle_async_error (async) — don't define sync handlers for async controllers and vice versa.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/error-handling.html
Don't catch APIError explicitly in error handlers
APIError has a built-in default handler that automatically converts it to an HttpResponse — you don't need to handle it manually.
Wrong:
from http import HTTPStatus
from django.http import HttpResponse
from typing_extensions import override
from dmr import APIError, Controller
from dmr.endpoint import Endpoint
from dmr.plugins.msgspec import MsgspecSerializer
class MyController(Controller[MsgspecSerializer]):
@override
def handle_error(
self,
endpoint: Endpoint,
controller: Controller[MsgspecSerializer],
exc: Exception,
) -> HttpResponse:
if isinstance(exc, APIError):
return self.to_error(
exc.args[0],
status_code=exc.status_code,
)
raise exc from None
Correct:
from http import HTTPStatus
from django.http import HttpResponse
from typing_extensions import override
from dmr import Controller
from dmr.endpoint import Endpoint
from dmr.plugins.msgspec import MsgspecSerializer
class MyController(Controller[MsgspecSerializer]):
@override
def handle_error(
self,
endpoint: Endpoint,
controller: Controller[MsgspecSerializer],
exc: Exception,
) -> HttpResponse:
if isinstance(exc, SomeSpecificError):
return self.to_error(
self.format_error(str(exc)),
status_code=HTTPStatus.BAD_REQUEST,
)
raise exc from None
Limitations: only catch specific errors you know how to handle — always re-raise unfamiliar errors to let the next handler level deal with them.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/error-handling.html
Customize error messages with error_model and format_error on the controller
Defining a custom error_model and overriding format_error on the controller gives consistent error formatting across all endpoints and updates the OpenAPI schema automatically.
Wrong:
from http import HTTPStatus
from dmr import APIError, Controller
from dmr.plugins.msgspec import MsgspecSerializer
class MyController(Controller[MsgspecSerializer]):
def post(self) -> str:
raise APIError(
{'errors': [{'message': 'test'}]},
status_code=HTTPStatus.BAD_REQUEST,
)
Correct:
from http import HTTPStatus
from typing import Any
from typing_extensions import TypedDict, override
from dmr import APIError, Body, Controller, ResponseSpec, modify
from dmr.errors import ErrorType, format_error
from dmr.plugins.msgspec import MsgspecSerializer
class CustomErrorDetail(TypedDict):
message: str
class CustomErrorModel(TypedDict):
errors: list[CustomErrorDetail]
class MyController(Controller[MsgspecSerializer]):
error_model = CustomErrorModel
@override
def format_error(
self,
error: str | Exception,
*,
loc: str | list[str | int] | None = None,
error_type: str | ErrorType | None = None,
) -> Any:
default = format_error(error, loc=loc, error_type=error_type)
return {
'errors': [
{'message': detail['msg']} for detail in default['detail']
],
}
@modify(
extra_responses=[
ResponseSpec(
return_type=CustomErrorModel,
status_code=HTTPStatus.BAD_REQUEST,
),
],
)
def post(self, parsed_body: Body[dict[str, str]]) -> str:
raise APIError(
self.format_error('test msg'),
status_code=HTTPStatus.BAD_REQUEST,
)
Limitations: error_model and format_error are per-controller — you can't customize error format per-endpoint, only per-controller or globally.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/error-handling.html
Do not handle errors in the endpoints body
We have a separate layer in the app specifically for error handling.
Instead of handling errors in place, prefer to use the error handling methods,
like handle_error and handle_async_error.
Wrong:
from http import HTTPStatus
from django.http import HttpResponse
from typing_extensions import override
from dmr import Controller
from dmr.endpoint import Endpoint
from dmr.plugins.msgspec import MsgspecSerializer
from myapp import SomeSpecificError, some_logic
class MyController(Controller[MsgspecSerializer]):
def get(self) -> str:
try:
return some_logic()
except SomeSpecificError:
return self.to_error(
self.format_error(str(exc)),
status_code=HTTPStatus.BAD_REQUEST,
)
Correct:
from http import HTTPStatus
from django.http import HttpResponse
from typing_extensions import override
from dmr import Controller
from dmr.endpoint import Endpoint
from dmr.plugins.msgspec import MsgspecSerializer
from myapp import SomeSpecificError, some_logic
class MyController(Controller[MsgspecSerializer]):
def get(self) -> str:
return some_logic()
@override
def handle_error(
self,
endpoint: Endpoint,
controller: Controller[MsgspecSerializer],
exc: Exception,
) -> HttpResponse:
if isinstance(exc, SomeSpecificError):
return self.to_error(
self.format_error(str(exc)),
status_code=HTTPStatus.BAD_REQUEST,
)
raise exc from None
If error is handled in most controllers, you can move it to a global handler.
Validation
Disable response validation in production, keep it on in development
Response validation catches schema mismatches during development, but adds overhead in production — disable it globally for deployed apps.
Wrong:
DMR_SETTINGS = {}
Correct:
from dmr.settings import Settings
DMR_SETTINGS = {
Settings.validate_responses: False,
}
Limitations: only disable for production — fix schema errors during development instead of turning off validation.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/validation.html
Do not disable response validation when retuning extra data
Instead of disabling response validation when retuning some extra data,
add new ResponseSpec objects.
Wrong:
from http import HTTPStatus
from dmr import APIError, Body, Controller
from dmr.plugins.msgspec import MsgspecSerializer
class MyController(Controller[MsgspecSerializer]):
validate_responses = False
def post(self, parsed_body: Body[dict[str, str]]) -> str:
if not parsed_body:
raise APIError(
self.format_error('empty body'),
status_code=HTTPStatus.GONE,
)
return 'saved'
Correct:
from http import HTTPStatus
from dmr import APIError, Body, Controller, ResponseSpec, modify
from dmr.plugins.msgspec import MsgspecSerializer
class MyController(Controller[MsgspecSerializer]):
@modify(
extra_responses=[
ResponseSpec(
return_type=Controller.error_model,
status_code=HTTPStatus.GONE,
),
],
)
def post(self, parsed_body: Body[dict[str, str]]) -> str:
if not parsed_body:
raise APIError(
self.format_error('empty body'),
status_code=HTTPStatus.GONE,
)
return 'saved'
Don't override HttpSpec validation unless implementing legacy APIs
HttpSpec validation is already disabled by default for problematic cases — overriding it should only be done for very specific legacy API compatibility reasons.
Wrong:
from http import HTTPStatus
from dmr import Controller, modify
from dmr.plugins.pydantic import PydanticSerializer
from dmr.settings import HttpSpec
class JobController(Controller[PydanticSerializer]):
no_validate_http_spec = frozenset((HttpSpec.empty_response_body,))
@modify(status_code=HTTPStatus.NO_CONTENT)
def post(self) -> int:
return 4
Correct:
from http import HTTPStatus
from dmr import Controller, modify
from dmr.plugins.pydantic import PydanticSerializer
class JobController(Controller[PydanticSerializer]):
@modify(status_code=HTTPStatus.NO_CONTENT)
def post(self) -> None:
print('Job created')
Limitations: override no_validate_http_spec only when implementing old legacy APIs that cannot follow HTTP spec properly.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/validation.html
Authentication
Use typed request for authenticated controllers
Annotating self.request with a typed subclass of HttpRequest gives type-safe access to the authenticated user.
Wrong:
from dmr import Controller
from dmr.plugins.pydantic import PydanticSerializer
from dmr.security.django_session import DjangoSessionSyncAuth
class APIController(Controller[PydanticSerializer]):
auth = (DjangoSessionSyncAuth(),)
def get(self) -> str:
return f'hello {self.request.user}'
Correct:
from django.contrib.auth.models import User
from django.http import HttpRequest
from dmr import Controller
from dmr.plugins.msgspec import MsgspecSerializer
from dmr.security import AuthenticatedHttpRequest
from dmr.security.django_session import DjangoSessionSyncAuth
class APIController(Controller[MsgspecSerializer]):
request: AuthenticatedHttpRequest[User]
auth = (DjangoSessionSyncAuth(),)
def get(self) -> str:
return f'hello {self.request.user.username}'
Limitations: the typed request annotation is for type checking only — it does not enforce the user type at runtime.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/auth/django-session.html
Throttling
Protect auth endpoints with throttling BEFORE authentication
Always throttle authentication endpoints before auth runs to prevent brute force attacks — use runs_before_auth=True (default) on cache keys.
Wrong:
from dmr import Controller, modify
from dmr.plugins.pydantic import PydanticSerializer
from dmr.security.django_session import DjangoSessionSyncAuth
from dmr.throttling import Rate, SyncThrottle
from dmr.throttling.cache_keys import RemoteAddr
class LoginController(Controller[PydanticSerializer]):
@modify(
auth=[DjangoSessionSyncAuth()],
throttling=[
SyncThrottle(
5,
Rate.minute,
cache_key=RemoteAddr(runs_before_auth=False),
),
],
)
def post(self) -> str:
return 'logged in'
Correct:
from dmr import Controller, modify
from dmr.plugins.pydantic import PydanticSerializer
from dmr.security.django_session import DjangoSessionSyncAuth
from dmr.throttling import Rate, SyncThrottle
from dmr.throttling.cache_keys import RemoteAddr
class LoginController(Controller[PydanticSerializer]):
@modify(
auth=[DjangoSessionSyncAuth()],
throttling=[
SyncThrottle(
5,
Rate.minute,
cache_key=RemoteAddr(),
),
],
)
def post(self) -> str:
return 'logged in'
Limitations: runs_before_auth=True is the default for RemoteAddr, so you only need to be explicit when switching it off for non-auth endpoints.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/throttling.html
Testing
Prefer pytest style test cases over django.test.TestCase ones
Wrong:
from django.test import TestCase
from typing_extensions import override
from dmr.test import DMRRequestFactory
from myapp.views import UserController
class TestUsers(TestCase):
@override
def setUp(self) -> None:
self.rf = DMRRequestFactory()
def test_create_user(self) -> None:
request = self.rf.get('/users/', content_type='application/json')
response = UserController.as_view()(request)
assert isinstance(response, HttpResponse)
assert response.status_code == HTTPStatus.CREATED
Correct:
from http import HTTPStatus
from django.http import HttpResponse
from dmr.test import DMRRequestFactory
from myapp.views import UserController
def test_create_user(dmr_rf: DMRRequestFactory) -> None:
request = dmr_rf.get('/users/')
response = UserController.as_view()(request)
assert isinstance(response, HttpResponse)
assert response.status_code == HTTPStatus.OK
Use DMRClient and DMRRequestFactory instead of plain Django test tools
DMRClient and DMRRequestFactory default Content-Type to application/json, simplifying JSON API testing.
Wrong:
from http import HTTPStatus
from django.http import HttpResponse
from django.test import RequestFactory
from myapp.views import UserController
def test_create_user(rf: RequestFactory) -> None:
request = dmr_rf.get('/users/', content_type='application/json')
response = UserController.as_view()(request)
assert isinstance(response, HttpResponse)
assert response.status_code == HTTPStatus.OK
Correct:
from http import HTTPStatus
from django.http import HttpResponse
from dmr.test import DMRRequestFactory
from myapp.views import UserController
def test_create_user(dmr_rf: DMRRequestFactory) -> None:
request = dmr_rf.get('/users/')
response = UserController.as_view()(request)
assert isinstance(response, HttpResponse)
assert response.status_code == HTTPStatus.OK
Limitations: for async controllers, use DMRAsyncRequestFactory and DMRAsyncClient instead.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/testing.html
Use DMRRequestFactory for faster unit tests
DMRRequestFactory allows testing controllers directly without going through Django's URL routing and middleware, making tests significantly faster.
Wrong:
from http import HTTPStatus
from django.http import HttpResponse
from dmr.test import DMRClient
def test_create_user(dmr_client: DMRClient) -> None:
response = dmr_client.get('/users/')
assert isinstance(response, HttpResponse)
assert response.status_code == HTTPStatus.OK
Correct:
from http import HTTPStatus
from django.http import HttpResponse
from dmr.test import DMRRequestFactory
from myapp.views import UserController
def test_create_user(dmr_rf: DMRRequestFactory) -> None:
request = dmr_rf.get('/users/')
response = UserController.as_view()(request)
assert isinstance(response, HttpResponse)
assert response.status_code == HTTPStatus.OK
Limitations: DMRRequestFactory tests skip URL routing and middleware — use DMRClient when you need to test the full request/response cycle.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/testing.html
Use Polyfactory for structured test data generation
Polyfactory generates random test data from your model types, helping you find unexpected corner cases without manually crafting payloads.
Wrong:
from dmr.test import DMRRequestFactory
from myapp.views import UserController
def test_create_user(dmr_rf: DMRRequestFactory) -> None:
request = dmr_rf.post('/url/', data={'email': 'a@b.com', 'age': 20})
response = UserController.as_view()(request)
assert response.status_code == 201
Correct:
from polyfactory.factories.pydantic_factory import ModelFactory
from dmr.test import DMRRequestFactory
from myapp.views import UserController, UserCreateModel
class UserCreateModelFactory(ModelFactory[UserCreateModel]):
__check_model__ = True
def test_create_user(dmr_rf: DMRRequestFactory) -> None:
request_data = UserCreateModelFactory.build().model_dump(mode='json')
request = dmr_rf.post('/url/', data=request_data)
response = UserController.as_view()(request)
assert response.status_code == 201
Limitations: Polyfactory supports pydantic, msgspec, @dataclass, and TypedDict models — check its docs for your specific model type.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/testing.html
Use schemathesis for property-based API testing
schemathesis generates thousands of tests from your OpenAPI schema, and in simple cases can eliminate the need for hand-written integration tests.
Wrong:
from dmr.test import DMRClient
def test_get_users(dmr_client: DMRClient) -> None:
response = dmr_client.get('/api/users/')
assert response.status_code == 200
def test_get_users_invalid(dmr_client: DMRClient) -> None:
response = dmr_client.get('/api/users/?page=-1')
assert response.status_code == 422
Correct: use schemathesis. Check its official docs for more details.
https://schemathesis.readthedocs.io/en/stable/
Limitations: schemathesis is not bundled with django-modern-rest — install it separately with uv add --group dev schemathesis.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/testing.html
Middleware
Wrap Django middleware with wrap_middleware to keep OpenAPI docs and validation
wrap_middleware ensures middleware responses are documented in OpenAPI schema and subject to response validation.
Wrong:
from django.views.decorators.csrf import csrf_protect
from dmr import Controller
from dmr.plugins.msgspec import MsgspecSerializer
@csrf_protect
class ProtectedController(Controller[MsgspecSerializer]):
def post(self) -> dict[str, str]:
return {'message': 'created'}
Correct:
from http import HTTPStatus
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_protect
from dmr import Controller, ResponseSpec
from dmr.response import build_response
from dmr.decorators import wrap_middleware
from dmr.errors import ErrorModel, format_error
from dmr.plugins.msgspec import MsgspecSerializer
@wrap_middleware(
csrf_protect,
ResponseSpec(
return_type=ErrorModel,
status_code=HTTPStatus.FORBIDDEN,
),
)
def csrf_protect_json(response: HttpResponse) -> HttpResponse:
return build_response(
MsgspecSerializer,
raw_data=format_error('csrf error'),
status_code=HTTPStatus.FORBIDDEN,
)
@csrf_protect_json
class ProtectedController(Controller[MsgspecSerializer]):
responses = csrf_protect_json.responses
def post(self) -> dict[str, str]:
return {'message': 'created'}
Limitations: wrap_middleware handles both sync and async automatically — always add responses = wrapped_func.responses to the controller for OpenAPI docs.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/middleware.html
Configuration
Do not disable semantic_responses
semantic_responses=True (default) automatically injects common error responses (auth errors, validation errors, throttling) into your OpenAPI schema.
Wrong:
from dmr.settings import Settings
DMR_SETTINGS = {
Settings.semantic_responses: False,
}
Correct: do not override this setting, unless 100% required.
Limitations: you can exclude specific status codes from semantic responses using Settings.exclude_semantic_responses if they don't apply to your API.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/configuration.html
Always have at least one default parser and renderer in settings
Settings must always include at least one parser and one renderer for fallback error handling — even if you override parsers/renderers on controllers.
Wrong:
from dmr.settings import Settings
DMR_SETTINGS = {
Settings.parsers: [],
Settings.renderers: [],
}
Correct:
from dmr.settings import Settings
DMR_SETTINGS = {
}
Limitations: custom parsers and renderers can be added per-controller or per-endpoint on top of the global defaults.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/configuration.html
Project structure
Separate sync and async into different application instances
Running sync and async endpoints in separate Django instances avoids the threadpool overhead and performance penalty of mixed sync/async handling.
Wrong:
from dmr.routing import path
from myapp.views import AsyncUserController, SyncUserController
urlpatterns = [
path('sync-users/', SyncUserController.as_view()),
path('async-users/', AsyncUserController.as_view()),
]
Correct:
from dmr.routing import path
from myapp.views import SyncUserController
urlpatterns = [
path('users/', SyncUserController.as_view()),
]
from dmr.routing import path
from myapp.views import AsyncUserController
urlpatterns = [
path('users/', AsyncUserController.as_view()),
]
Limitations: this pattern is only beneficial for larger applications — small apps with few endpoints can safely mix sync and async in one instance.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/structure/sync-and-async.html
Components
Always name parsed body as parsed_body and parsed query as parsed_query
The framework uses parameter names parsed_body and parsed_query to identify which components to inject — using other names silently skips parsing.
Wrong:
import msgspec
from dmr import Body, Controller, Query
from dmr.plugins.msgspec import MsgspecSerializer
class UserModel(msgspec.Struct):
email: str
class FilterModel(msgspec.Struct):
active: bool = True
class UserController(Controller[MsgspecSerializer]):
def post(
self,
body: Body[UserModel],
query: Query[FilterModel],
) -> UserModel:
return body
Correct:
import msgspec
from dmr import Body, Controller, Query
from dmr.plugins.msgspec import MsgspecSerializer
class UserModel(msgspec.Struct):
email: str
class FilterModel(msgspec.Struct):
active: bool = True
class UserController(Controller[MsgspecSerializer]):
def post(
self,
parsed_body: Body[UserModel],
parsed_query: Query[FilterModel],
) -> UserModel:
return parsed_body
Limitations: this naming convention also applies to parsed_headers: Headers[...], parsed_path: Path[...], and parsed_cookies: Cookies[...].
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/components/index.html
OpenAPI
Add docstrings to controllers and endpoints for OpenAPI descriptions
Controller and endpoint docstrings are automatically used as descriptions in the generated OpenAPI schema.
Wrong:
from dmr import Controller
from dmr.plugins.pydantic import PydanticSerializer
class UserController(Controller[PydanticSerializer]):
def get(self) -> str:
return 'hello'
def post(self) -> str:
return 'created'
Correct:
from dmr import Controller
from dmr.plugins.pydantic import PydanticSerializer
class UserController(Controller[PydanticSerializer]):
"""Manage user accounts."""
def get(self) -> str:
"""Retrieve the current user profile."""
return 'hello'
def post(self) -> str:
"""Create a new user account."""
return 'created'
Limitations: docstrings only populate the description field in OpenAPI — use @modify or @validate for operation-level customization of other OpenAPI fields.
Docs: https://django-modern-rest.readthedocs.io/en/latest/pages/openapi/openapi.html