Using controllers#

Creating endpoints#

Controllers consist of Endpoint objects. Each HTTP method is an independent endpoint.

The simplest way to create an endpoint is to define a sync or async method with the right name (any HTTP method verb):

Run result

$ curl http://127.0.0.1:8000/api/example/ -X POST
"ok"

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"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/mycontroller/": {
      "post": {
        "operationId": "postMyControllerApiMycontroller",
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "description": "Created"
          },
          "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"
          }
        }
      }
    }
  }
}

There will be several things that django-modern-rest will do for you here:

  1. It will know that the post endpoint will handle the 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 (CREATED). All other endpoints have 200 (OK) 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 the OpenAPI spec

django-modern-rest never creates implicit methods for you. No HEAD, no OPTIONS. Create them explicitly if you need them.

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 granular 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 the modify decorator to modify the response spec that will be generated by default.

Run result

$ curl http://127.0.0.1:8000/api/user/ -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json'
{"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"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "post": {
        "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 the extra_responses param to modify, the responses Controller attribute, or the responses global setting. The most specific one wins, they are not merged, see Configuration levels.

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 you don’t have a controller / endpoint instance (like in a middleware, for example), you can fallback to using the 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. This makes it more configurable.

“Real endpoints” can use the validate decorator, the responses Controller attribute, or the responses global setting to specify all possible responses. The most specific one wins, they are not merged, see Configuration levels.

To do that we utilize ResponseSpec:

Run result

$ curl http://127.0.0.1:8000/api/user/ -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json'
{"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"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "post": {
        "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"
          }
        }
      }
    }
  }
}

The @validate decorator is useful but is not required for real endpoint declarations. Instead, you can specify response specs in the response field of the controller / settings.

Run result

$ curl http://127.0.0.1:8000/api/user/ -X POST -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json'
{"email":"user@wms.org"}

$ curl http://127.0.0.1:8000/api/user/ -X PUT -d '{"email": "user@wms.org"}' -H 'Content-Type: application/json'
{"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"
      }
    }
  },
  "info": {
    "title": "Django Modern Rest",
    "version": "0.1.0"
  },
  "openapi": "3.2.0",
  "paths": {
    "/api/usercontroller/": {
      "post": {
        "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": {
        "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 all is 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 the request / response lifecycle looks like:

        ---
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];
    

Request lifecycle#

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.

  • allowed_http_methods to support custom HTTP methods like QUERY or your custom DSLs on top of HTTP

  • csrf_exempt to customize whether or not this controller is exempted from CSRF. See CSRF for more info

  • login_required to customize whether or not login is required for this controller. By default, login is not required for this controller to exempt it from Django’s LoginRequiredMiddleware. Users should make use of authentication in django-modern-rest. See How authentication works for more details.

  • controller_validator_cls to customize how this controller is validated at import time

  • annotations_context to customize how this controller resolves its annotations

  • endpoint_cls to customize how endpoints are created

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

Basically, everything is customizable. We define all classes as attributes of the class that uses them, so you can redefine anything that you want to redefine.

Customizability is a design goal.

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