:layout: landing .. figure:: /_images/logo-light.svg :figclass: light-only :width: 700 :align: center .. figure:: /_images/logo-dark.svg :figclass: dark-only :width: 700 :align: center .. container:: badges :name: badges .. image:: https://img.shields.io/badge/%20-wemake.services-green.svg?label=%20&logo=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC%2FxhBQAAAAFzUkdCAK7OHOkAAAAbUExURQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP%2F%2F%2F5TvxDIAAAAIdFJOUwAjRA8xXANAL%2Bv0SAAAADNJREFUGNNjYCAIOJjRBdBFWMkVQeGzcHAwksJnAPPZGOGAASzPzAEHEGVsLExQwE7YswCb7AFZSF3bbAAAAABJRU5ErkJggg%3D%3D :alt: wemake.services :target: https://github.com/wemake-services .. image:: https://img.shields.io/badge/Modern%20REST-0C4B33?logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTA4MCIgaGVpZ2h0PSIxMDgwIiB2aWV3Qm94PSIwIDAgMTA4MCAxMDgwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBkPSJNMiA3MDQuMDJMMTQ1LjQ1OSA0NjYuMTlMMjc3Ljg4MyA3MDQuMDJMMTQ1LjQ1OSA5NDEuODQ5TDIgNzA0LjAyWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTE0NS40NTkgOTQxLjg0OUwyIDcwNC4wMkgyNzcuODgzTDE0NS40NTkgOTQxLjg0OVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik02NzguOTQ4IDcwNC4wMzVMMzQxLjIzIDEzOEwyMjcuMDcxIDMyOC4yNjRMNDM2LjM2MiA3MDQuMDM1TDMwMy4xNzcgOTQxLjg2NEg1MzYuMjVMNjc4Ljk0OCA3MDQuMDM1WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTY3OC45MzcgNzA0LjAyNkg0MzYuMzVMMzAzLjE2NiA5NDEuODU2SDUzNi4yMzlMNjc4LjkzNyA3MDQuMDI2WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTEwNzguMTcgNzA0LjAzNUw3NDAuNDUxIDEzOEw2MjYuMjkzIDMyOC4yNjRMODM1LjU4MyA3MDQuMDM1TDcwMi4zOTkgOTQxLjg2NEg5MzUuNDcyTDEwNzguMTcgNzA0LjAzNVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGQ9Ik0xMDc4LjE3IDcwNC4wMzVIODM1LjU4M0w3MDIuMzk5IDk0MS44NjRIOTM1LjQ3MkwxMDc4LjE3IDcwNC4wMzVaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K&color=35544A :alt: Modern REST :target: https://github.com/wemake-services/django-modern-rest .. image:: https://github.com/wemake-services/django-modern-rest/actions/workflows/test.yml/badge.svg?event=push :alt: Tests result :target: https://github.com/wemake-services/django-modern-rest/actions/workflows/test.yml .. image:: https://codecov.io/gh/wemake-services/django-modern-rest/branch/master/graph/badge.svg :alt: Codecov :target: https://codecov.io/gh/wemake-services/django-modern-rest .. image:: https://img.shields.io/badge/no-slop-purple.svg :alt: No AI slop :target: https://github.com/wemake-services/django-modern-rest/blob/master/.github/AI_POLICY.md .. image:: https://static.pepy.tech/personalized-badge/django-modern-rest?period=total&units=INTERNATIONAL_SYSTEM&left_color=GREY&right_color=BRIGHTGREEN&left_text=downloads :alt: PyPI Downloads :target: https://pepy.tech/projects/django-modern-rest .. image:: https://img.shields.io/pypi/pyversions/django-modern-rest.svg :alt: Supported Python Version :target: https://pypi.org/project/django-modern-rest/ .. image:: https://img.shields.io/badge/style-wemake-000000.svg :alt: Code Style :target: https://github.com/wemake-services/wemake-python-styleguide .. image:: https://deepwiki.com/badge.svg :alt: Ask DeepWiki :target: https://deepwiki.com/wemake-services/django-modern-rest .. image:: https://img.shields.io/badge/chat-join-blue.svg?logo=telegram :alt: Telegram Chat :target: https://t.me/django_modern_rest .. rst-class:: lead Modern REST framework for Django with types and async support! This guide will walk you through all the details of how to install, use, and extend ``django-modern-rest`` framework. .. container:: buttons :doc:`pages/getting-started` `GitHub `_ .. rubric:: Testimonials .. container:: testimonials .. epigraph:: The one thing I really love about ``django-modern-rest`` is its pluggable serializers and validators. Frameworks that are tightly coupled with ``pydantic`` can be really painful to work with. -- `Kirill Podoprigora `_, CPython core developer .. epigraph:: Using ``django-modern-rest`` has been a game-changer for my productivity. The strict type safety and schema validation for both requests and responses mean I spend less time debugging and more time building. -- `Josiah Kaviani `_, Django core developer .. epigraph:: I rarely see frameworks that treat their OpenAPI schema as a first-class citizen. ``django-modern-rest`` not only generates a schema that accurately reflects your code, but also gives you the tools to verify it. -- `Dmitry Dygalo `_, author of Schemathesis .. rubric:: Main features include: .. grid:: 1 1 2 3 :class-row: surface :padding: 0 :gutter: 2 .. grid-item-card:: :octicon:`terminal` REST :link: pages/core-concepts :link-type: doc Semantic REST APIs with 100% typed API and strict schema validation for both requests and responses. You would never miss an important status code in the docs anymore! .. grid-item-card:: :octicon:`zap` Blazingly Fast! :link: pages/deep-dive/performance :link-type: doc Built with performance in mind. Import time optimizations, only one validation per request, best ``json`` parsing tools in class. And ``msgspec`` support allows users to have `x5-15 times faster `_ APIs than the alternatives. .. grid-item-card:: :octicon:`star` Sync and Async support :link: pages/core-concepts :link-type: doc Fully utilizes best of the both worlds in ``django``. Create your APIs as sync or async, your choice. Both ``wsgi`` and ``asgi`` are supported. .. grid-item-card:: :octicon:`beaker` Not just schema generation :link: pages/openapi/openapi :link-type: doc Of course, OpenAPI schema generation and modification are available out of the box. But, there's more: we also provide validation and testing tools for your schema! Powered by `schemathesis `_ .. grid-item-card:: :octicon:`rocket` Still good old Django :link: pages/core-concepts :link-type: doc We don't reinvent the wheel, this is just good old Django. We only add fast ``json`` parsing and schema for requests and responses. And that's it. You can still use all packages and features from regular Django apps. No new concepts to learn, no new APIs to be compatible with. Just drop this package into any existing Django application! .. grid-item-card:: :octicon:`gear` Customizable to the core :link: pages/deep-dive/public-api :link-type: doc Every part of the framework can be customized and extended. Since, there's no magic happening, it would be really easy to do. Our docs and tests provide multiple examples of that. Public API stability is guaranteed from 1.0.0 release. Contributors ------------ Here are our amazing people who made this project possible. .. container:: rounded-image .. contributors:: wemake-services/django-modern-rest :avatars: :contributions: :names: :exclude: dependabot[bot],pre-commit-ci[bot],Copilot .. toctree:: :caption: Getting started guides :hidden: pages/getting-started.rst pages/core-concepts.rst pages/using-controller/index pages/routing.rst pages/components/index pages/configuration.rst pages/plugins.rst pages/queryset.rst pages/negotiation.rst pages/error-handling.rst pages/throttling.rst pages/middleware.rst pages/validation.rst pages/reusable-code.rst pages/integrations.rst pages/internationalization.rst pages/testing.rst .. toctree:: :caption: Auth :hidden: pages/auth/common.rst pages/auth/http-basic.rst pages/auth/django-session.rst pages/auth/jwt.rst .. toctree:: :caption: Streaming :hidden: pages/streaming/common.rst pages/streaming/sse.rst pages/streaming/jsonl.rst .. toctree:: :caption: OpenAPI :hidden: pages/openapi/schema.rst pages/openapi/openapi.rst pages/openapi/openapi-reference.rst .. toctree:: :caption: AI and LLMs :hidden: pages/ai/spec-first.rst pages/ai/dmr-from-ninja.rst pages/ai/dmr-from-drf.rst .. toctree:: :caption: Project structure :hidden: pages/structure/micro-framework.rst pages/structure/sync-and-async.rst .. toctree:: :caption: Deep Dive :hidden: pages/deep-dive/performance.rst pages/deep-dive/public-api.rst pages/deep-dive/internal-api.rst pages/deep-dive/security.rst pages/deep-dive/changelog.rst pages/deep-dive/contributing.rst .. toctree:: :caption: Community :hidden: pages/community/logos.rst Getting started =============== Installation ------------ Works for: - CPython 3.11+ or PyPy 3.11+ - Django 4.2+ .. tabs:: .. tab:: :iconify:`material-icon-theme:uv` uv .. code-block:: bash uv add django-modern-rest .. tab:: :iconify:`devicon:poetry` poetry .. code-block:: bash poetry add django-modern-rest .. tab:: :iconify:`devicon:pypi` pip .. code-block:: bash pip install django-modern-rest Extras for different serializers: - ``'django-modern-rest[pydantic]'`` for ``pydantic`` support - ``'django-modern-rest[attrs]'`` for ``attrs`` support - ``'django-modern-rest[msgspec]'`` for ``msgspec`` support and the fastest ``json`` parsing Extras for different features: - ``'django-modern-rest[jwt]'`` for `jwt `_ support - ``'django-modern-rest[openapi]'`` for `OpenAPI schema validation `_ and better examples generation .. important:: We highly recommend to always install `msgspec `_, even when using just `pydantic `_ for APIs, because we use ``msgspec`` to parse ``json``, when it is available, since it is `the fastest `_ library out there for this task. We also recommend to always install `django-stubs `_ for typing the Django itself. .. note:: You don't need to add ``'dmr'`` to the ``INSTALLED_APPS``, unless you want to serve static files for the OpenAPI. LLMs support ------------ Are you using AI for assisted coding? We got you covered, use these files for context to make sure that the LLM knows our framework: - https://django-modern-rest.readthedocs.io/llms.txt for indexes with links to different pages and topics - https://django-modern-rest.readthedocs.io/llms-full.txt for complete docs We also support `Context7 `_ for up-to-date docs for the LLMs. Use cases we officially support: - Learning ``django-modern-rest`` with the help of `DeepWiki `_ - AI-guided migrations for any API changes. We break something? We provide a prompt for you, so you can automatically upgrade to a newer version using an AI tool of your choice We support several custom agent skills: - ``$dmr`` to enforce ``django-modern-rest`` best practices with fast and secure approaches - ``$dmr-openapi-skeleton`` to generate a :doc:`working project boilerplate ` from a single ``openapi.json`` file (the "Spec First" approach) - ``$dmr-from-django-ninja`` to help with :doc:`migrating from Django Ninja ` - ``$dmr-from-drf`` to help with :doc:`migrating from Django REST Framework ` Showcase -------- Let's see the basics and learn how to use ``dmr`` in a single example: .. tabs:: .. tab:: msgspec We support :class:`msgspec.Struct` via :class:`~dmr.plugins.msgspec.MsgspecSerializer`. .. literalinclude:: /examples/getting_started/msgspec_controller.py :caption: views.py :language: python :linenos: .. tab:: pydantic We support :class:`pydantic.BaseModel` via :class:`~dmr.plugins.pydantic.PydanticSerializer`. .. tip:: If you only use ``json`` :doc:`parsers and renderers `, it would be faster to use :class:`~dmr.plugins.pydantic.PydanticFastSerializer` instead. .. literalinclude:: /examples/getting_started/pydantic_controller.py :caption: views.py :language: python :linenos: .. tab:: attrs We support :func:`attrs.define` via :class:`~dmr.plugins.msgspec.MsgspecSerializer`. See `msgspec docs `_ on ``attrs`` support. .. literalinclude:: /examples/getting_started/attrs_controller.py :caption: views.py :language: python :linenos: .. tab:: dataclasses We support :func:`dataclasses.dataclass` via both :class:`~dmr.plugins.msgspec.MsgspecSerializer` and :class:`~dmr.plugins.pydantic.PydanticSerializer`. .. literalinclude:: /examples/getting_started/dataclasses_controller.py :caption: views.py :language: python :linenos: .. tab:: TypedDict We support :class:`typing.TypedDict` via both :class:`~dmr.plugins.msgspec.MsgspecSerializer` and :class:`~dmr.plugins.pydantic.PydanticSerializer`. .. literalinclude:: /examples/getting_started/typed_dict_controller.py :caption: views.py :language: python :linenos: .. tab:: NamedTuple We support :class:`typing.NamedTuple` via :class:`~dmr.plugins.pydantic.PydanticSerializer`. .. literalinclude:: /examples/getting_started/named_tuple_controller.py :caption: views.py :language: python :linenos: .. important:: You can choose a serializer per controller, which will give you the freedom to choose the best serializer and model for the job. ``msgspec`` gives you more speed, while ``pydantic`` gives you more flexibility. In this example: 1. We defined regular ``pydantic``, ``msgspec``, or whatever models that we will use for our API 2. We added two component parsers: one for request's :data:`~dmr.components.Body` and one for :data:`~dmr.components.Headers` which will parse them into the typed models that we pass to these components as type parameters 3. Next we created a :class:`~dmr.controller.Controller` class with :class:`~dmr.plugins.pydantic.PydanticSerializer` or :class:`~dmr.plugins.msgspec.MsgspecSerializer` to serialize input and output data for us 4. We also defined ``post`` API endpoint and returned a simple model response from it, it will be automatically transformed into :class:`django.http.HttpResponse` instance by ``django-modern-rest`` Now, let's add our controller to the list of URLs: .. literalinclude:: /examples/getting_started/urls.py :caption: urls.py :language: python :linenos: Your first ``django-modern-rest`` API is ready. Next, you can learn: - How to generate OpenAPI schema - How to handle errors - How to customize controllers and endpoints Full example ------------ If you were ever told that Django is too big and complicated, that was misleading, to say the least. Here's a :doc:`single-file application ` that looks pretty much the same as any other micro-framework, like: FastAPI, Litestar, or Flask. .. literalinclude:: /examples/structure/micro_framework/single_file_asgi.py :language: python :linenos: You can copy it by clicking "Copy" in the right upper corner of the example, it shows up on hovering the code example. Paste it as ``example.py``, install the ``django-modern-rest`` and run it with: .. tabs:: .. tab:: :iconify:`material-icon-theme:uv` uv .. code-block:: bash uv run example.py runserver .. tab:: :iconify:`devicon:poetry` poetry .. code-block:: bash poetry run python example.py runserver .. tab:: :iconify:`devicon:pypi` pip .. code-block:: bash python example.py runserver Your API is now live: - ``POST`` http://localhost:8000/api/user/ — create a user And then visit https://localhost:8000/docs/swagger/ for the interactive docs. .. image:: /_images/swagger.png :alt: Swagger view :align: center That's it, enjoy your new project! But, this is too simple for my use-case! ---------------------------------------- What is great about Django is that it scales. You can start with a single file app and scale it up to a full featured monolith with strict context boundaries, DDD, reusable apps, etc. We recommend starting new big projects with https://github.com/wemake-services/wemake-django-template It is strict, security-first, battle-proven, highload-tested boilerplate for real apps of the modern age. Next up ------- .. grid:: 1 1 2 2 :class-row: surface :padding: 0 :gutter: 2 .. grid-item-card:: :octicon:`rocket` Core Concepts :link: core-concepts :link-type: doc Learn the fundamentals. .. grid-item-card:: :octicon:`gear` Configuration :link: configuration :link-type: doc Learn how to configure ``django-modern-rest``. Core concepts ============= To learn ``django-modern-rest`` you have to learn just a couple of things: .. glossary:: Endpoint :class:`~dmr.endpoint.Endpoint` is a single API route. It is defined by its name – HTTP method – and its :term:`Metadata`, what response schema it returns, what status codes it can return, etc. Each endpoint might have different :term:`Component` types for parsing the inputs. Controller :class:`~dmr.controller.Controller` is a collection of one or more :term:`endpoints ` with the same set of :term:`components `. Controller is a subclass of :class:`~django.views.generic.base.View`, so it can be used in a routing directly. Component Controllers parse data via components like :data:`~dmr.components.Body` or :data:`~dmr.components.Headers`. You can write your own components. Metadata A collection of all the things each :term:`Endpoint` accepts and returns. It is used for request parsing, response validation, and OpenAPI schema. Serializer :class:`~dmr.serializer.BaseSerializer` subclass that knows how to load and dump raw data into models. We have 2 bundled serializers in :doc:`plugins `\ : for ``pydantic`` and ``msgspec``, you can write your own serializers for other libraries. Routing Routing is a mapping of URLs to controllers. We use default Django's URL routing. Controllers might have many URLs, for example: ``/api/v1/users/`` and ``/api/v2/users/`` Example: .. literalinclude:: /examples/core_concepts/glossary.py :caption: views.py :language: python :linenos: Async vs Sync ------------- We support both Django modes: sync and async, the same way regular Django `supports `_ them. We don't do anything special with the async mode, so any existing guides, tools, deployment strategies should just work with ``django-modern-rest`` if they work for Django. Maximum integration with Django ------------------------------- We try to keep Django compatibility as our main goal. Everything should work by default, starting from `django-cors-headers `_ up to `django-ratelimit `_. We also provide :doc:`middleware` wrapper tools to convert any middleware response to the required API schema and set needed ``Content-Type``, etc. We support all existing mixins: because :class:`~dmr.controller.Controller` is a subclass of Django's :class:`django.views.generic.base.View` class. We support all existing decorators: because we have :func:`~dmr.decorators.endpoint_decorator` and :func:`~dmr.decorators.dispatch_decorator` utilities that can decorate endpoints and controllers. Works best with `django-stubs `_. Read next: our :doc:`integrations` guide. Next up ------- .. grid:: 1 1 2 2 :class-row: surface :padding: 0 :gutter: 2 .. grid-item-card:: :octicon:`rocket` Using Controller :link: using-controller/index :link-type: doc Learn how controllers work. .. grid-item-card:: :octicon:`gear` Configuration :link: configuration :link-type: doc Learn how to configure ``django-modern-rest``. Using controller ================ Creating endpoints ------------------ Controllers consist of :class:`~dmr.endpoint.Endpoint` objects. Each HTTP method is an independent endpoint. The simplest way to create an endpoint is to define sync or async method with the right name (any HTTP method verb): .. literalinclude:: /examples/using_controller/first_example.py :caption: views.py :language: python :linenos: There will be several things that ``django-modern-rest`` will do for you here: 1. It will know that ``post`` endpoint will handle ``POST`` HTTP method, it is true for all HTTP methods, except :ref:`OPTIONS ` (click to know why) 2. It will know that ``post`` will return :class:`str` as a response type spec. There's **no** implicit type conversions in ``django-modern-rest``. If your endpoint declares something to be returned, it must return this type 3. It will infer the default status code for ``post``, which will be ``201``. All other endpoints would have ``200`` as the default 4. All this metadata will be used to validate responses from this endpoint. Returning ``[]`` from ``post`` would trigger :exc:`~dmr.exceptions.ResponseSchemaError`, unless :ref:`response_validation` is explicitly turned off 5. The same metadata will be used to render OpenAPI spec ``django-modern-rest`` never creates implicit methods for you. No ``HEAD``, no :ref:`OPTIONS `, if you need them – create them explicitly. Returning responses ------------------- We have two general modes of working with responses: 1. Returning just raw data from "raw endpoints" 2. Returning real :class:`~django.http.HttpResponse` instances with granular configuration from "real endpoints" Raw endpoints ------------- .. note:: "Raw endpoints" always have a response spec generated by default. Prefer :func:`~dmr.endpoint.modify` in simpler cases. "Raw endpoints" can be either undecorated or can use :func:`~dmr.endpoint.modify` decorator to modify the response spec that will be **generated by default**. .. literalinclude:: /examples/using_controller/modify.py :caption: views.py :language: python :linenos: :emphasize-lines: 14 Other response specs can be specified via ``extra_responses`` param to :func:`~dmr.endpoint.modify`, :attr:`~dmr.controller.Controller.responses` ``Controller`` attribute, or :attr:`~dmr.settings.Settings.responses` global setting. Make sure that all responses that can be returned are described! .. important:: Despite the fact, that ``django-modern-rest`` does not have its own request and response primitives and uses :class:`~django.http.HttpRequest` and :class:`~django.http.HttpResponse`, users must not return Django responses directly. Instead, use any of the public APIs: - :meth:`~dmr.controller.Controller.to_response` - :meth:`~dmr.controller.Controller.to_error` - :exc:`~dmr.response.APIError` In case when you don't have a controller / endpoint instance (like in a middleware, for example), you can fallback to using :func:`~dmr.response.build_response` lower level primitive. Why? 1. You can mess up the default headers / status codes 2. You won't have the right json serializer / deserializer, which can be both slow and error-prone Real endpoints -------------- .. note:: No response spec is generated by default for "real endpoints". All response specs must be provided manually. But, this way is way more configurable. "Real endpoints" can use :func:`~dmr.endpoint.validate` decorator, :attr:`~dmr.controller.Controller.responses` ``Controller`` attribute, or :attr:`~dmr.settings.Settings.responses` global setting to specify all possible responses. To do that we utilize :class:`~dmr.metadata.ResponseSpec`: .. literalinclude:: /examples/using_controller/validate.py :caption: views.py :language: python :linenos: :emphasize-lines: 15-20 ``@validate`` decorator is useful but is not required for real endpoint's declaration. Instead, you can specify response specs in ``response`` field of controller / settings. .. literalinclude:: /examples/using_controller/implicit_validate.py :caption: views.py :language: python :linenos: :emphasize-lines: 15-18 If :ref:`response validation ` passes, then it is all fine! .. important:: At least one explicit response spec is required for ``@validate`` endpoints. Note that semantic responses from auth / components / etc are not counted when validating real endpoints. You still have to use at least one explicit specification declaration. Request lifecycle ----------------- Here's the top level view on how request / response lifecycle looks like: .. mermaid:: :caption: Request lifecycle :config: {"theme": "forest"} graph Start[New request] --> BeforeThrottle[Throttling based on IP if any or 429]; BeforeThrottle --> RendererNegotiation[Renderer is negotiated or 406]; RendererNegotiation --> Auth[Auth if any or 401]; Auth --> AfterThrottle[Throttling based on auth if any or 429]; AfterThrottle --> ParserNegotiation[Parser is negotiated if any or 400]; ParserNegotiation --> DataValidation[Request data is validated if any or 400]; DataValidation --> BusinessLogic[Business logic]; BusinessLogic --> Renderer[Response rendering]; Renderer --> ResponseValidation[Response validation if any or 419]; Customizing controllers ----------------------- .. tip:: This is a sneak peek into our advanced API. 90% of users will never need this. ``Controller`` is built to be customized with a class-level API. If you need granular control, you can change anything. - :attr:`~dmr.controller.Controller.allowed_http_methods` to support custom HTTP methods like ``QUERY`` or your custom DSLs on top of HTTP - :attr:`~dmr.controller.Controller.endpoint_cls` to customize how endpoints are created - :attr:`~dmr.controller.Controller.csrf_exempt` to customize whether or not this controller is exempted from the CSRF - :attr:`~dmr.controller.Controller.controller_validator_cls` to customize how controller is validated in import time You can also customize :class:`~dmr.endpoint.Endpoint` to change how API methods are executed: - :attr:`~dmr.endpoint.Endpoint.serializer_context_cls` to customize how model for serialization of incoming data is created Check out our :doc:`Public API <../deep-dive/public-api>` for the most advanced features. What's next? ------------ .. grid:: 3 3 2 2 :class-row: surface :padding: 0 :gutter: 2 .. grid-item-card:: Headers and cookies :link: headers-and-cookies :link-type: doc Learn how to describe response headers and cookies. .. grid-item-card:: Redirects :link: redirects :link-type: doc Learn how to return HTTP redirect responses. .. grid-item-card:: Files :link: files :link-type: doc Learn how to return file responses. .. grid-item-card:: Validation :link: validation :link-type: doc Learn about optional response validation. .. toctree:: :hidden: validation.rst headers-and-cookies.rst redirects.rst files.rst meta.rst .. _response_validation: Response validation =================== By default, all responses (not just requests!) are validated at runtime to match the schema. This allows us to be super strict about schema generation as a pro, but as a con, it is slower than it could possibly be. So, you can disable response validation via configuration: .. warning:: Disabling response validation makes sense only in production for better performance. It is not recommended to disable response validation for any other reason. Instead, fix your schema errors! Keep it on in development, but disable it in production to get the best of both worlds. .. tabs:: .. tab:: Active validation .. literalinclude:: /examples/using_controller/active_validation.py :caption: views.py :language: python :linenos: .. tab:: Disable per endpoint .. literalinclude:: /examples/using_controller/per_endpoint.py :caption: views.py :language: python :linenos: :emphasize-lines: 19 .. tab:: Disable per controller .. literalinclude:: /examples/using_controller/per_controller.py :caption: views.py :language: python :linenos: :emphasize-lines: 20 .. tab:: Disable globally See :data:`dmr.settings.Settings.validate_responses`. .. code-block:: python :caption: settings.py >>> from dmr.settings import Settings >>> DMR_SETTINGS = {Settings.validate_responses: False} The right way ------------- The "right way" is not to disable the validation, but to specify the correct schema to be returned from an endpoint. .. literalinclude:: /examples/using_controller/right_way.py :caption: views.py :language: python :linenos: :emphasize-lines: 20-25 And to disable the validation for ``production`` environment. Example: https://github.com/wemake-services/wemake-django-template/blob/c003757fd33ba7dd1a9e7af7c3a175883d0c033b/%7B%7Bcookiecutter.project_name%7D%7D/server/settings/environments/production.py#L86 Describing response headers and cookies ======================================= Describing headers ------------------ You also must specify which headers are returned (if any). When using "real endpoints", you can provide ``headers`` parameter to :class:`~dmr.metadata.ResponseSpec` if there are headers you want to describe. :class:`~dmr.headers.HeaderSpec` is here to help. You can create both ``required=True`` (always must be present on the response object) and ``required=False`` headers (might be missing in some cases): .. literalinclude:: /examples/using_controller/validate_headers.py :caption: views.py :language: python :linenos: :emphasize-lines: 29-32 .. note:: All headers from the response objects are checked. We will report: - Required headers that exist in the spec, but not on the ``response`` - Any headers that exist on the ``response``, but not present in the spec ``Content-Type`` header is the only one that is always added automatically. With "raw endpoints" you can also use :class:`~dmr.headers.NewHeader` marker which can set headers with known values to the final response. .. literalinclude:: /examples/using_controller/modify_headers.py :caption: views.py :language: python :linenos: :emphasize-lines: 20 If you need headers with not static, but dynamic values, use "real endpoints" and pass ``headers`` dict to :meth:`~dmr.controller.Controller.to_response` method. The last important thing about headers is :attr:`~dmr.headers.HeaderSpec.skip_validation` attribute. It is used to describe headers that: 1. Will be set in the response by someone else outside the framework, like HTTP proxy or Django's own middleware. See :class:`django.contrib.sessions.middleware.SessionMiddleware` as a notable example 2. Will be validated to be **NOT** present in the response from our framework. Since it is designed to be added later, it should not be already present .. important:: Header definitions are case insensitive according to the HTTP spec. ``Session`` and ``session`` is the same header. Describing cookies ------------------ .. warning:: Some may say that returning cookies is not "RESTful", because cookies is an implicit state, that RESTful APIs must not have. Be careful, only use this feature when you need to. See: https://parottasalna.hashnode.dev/is-it-okay-to-add-cookie-to-a-rest-api We also support setting and validating response cookies. You can use :class:`~dmr.cookies.NewCookie` to add new cookies with statically known values to "raw endpoints". Or :class:`~dmr.cookies.CookieSpec` with both types of endpoints to describe response cookies. .. literalinclude:: /examples/using_controller/modify_cookies.py :caption: views.py :language: python :linenos: :emphasize-lines: 16 And you can set any cookies to :attr:`django.http.HttpResponse.cookies` with "real endpoints". Since we have strict schemas, it is required to describe the set cookies with :class:`~dmr.cookies.CookieSpec`: .. literalinclude:: /examples/using_controller/validate_cookies.py :caption: views.py :language: python :linenos: :emphasize-lines: 23-24 The last important thing about cookies is :attr:`~dmr.cookies.CookieSpec.skip_validation` attribute. It is used to describe cookies that: 1. Will be set in the response by someone else outside the framework, like HTTP proxy or Django's own middleware. See :class:`django.contrib.sessions.middleware.SessionMiddleware` as a notable example 2. Will be validated to be **NOT** present in the response from our framework. Since it is designed to be added later, it should not be already present .. note:: All cookie parts are validated by default. Except ``expires`` field, because it is relative to the current time. .. important:: Cookie definitions are case sensitive according to the HTTP spec. ``Session`` and ``session`` are two different cookies. Returning redirects =================== We support returning redirects from API endpoints with :class:`~dmr.response.RedirectTo` exception with :func:`~dmr.endpoint.modify`: .. literalinclude:: /examples/using_controller/redirect_error.py :caption: views.py :language: python :linenos: We model ``RedirectTo`` as an exception, because you are not allowed to return :class:`~django.http.HttpResponse` objects from :func:`~dmr.endpoint.modify` endpoints. .. note:: :class:`~dmr.response.APIError` does not support ``3xx`` status codes. Redirects are different from regular errors. The second way is to use default Django's :class:`django.http.HttpResponseRedirect` together with :func:`~dmr.endpoint.validate`: .. literalinclude:: /examples/using_controller/redirect_response.py :caption: views.py :language: python :linenos: Note that in both cases you would need to document ``Location`` header in a response spec. API Reference ------------- .. autoexception:: dmr.response.RedirectTo :members: Returning files =============== We support file and other binary responses. .. warning:: Returning files via Python and Django in particular is very performance inefficient. It should not be used for anything serious. Instead return files with S3-like systems or at least on a proxy-server level. To do so, you indicate that you will return a file with :class:`dmr.files.FileResponseSpec` and specify a file renderer. We provide :class:`dmr.renderers.FileRenderer` for this case. By default, ``FileResponseSpec()`` describes an inline file response. It matches Django's ``FileResponse`` default behavior: .. literalinclude:: /examples/using_controller/inline_file_response.py :caption: views.py :language: python :linenos: :emphasize-lines: 13-14 Set ``as_attachment=True`` when Django's :class:`django.http.FileResponse` is returned as an attachment. In this mode `Content-Disposition `_ is always set and usually contains the filename sent to the client: .. literalinclude:: /examples/using_controller/file_response.py :caption: views.py :language: python :linenos: :emphasize-lines: 16-17, 22-23 The difference comes from the ``Content-Disposition`` HTTP header: it tells clients whether the response body is expected to be displayed inline or downloaded as an attachment. API Reference ------------- .. autoclass:: dmr.files.FileBody :members: .. autoclass:: dmr.files.FileResponseSpec :members: .. autofunction:: dmr.files.file_response_headers .. _meta: Defining ``OPTIONS`` or ``meta`` method ======================================= `RFC 9110 `_ defines the ``OPTIONS`` HTTP method, but sadly Django's :class:`~django.views.generic.base.View` which we use as a base class for all controllers, already has :meth:`~django.views.generic.base.View.options` method. It would generate a typing error to redefine it with a different signature that we need for our endpoints. That's why we created our own ``meta`` controller method as a replacement for older Django's ``options`` name. To use it you have two options: 1. Use :class:`~dmr.options_mixins.MetaMixin` or :class:`~dmr.options_mixins.AsyncMetaMixin` with the default implementation: we provide ``Allow`` header with all the allowed HTTP methods in this controller 2. Define the ``meta`` endpoint yourself and provide a custom implementation Using pre-defined mixins ------------------------ We have two versions: for sync and async controllers. Their features are identical: .. tabs:: .. tab:: sync .. literalinclude:: /examples/using_controller/meta_sync.py :caption: dtos.py :language: python :linenos: .. tab:: async .. literalinclude:: /examples/using_controller/meta_async.py :caption: views.py :language: python :linenos: Both of them: - Provide ``meta`` method sync or async - Provide the same response spec for the OpenAPI schema Custom meta implementation -------------------------- Since our mixins do not anything magical, you can write our own version, if you need a behavior change, for example. Here's an example of a custom ``meta`` implementation: .. literalinclude:: /examples/using_controller/meta_custom.py :caption: views.py :language: python :linenos: You would need to: - Define ``meta`` method (sync or async) with the desired implementation - Provide the required response spec Routing ======= Our :term:`Controller` is built without knowing anything about its future URL. Why so? 1. Because Django already has an amazing URL `routing system `_ and we don't need to duplicate it 2. Because all controllers might be used in multiple URLs, for example in ``/api/v1/`` and ``/api/v2/``. Our design allows any possible customizations .. literalinclude:: /examples/getting_started/urls.py :caption: views.py :language: python :linenos: .. note:: If you want to parse path parameters, see :doc:`components/path` and :data:`dmr.components.Path`. Handling 404 errors ------------------- By default, Django returns HTML 404 pages. This is not what we want for API endpoints. Instead, we want to return API responses with proper error structure and content negotiation (e.g. JSON or XML based on the ``Accept`` header). But, we still want HTML 404 pages for non API views. .. important:: Overriding :data:`django.conf.urls.handler404` has no effect while ``DEBUG = True`` is set. This is how Django behaves: https://docs.djangoproject.com/en/stable/ref/views/#the-404-page-not-found-view To achieve this, you can use :func:`~dmr.routing.build_404_handler` helper. It creates a handler that returns API-style 404 responses for specific path prefixes (using the same serializer and renderers as your API), and falls back to Django's default handler for everything else. Here is how you can use it in your root ``urls.py`` (in your `ROOT_URLCONF `_): .. literalinclude:: /examples/routing/handler404.py :caption: urls.py :language: python :linenos: This returns json responses for ``api/`` prefixed paths. But, will still return regular Django HTML responses for any other path. .. _handler500: Handling 500 errors ------------------- By default, Django returns HTML 500 pages. This is not what we want for API endpoints. Instead, we want to return API responses with proper error structure and content negotiation (e.g. JSON or XML based on the ``Accept`` header). But, we still want HTML 500 pages for non API views. .. important:: Overriding :data:`django.conf.urls.handler500` has no effect while ``DEBUG = True`` is set. This is how Django behaves: https://docs.djangoproject.com/en/stable/ref/views/#the-500-server-error-view To achieve this, you can use :func:`~dmr.routing.build_500_handler` helper. It creates a handler that returns API-style 500 responses for specific path prefixes (using the same serializer and renderers as your API), and falls back to Django's default handler for everything else. Here is how you can use it in your root ``urls.py`` (in your `ROOT_URLCONF `_): .. literalinclude:: /examples/routing/handler500.py :caption: views.py :language: python :linenos: .. seealso:: :doc:`error-handling` if you want to learn how to handle different errors on different levels and fix these ``500`` exceptions. Optimized URL Routing --------------------- ``django-modern-rest`` provides an optimized :func:`dmr.routing.path` function that is a **drop-in replacement** for Django's :func:`django.urls.path`. The custom implementation uses prefix-based pattern matching for faster routing. Instead of immediately running Django's regex engine on every request, it performs a quick prefix check first. Performance Impact ~~~~~~~~~~~~~~~~~~ Benchmark results on MacBook Pro M4 Pro: - **Best case**: 9% faster (match found in first few URL patterns) - **Average case**: 13% faster (match found in middle of URL patterns list) - **Worst case**: 31% faster (404 Not Found, all patterns checked) The prefix-based optimization dramatically reduces regex operations: - **Static routes**: Simple string comparison (no regex at all) - **Dynamic routes**: Regex only runs when prefix matches - **Failed matches**: Eliminated in one operation (startswith check) This is especially beneficial for applications with: - Large number of routes - High traffic Migration ~~~~~~~~~ Simply replace Django's ``path`` with :func:`dmr.routing.path`: .. code:: python # Instead of ``from django.urls import path``: from dmr.routing import path from django.urls import include urlpatterns = [ path('api/', include('myapp.urls')), ] This is a drop-in replacement with no API changes required. Components ========== ``django-modern-rest`` utilizes component approach to parse all the unstructured things like headers, body, and cookies into a strongly typed and validated model. To use a component, you can just add it as a parameter to your endpoint method inside a :class:`~dmr.controller.Controller`. How does it work? - In **import time**, when controller is first created, we iterate over all existing endpoints in this class - For each endpoint we fetch method annotations and find all :class:`~dmr.components.ComponentParser` objects, they will be treated as component parsers - Next, we create a request parsing model during the import time, with all combined fields to be parsed later - In **runtime**, when request is received, we provide the needed data for this single parsing model - If everything is ok, we call the needed endpoint with the correct data - If there's a parsing error we raise :exc:`~dmr.exceptions.RequestSerializationError` and return a beautiful error message for the user You can use existing ones or create your own. .. note:: All existing components should only be inherited for parsing. If you want to change the implementation details of a component – create a new one from scratch. You can still delegate parts of the work to existing ones. What is inside a component? --------------------------- All components consist of two parts: 1. The first one is a :class:`~dmr.components.ComponentParser` subclass, which knows how to provide the required data for itself, build OpenAPI schemas and etc. For example: :class:`~dmr.components.QueryComponent` 2. The second part is a :data:`typing.Annotated` based annotation that has a component parser instance as metadata. These annotations will be used by the end users. For example, :data:`~dmr.components.Query` Browse components ----------------- .. grid:: 3 3 2 2 :class-row: surface :padding: 0 :gutter: 2 .. grid-item-card:: Query :link: query :link-type: doc Parsing query parameters. .. grid-item-card:: Headers :link: headers :link-type: doc Parsing header parameters. .. grid-item-card:: Cookies :link: cookies :link-type: doc Parsing cookie parameters. .. grid-item-card:: Path :link: path :link-type: doc Parsing path parameters. .. grid-item-card:: Body :link: body :link-type: doc Parsing request body. .. grid-item-card:: Files :link: files :link-type: doc Uploading files. API Reference ------------- .. autoclass:: dmr.components.ComponentParser :members: .. toctree:: :hidden: query.rst headers.rst cookies.rst path.rst body.rst files.rst Query parameters ================ You can define ``Query`` parameters the same way you define :data:`~dmr.components.Headers`, :data:`~dmr.components.Path` and :data:`~dmr.components.Cookies` parameters. .. note:: Parsed ``Query`` parameter must be named ``parsed_query``. This is how you can parse ``Query`` parameters: .. tabs:: .. tab:: msgspec .. literalinclude:: /examples/components/query_msgspec.py :caption: views.py :language: python :linenos: .. tab:: pydantic .. literalinclude:: /examples/components/query_pydantic.py :caption: views.py :language: python :linenos: What happens in this example? 1. We define a ``Query`` model using :class:`msgspec.Struct` or :class:`pydantic.BaseModel`. Other types are also supported: :class:`typing.TypedDict`, :func:`dataclasses.dataclass`, etc 2. Next, we use :data:`~dmr.components.Query` component, provide the model as a type parameter, and subclass it when defining :class:`~dmr.controller.Controller` type 3. Then we use ``self.parsed_query`` that will have the correct model type Customizing OpenAPI metadata for Query -------------------------------------- See :ref:`customizing_parameter_openapi`. Forcing query params to be a list --------------------------------- Internally query parameters are represented as :class:`django.utils.datastructures.MultiValueDict` in Django. It supports setting and getting several values for a single key. Users can customize how they want their values: as single values or as lists of values. To do so, use ``__dmr_force_list__`` optional attribute. Set it to :class:`frozenset` of field aliases that need to be lists. All other values will be regular single values: .. literalinclude:: /examples/components/query_list.py :caption: views.py :language: python :linenos: We don't infer ``__dmr_force_list__`` value in any way, it is up to users to set. Casting nulls ------------- Queries in Django cannot be ``None`` by default. So, when some tools send ``'null'`` as a way to represent ``None``, we need to handle that. To do so, set the field aliases that should do that into ``__dmr_cast_null__``: .. literalinclude:: /examples/components/query_cast.py :caption: views.py :language: python :linenos: We don't infer ``__dmr_cast_null__`` value in any way, it is up to users to set. API Reference ------------- .. autodata:: dmr.components.Query .. autoclass:: dmr.components.QueryComponent :members: :show-inheritance: Header parameters ================= You can define ``Headers`` parameters the same way you define :data:`~dmr.components.Query`, :data:`~dmr.components.Path` and :data:`~dmr.components.Cookies` parameters. .. note:: Parsed ``Header`` parameter must be named ``parsed_headers``. Since most headers use ``-`` to separate words, but a variable like ``cache-control`` is not a valid variable name in Python. So, you would have to use aliases for field names. Remember, that headers are also case insensitive: .. tabs:: .. tab:: msgspec .. literalinclude:: /examples/components/headers_msgspec.py :caption: views.py :language: python :linenos: .. tab:: pydantic .. literalinclude:: /examples/components/headers_pydantic.py :caption: views.py :language: python :linenos: What happens in this example? 1. We define a ``Headers`` model using :class:`msgspec.Struct` or :class:`pydantic.BaseModel`. Other types are also supported: :class:`typing.TypedDict`, :func:`dataclasses.dataclass`, etc 2. Next, we use :data:`~dmr.components.Headers` component, provide the model as a type parameter, and subclass it when defining :class:`~dmr.controller.Controller` type 3. Then we use ``self.parsed_headers`` that will have the correct model type Duplicated headers ------------------ By default Django joins several headers into a single value. This is a limitation of a WSGI protocol. But, even ASGI workers are forced to do the same in Django for compatibility. .. code:: X-Tag: a X-Tag: b Would become: .. code:: python {'X-Tag': 'a,b'} To force ``X-Tag`` to be a list you can use ``__dmr_split_commas__``. Specify lower-case header field aliases which needs to be split by a ``','`` char: .. literalinclude:: /examples/components/headers_split.py :caption: views.py :language: python :linenos: We don't infer ``__dmr_split_commas__`` value in any way, it is up to users to set. .. danger:: Some headers like .. code:: Accept: text/html, application/json Cache-Control: no-cache, no-store can naturally contain values with the ``','`` char. If you split them, you might get a messed up value. Customizing OpenAPI metadata for Headers ---------------------------------------- See :ref:`customizing_parameter_openapi`. API Reference ------------- .. autodata:: dmr.components.Headers .. autoclass:: dmr.components.HeadersComponent :members: :show-inheritance: Cookie parameters ================= You can define ``Cookies`` parameters the same way you define :data:`~dmr.components.Headers`, :data:`~dmr.components.Path` and :data:`~dmr.components.Query` parameters. .. note:: Parsed ``Cookie`` parameter must be named ``parsed_cookies``. This is how you can parse ``Cookies`` parameters: .. tabs:: .. tab:: msgspec .. literalinclude:: /examples/components/cookies_msgspec.py :caption: views.py :language: python :linenos: .. tab:: pydantic .. literalinclude:: /examples/components/cookies_pydantic.py :caption: views.py :language: python :linenos: What happens in this example? 1. We define a ``Cookies`` model using :class:`msgspec.Struct` or :class:`pydantic.BaseModel`. Other types are also supported: :class:`typing.TypedDict`, :func:`dataclasses.dataclass`, etc 2. Next, we use :data:`~dmr.components.Cookies` component, provide the model as a type parameter, and subclass it when defining :class:`~dmr.controller.Controller` type 3. Then we use ``self.parsed_cookies`` that will have the correct model type Cookies are case-sensitive. Customizing OpenAPI metadata for Cookies ---------------------------------------- See :ref:`customizing_parameter_openapi`. API Reference ------------- .. autodata:: dmr.components.Cookies .. autoclass:: dmr.components.CookiesComponent :members: :show-inheritance: Path parameters =============== Using native Django path params ------------------------------- You don't have to use :data:`~dmr.components.Path` to parse url parameters. By default Django puts all url parameters into ``self.args`` and ``self.kwargs``. Let's take a look at the full example: .. literalinclude:: /examples/components/path_raw.py :caption: views.py :language: python :linenos: What happens here? 1. We define a controller that uses regular ``self.kwargs`` dict with path params with no extra parsing from our side 2. We define a custom :class:`~dmr.metadata.ResponseSpec` instance with ``404`` as a response code, :data:`~dmr.components.Path` injects this response automatically, but since we don't use – we have to do that manually for our :ref:`response_validation` to work 3. We also show how one can use :class:`~dmr.response.APIError` to raise custom ``404`` errors when some objects are not found 4. We define an api url with :func:`django.urls.path` (or with :func:`django.urls.re_path`) and a common Django syntax for path parameters: ``'user//post//'`` Django supports multiple pre-defined path converter types: ``int``, ``uuid``, ``str``, ``slug``, ``path``. .. seealso:: - https://docs.djangoproject.com/en/stable/topics/http/urls/ - https://docs.djangoproject.com/en/stable/ref/urlresolvers/ The main downside of this method is that ``self.kwargs`` is typed as ``dict[str, Any]``. Which is not always ideal. If you need typed path parameters, use :data:`~dmr.components.Path` component with a model. .. note:: If you are using custom URL converters and :func:`django.urls.register_converter`, we won't know your url parameter schema type in advance. We default to ``str`` type for all url converters. However, if you are using a different converter schema type, you can use set ``__dmr_converter_schema__`` attribute with the specific type that you need in the schema. Using Path component and parsing models --------------------------------------- When do you need to parse path parameters into models? 1. When you need typed path parameter model 2. When they have more metadata than regular Django can provide. For example: only positive integers. Or ``str`` with an exact length 3. When you only need ``self.kwargs`` to be parsed, because ``Path`` does not support variadic url args from ``self.args`` You can define ``Path`` parameters the same way you define :data:`~dmr.components.Headers`, :data:`~dmr.components.Query` and :data:`~dmr.components.Cookies` parameters. .. note:: Parsed ``Path`` parameter must be named ``parsed_path``. This is how you can parse ``Path`` parameters into a model: .. tabs:: .. tab:: msgspec .. literalinclude:: /examples/components/path_msgspec.py :caption: views.py :language: python :linenos: .. tab:: pydantic .. literalinclude:: /examples/components/path_pydantic.py :caption: views.py :language: python :linenos: What happens in this example? 1. We define a ``Path`` model using :class:`msgspec.Struct` or :class:`pydantic.BaseModel`. Other types are also supported: :class:`typing.TypedDict`, :func:`dataclasses.dataclass`, etc 2. Next, we use :data:`~dmr.components.Path` component, provide the model as a type parameter, and subclass it when defining :class:`~dmr.controller.Controller` type 3. Then we use ``self.parsed_path`` that will have the correct model type What is the difference from the raw ``path()`` model? 1. ``Path`` component automatically injects ``404`` error into the final schema 2. It performs a second validation of the ``self.kwargs`` with new extra metadata from the ``Path`` model 3. It adds ``self.parsed_path`` attribute .. important:: Make sure that your ``path()`` URL pattern and ``Path`` model fields match. We don't automatically validate it. Customizing OpenAPI metadata for Path ------------------------------------- See :ref:`customizing_parameter_openapi`. API Reference ------------- .. autodata:: dmr.components.Path .. autoclass:: dmr.components.PathComponent :members: :show-inheritance: Request body ============ Body can be anything: json, xml, ``application/x-www-form-urlencoded``, or ``multipart/form-data``. It depends on the :class:`~dmr.parsers.Parser` that is being used for the endpoint. .. note:: Parsed ``Body`` parameter must be named ``parsed_body``. Parsing JSON ------------ Here's how you can parse ``Body`` with a model: .. tabs:: .. tab:: msgspec We support :class:`msgspec.Struct` via :class:`~dmr.plugins.msgspec.MsgspecSerializer`. .. literalinclude:: /examples/components/body_msgspec.py :caption: views.py :language: python :linenos: .. tab:: pydantic We support :class:`pydantic.BaseModel` via :class:`~dmr.plugins.pydantic.PydanticSerializer`. .. literalinclude:: /examples/components/body_pydantic.py :caption: views.py :language: python :linenos: .. tab:: attrs We support :func:`attrs.define` via :class:`~dmr.plugins.msgspec.MsgspecSerializer`. .. literalinclude:: /examples/components/body_attrs.py :caption: views.py :language: python :linenos: .. tab:: dataclasses We support :func:`dataclasses.dataclass` via both :class:`~dmr.plugins.msgspec.MsgspecSerializer` and :class:`~dmr.plugins.pydantic.PydanticSerializer`. .. literalinclude:: /examples/components/body_dataclasses.py :caption: views.py :language: python :linenos: .. tab:: TypedDict We support :class:`typing.TypedDict` via both :class:`~dmr.plugins.msgspec.MsgspecSerializer` and :class:`~dmr.plugins.pydantic.PydanticSerializer`. .. literalinclude:: /examples/components/body_typed_dict.py :caption: views.py :language: python :linenos: What happens in this example? 1. We define a ``Body`` model using :class:`msgspec.Struct`, :class:`pydantic.BaseModel`, :func:`attrs.define`, :class:`typing.TypedDict`, or :func:`dataclasses.dataclass`. Basically, model definition is only limited by the :class:`~dmr.serializer.BaseSerializer` support 2. Next, we use :data:`~dmr.components.Body` component, provide the model as a type parameter, and subclass it when defining :class:`~dmr.controller.Controller` type 3. Then we use ``self.parsed_body`` that will have the correct model type Parsing MsgPack --------------- .. note:: This feature requires ``'django-modern-rest[msgpack]'`` to be installed. MsgPack is a binary, compact and really fast format for modern APIs. Docs: https://msgpack.org Bodies can be parsed using different :class:`dmr.parsers.Parser` types. See our :doc:`../negotiation` guide on more information about content negotiations. Here's how ``msgpack`` will represent ``{"username": "example", "age": 22}`` (since it is a binary format, it will show some random unicode symbols: - `examples/components/body.msgpack `_ - `examples/components/body_wrong.msgpack `_ The only visible difference from parsing JSON is specifying a different :attr:`~dmr.controller.Controller.parsers` instance. .. literalinclude:: /examples/components/body_msgpack.py :caption: views.py :language: python :linenos: Customizing OpenAPI metadata for Body ------------------------------------- See :ref:`customizing_body_openapi`. Parsing forms ------------- .. note:: We don't recommend using forms. If you can avoid using this feature and switch to json – you totally should. Forms are only needed for compatibility with older APIs, strange libs, existing workflows. Here's an example how one can send ``application/x-www-form-urlencoded`` form data to an API endpoint with the help of :class:`~dmr.parsers.FormUrlEncodedParser`: .. literalinclude:: /examples/components/body_form.py :caption: views.py :language: python :linenos: Forcing lists and casting nulls in forms ---------------------------------------- .. warning:: All of the features below only work for ``application/x-www-form-urlencoded`` and ``multipart/form-data`` parsers. Json and other "modern" formats are not affected. Django's form parsing algorithm is 20+ years old at the moment of writing this doc. There are some known quirks to it. Forcing lists ~~~~~~~~~~~~~ Django uses :class:`django.utils.datastructures.MultiValueDict` to store body data, when parsing forms. Due to its API, it does not give ``list`` objects back easily. So, when we need a list for a field, we need to force it like this: .. literalinclude:: /examples/components/body_force_list.py :caption: views.py :language: python :linenos: Split commas ~~~~~~~~~~~~ Another problem that might happen is that some field might look like ``{'foo': 'bar,baz'}``, not ``{'foo': ['bar', 'baz']}``. To solve this, one can use a different magic attribute: .. literalinclude:: /examples/components/body_split_commas.py :caption: views.py :language: python :linenos: .. warning:: We split all data by ``','``, if your data contains ``','`` as a regular value, it might be corrupted. Be careful to use this with fields which do not contain ``','``. Like list of ints, uuids, or slugs. Casting nulls ~~~~~~~~~~~~~ It is hard to pass ``None`` as a value in a form. To solve the need for ``None`` many places offer to pass ``'null'`` as a string. We can cast ``'null'`` back to ``None`` if ``__dmr_cast_null__`` is specified. .. literalinclude:: /examples/components/body_cast_null.py :caption: views.py :language: python :linenos: You can combine this feature with both ``__dmr_split_commas__`` and ``__dmr_force_list__`` as well. API Reference ------------- .. autodata:: dmr.components.Body .. autoclass:: dmr.components.BodyComponent :members: :show-inheritance: Uploading files =============== There are several ways users can send files to a REST API: 1. Via ``multipart/form-data`` requests. It supports passing multiple files at once, it also supports sending other body parameters together with the files. It is the best option for 95% of cases. This way requires our :class:`~dmr.parsers.MultiPartParser` to be used 2. Via direct requests with a single file and a concrete content-type metadata 3. Via base64-encoded strings inside a JSON or XML files. It is only suitable for really small files Currently we support only the first option. The second option is not supported yet, but it may be supported in future releases. Currently users can implement their own :class:`~dmr.parsers.Parser` to do that. While the third way has no specific support, but it can be implemented by users directly. .. danger:: Uploading files with Python and Django can be really slow. For most cases it would be a better idea to use S3-like system to upload user-generated content. Sync uploads must never be used. Even a very small amount of traffic will completely block your app. Parsing files ------------- .. note:: Parsed ``FileMetadata`` parameter must be named ``parsed_file_metadata``. While file objects themselves are available as ``self.request.FILES``. See :attr:`django.http.HttpRequest.FILES` for more info. We don't provide any extra abstractions on top of Django's file uploads. .. note:: Official Django docs: https://docs.djangoproject.com/en/stable/topics/http/file-uploads/ All Django features for file uploads work as well for ``django-modern-rest``. Like `FILE_UPLOAD_MAX_MEMORY_SIZE `_ or `FILE_UPLOAD_HANDLERS `_. So, we do not touch Django's internal logic for file uploads. What we do instead is: we provide extra metadata to be validated / rendered in the schema: .. tabs:: .. tab:: msgspec .. literalinclude:: /examples/components/files_msgspec.py :caption: views.py :language: python :linenos: .. tab:: pydantic .. literalinclude:: /examples/components/files_pydantic.py :caption: views.py :language: python :linenos: What happens in this example? 1. We define a ``FileMetadata`` model using :class:`msgspec.Struct` or :class:`pydantic.BaseModel`. Other types are also supported: :class:`typing.TypedDict`, :func:`dataclasses.dataclass`, etc 2. Next, we use :data:`~dmr.components.FileMetadata` component, provide the model as a type parameter, and subclass it when defining :class:`~dmr.controller.Controller` type 3. Then we use ``self.parsed_file_metadata`` that will have the correct model type .. note:: Unlike raw Django, ``django-modern-rest`` allows file uploads for all HTTP methods with defined bodies. Here's the list of fields that we support as a metadata: .. code:: python [ 'size', 'name', 'content_type', 'charset', 'content_type_extra', ] We don't copy content for the validation, only metadata. Customizing OpenAPI metadata for FileMetadata --------------------------------------------- See :ref:`customizing_body_openapi`. Sending files with extra body parameters ---------------------------------------- It might be required to send some files as ``multipart/form-data`` together with some extra information, like ``user_id``. This is also supported: .. literalinclude:: /examples/components/files_with_body.py :caption: views.py :language: python :linenos: To do that also define :data:`~dmr.components.Body` component in the same controller. Sending files with json as a body parameter ------------------------------------------- You can send complex data together with files as ``multipart/form-data``. To do so, you would need to encode json as a string and attach it to a form data field. .. literalinclude:: /examples/components/files_with_json_body.py :caption: views.py :language: python :linenos: The easiest way to do this would be to declare a field as ``pydantic.Json``. However, this can be done with ``msgspec`` as well. API Reference ------------- .. autodata:: dmr.components.FileMetadata .. autoclass:: dmr.components.FileMetadataComponent :members: :show-inheritance: Configuration ============= We use ``DMR_SETTINGS`` dictionary object to store all the configuration. All keys are typed with :class:`~dmr.settings.Settings` enum keys which can be used to both set and get settings. .. note:: Remember, that ``django-modern-rest`` settings are cached after the first access. If you need to modify settings dynamically in runtime use :func:`~dmr.settings.clear_settings_cache`. You can modify the size of cache with adjusting :envvar:`DMR_MAX_CACHE_SIZE` value. Here are all keys and values that can be set. As usual, all settings go to ``settings.py`` file in your Django project. .. seealso:: - Official Django settings docs: https://docs.djangoproject.com/en/5.2/topics/settings - ``django-split-settings`` configuration helper https://github.com/wemake-services/django-split-settings We also validate defined settings in import time. See :ref:`settings_validation` for more details. Settings -------- Class that can be used to properly type settings in user's code: .. autoclass:: dmr.settings.SettingsDict :members: Class with all possible setting keys as enum: .. autoclass:: dmr.settings.Settings :show-inheritance: To get settings use :func:`~dmr.settings.resolve_setting` function together with ``Settings`` keys: .. code:: python >>> from dmr.settings import Settings, resolve_setting >>> resolve_setting(Settings.responses) [] To set settings use: .. code:: python >>> DMR_SETTINGS = {Settings.responses: []} Content negotiation ------------------- .. note:: It is recommended to always install ``msgspec`` with ``'django-modern-rest[msgspec]'`` extra for better performance. .. data:: dmr.settings.Settings.parsers Default: :class:`dmr.parsers.JsonParser` or :class:`dmr.plugins.msgspec.MsgspecJsonParser` if installed. A list of instances of subtypes of :class:`~dmr.parsers.Parser` to serialize data from the requested text format, like json or xml, into python object. Custom configuration example, let's say you want to always use ``ujson``: .. code-block:: python :caption: settings.py >>> from dmr.parsers import JsonParser >>> DMR_SETTINGS = {Settings.parsers: [JsonParser()]} .. data:: dmr.settings.Settings.renderers Default: :class:`dmr.renderers.JsonRenderer` or :class:`dmr.plugins.msgspec.MsgspecJsonRenderer` if installed. A list of instances of subtypes of :class:`~dmr.renderers.Renderer` to serialize python objects to the requested text format, like json or xml. Custom configuration example, let's say you want to always use ``ujson``: .. code-block:: python :caption: settings.py >>> from dmr.renderers import JsonRenderer >>> DMR_SETTINGS = {Settings.renderers: [JsonRenderer()]} .. data:: dmr.settings.Settings.validate_negotiation Default: ``None`` Should we validate content negotiation? Meaning: ``django-modern-rest`` finds which parser and which renderer to use based on ``Content-Type`` and ``Accept`` headers. However, by mistake people can return responses with wrong ``Content-Type`` if they construct responses manually. See :doc:`negotiation` for more info. Defaults to the value set in :data:`~dmr.settings.Settings.validate_responses` for convenience if this value is ``None``. To disable the content negotiation validation globally, use: .. code-block:: python :caption: settings.py >>> DMR_SETTINGS = { ... Settings.validate_negotiation: False, ... } Response handling ----------------- .. data:: dmr.settings.Settings.responses Default: ``[]`` The list of global :class:`~dmr.metadata.ResponseSpec` object that will be added to all endpoints' metadata as a possible response schema. Use it to set global responses' status codes like ``500``: .. code-block:: python :caption: settings.py >>> from http import HTTPStatus >>> from typing_extensions import TypedDict >>> from dmr import ResponseSpec >>> class Error(TypedDict): ... detail: str >>> # If our API can always return a 500 response with `{"detail": str}` >>> # error message: >>> DMR_SETTINGS = { ... Settings.responses: [ ... ResponseSpec( ... Error, ... status_code=HTTPStatus.INTERNAL_SERVER_ERROR, ... ), ... ], ... } .. data:: dmr.settings.Settings.validate_responses Default: ``True`` When some endpoint returns any data, we by default validate that this data matches the endpoint schema. So, this code will produce not only static typing error, but also a runtime error: .. code:: python >>> from dmr import Controller >>> from dmr.plugins.pydantic import PydanticSerializer >>> class MyController(Controller[PydanticSerializer]): ... def get(self) -> list[str]: ... return [1, 2] # <- both static typing and runtime error But, there's a runtime cost to this. It is recommended to switch this validation off for production: .. code-block:: python :caption: settings.py >>> DMR_SETTINGS = {Settings.validate_responses: False} .. note:: You can also switch off this validation per-controller with :attr:`~dmr.controller.Controller.validate_responses` and per-endpoint with ``validate_responses`` argument to :func:`~dmr.endpoint.modify` and :func:`~dmr.endpoint.validate`. .. data:: dmr.settings.Settings.semantic_responses Default: ``True`` When ``True``, parsers, renderers, and authentication classes automatically inject their semantic response specs (e.g. ``422 Unprocessable Entity``, ``406 Not Acceptable``) into every endpoint's metadata. These responses are then visible in the generated OpenAPI spec. Set to ``False`` to disable this auto-injection globally. User-defined responses (via ``@modify``, ``@validate``, controller ``responses``, or ``Settings.responses``) are not affected by this flag. Runtime response validation still works as configured by ``Settings.validate_responses``. .. code-block:: python :caption: settings.py >>> DMR_SETTINGS = {Settings.semantic_responses: False} .. data:: dmr.settings.Settings.exclude_semantic_responses Default: ``frozenset()`` Pass any status code, that you wanna exclude from semantic responses. .. code-block:: python :caption: settings.py >>> from http import HTTPStatus >> DMR_SETTINGS = { ... Settings.exclude_semantic_responses: { ... HTTPStatus.CONFLICT, ... }, ... } When this value is set to ``None`` at any level, this means that the value is reset. For example, setting ``exclude_semantic_responses=None`` on endpoint level will cancel all controller and settings level values and enable all responses back again. Error handling -------------- .. data:: dmr.settings.Settings.global_error_handler Default: ``'dmr.errors.global_error_handler'`` Globally handle all errors in the application. You can use real object or string path for the object to be imported. Here's our error handling hierarchy: 1. Per-endpoint with :meth:`~dmr.endpoint.Endpoint.handle_error` and :meth:`~dmr.endpoint.Endpoint.handle_async_error` 2. Per-controller with :meth:`~dmr.controller.Controller.handle_error` or :meth:`~dmr.controller.Controller.handle_async_error` 3. If nothing helped, ``'global_error_handler'`` is called See :func:`~dmr.errors.global_error_handler` for the callback type. .. code-block:: python :caption: settings.py >>> DMR_SETTINGS = {Settings.global_error_handler: 'path.to.your.handler'} Authentication -------------- .. data:: dmr.settings.Settings.auth Default: ``[]`` Configure authentication rules for the whole API. To enable auth for all endpoints you can use: .. code-block:: python :caption: settings.py >>> from dmr.security.django_session import DjangoSessionSyncAuth >>> DMR_SETTINGS = { ... Settings.auth: [ ... DjangoSessionSyncAuth(), ... ], ... } All auth types must be importable in settings. Throttling ---------- .. data:: dmr.settings.Settings.throttling Default: ``[]`` Configure throttling rules for the whole API. To enable throttling for all endpoints you can use: .. code-block:: python :caption: settings.py >>> from dmr.settings import Settings >>> from dmr.throttling import SyncThrottle, Rate >>> DMR_SETTINGS = { ... Settings.throttling: [SyncThrottle(10, Rate.second)], ... } All throttle types must be importable in settings. .. data:: dmr.settings.Settings.throttling_allow_unsafe_cache Default: ``True`` By default we emit :class:`~dmr.throttling.backends.django_cache.UnsafeCacheBackendWarning` at startup if an unsafe cache backend is detected for throttling backend instance (``LocMemCache``, ``DummyCache``). These backends do not share state between processes, so throttling counters are not consistent in multi-process deployments. When set to ``False``, we raise :exc:`~django.core.exceptions.ImproperlyConfigured` instead. Set to ``None`` to completely disable this check: no warnings or errors will be produced. .. code-block:: python :caption: settings.py >>> from dmr.settings import Settings >>> DMR_SETTINGS = { ... Settings.throttling_allow_unsafe_cache: False, ... } HTTP Spec validation -------------------- .. data:: dmr.settings.Settings.no_validate_http_spec Default: ``frozenset()`` A set of unique :class:`~dmr.settings.HttpSpec` codes to be globally disabled. We don't recommend disabling any of these checks globally. .. code-block:: python :caption: settings.py >>> from dmr.settings import HttpSpec >>> DMR_SETTINGS = { ... Settings.no_validate_http_spec: { ... HttpSpec.empty_request_body, ... }, ... } When this value is set to ``None`` at any level, this means that the value is reset. For example, setting ``no_validate_http_spec=None`` on endpoint level will cancel all controller and settings level values and enable all validation back again. .. autoclass:: dmr.settings.HttpSpec :show-inheritance: :members: Streaming --------- .. data:: dmr.settings.Settings.validate_events Default: ``None`` Should we validate the events in all streams? Defaults to the value set in :data:`~dmr.settings.Settings.validate_responses` for convenience if this value is ``None``. To disable the event validation globally, use: .. code-block:: python :caption: settings.py >>> DMR_SETTINGS = { ... Settings.validate_events: False, ... } OpenAPI ------- .. data:: dmr.settings.Settings.openapi_config Default: ``OpenAPIConfig(title='Django Modern Rest', version='0.1.0')`` Metadata to be used in the OpenAPI schema. See :class:`~dmr.openapi.OpenAPIConfig` for the available fields and their description. It can also be used to change the default OpenAPI spec version. .. code-block:: python :caption: settings.py >>> from dmr.openapi.config import OpenAPIConfig >>> DMR_SETTINGS = { ... Settings.openapi_config: OpenAPIConfig( ... title='My Amazing App', ... version='13.22.3', ... openapi_version='3.2.0', ... ), ... } .. data:: dmr.settings.Settings.openapi_examples_seed Default: ``None`` Random seed to use when generating missing examples in the OpenAPI spec. If set to ``None``, no examples are generated. Existing examples won't be overridden. It only works if ``'django-modern-rest[openapi]'`` extra is installed. If `polyfactory `_ package is missing, no examples are generated. .. code-block:: python :caption: settings.py >>> from dmr.settings import HttpSpec >>> from dmr.openapi.config import OpenAPIConfig >>> DMR_SETTINGS = { ... Settings.openapi_examples_seed: 10, ... } .. data:: dmr.settings.Settings.openapi_static_cdn Default: ``{}`` Optional mapping to switch OpenAPI renderers to CDN resources. Only renderers explicitly listed in this mapping will use CDN, the rest will use local bundled static files served by Django. Supported keys are: - ``swagger``: base URL to ``swagger-ui-dist`` (without file name) - ``redoc``: full URL to ``redoc.standalone.js`` - ``scalar``: full URL to ``@scalar/api-reference`` standalone bundle - ``stoplight``: base URL to ``@stoplight/elements`` (without file name) You can also modify the exact versions that we use for each tool this way. .. code-block:: python :caption: settings.py >>> DMR_SETTINGS = { ... Settings.openapi_static_cdn: { ... 'swagger': 'https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.32.1', ... 'redoc': 'https://cdn.redoc.ly/redoc/2.5.2/bundles/redoc.standalone.js', ... 'scalar': 'https://cdn.jsdelivr.net/npm/@scalar/api-reference@1.49.2/dist/browser/standalone.js', ... 'stoplight': 'https://unpkg.com/@stoplight/elements@9.0.16', ... }, ... } Hacks ----- .. data:: dmr.settings.Settings.django_treat_as_post Default: ``frozenset({'PUT', 'PATCH'})`` By default Django only populates :attr:`django.http.HttpRequest.POST` and :attr:`django.http.HttpRequest.FILES` for ``POST`` requests. Some parsers like :class:`~dmr.parsers.MultiPartParser` require ``.POST`` and ``.FILES`` to be set to work with :data:`~dmr.components.Body` and :data:`~dmr.components.FileMetadata`. However, we build REST APIs where more methods are in use, not just ``POST``. So, we use this setting to populate ``.POST`` and ``.FILES`` when parser is a subtype of either :class:`~dmr.parsers.SupportsFileParsing` or :class:`~dmr.parsers.SupportsDjangoDefaultParsing` and we work with components that require request body. .. note:: This is a really advanced setting. If you are not creating your own components or your own parsers - do not change it. Environment variables --------------------- .. envvar:: DMR_MAX_CACHE_SIZE Default: ``256`` We use :func:`functools.lru_cache` in many places internally. For example: - To create json encoders and decoders only once - To create type validation objects in :class:`~dmr.serializer.BaseEndpointOptimizer` You can control the size / memory usage with this setting. Increase if you have a lot of different return types. .. envvar:: DMR_USE_COMPILED Default: ``1`` We compile some modules to C-extensions with :ref:`mypyc`. If you want to disable the extensions and fallback to pure Python implementation, set this variable to ``0``. It is only recommended for debugging. It should be set to ``1`` in production for maximum speed. API Reference ------------- .. autofunction:: dmr.settings.resolve_setting .. autofunction:: dmr.settings.clear_settings_cache Plugins ======= To be able to support multiple :term:`serializer` models like ``pydantic`` and ``msgspec``, we have a concept of a plugin. There are several bundled ones, but you can write your own as well. To do that see our advanced :ref:`serializer` guide. As a user you are only interested in choosing the right plugin for the :term:`controller` definition. .. tabs:: .. tab:: msgspec .. code:: python from dmr.plugins.msgspec import MsgspecSerializer .. tab:: pydantic .. tip:: If you only use ``json`` :doc:`parsers and renderers `, it would be faster to use :class:`~dmr.plugins.pydantic.PydanticFastSerializer` instead. .. code:: python from dmr.plugins.pydantic import PydanticSerializer Customizing serializers ----------------------- There are several things why you can possibly want to customize an existing serializer. Support more data types ~~~~~~~~~~~~~~~~~~~~~~~ By default, :meth:`~dmr.serializer.BaseSerializer.serialize_hook` and :meth:`~dmr.serializer.BaseSerializer.deserialize_hook` support not that many types. You can customize the serializer to know how to serializer / deserialize more types by extending it and customizing the method you need. Customizing the serializer context ---------------------------------- We use :class:`dmr.endpoint.SerializerContext` type to deserialize all components from a single model, so it would be much faster than parsing each component separately. This class can be customized for several reasons. Change the default strictness ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Tools like ``pydantic`` offer several useful type conversions in non-strict mode. For example, ``'1'`` can be parsed as ``1`` if strict mode is not enabled. It is kinda useful for request bodies, where you don't control the clients. Here's how we determine the default strictness for ``pydantic`` models: 1. If :attr:`~dmr.endpoint.SerializerContext.strict_validation` is not ``None``, we return the serializer-level strictness 2. Then ``pydantic`` looks at ``strict`` attribute in :class:`~pydantic.config.ConfigDict` 3. Then ``pydantic`` looks at ``strict`` attribute for individual :func:`~pydantic.fields.Field` items We recommend to change the strictness on a per-model basis, but if you want to, you can subclass the ``SerializerContext`` to be strict / non-strict and use it for all controllers. Endpoint optimizers ------------------- Before actually serving any requests, during import-time, we try to optimize the future validation. For example, :class:`pydantic.TypeAdapter` takes time to be created. Why doing it on the first request, when we can do that during the import time? Each serializer must provide a type, which must be a subclass of :class:`~dmr.serializer.BaseEndpointOptimizer` to optimize / pre-compile / create / cache things that it can. Writing a custom plugin ------------------------ Our API is flexible enough to potentially support any custom third-party serializers of your choice, like: - https://github.com/python-attrs/cattrs - https://github.com/reagento/adaptix - etc Follow the API of :class:`~dmr.plugins.pydantic.PydanticSerializer` and :class:`~dmr.plugins.msgspec.MsgspecSerializer`. You would need to: - Provide a way to serializer and deserialize your models - Provide serializer error converter by overriding :meth:`~dmr.serializer.BaseSerializer.serialize_validation_error` method - Provide a way to get the OpenAPI / JsonSchema schema from your models, see :class:`dmr.serializer.BaseSchemaGenerator`. Example implementations: :class:`~dmr.plugins.pydantic.schema.PydanticSchemaGenerator` and :class:`~dmr.plugins.msgspec.schema.MsgspecSchemaGenerator` Pydantic plugin --------------- PydanticFastSerializer ~~~~~~~~~~~~~~~~~~~~~~ ``pydantic`` plugin contains one extra serializer optimized for ``json`` usage. Our regular API requires :doc:`parsers and renderers ` to format the final response, so you can negotiate the request and response formats. However, for cases when you only have ``json`` requests and responses (which is quite common), use :class:`~dmr.plugins.pydantic.PydanticFastSerializer`. .. warning:: It will ignore all parsers and serializers and use the ``pydantic`` own way to serialize and deserialize objects to ``json`` bytestring. It will work from **3 up 10 times** faster depending on the data then the common serializer. .. literalinclude:: /examples/plugins/pydantic_fast.py :caption: views.py :language: python :linenos: :emphasize-lines: 12 No API changes are required to use it if you don't use other request / response formats. Serialization / deserialization flags ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We have to special attributes to change how ``pydantic`` serializes data: 1. :attr:`~dmr.plugins.pydantic.PydanticSerializer.to_json_kwargs` for serialization purposes 2. :attr:`~dmr.plugins.pydantic.PydanticSerializer.to_model_kwargs` for deserialization purposes By default these flags only pass ``{'by_alias': True}`` to support field aliases, when they are defined. For example, when working with :class:`pydantic.types.Json`, one can set ``round_trip`` to ``True`` (which is not passed by default, because it disables :func:`computed fields `): .. literalinclude:: /examples/plugins/pydantic_round_trip.py :caption: views.py :language: python :linenos: .. seealso:: Docs: https://docs.pydantic.dev/2.3/usage/types/json/ Msgspec plugin -------------- attrs support ~~~~~~~~~~~~~ We support :func:`attrs.define` via ``msgspec`` compatibility layer. It has its own limitations. See `msgspec docs `_. Native support of ``attrs`` can be implemented in the future with its own serializer. Serializing models and Querysets ================================ .. rubric:: Quote of the day .. epigraph:: | There are things that are easy. | There are things that seem easy. | Usually they are different things. -- `Nikita Sobolev `_, CPython core developer Django is built around its :class:`~django.db.models.query.QuerySet` type. Of course, we have to make sure that it is supported. Prepare yourself for a wild ride! This section will not only be about queryset, but also about architecture of Django applications. .. important:: We offer a brand new way of working with :class:`~django.db.models.query.QuerySet`. It is the best thing that ever happened to Django's serialization. We built our serialization patterns on: 1. Simplicity 2. Database table independence from the serialization schema 3. Customizability: you can change anything at anytime 4. Performance: no extra fields, no extra queries, fast serialization 5. Everything must be typed at all times Oversimplied example -------------------- Let's start with the very simple definition of a regular model, with no foreign keys or many-to-many fields. Our approach is to always move all the business logic away from the view. Even in examples, because they form the habit of developers and LLMs who are reading it. .. seealso:: Full-featured example with the proper layers can be found here: https://github.com/wemake-services/wemake-django-template/blob/master/%7B%7Bcookiecutter.project_name%7D%7D/server/apps/main/api/views.py Model ~~~~~ Our regular :class:`~django.db.models.Model` definition: .. literalinclude:: ../django_test_app/server/apps/model_simple/models.py :caption: models.py :language: python :linenos: For this example, the model won't have any FK or M2M fields. In the next examples we will show how to work with them as well. Serializer schemas ~~~~~~~~~~~~~~~~~~ Next, let's define serializer schemas to get the incoming data and return the response. You can use any schema type, including :class:`pydantic.BaseModel`, :func:`attrs.define`, :class:`typing.TypedDict`, etc. For this example we will use ``pydantic``, because it is the most familiar tools for the most programmers: .. literalinclude:: ../django_test_app/server/apps/model_simple/serializers.py :caption: serializers.py :language: python :linenos: .. important:: This step is really important! Because we **have to** separate database models and serializer schemas from each other. Why? 1. Because it gives us more control over the serialization process both ways 2. Because new database fields will not automatically appear in your API spec 3. Because removing a database field will not break the API contract 4. Because versioning the API becomes much easier 5. Because explicit typing will let you catch more errors earlier Services ~~~~~~~~ Now, let's define our business logic. .. note:: We don't give any architectural advice here, you can use any approach that you like for your projects: DDD, Clean or Hexagonal Architecture, Functional Core and Imperative Shell, whatever you like. But, you business logic must be separated from views for better testing and better composition. For this example, we will use the simplest ``services.py`` layer for our business logic: .. literalinclude:: ../django_test_app/server/apps/model_simple/services.py :caption: services.py :language: python :linenos: There's nothing fancy about it. Just creating a model from the typed input data. Views ~~~~~ Now, we can define views that will use everything from the above. .. tabs:: .. tab:: Minimalistic Just convert models from attributes, using the builtin ``.model_validate()`` converter. For ``msgspec`` one can use :func:`msgspec.convert`. - The main reason to use this approach is that it is short and easy - The main reason not to use this approach is that errors will only show up during tests. Not during type-checking. For example, removing ``customer_service_uid`` from ``models.py`` (for some business reason) will not trigger any type checking errors. If this line is not covered with tests, your API will not work: .. code-block:: Traceback (most recent call last): File "server/apps/model_simple/views/minimalistic.py", line 42 return UserSchema.model_validate( ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^ pydantic.ValidationError: Object missing required field `customer_service_uid` .. literalinclude:: ../../django_test_app/server/apps/model_simple/views/minimalistic.py :caption: views.py :language: python :linenos: .. tab:: Detailed Convert models to schemas manually. It might seem like a lot of code, but actually, it is pretty simple to do with (especially with the help of LLM). - The main reason to use this approach is correctness. For example, removing ``customer_service_uid`` model field will now trigger an early type-checking error, unlike the "Minimalistic" version, which will only fail in runtime: .. code-block:: server/apps/model_simple/views/detailed.py:28: error: "User" has no attribute "customer_service_uid" [attr-defined] server/apps/model_simple/views/detailed.py:48: error: "User" has no attribute "customer_service_uid" [attr-defined] - The main reason not to use this approach is its verbosity .. literalinclude:: ../django_test_app/server/apps/model_simple/views/detailed.py :caption: views.py :language: python :linenos: Now you have your REST API that returns fully typed model responses and can work with :class:`~django.db.models.query.QuerySet` and :class:`~django.db.models.Model` instances. Realistic example ----------------- Now, let's see how a more realistic layout may look like. It will include: - ``ForeignKey`` relationship - ``ManyToMany`` relationship - DI for realistic expectations - Mappers and services separate layers Models ~~~~~~ We start with models definitions. .. literalinclude:: ../django_test_app/server/apps/model_fk/models.py :caption: models.py :language: python :linenos: We added two models: - ``Role`` for foreign key relation - ``Tag`` for many-to-many relation Serializer schemas ~~~~~~~~~~~~~~~~~~ Next, let's see how serializer schemas are defined. .. literalinclude:: ../django_test_app/server/apps/model_fk/serializers.py :caption: serializers.py :language: python :linenos: Note that we model foreign key and many-to-many relations here as nested schemas or list of nested schemas. However, you can also model the same thing as ``role_id: int`` and ``tags: list[int]`` to support ids for linking. That's the beautify of this extremely simple approach: it is customizable to the core. Views ~~~~~ In this example, it would be easier to start with ``views.py``: .. literalinclude:: ../django_test_app/server/apps/model_fk/views.py :caption: views.py :language: python :linenos: What happens here? 1. We refactored our views to only run an instance of a specific service, which we get using ``HasContainer.resolve`` call, which is our DI 2. We now don't construct any serializer schemas inside our views, we move to its independent infra layer 3. We now use :ref:`pagination` to list all users Our views here are reduced to a single line of code, which do everything inside the business logic. Exactly the way it should be for scalable and reliable applications. DI ~~ In this example we use `punq `_ as a simplistic DI container to show how big projects really handle such cases. .. note:: You are not forced to use ``punq``, we don't enforce any DI framework. Our principle is to :ref:`bring-your-own-di`. The DI part looks like this: .. literalinclude:: ../django_test_app/server/apps/model_fk/implemented.py :caption: views.py :language: python :linenos: What happens here? 1. We define a class that will be used as a mixin for all future controllers 2. This class provides a pre-built container with all the dependencies 3. Users can call ``self.resolve`` inside controllers to resolve specific dependencies Now, let's see how we create objects in the database. Services ~~~~~~~~ Again, this is just an example. You are not forced to create this specific service-based architecture. Use whatever layers separation practice as you want. Our big example uses `usecases `_ as the main logic entities and entry points. However, our ``services.py`` is the simplest and the shortest way. That's why we are using them as an example: .. literalinclude:: ../django_test_app/server/apps/model_fk/services.py :caption: services.py :language: python :linenos: What happens here? 1. We define three services: one per create operation 2. Some of them have dependencies defined as dataclass fields, like ``_mapper: UserMap``, these fields will be resolved by our DI 3. Each service does just a single thing, it would be easy to compose them Now we are ready for the final layer: mapping of the created database models. Mappers ~~~~~~~ Mappers just map database models into serialization schemas. .. tip:: In real projects, it is better to have a separate layer that does mapping of database models into `serialization schemas `_ It will allow you to compose mappers freely. For example, it allows you to create compatibility layers, when some model have some database fields removed, but still need a way to send users the same API schema. Or it can do some small representation logic, like combining ``first_name`` and ``last_name`` of users into ``full_name``. Or handle :ref:`pagination`, as we do in this example. .. literalinclude:: ../django_test_app/server/apps/model_fk/mappers.py :caption: mappers.py :language: python :linenos: Now we have a fully working application. With composable and flexible schemas, which will: 1. Report any type errors early 2. Be customizable to the core 3. Can be used in a good architecture in big real business apps 4. Change independently from models Conclusions ~~~~~~~~~~~ Here's the final table to help you decide what to use: +------------------+----------------------+ | Your App | What to Use | +==================+======================+ | Todo App | Minimalistic Example | +------------------+----------------------+ | Real Application | Mappers | +------------------+----------------------+ django-mantle ------------- If you want to automate the mapping part and automagically convert ``QuerySet`` into typed models, you can use `django-mantle `_. - Allows you to move your business logic into type-safe Python classes, decoupled from the Django ORM. - Provides automatic generation (with declarative overrides) of efficient ORM queries, including limited field fetches with ``only()`` and ``defer()`` and prefetching related objects, avoiding N+1 query problems. - Uses a modern and performant approach to serialisation and validation. - Provides a progressive API, with a minimal surface area by default, and depth when needed. It’s your type-safe layer around Django’s liquid core. .. literalinclude:: /examples/queryset/django_mantle.py :caption: views.py :language: python :linenos: Content negotiation =================== ``django-modern-rest`` supports content negotiation. We have two abstractions to do that: - Parsers: instances of subtypes of :class:`~dmr.parsers.Parser` type that parses request body based on `Content-Type `_ header into python primitives - Renderers: instances of subtypes of :class:`~dmr.renderers.Renderer` type that renders python primitives into a requested format based on the `Accept `_ header By default ``json`` parser and renderer are configured to use ``msgspec`` if it is installed (recommended). We fallback to pure-python implementation if ``msgspec`` is not installed. Supported content types ----------------------- We ship several pre-defined parsers and renderers. Parsers: - ``application/json`` with :class:`~dmr.plugins.msgspec.MsgspecJsonParser` and :class:`~dmr.parsers.JsonParser` - ``application/msgpack`` with :class:`~dmr.plugins.msgspec.MsgpackParser` - ``multipart/form-data`` with :class:`~dmr.parsers.MultiPartParser` - ``application/x-www-form-urlencoded`` with :class:`~dmr.parsers.FormUrlEncodedParser` Renderers: - ``application/json`` with :class:`~dmr.plugins.msgspec.MsgspecJsonRenderer` and :class:`~dmr.renderers.JsonRenderer` - ``application/msgpack`` with :class:`~dmr.plugins.msgspec.MsgpackRenderer` - ``*/*`` with :class:`~dmr.renderers.FileRenderer` You can :ref:`write your own `! How parser and renderer are selected ------------------------------------ We select a :class:`~dmr.parsers.Parser` instance if there's a :data:`~dmr.components.Body` or :data:`~dmr.components.FileMetadata` components to parse. Otherwise, for performance reasons, no parser is selected at all. Nothing to parse - no parser is selected. Here's how we select a parser, when it is needed: 1. We look at the ``Content-Type`` header 2. If it is not provided, we take the default parser, which is the first specified parser for the endpoint, aka the most specific one 3. If there's a ``Content-Type`` header, we try to exactly match known parsers based on their :attr:`~dmr.parsers.Parser.content_type` attribute. This is a positive path optimization 4. If there's no direct match, we now include parsers that have ``*`` pattern in supported content types. We match them in order based on ``'specificity', 'quality'``, the first match wins 5. If no parser fits the request's content type, we raise :exc:`~dmr.exceptions.RequestSerializationError` We select :class:`~dmr.renderers.Renderer` instance for all responses (including error responses), before performing any logic. If the selection fails, we don't even try to run the endpoint. Here's how we select a renderer: 1. We look at ``Accept`` header 2. If it is not provided, we take the default renderer, which is the first specified renderer for the endpoint, aka the most specific one 3. If there's an ``Accept`` header, we use all renderers specified for this endpoint to match the best accepted type, based on ``quality, specificity``, the first match wins 4. If no renderer fits for the accepted content types, we raise :exc:`~dmr.exceptions.ResponseSchemaError` .. note:: When constructing responses manually, like: .. code-block:: python >>> from django.http import HttpResponse >>> response = HttpResponse(b'[]') The renderer is selected as usual, but no actual rendering is done. However, all other validation works as expected. Which means that even though renderer is not actually used, its metadata is still required to validate the response content type. But, when using :meth:`~dmr.controller.Controller.to_response` method, renderer will be executed. So, it is a preferred method for regular responses. .. important:: Settings always must have one parser and one renderer defined at all times, because utils like :func:`dmr.response.build_response` fallbacks to settings-defined renderers in some error cases. .. _alternative-json: Alternative JSON backends ------------------------- By default, we use ``msgspec`` if it installed. When it is not, we fallback to :class:`~dmr.parsers.JsonParser` and :class:`~dmr.renderers.JsonRenderer` types, which use native pure Python :mod:`json` with limited features support and very low performance. For users, who does not want to use ``msgspec``, but prefer `orjson `_ for some reason, we provide the following API: .. literalinclude:: /examples/negotiation/orjson_integration.py :caption: views.py :language: python :linenos: Any module that exposes API that fits :class:`~dmr.internal.json.JsonModule` protocol is supported. If some API does not fit exactly, you can create a small wrapper that would fit, like :class:`~dmr.internal.json.NativeJson`. ``orjson`` is the recommended alternative because it is really fast, returns ``bytes`` directly, avoiding an extra encode step, and is significantly faster than the standard library ``json``. Customizing negotiation process ------------------------------- .. note:: If you only use ``json`` API - there's no need to change anything. However, if you want to support other formats like ``xml`` or custom ones, you can write and configure your own parsers and renderers. Parsers and renderers might be defined on different levels. Here are all the possible ways starting with the most specific one, going back to the less specific: .. tabs:: .. tab:: per endpoint .. literalinclude:: /examples/negotiation/per_endpoint.py :caption: views.py :language: python :linenos: :emphasize-lines: 35 .. tab:: per controller .. literalinclude:: /examples/negotiation/per_controller.py :caption: views.py :language: python :linenos: :emphasize-lines: 39-40 .. tab:: per settings .. literalinclude:: /examples/negotiation/settings.py :caption: settings.py :language: python :linenos: :emphasize-lines: 6-7 First parsers / renderers definition found, starting from the top, will win and be used for the endpoint. You can also modify :attr:`dmr.endpoint.Endpoint.request_negotiator_cls` and :attr:`dmr.endpoint.Endpoint.response_negotiator_cls` to completely change the negotiation logic to fit your needs. This is possible on per-controller level. .. _custom-parsers-and-renderers: Writing custom parsers and renderers ------------------------------------ And here's how our test ``xml`` parser and renderer are defined: .. literalinclude:: /examples/negotiation/negotiation.py :caption: negotiation.py :language: python :linenos: .. warning:: This parser is only used as a demo, do not use it in production, prefer more tested and battle-proven solutions. .. _conditional-types: Using different schemes for different content types --------------------------------------------------- Sometimes we have to accept different schemas based on the content type. `According to the OpenAPI spec `_, :data:`~dmr.components.Body` should support different content types. We utilize :data:`typing.Annotated` and :func:`dmr.negotiation.conditional_type`: .. literalinclude:: /examples/negotiation/conditional_body_types.py :caption: views.py :language: python :linenos: We strictly validate that each content type will have its own unique model. As the last example shows, it is impossible to send ``_XMLRequestModel`` with ``Content-Type: application/json`` header. The same works for return types as well: .. literalinclude:: /examples/negotiation/conditional_return_types.py :caption: views.py :language: python :linenos: Depending on the content type - your return schema will be fully validated as well. In the example above, it would be an error to return something other than ``list[str]`` for ``json`` content type, and it would also be an error to return anything other than ``dict[str, str]`` for ``xml`` content type. You can combine conditional bodies and conditional return types in a type-safe and fully OpenAPI-compatible way. .. _error-model-negotiation: Using different error models for different content types -------------------------------------------------------- The same can be done with error models. Let's say you want to present JSON and XML error models differently. We utilize the same technique :data:`typing.Annotated` and :func:`dmr.negotiation.conditional_type`: .. literalinclude:: /examples/negotiation/conditional_error_model.py :caption: views.py :language: python :linenos: Note that you would also have to customize :meth:`~dmr.controller.Controller.format_error` accordingly. Limiting response specs to content types ---------------------------------------- Sometimes some responses can only be returned for some content types. We need a way to describe it: both for our validation and OpenAPI spec. To do so, we utilize :attr:`~dmr.metadata.ResponseSpec.limit_to_content_types` attribute: .. literalinclude:: /examples/negotiation/limit_to_content_types.py :caption: views.py :language: python :linenos: Disabling content negotiation validation ---------------------------------------- When :data:`~dmr.settings.Settings.validate_responses` is active, we also validate that the returned ``Content-Type`` header matches the negotiated content type. To disable this validation, if you for some reason, want to break the content negotiation protocol, you can set :data:`~dmr.settings.Settings.validate_negotiation` to ``False``. We support several layers of configuration: .. tabs:: .. tab:: per endpoint .. literalinclude:: /examples/negotiation/validate_negotiation_endpoint.py :caption: views.py :language: python :linenos: .. tab:: per controller .. literalinclude:: /examples/negotiation/validate_negotiation_controller.py :caption: views.py :language: python :linenos: .. tab:: per settings .. literalinclude:: /examples/negotiation/validate_negotiation_settings.py :caption: settings.py :language: python :linenos: Despite the fact that ``Content-Type`` is not validated, we still validate the response schema. So, you must still provide correct renderers and parsers to do validation. Otherwise, you would have to also disable :ref:`response_validation`. .. warning:: We do not ever recommend to do this in any sane setups. This only makes sense for legacy API contracts that you want to migrate to ``django-modern-rest``. Negotiation API --------------- .. autoclass:: dmr.negotiation.RequestNegotiator :members: .. autoclass:: dmr.negotiation.ResponseNegotiator :members: .. autoclass:: dmr.negotiation.ContentType :members: .. autofunction:: dmr.negotiation.conditional_type .. autofunction:: dmr.negotiation.request_parser .. autofunction:: dmr.negotiation.request_renderer .. autofunction:: dmr.negotiation.get_conditional_types Parser API ---------- .. autoclass:: dmr.parsers.Parser :members: Renderer API ------------ .. autoclass:: dmr.renderers.Renderer :members: Existing parsers and renderers ------------------------------ Parsers ~~~~~~~ .. autoclass:: dmr.plugins.msgspec.MsgspecJsonParser :members: .. autoclass:: dmr.plugins.msgspec.MsgpackParser :members: .. autoclass:: dmr.parsers.JsonParser :members: .. autoclass:: dmr.parsers.MultiPartParser :members: .. autoclass:: dmr.parsers.FormUrlEncodedParser :members: Renderers ~~~~~~~~~ .. autoclass:: dmr.plugins.msgspec.MsgspecJsonRenderer :members: .. autoclass:: dmr.plugins.msgspec.MsgpackRenderer :members: .. autoclass:: dmr.renderers.JsonRenderer :members: .. autoclass:: dmr.renderers.FileRenderer :members: Advanced API ------------ .. autoclass:: dmr.parsers.SupportsFileParsing :members: .. autoclass:: dmr.parsers.SupportsDjangoDefaultParsing :members: Error handling ============== ``django-modern-rest`` has 3 layers where errors might be handled. It provides flexible error handling logic on :class:`~dmr.endpoint.Endpoint`, :class:`~dmr.controller.Controller`, and :func:`global ` levels. All error handling functions always accept 3 arguments: 1. :class:`~dmr.endpoint.Endpoint` where error happened 2. :class:`~dmr.controller.Controller` where error happened 3. Exception that happened Here's how it works: 1. We first try to call ``error_handler`` that was passed into the endpoint definition via :func:`~dmr.endpoint.modify` or :func:`~dmr.endpoint.validate` 2. If it returns :class:`django.http.HttpResponse`, return it to the user 3. If it raises, call :meth:`~dmr.controller.Controller.handle_error` for sync controllers and :meth:`~dmr.controller.Controller.handle_async_error` for async controllers 4. If controller's handler returns :class:`~django.http.HttpResponse`, return it to the user 5. If it raises, call configured global error handler, by default it is :func:`~dmr.errors.global_error_handler` (it is always sync) .. warning:: There are two things to keep in mind: 1. Async endpoints will require async ``error_handler`` parameter, Sync endpoints will require sync ``error_handler`` parameter. This is validated on endpoint creation 2. We don't allow to define sync ``handle_error`` handlers for async controllers. We also don't allow async ``handle_async_error`` handlers for sync controllers. .. note:: :exc:`~dmr.response.APIError` does not follow any of these rules and has a default handler, which will convert an instance of ``APIError`` to :class:`~django.http.HttpResponse` via :meth:`~dmr.controller.Controller.to_error` call. You don't need to catch ``APIError`` in any way, unless you know what you are doing. Customizing endpoint error handler ---------------------------------- Let's pass custom error handling to a single endpoint: .. literalinclude:: /examples/error_handling/endpoint.py :caption: views.py :language: python :linenos: In this example we add error handling defined as ``division_error`` to ``patch`` endpoint (which serves as a division operation), while keeping ``post`` endpoint (which serves as a multiply operation) without a custom error handler. Because :exc:`ZeroDivisionError` can't happen in ``post``. Per-endpoint's error handling has a priority over per-controller and global handlers. You can also define endpoint error handlers as controller methods and pass them wrapped with :func:`~dmr.errors.wrap_handler` as handlers. Like so: .. literalinclude:: /examples/error_handling/wrap_endpoint.py :caption: views.py :language: python :linenos: Customizing controller error handler ------------------------------------ Let's create custom error handling for the whole controller: .. literalinclude:: /examples/error_handling/controller.py :caption: views.py :language: python :linenos: In this example we are using `zapros `_ HTTP client to proxy an HTTP ``GET`` and ``POST`` requests to some other API service. If we fail to send a request and raise a specific HTTP client error, we return an error with ``424`` error code. Going further ------------- Now you can understand how you can create: - Endpoints with custom error handlers - Controllers with custom error handlers - :class:`~dmr.metadata.ResponseSpec` objects for new error response schemas You can dive even deeper and: - Subclass :attr:`~dmr.controller.Controller` and provide default error handling for this specific subclass - Redefine :attr:`~dmr.controller.Controller.endpoint_cls` and change how one specific endpoint behaves on a deep level, see :meth:`~dmr.endpoint.Endpoint.handle_error` and :meth:`~dmr.endpoint.Endpoint.handle_async_error` Error handling diagram ---------------------- The same error handling logic can be represented as a diagram: .. mermaid:: :caption: Error handling logic :config: {"theme": "forest"} graph TB Start[Request] --> Error{Error?}; Error -->|Yes| Endpoint[Endpoint-level handler]; Endpoint --> EndpointHandler{Raises or returns response?}; EndpointHandler -->|response| Failure[Error response]; EndpointHandler -->|raises| Controller[Controller-level handler]; Controller --> ControllerHandler{Raises or returns response?}; ControllerHandler -->|response| Failure[Error response]; ControllerHandler -->|raises| Global[Global handler]; Global --> GlobalHandler{Raises or returns response?}; GlobalHandler -->|response| Failure[Error response]; GlobalHandler -->|raises| Reraises[Reraises error]; Error ---->|No| Success[Successful response]; .. note:: If :ref:`handler500` is configured, it will catch all unhandled errors in the provided scope and return ``500`` errors with the correct payload. .. _customizing-error-messages: Customizing error messages -------------------------- All error messages, including pre-defined ones, can be easily customized on a per-controller basis. To do so, you would need to change: 1. :attr:`~dmr.controller.Controller.error_model` attribute for all controllers that will be using this error message schema 2. :meth:`~dmr.controller.Controller.format_error` method to provide custom runtime error formatting .. literalinclude:: /examples/error_handling/custom_error_messages.py :caption: views.py :language: python :linenos: This will also change the OpenAPI schema for the affected controller. See :class:`~dmr.errors.ErrorModel` for the default error model schema. And :func:`~dmr.errors.format_error` for the default error formatting. See :ref:`content negotiation ` docs about how to use different error models for different content types. Customizing error headers and cookies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Let's say you want to customize how all errors responses behave and add a header, for example, ``X-Error-Id`` from your error tracking system. How this can be done? .. literalinclude:: /examples/error_handling/custom_error_headers.py :caption: views.py :language: python :linenos: To attach response headers or cookies to the error model we use :class:`~dmr.metadata.ResponseSpecMetadata` inside :data:`typing.Annotated` type. We also have to redefine :meth:`~dmr.controller.Controller.to_error` to add missing ``X-Error-Id`` headers for your error responses. You can do the same for all responses, not just failing ones. For this, override :meth:`~dmr.controller.Controller.to_response`. This can also be used to attach ``RateLimit`` headers and other :doc:`throttling` information. Problem Details --------------- .. seealso:: RFC: https://datatracker.ietf.org/doc/html/rfc9457 ``django-modern-rest`` supports customizing of all error message inside the framework, including builtin ones. :class:`~dmr.problem_details.ProblemDetailsError` is a great example of how it can be done. It is a regular subclass of :class:`~dmr.response.APIError`, which does not have any special handling inside our framework. This is done on purpose, so we can be sure that users also can to customize their exceptions any way they need. We support two main use-cases for Problem Details. Always raising Problem Details ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To always use :class:`~dmr.problem_details.ProblemDetailsError` inside your controller you would need to: 1. Define :attr:`~dmr.controller.Controller.error_model` attribute as :class:`~dmr.problem_details.ProblemDetailsModel` 2. Raise an exception itself, pass all the required fields 3. Convert other message to the Problem Details format using :meth:`~dmr.controller.Controller.format_error` method .. literalinclude:: /examples/error_handling/problem_details.py :caption: views.py :language: python :linenos: Conditionally raising Problem Details ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Another way is to :doc:`negotiate ` the error response format. How does it work? 1. When user sends a request with ``Accept`` header with ``application/problem+json`` content type, we will return Problem Details errors 2. When ``application/json`` or any other content type is sent, we return regular :class:`~dmr.errors.ErrorModel` error payloads To do so, you would need a slightly more difficult setup: 1. Define :attr:`~dmr.controller.Controller.error_model` attribute as the result of :meth:`~dmr.problem_details.ProblemDetailsError.error_model` method call. It will add :ref:`conditional schema types ` to your error responses 2. Define several :class:`~dmr.renderers.Renderer` types, including the one which will handle ``application/problem+json`` 3. Raise a conditional exception: use :meth:`~dmr.problem_details.ProblemDetailsError.conditional_error` to only raise Problem Details when the correct accepted type is passed 4. Convert other message to the Problem Details format using :meth:`~dmr.controller.Controller.format_error` method when the correct accepted type is passed .. literalinclude:: /examples/error_handling/problem_details_negotiation.py :caption: views.py :language: python :linenos: .. tip:: You can still make ``application/problem+json`` the default and when ``application/json`` (or any other type) is explicitly requested return the :class:`~dmr.errors.ErrorModel` errors. Handling validation errors from models -------------------------------------- When creating models with, for example, :class:`pydantic.BaseModel`, your validation can fail. This error will not be handled by design. Why? Because catching all specific validation errors for a specific serializer that can happen in your application will do more harm than good. This is the default behavior: .. literalinclude:: /examples/error_handling/pydantic_validation_error.py :caption: views.py :language: python :linenos: If you want to catch this error in a specific place and attach a specific behavior, use an error handler at a proper level. For example, here we would handle it on a controller level: .. literalinclude:: /examples/error_handling/pydantic_validation_handled.py :caption: views.py :language: python :linenos: Now, the error is handled: we modified its error text and status code. Remember not to dump all the error information out to users, since they might contain sensitive data. .. seealso:: See :ref:`handler500` if you want to change the ``500`` error rendering. API Reference ------------- .. autofunction:: dmr.errors.global_error_handler .. autofunction:: dmr.errors.wrap_handler .. autoclass:: dmr.errors.ErrorType :members: .. autoclass:: dmr.errors.ErrorModel :members: .. autoclass:: dmr.errors.ErrorDetail :members: .. autofunction:: dmr.errors.format_error Problem Details API ~~~~~~~~~~~~~~~~~~~ .. autoclass:: dmr.problem_details.ProblemDetailsError :members: :show-inheritance: .. autoclass:: dmr.problem_details.ProblemDetailsModel :members: Throttling ========== ``django-modern-rest`` ships its own throttling (also known as "rate limiting") mechanism. Here's how everything works. .. important:: If you have an option not to use ratelimiting in Django, but to use it on the HTTP Proxy side, you should prefer the proxy. It is significantly faster and more secure. Defining throttling ------------------- We have two classes to define throttling: - :class:`dmr.throttling.SyncThrottle` for sync endpoints - :class:`dmr.throttling.AsyncThrottle` for async endpoints We can define throttling on three different levels: .. tabs:: .. tab:: per endpoint .. literalinclude:: /examples/throttling/per_endpoint.py :caption: views.py :linenos: :language: python .. tab:: per controller .. literalinclude:: /examples/throttling/per_controller.py :caption: views.py :linenos: :language: python .. tab:: per settings .. code-block:: python :caption: settings.py :linenos: >>> from dmr.settings import Settings >>> from dmr.throttling import SyncThrottle, Rate >>> DMR_SETTINGS = {Settings.throttling: [SyncThrottle(5, Rate.minute)]} Providing several throttling instances means that all of them must succeed. When multiple throttling rules are defined on different levels, their rules are joined. For example: .. literalinclude:: /examples/throttling/multiple.py :caption: views.py :linenos: :language: python Will guard ``GET`` method with 2 throttling checks: 1. Not more ``<=`` than 1 request per minute 2. And not more ``<=`` than 5 requests per hour Customizing throttling ---------------------- Rates ~~~~~ :class:`~dmr.throttling.Rate` is passed as the second required parameter to throttle classes. However, all values that you pass are just numbers of seconds. So, you can fully customize throttling timings by passing any amount of seconds that you wish: .. code-block:: python :caption: settings.py :linenos: >>> from dmr.settings import Settings, DMR_SETTINGS >>> from dmr.throttling import SyncThrottle >>> DMR_SETTINGS = { ... Settings.throttling: [ ... SyncThrottle( ... max_requests=5, ... duration_in_seconds=10, ... ), ... ], ... } This will set a throttling rule: no more than 5 requests in 10 seconds. Backends ~~~~~~~~ Backends are used to define where we store throttling data. By default we use: - :class:`dmr.throttling.backends.SyncDjangoCache` for sync endpoints - :class:`dmr.throttling.backends.AsyncDjangoCache` for async endpoints All backends that we support can be further customized. .. tabs:: .. tab:: DjangoCache By default we store all the data in the ``'default'`` Django cache. You can customize which Django cache name is used. For example: .. literalinclude:: /examples/throttling/cache_customization.py :caption: views.py :linenos: :language: python .. tab:: Redis Any Redis-compliant tool is supported, including: Valkey, KeyDB, etc. You can fully customize the client: .. literalinclude:: /examples/throttling/redis_backend.py :caption: views.py :linenos: :language: python .. note:: Make sure that `redis `_ client library is installed, we don't ship it together with ``django-modern-rest``. You can also write your own backends, for example, to store throttling information in memory, filesystem, or somewhere else. To do so, you would need to subclass :class:`dmr.throttling.backends.BaseThrottleSyncBackend` or :class:`dmr.throttling.backends.BaseThrottleAsyncBackend` and override 2 methods. Full list of backends that we ship in ``django-modern-rest``: - :class:`~dmr.throttling.backends.SyncDjangoCache` and :class:`~dmr.throttling.backends.AsyncDjangoCache`, default - :class:`~dmr.throttling.backends.redis.SyncRedis` and :class:`~dmr.throttling.backends.redis.AsyncRedis` .. warning:: When using :class:`~dmr.throttling.backends.SyncDjangoCache` or :class:`~dmr.throttling.backends.AsyncDjangoCache` the final behavior will depend on the cache that you use. Some Django cache backends like ``django.core.cache.backends.locmem.LocMemCache`` store cache in memory per-process. So, any multiprocess environments with ``N`` processes will allow to use ``N * max_request`` requests. Using such cache backends is not safe. Some like ``django.core.cache.backends.dummy.DummyCache`` do nothing at all. Choosing a backend ^^^^^^^^^^^^^^^^^^ .. list-table:: :header-rows: 1 :widths: 22 18 12 24 24 * - Backend - Atomicity - Overhead - Supported algorithms - Best suited for * - ``DjangoCache`` - Per-process: multiprocess deployments may face problems - Very low (depends on the cache type) - All - Non-critical IP based checks with not-strict windows and limits * - ``Redis`` - Full - Low - All builtin ones, but requires ``lua`` scripting support - Strict distributed limits Unsafe backend warning ^^^^^^^^^^^^^^^^^^^^^^ By default, ``django-modern-rest`` emits a :class:`~dmr.throttling.backends.django_cache.UnsafeCacheBackendWarning` warning when detecting an unsafe cache backend for throttling. You can configure this check on three levels as usual: 1. Per endpoint: pass ``throttling_allow_unsafe_cache`` parameter 2. Per controller: by setting ``throttling_allow_unsafe_cache`` attribute 3. In settings, see :data:`~dmr.settings.Settings.throttling_allow_unsafe_cache` When ``throttling_allow_unsafe_cache`` is set to ``False``, we raise a :exc:`dmr.exceptions.EndpointMetadataError` exception instead of a warning. This setting will ensure the maximum safety. To suppress this check completely and run throttling at your own risk, set :data:`~dmr.settings.Settings.throttling_allow_unsafe_cache` to ``None``. Algorithms ~~~~~~~~~~ Algorithms are used to define the logic of how requests are counted. By default we use :class:`dmr.throttling.algorithms.SimpleRate` as the algorithm. It defines a fixed window with a fixed amount of requests possible. When window is expired, it resets the count of requests. Here's how you can customize the algorithm for a throttling: .. literalinclude:: /examples/throttling/algorithm_customization.py :caption: views.py :linenos: :language: python You can also write your own algorithms. To do so, you would need to subclass :class:`dmr.throttling.algorithms.BaseThrottleAlgorithm` and override 2 required methods. Full list of algorithms that we ship in ``django-modern-rest``: - :class:`~dmr.throttling.algorithms.SimpleRate`, default - :class:`~dmr.throttling.algorithms.LeakyBucket` where requests fill the bucket; tokens leak at a steady rate. Unlike ``SimpleRate``, drains continuously providing smoother rate-limiting without allowing bursts at window boundaries. .. warning:: :class:`~dmr.throttling.algorithms.SimpleRate` uses a **fixed window** that resets when the window expires. This allows a boundary burst pattern: a client can send ``N`` requests right before the window resets and ``N`` more right after, effectively firing ``2N`` requests in a short interval while remaining within the configured per-window limit. For abuse-sensitive endpoints (login, OTP, password reset) prefer :class:`~dmr.throttling.algorithms.LeakyBucket`, which drains continuously and eliminates this burst window. Choosing an algorithm ^^^^^^^^^^^^^^^^^^^^^ .. list-table:: :header-rows: 1 :widths: 22 18 12 24 24 * - Algorithm - Window type - Overhead - Boundary burst risk - Best suited for * - :class:`~dmr.throttling.algorithms.SimpleRate` - Fixed — resets after ``duration`` - Very low - Yes — up to ``2N`` requests in a short burst - General-purpose, internal, and admin endpoints * - :class:`~dmr.throttling.algorithms.LeakyBucket` - Continuous drain - Low - No — traffic is smoothed regardless of timing - Auth endpoints (login, OTP, password reset), public APIs For auth and abuse-sensitive endpoints, use :class:`~dmr.throttling.algorithms.LeakyBucket`: .. literalinclude:: /examples/throttling/auth_leaky_bucket.py :caption: views.py :linenos: :language: python Cache keys ~~~~~~~~~~ Cache keys is what defines how requests are identified. By default we use :func:`dmr.throttling.cache_keys.RemoteAddr` cache key, which identifies requests by IP taken from `REMOTE_ADDR `_ value from ``request.META``. .. warning:: If you are using reverse proxies, make sure to correctly configure how they pass request headers, to ``REMOTE_ADDR`` would be correct. You can write your own cache keys, they are subclasses of :class:`~dmr.throttling.cache_keys.BaseThrottleCacheKey` and must return a string or ``None``. If cache key returns ``None``, it means that this request will be skipped from this exact throttling check. However, other keys may still be applied. It is useful to skip some requests from throttling checks, for example, from paid or stuff users. Full list of cache keys that we ship in ``django-modern-rest``: - :class:`~dmr.throttling.cache_keys.RemoteAddr`, default - :class:`~dmr.throttling.cache_keys.UserPk`, based on ``request.user``, by default we use ``request.user.pk`` if it exists. You can pass ``exclude_stuff`` argument as ``False`` to also limit ``is_stuff`` users, or you can pass ``exclude_superuser`` argument as ``False`` to also limit super users - :class:`~dmr.throttling.cache_keys.JwtToken`, based on ``request.__dmr_jwt__``. Uses ``jti`` claim when present and falls back to ``sub`` claim. Raw value is hashed before being used as a cache key. Returns ``None`` when ``request.__dmr_jwt__`` is not set. When throttling is executed ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Throttling is executed in two stages: before auth and after auth. Why? Because we need to: 1. Protect auth from abusive requests and brute forcing 2. Make sure we can base throttling rules on the auth info The same can be said about content negotiation, it also must be protected by throttling. Otherwise, people can abuse content negotiation without any request limits. .. mermaid:: :caption: Throttling execution :config: {"theme": "forest"} graph Start[New request] --> BeforeThrottle[Throttling based on IP or 429]; BeforeThrottle --> RendererNegotiation[Renderer is negotiated or 406]; RendererNegotiation --> Auth[Auth or 401]; Auth --> AfterThrottle[Throttling based on auth or 429]; All cache keys know when to execute by default, however you can customize this. For example, you can run some IP based throttling checks after the auth itself: .. literalinclude:: /examples/throttling/throttling_after_auth.py :caption: views.py :linenos: :language: python .. warning:: It is **strongly** not recommended to have auth without any throttling before it. Auth must be protected from brute force and denial of service attacks! For example, one can also use `django-axes `_ for this. `wemake-django-template `_ has this configured properly. Note that it won't make any sense to run auth-based throttling before auth. So, customize it with care. Headers ~~~~~~~ By default on `429 Too Many Requests `_ error we return four headers: - ``X-RateLimit-Limit`` - The maximum number of requests permitted in the current time window - ``X-RateLimit-Remaining`` - The number of requests remaining in the current time window - ``X-RateLimit-Reset`` - The number of seconds until the current rate limit window resets - ``Retry-After`` - The number of seconds until the current rate limit window resets, see `RFC-6585 `_ and `RFC-7231 `_ .. note:: Headers with ``X-`` prefix means that they are custom ones, there's no spec behind them. However, this convention is the most popular one as of right now. OpenAPI support is built in for this feature. All headers classes will provide the proper :class:`~dmr.headers.HeaderSpec` for the ``429`` response. You might want to customize the returned headers. To do so, you can pass ``response_headers`` argument to throttling classes with header classes that you actually want to support. For example, you can disable `Retry-After `_ header with: .. literalinclude:: /examples/throttling/header_disable_retry_after.py :caption: views.py :linenos: :language: python Or if you want to support the `latest draft `_ about ``RateLimit`` and ``RateLimit-Policy`` headers, you can use: .. literalinclude:: /examples/throttling/header_ietf_draft.py :caption: views.py :linenos: :language: python You can also combine these headers with each other in any combinations. You can write your own classes with custom headers support. To do so, subclass :class:`dmr.throttling.headers.BaseResponseHeadersProvider`. You can completely disable any extra response headers by passing an empty list. Full list of header providers that we ship in ``django-modern-rest``: - :class:`~dmr.throttling.headers.XRateLimit`, default - :class:`~dmr.throttling.headers.RetryAfter`, default - :class:`~dmr.throttling.headers.RateLimitIETFDraft` Security -------- Key considerations: - Be sure to correctly setup your HTTP Proxy server to send correct IP headers - Be especially careful with ``X-Forwarded-For`` header, because it can contain several layers of proxies - Never rate limit on user supplied data such as ``User-Agent``, because this data can easily be changed - Denial of service: be careful not to limit other users when limiting just one - Do not store sensitive or personal users' data in your cache keys, because it is stored with no protection / encryption .. seealso:: https://django-ratelimit.readthedocs.io/en/latest/security.html Throttling reports ------------------ If you need to attach any throttling headers to successful responses, you can do it as well. For this we offer two APIs: - :meth:`dmr.throttling.ThrottlingReport.report` for sync APIs - :meth:`dmr.throttling.ThrottlingReport.areport` for async APIs All our regular rules apply: - All new headers must be added to the corresponding :class:`~dmr.metadata.ResponseSpec` definitions - When settings headers, you would need to use :func:`~dmr.endpoint.validate` .. literalinclude:: /examples/throttling/reports.py :caption: views.py :linenos: :language: python Use ``headers`` argument to :meth:`~dmr.controller.Controller.to_response` to add needed headers. .. warning:: :class:`~dmr.throttling.ThrottlingReport` will make ``N`` cache requests when building header reports (where ``N`` is the number of throttle instances used for this endpoint). It might be slow, depending on the number of throttles and your cache. It might also fail, we don't handle any errors in the reports building process. Use this feature only when there's a serious need for it. You can also provide the same headers not just for successful responses, but for any errors that consume ratelimit quota as well. To so, you would need to :ref:`customize headers spec ` of your error model: .. literalinclude:: /examples/throttling/reports_for_errors.py :caption: views.py :linenos: :language: python Method ``to_response`` is used for both successful and error responses. This way both your successful responses and error responses will have the needed ratelimiting headers. API Reference ------------- Base ~~~~ .. autoclass:: dmr.throttling.SyncThrottle :members: :inherited-members: .. autoclass:: dmr.throttling.AsyncThrottle :members: :inherited-members: .. autoclass:: dmr.throttling.Rate :members: .. autoclass:: dmr.throttling.ThrottlingReport :members: Backends ~~~~~~~~ .. autoclass:: dmr.throttling.backends.CachedRateLimit :members: :show-inheritance: .. autoclass:: dmr.throttling.backends.BaseThrottleSyncBackend :members: .. autoclass:: dmr.throttling.backends.BaseThrottleAsyncBackend :members: .. autoclass:: dmr.throttling.backends.SyncDjangoCache :members: .. autoclass:: dmr.throttling.backends.AsyncDjangoCache :members: .. autoclass:: dmr.throttling.backends.django_cache.UnsafeCacheBackendWarning :members: .. autoclass:: dmr.throttling.backends.redis.SyncRedis :members: .. autoclass:: dmr.throttling.backends.redis.AsyncRedis :members: Algorithms ~~~~~~~~~~ .. autoclass:: dmr.throttling.algorithms.BaseThrottleAlgorithm :members: .. autoclass:: dmr.throttling.algorithms.SimpleRate :members: .. autoclass:: dmr.throttling.algorithms.LeakyBucket :members: Cache keys ~~~~~~~~~~ .. autoclass:: dmr.throttling.cache_keys.BaseThrottleCacheKey :members: .. autoclass:: dmr.throttling.cache_keys.RemoteAddr :members: .. autoclass:: dmr.throttling.cache_keys.UserPk :members: .. autoclass:: dmr.throttling.cache_keys.JwtToken :members: Headers ~~~~~~~ .. autoclass:: dmr.throttling.headers.BaseResponseHeadersProvider :members: .. autoclass:: dmr.throttling.headers.XRateLimit :members: .. autoclass:: dmr.throttling.headers.RetryAfter :members: .. autoclass:: dmr.throttling.headers.RateLimitIETFDraft :members: Middleware ========== As per our main principle, you can use any default Django middleware with your API. But, it has several minor problems by default: 1. Any middleware responses won't show up in your schema 2. Responses won't have the right ``'Content-Type'`` 3. Responses won't be validated That's why ``django-modern-rest`` provides a powerful middleware system that allows you to wrap Django middleware around your controllers while maintaining proper OpenAPI documentation and response handling. The main function for this is :func:`~dmr.decorators.wrap_middleware`, which creates reusable decorators that can be applied to controller classes. How it works ------------ ``wrap_middleware`` is a factory function that creates decorators with pre-configured middleware. It takes: 1. A middleware function or class 2. One or more :class:`~dmr.metadata.ResponseSpec` objects 3. Returns a decorator factory that takes a response converter function The created decorator: - Wraps the controller's dispatch method with the specified middleware - Handles both sync and async controllers automatically - Applies response conversion when the middleware returns a specific status code - Adds the response descriptions to the controller's OpenAPI schema Basic Usage ----------- Let's create a simple middleware decorator for CSRF protection: .. literalinclude:: /examples/middleware/csrf_protect_json.py :linenos: :language: python In this example: 1. We create a middleware decorator using ``wrap_middleware`` 2. The decorator wraps ``csrf_protect`` middleware around the controller 3. When CSRF verification fails, our converter function transforms the response to JSON 4. The response description is automatically added to the OpenAPI schema Custom Middleware ----------------- You can also create custom middleware functions. Here's an example of a rate limiting middleware: .. literalinclude:: /examples/middleware/rate_limit.py :linenos: :language: python Multiple Response Descriptions ------------------------------ You can specify multiple response descriptions for different status codes: .. literalinclude:: /examples/middleware/multi_status.py :linenos: :language: python Async Controllers ----------------- ``wrap_middleware`` works seamlessly with both sync and async controllers: .. literalinclude:: /examples/middleware/async_controller.py :linenos: :language: python The middleware will automatically detect whether the controller is async and handle it appropriately. Response Converter Function --------------------------- The response converter function is called when the middleware returns a response with a status code that matches one of the provided response descriptions. This allows you to: - Transform error responses to JSON format - Add custom headers - Modify response content - Apply consistent error formatting across your API The converter function receives the original response and should return a modified :class:`django.http.HttpResponse`. Understanding the Two-Phase Middleware Pattern ----------------------------------------------- Django middleware operates in two distinct phases around the view execution. Understanding this pattern is crucial for effectively using middleware with ``django-modern-rest``. get_response callback ~~~~~~~~~~~~~~~~~~~~~ Every Django middleware receives a ``get_response`` callable parameter. This is **not** the actual response - it's a callback that represents the next middleware in the chain or the final view function. .. literalinclude:: /examples/middleware/get_response.py :linenos: :language: python Phase 1: Process Request (before get_response) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Before calling ``get_response``, you can: - Read and validate request data - Add attributes to the request object - Perform authentication/authorization - Short-circuit and return early (without calling the view) .. literalinclude:: /examples/middleware/add_request_id.py :linenos: :language: python Wrap your middleware: .. literalinclude:: /examples/middleware/wrap_add_request_id.py :linenos: :language: python Now your controller can access ``self.request.request_id``: .. literalinclude:: /examples/middleware/usage_add_request_id.py :linenos: :language: python Phase 2: Process Response (after get_response) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ After calling ``get_response``, you can: - Modify the response object - Add headers - Log response details - Transform response content .. literalinclude:: /examples/middleware/custom_header.py :linenos: :language: python Short-Circuiting: Returning Without Calling get_response ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Middleware can return a response **without** calling ``get_response``. This is called "short-circuiting" - the view is never executed. Common use cases: - Rate limiting (return 429) - Request validation failures (return 400) - Cache hits (return cached response) - Custom authentication/authorization checks Example with rate limiting: .. literalinclude:: /examples/middleware/rate_limit.py :linenos: :language: python Wrapping Django's Built-in Decorators ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can wrap Django's built-in authentication decorators like ``login_required`` to make them REST API friendly. By default, ``login_required`` returns a 302 redirect, but you can convert it to a JSON 401 response: .. literalinclude:: /examples/middleware/built_in_decorators.py :linenos: :language: python Visual Flow ~~~~~~~~~~~ Here's how a request flows through middleware: .. mermaid:: :caption: Middleware execution flow :config: {"theme": "forest"} graph TB A[HTTP Request] --> B1[Middleware 1
Phase 1: process request] B1 --> B2[Middleware 2
Phase 1: process request] B2 --> C[Controller/View executes] C --> D2[Middleware 2
Phase 2: process response] D2 --> D1[Middleware 1
Phase 2: process response] D1 --> E[HTTP Response] Best Practices -------------- 1. **Always include response descriptions**: This ensures your OpenAPI documentation is complete and accurate. 2. **Use consistent error formatting**: Create reusable converter functions that format errors consistently across your API. 3. **Handle both sync and async**: The same middleware decorator works with both sync and async controllers. 4. **Test your middleware**: Make sure to test both the success and error cases for your middleware. 5. **Document your middleware**: Add docstrings to explain what your middleware does and when it's triggered. Example: Complete CSRF Protection Setup ---------------------------------------- Here's a complete example showing how to set up CSRF protection for a REST API: .. literalinclude:: /examples/middleware/complete_csrf_setup.py :linenos: :language: python Validation ========== ``django-modern-rest`` has several layers of import-time and runtime validation. We try to do everything that we can during import-time, so it won't affect your requests and responses. However, we have to validate requests and responses during the runtime. Responses validation can be :ref:`turned off ` in production for speed. What do we validate and how? .. _settings_validation: Settings validation ------------------- We start with settings validation. We only validate settings once per application, we do it when the first :class:`~dmr.controller.Controller` is created. We also validate our own default values to be correct. See :class:`~dmr.validation.settings.SettingsValidator` for the API. Endpoint validation ------------------- Next, when controller is being created, we run :class:`~dmr.endpoint.Endpoint` validation. Here we can detect all kinds of problems with how endpoints are defined: - Invalid :func:`~dmr.endpoint.modify` or :func:`~dmr.endpoint.validate` usage - Or invalid :class:`~dmr.settings.HttpSpec` usage See :class:`~dmr.validation.endpoint_metadata.EndpointMetadataBuilder` and :class:`~dmr.validation.endpoint_metadata.EndpointMetadataBuilder` for the API. HttpSpec validation ~~~~~~~~~~~~~~~~~~~ You can customize the strictness of HTTP Spec validation with overriding disabled :class:`~dmr.settings.HttpSpec` options per-endpoint, per-controller, and globally. .. warning:: We don't recommend overriding any of these settings by default. It only makes sense to change, when implementing some old legacy API the "same" way as it used to be. And only when you need this for a very specific reason. .. tabs:: .. tab:: :octicon:`checklist` The right way .. literalinclude:: /examples/validation/httpspec/right_way.py :caption: views.py :language: python :linenos: .. tab:: Per endpoint .. literalinclude:: /examples/validation/httpspec/per_endpoint.py :language: python :caption: views.py :linenos: :emphasize-lines: 11 .. tab:: Per controller .. literalinclude:: /examples/validation/httpspec/per_controller.py :language: python :caption: views.py :linenos: :emphasize-lines: 9 Controller validation --------------------- The last step is the final :class:`~dmr.controller.Controller` validation which has everything ready: - :attr:`~dmr.controller.Controller.api_endpoints` Here we validate: - That all ``Controller`` classes have unique methods - That all endpoints are either sync or async - All per-controller and per-endpoint error handling See :class:`~dmr.validation.controller.ControllerValidator` for the API. Response validation ------------------- The last step is to validate the response when returning it from the endpoint in runtime. We need this to make sure that API responses always match response schemas. It can be :ref:`turned off `. See :class:`~dmr.validation.response.ResponseValidator` for the API. API Reference ------------- Settings ~~~~~~~~ .. autoclass:: dmr.validation.settings.SettingsValidator :members: Endpoint ~~~~~~~~ .. autoclass:: dmr.validation.endpoint_metadata.EndpointMetadataBuilder :members: .. autoclass:: dmr.validation.endpoint_metadata.EndpointMetadataValidator :members: Controller ~~~~~~~~~~ .. autoclass:: dmr.validation.controller.ControllerValidator :members: Response ~~~~~~~~ .. autoclass:: dmr.validation.response.ResponseValidator :members: .. autoclass:: dmr.validation.response.ValidatedModification :members: Reusable code ============= One of the worst things about the current generation of Python REST frameworks is code re-usability. - ``django-rest-framework`` is very flexible, but all the flexibility comes from importing fully qualified object's path strings taken from app's settings. It is very hard to properly type a code base like this. Using it is also really hard, because you can't easily navigate in your source code. - ``fastapi`` does not even offer a way to write reusable code, because it is based on functions, which are really hard to reuse and modify. That's why you have to copy paste lots of code just to, for example, use the most common things such as JWT auth. What does ``django-modern-rest`` offer instead? .. _reusable-controllers: Reusable controllers -------------------- We offer a concept of a "reusable controllers". To make a reusable controller, you need to provide :class:`typing.TypeVar` instead of a real :class:`~dmr.serializer.BaseSerializer` type. Here's an example: .. literalinclude:: /examples/reusable_code/reusable_controller.py :caption: views.py :linenos: :language: python This code can work with both ``pydantic`` and ``msgspec`` as serializers. Let's try to create two exact controllers with exact serializers: .. tabs:: .. tab:: msgspec .. literalinclude:: /examples/reusable_code/msgspec_controller.py :caption: views.py :linenos: :language: python .. tab:: pydantic .. literalinclude:: /examples/reusable_code/pydantic_controller.py :caption: views.py :linenos: :language: python Basically - we just specify what kind of serializer to use. And that's it. But, this is just the first step. We can do much more! Generic parsing and response models ----------------------------------- Next, let's define a reusable controller that will have: - customizable serializer - customizable request model - customizable response body The process will look exactly the same: .. literalinclude:: /examples/reusable_code/reusable_parsing.py :caption: views.py :linenos: :language: python Here we use 3 type variables. One of each of the parts we want to customize. Important part here is that we defined our own abstract ``convert`` method to convert unknown request model into an unknown response body. We would need to implement this method in all of our concrete controllers. .. tabs:: .. tab:: msgspec .. literalinclude:: /examples/reusable_code/parsing_msgspec.py :caption: views.py :linenos: :language: python .. tab:: pydantic .. literalinclude:: /examples/reusable_code/parsing_pydantic.py :caption: views.py :linenos: :language: python Note that ``msgspec`` and ``pydantic`` controllers in this case have completely different request and response bodies and completely different OpenAPI schemas. We can completely customize each controller and all parsing components and return type validation. .. important:: All schema generation and validation rules work the same way for concrete controllers. We infer the passed values during import time and use real types. 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: 1. :attr:`~dmr.controller.Controller.csrf_exempt` is set to ``False`` for a specific controller 2. Endpoints protected by :class:`~dmr.security.django_session.auth.DjangoSessionSyncAuth` or :class:`~dmr.security.django_session.auth.DjangoSessionAsyncAuth` will require CSRF as well. Because using Django sessions without CSRF is not secure .. _bring-your-own-di: 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 - https://github.com/bobthemighty/punq 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: Pagination ---------- Limit Offset pagination ~~~~~~~~~~~~~~~~~~~~~~~ We support built-in :class:`django.core.paginator.Paginator`. To do so, we only provide metadata for the default pagination: .. literalinclude:: /examples/integrations/pagination.py :caption: views.py :language: python :linenos: 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 ~~~~~~~~~ .. autoclass:: dmr.pagination.Paginated :members: .. autoclass:: dmr.pagination.Page :members: Filters ------- No special integration with `django-filter `_ is required. Everything just works: .. literalinclude:: /examples/integrations/filters.py :caption: views.py :language: python :linenos: 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: 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 ``