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):
1from dmr import Controller
2from dmr.plugins.msgspec import MsgspecSerializer
3
4
5class MyController(Controller[MsgspecSerializer]):
6 def post(self) -> str:
7 return 'ok'
8
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:
It will know that the
postendpoint will handle thePOSTHTTP method, it is true for all HTTP methods, except OPTIONS (click to know why)It will know that
postwill returnstras a response type spec. There’s no implicit type conversions indjango-modern-rest. If your endpoint declares something to be returned, it must return this typeIt will infer the default status code for
post, which will be201(CREATED). All other endpoints have200(OK) as the defaultAll this metadata will be used to validate responses from this endpoint. Returning
[]frompostwould triggerResponseSchemaError, unless Response validation is explicitly turned offThe 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:
Returning just raw data from “raw endpoints”
Returning real
HttpResponseinstances 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.
1from http import HTTPStatus
2
3import pydantic
4
5from dmr import Body, Controller, modify
6from dmr.plugins.pydantic import PydanticFastSerializer
7
8
9class UserModel(pydantic.BaseModel):
10 email: str
11
12
13class UserController(Controller[PydanticFastSerializer]):
14 @modify(status_code=HTTPStatus.OK)
15 def post(self, parsed_body: Body[UserModel]) -> UserModel:
16 # This response would have an explicit status code `200`:
17 return parsed_body
18
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?
You can mess up the default headers / status codes
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:
1from http import HTTPStatus
2
3import pydantic
4from django.http import HttpResponse
5
6from dmr import Body, Controller, ResponseSpec, validate
7from dmr.plugins.pydantic import PydanticFastSerializer
8
9
10class UserModel(pydantic.BaseModel):
11 email: str
12
13
14class UserController(Controller[PydanticFastSerializer]):
15 @validate( # <- describes unique return types from this endpoint
16 ResponseSpec(
17 UserModel,
18 status_code=HTTPStatus.OK,
19 ),
20 )
21 def post(self, parsed_body: Body[UserModel]) -> HttpResponse:
22 # This response would have an explicit status code `200`:
23 return self.to_response(
24 parsed_body,
25 status_code=HTTPStatus.OK,
26 )
27
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.
1from http import HTTPStatus
2
3import pydantic
4from django.http import HttpResponse
5
6from dmr import Body, Controller, ResponseSpec
7from dmr.plugins.pydantic import PydanticFastSerializer
8
9
10class UserModel(pydantic.BaseModel):
11 email: str
12
13
14class UserController(Controller[PydanticFastSerializer]):
15 responses = (
16 # Describes unique return types for this controller:
17 ResponseSpec(UserModel, status_code=HTTPStatus.OK),
18 )
19
20 def post(self, parsed_body: Body[UserModel]) -> HttpResponse:
21 # This response would have an explicit status code `200`:
22 return self.to_response(
23 parsed_body,
24 status_code=HTTPStatus.OK,
25 )
26
27 def put(self, parsed_body: Body[UserModel]) -> HttpResponse:
28 return self.to_response(
29 parsed_body,
30 status_code=HTTPStatus.OK,
31 )
32
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_methodsto support custom HTTP methods likeQUERYor your custom DSLs on top of HTTPcsrf_exemptto customize whether or not this controller is exempted from CSRF. See CSRF for more infologin_requiredto customize whether or not login is required for this controller. By default, login is not required for this controller to exempt it from Django’sLoginRequiredMiddleware. Users should make use of authentication indjango-modern-rest. See How authentication works for more details.controller_validator_clsto customize how this controller is validated at import timeannotations_contextto customize how this controller resolves its annotationsendpoint_clsto customize how endpoints are created
You can also customize Endpoint
to change how API methods are executed:
serializer_context_clsto customize how the model for serialization of incoming data is createdrequest_negotiator_clsandresponse_negotiator_clsto customize how content negotiation works for this endpointresponse_validator_clsto customize how Response validation works
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?#
Learn how to describe response headers and cookies.
Learn how to return HTTP redirect responses.
Learn how to return file responses.
Learn about optional response validation.