Reusable code

One of the worst thing about 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

We offer a concept of a “reusable controllers”.

To make a reusable controller, you need to provide typing.TypeVar instead of a real BaseSerializer type.

Here’s an example:

This code can work with both pydantic and msgspec as serializers. Let’s try to create two exact controllers with exact serializers:

Run result

$ curl http://127.0.0.1:8000/api/example/ -X GET
{"message":"hello from MsgspecSerializer"}

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:

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.

Run result

$ curl http://127.0.0.1:8000/api/example/ -X POST -d '{"username": "sobolevn"}' -H 'Content-Type: application/json'
{"message":"Hello, sobolevn"}

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "ErrorDetail": {
        "description": "Base schema for error details description.",
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            },
            "type": "array"
          },
          "msg": {
            "type": "string"
          },
          "type": {
            "type": "string"
          }
        },
        "required": [
          "msg"
        ],
        "title": "ErrorDetail",
        "type": "object"
      },
      "ErrorModel": {
        "description": "Default error response schema.\n\nCan be customized.\nSee :ref:`customizing-error-messages` for more details.",
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ErrorDetail"
            },
            "type": "array"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ErrorModel",
        "type": "object"
      },
      "_RequestModel": {
        "properties": {
          "username": {
            "type": "string"
          }
        },
        "required": [
          "username"
        ],
        "title": "_RequestModel",
        "type": "object"
      },
      "_ResponseBody": {
        "properties": {
          "message": {
            "type": "string"
          }
        },
        "required": [
          "message"
        ],
        "title": "_ResponseBody",
        "type": "object"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/msgspeccontroller/": {
      "post": {
        "deprecated": false,
        "operationId": "postMsgspeccontrollerApiMsgspeccontroller",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/_RequestModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/_ResponseBody"
                }
              }
            },
            "description": "Created"
          },
          "400": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when request components cannot be parsed"
          },
          "406": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when provided `Accept` header cannot be satisfied"
          },
          "422": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorModel"
                }
              }
            },
            "description": "Raised when returned response does not match the response schema"
          }
        }
      }
    }
  }
}

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.