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

```python
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(  # <- describes unique return types from this endpoint
        ResponseSpec(
            UserModel,
            status_code=HTTPStatus.OK,
        ),
    )
    def post(self, parsed_body: Body[UserModel]) -> HttpResponse:
        # This response would have an explicit status code `200`:
        return self.to_response(
            parsed_body,
            status_code=HTTPStatus.OK,
        )
```

should be rewritten and simplified as:

```python
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:
        # This response would have an explicit status code `200`:
        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:

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

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

```python
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:
        # This response would have an explicit status code `200`:
        return parsed_body
```

Should be rewritten as:

```python
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:
        # This response would have an explicit status code `200`:
        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:

```python
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:
        # This response would have an explicit status code `200`:
        return parsed_body
```

To be:

```python
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:
        # This response would have an explicit status code `200`:
        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:

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

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

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

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

```python
from django.urls import include, path

urlpatterns = [
    path('api/', include('myapp.urls')),
]
```

Correct:

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

```python
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')),
]
# No custom 404 handler — API gets HTML error pages
```

Correct:

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

```python
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')),
]
# No custom 500 handler — API gets HTML error pages
```

Correct:

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

```python
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:  # async endpoint with sync handler!
        return 'hello'
```

Correct:

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

```python
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):  # unnecessary!
            return self.to_error(
                exc.args[0],
                status_code=exc.status_code,
            )
        raise exc from None
```

Correct:

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

```python
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'}]},  # ad-hoc format
            status_code=HTTPStatus.BAD_REQUEST,
        )
```

Correct:

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

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

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

```python
# settings.py — production with validation still on (slow):
DMR_SETTINGS = {}  # validate_responses defaults to True
```

Correct:

```python
# settings.py — production:
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:

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

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

```python
from http import HTTPStatus

from dmr import Controller, modify
from dmr.plugins.pydantic import PydanticSerializer
from dmr.settings import HttpSpec


class JobController(Controller[PydanticSerializer]):
    # Disabling just to avoid fixing the real issue:
    no_validate_http_spec = frozenset((HttpSpec.empty_response_body,))

    @modify(status_code=HTTPStatus.NO_CONTENT)
    def post(self) -> int:
        return 4
```

Correct:

```python
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')  # noqa: WPS421
```

**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:

```python
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:
        # self.request.user is `AbstractBaseUser | AnonymousUser` by default:
        return f'hello {self.request.user}'
```

Correct:

```python
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:
        # self.request.user is now typed as `User`:
        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:

```python
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:  # throttle runs AFTER auth — brute force possible!
        return 'logged in'
```

Correct:

```python
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:  # throttle runs BEFORE auth — brute force prevented
        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:

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

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

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

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

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

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

```python
from dmr.test import DMRRequestFactory
from myapp.views import UserController


def test_create_user(dmr_rf: DMRRequestFactory) -> None:
    # Manually crafted payload — misses edge cases:
    request = dmr_rf.post('/url/', data={'email': 'a@b.com', 'age': 20})
    response = UserController.as_view()(request)
    assert response.status_code == 201
```

Correct:

```python
from polyfactory.factories.pydantic_factory import ModelFactory

from dmr.test import DMRRequestFactory
from myapp.views import UserController, UserCreateModel


class UserCreateModelFactory(ModelFactory[UserCreateModel]):
    # Enables strict model validation during factory builds:
    __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:

```python
# Manually writing integration tests for every endpoint:
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


# ... many more tests for each edge case
```

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:

```python
from django.views.decorators.csrf import csrf_protect

from dmr import Controller
from dmr.plugins.msgspec import MsgspecSerializer


@csrf_protect  # not tracked in OpenAPI, no response validation
class ProtectedController(Controller[MsgspecSerializer]):
    def post(self) -> dict[str, str]:
        return {'message': 'created'}
```

Correct:

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

```python
# settings.py — no semantic responses, error schemas missing from OpenAPI:
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:

```python
# settings.py — no parsers/renderers at all:
from dmr.settings import Settings

DMR_SETTINGS = {
    Settings.parsers: [],
    Settings.renderers: [],
}
```

Correct:

```python
# settings.py:
from dmr.settings import Settings

DMR_SETTINGS = {
    # Default JSON parsers/renderers are included automatically
    # when not specified — don't set empty lists.
}
```

**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:

```python
# urls.py — mixing sync and async in one URL config:
from dmr.routing import path

from myapp.views import AsyncUserController, SyncUserController

urlpatterns = [
    path('sync-users/', SyncUserController.as_view()),
    path('async-users/', AsyncUserController.as_view()),
]
# Running with a single gunicorn or uvicorn process
```

Correct:

```python
# urls.py — sync endpoints only:
from dmr.routing import path

from myapp.views import SyncUserController

urlpatterns = [
    path('users/', SyncUserController.as_view()),
]

# async_urls.py — async endpoints only:
from dmr.routing import path

from myapp.views import AsyncUserController

urlpatterns = [
    path('users/', AsyncUserController.as_view()),
]

# Run sync with gunicorn, async with uvicorn
# Route in proxy: /async/* → uvicorn, others → gunicorn
```

**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:

```python
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],  # wrong name!
        query: Query[FilterModel],  # wrong name!
    ) -> UserModel:
        return body
```

Correct:

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

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

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