Using controller

Creating endpoints

Controllers consist of 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:

>>> from dmr import Controller
>>> from dmr.plugins.pydantic import PydanticSerializer

>>> class MyController(Controller[PydanticSerializer]):
...     def post(self) -> str:
...         return 'ok'

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 OPTIONS (click to know why)

  2. It will know that post will return 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 ResponseSchemaError, unless 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 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 HttpResponse instances with granual configuration from “real endpoints”

Raw endpoints

Note

“Raw endpoints” always have a response spec generated by default. Prefer modify() in simpler cases.

“Raw endpoints” can be either undecorated or can use modify() decoratator to modify the response spec that will be generated by default.

Run result

$ curl http://127.0.0.1:8000/api/user/ -D - -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json'
HTTP/1.1 200 OK
date: Sun, 29 Mar 2026 18:53:27 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 24
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"email":"user@wms.org"}

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "ErrorDetail": {
        "description": "Base schema for error details description.",
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            },
            "title": "Loc",
            "type": "array"
          },
          "msg": {
            "title": "Msg",
            "type": "string"
          },
          "type": {
            "title": "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"
            },
            "title": "Detail",
            "type": "array"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ErrorModel",
        "type": "object"
      },
      "UserModel": {
        "properties": {
          "email": {
            "title": "Email",
            "type": "string"
          }
        },
        "required": [
          "email"
        ],
        "title": "UserModel",
        "type": "object"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "post": {
        "deprecated": false,
        "operationId": "postUsercontrollerApiUsercontroller",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserModel"
                }
              }
            },
            "description": "OK"
          },
          "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"
          }
        }
      }
    }
  }
}

Other response specs can be specified via extra_responses param to modify(), responses Controller attribute, or 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 HttpRequest and HttpResponse, users must not return Django responses directly.

Instead, use any of the public APIs:

In case when you don’t have a controller / endpoint instance (like in a middleware, for example), you can fallback to using 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 validate() decorator, responses Controller attribute, or responses global setting to specify all possible responses.

To do that we utilize ResponseSpec:

Run result

$ curl http://127.0.0.1:8000/api/user/ -D - -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json'
HTTP/1.1 200 OK
date: Sun, 29 Mar 2026 18:53:27 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 24
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"email":"user@wms.org"}

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "ErrorDetail": {
        "description": "Base schema for error details description.",
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            },
            "title": "Loc",
            "type": "array"
          },
          "msg": {
            "title": "Msg",
            "type": "string"
          },
          "type": {
            "title": "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"
            },
            "title": "Detail",
            "type": "array"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ErrorModel",
        "type": "object"
      },
      "UserModel": {
        "properties": {
          "email": {
            "title": "Email",
            "type": "string"
          }
        },
        "required": [
          "email"
        ],
        "title": "UserModel",
        "type": "object"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "post": {
        "deprecated": false,
        "operationId": "postUsercontrollerApiUsercontroller",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserModel"
                }
              }
            },
            "description": "OK"
          },
          "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"
          }
        }
      }
    }
  }
}

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

Run result

$ curl http://127.0.0.1:8000/api/user/ -D - -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json'
HTTP/1.1 200 OK
date: Sun, 29 Mar 2026 18:53:28 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 24
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"email":"user@wms.org"}

$ curl http://127.0.0.1:8000/api/user/ -D - -X PUT -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json'
HTTP/1.1 200 OK
date: Sun, 29 Mar 2026 18:53:28 GMT
server: uvicorn
Content-Type: application/json
X-Frame-Options: DENY
Vary: Accept-Language
Content-Language: en
Content-Length: 24
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Cross-Origin-Opener-Policy: same-origin

{"email":"user@wms.org"}

OpenAPI Schema

Preview openapi.json
{
  "components": {
    "schemas": {
      "ErrorDetail": {
        "description": "Base schema for error details description.",
        "properties": {
          "loc": {
            "items": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "string"
                }
              ]
            },
            "title": "Loc",
            "type": "array"
          },
          "msg": {
            "title": "Msg",
            "type": "string"
          },
          "type": {
            "title": "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"
            },
            "title": "Detail",
            "type": "array"
          }
        },
        "required": [
          "detail"
        ],
        "title": "ErrorModel",
        "type": "object"
      },
      "UserModel": {
        "properties": {
          "email": {
            "title": "Email",
            "type": "string"
          }
        },
        "required": [
          "email"
        ],
        "title": "UserModel",
        "type": "object"
      }
    },
    "securitySchemes": {}
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "post": {
        "deprecated": false,
        "operationId": "postUsercontrollerApiUsercontroller",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserModel"
                }
              }
            },
            "description": "OK"
          },
          "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"
          }
        }
      },
      "put": {
        "deprecated": false,
        "operationId": "putUsercontrollerApiUsercontroller",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserModel"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserModel"
                }
              }
            },
            "description": "OK"
          },
          "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"
          }
        }
      }
    }
  }
}

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

Customizing controllers

Tip

This is sneak peak 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 granual control, you can change anything.

You can also customize Endpoint to change how API methods are executed:

Check out our Public API for the most advanced features.

What’s next?

Headers and cookies

Learn how to describe response headers and cookies.

Describing response headers and cookies
Redirects

Learn how to return HTTP redirect responses.

Returning redirects
Files

Learn how to return file responses.

Returning files
Validation

Learn about optional response validation.

Response validation