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

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:

Run result

$ curl http://127.0.0.1:8000/api/ratelimit/ -X POST
{"message":"Request processed"}

$ curl http://127.0.0.1:8000/api/ratelimit/ -D - -X POST -H 'X-Rate-Limited: true'
HTTP/1.1 429 Too Many Requests
date: Thu, 01 Oct 2026 11:36:44 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 42
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"detail":[{"msg":"Rate limit exceeded"}]}

Multiple Response Descriptions#

You can specify multiple response descriptions for different status codes:

Async Controllers#

wrap_middleware works seamlessly with both sync and async controllers:

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

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)

Wrap your middleware:

Now your controller can access self.request.request_id:

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

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

See the rate limiting middleware in Custom Middleware for an example.

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:

Visual Flow#

Here’s how a request flows through middleware:

        ---
config:
  theme: forest

---
  graph TB
    A[HTTP Request] --> B1[Middleware 1<br/>Phase 1: process request]
    B1 --> B2[Middleware 2<br/>Phase 1: process request]
    B2 --> C[Controller/View executes]
    C --> D2[Middleware 2<br/>Phase 2: process response]
    D2 --> D1[Middleware 1<br/>Phase 2: process response]
    D1 --> E[HTTP Response]
    

Middleware execution flow#

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:

Run result

$ curl http://127.0.0.1:8000/api/publiccontroller/ -D - -X GET
HTTP/1.1 200 OK
date: Thu, 01 Oct 2026 11:36:44 GMT
server: uvicorn
Content-Type: application/json
Vary: Cookie, Accept-Language
X-Frame-Options: DENY
Content-Language: en
Content-Length: 29
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin
Set-Cookie: csrftoken=IWb3Rrf1rn0n8058UJcGxKSAF9uTNa7K; expires=Thu, 30 Sep 2027 11:36:45 GMT; Max-Age=31449600; Path=/; SameSite=Lax

{"message":"CSRF cookie set"}