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:
It will know that
postendpoint will handlePOSTHTTP 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. All other endpoints would have200as 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 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:
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 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 PydanticSerializer
7
8
9class UserModel(pydantic.BaseModel):
10 email: str
11
12
13class UserController(Controller[PydanticSerializer]):
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"
}
},
"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?
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. 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:
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 PydanticSerializer
8
9
10class UserModel(pydantic.BaseModel):
11 email: str
12
13
14class UserController(Controller[PydanticSerializer]):
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"
}
},
"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.
1from http import HTTPStatus
2
3import pydantic
4from django.http import HttpResponse
5
6from dmr import Body, Controller, ResponseSpec
7from dmr.plugins.pydantic import PydanticSerializer
8
9
10class UserModel(pydantic.BaseModel):
11 email: str
12
13
14class UserController(Controller[PydanticSerializer]):
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"
}
},
"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.
Request lifecycle¶
Here’s the top level view on how 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 HTTPendpoint_clsto customize how endpoints are createdcsrf_exemptto customize whether or not this controller is exempted from the CSRFcontroller_validator_clsto customize how controller is validated in import time
You can also customize Endpoint
to change how API methods are executed:
serializer_context_clsto customize how model for serialization of incoming data is created
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.