Micro-framework out of Django#
Single file Django#
You don’t need microframeworks to build small APIs, because Django is a microframework itself. With the only difference: it scales!
1import secrets
2import sys
3import uuid
4
5import pydantic
6from django.conf import settings
7from django.core.management import execute_from_command_line
8
9from dmr import Body, Controller
10from dmr.openapi import build_schema
11from dmr.openapi.views import OpenAPIJsonView, SwaggerView
12from dmr.plugins.pydantic import PydanticFastSerializer
13from dmr.routing import Router, path
14
15if not settings.configured:
16 settings.configure(
17 ROOT_URLCONF=__name__,
18 ALLOWED_HOSTS='*',
19 DEBUG=True,
20 INSTALLED_APPS=['dmr', 'django.contrib.staticfiles'],
21 STATIC_URL='/static/',
22 STATICFILES_FINDERS=[
23 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
24 ],
25 TEMPLATES=[
26 {
27 'APP_DIRS': True,
28 'BACKEND': 'django.template.backends.django.DjangoTemplates',
29 },
30 ],
31 # Secret key for tests, will be new on each run,
32 # in production it must be the same token, kept in secret:
33 SECRET_KEY=secrets.token_hex(),
34 )
35
36
37class UserCreateModel(pydantic.BaseModel):
38 email: str
39
40
41class UserResponseModel(UserCreateModel):
42 uid: uuid.UUID
43
44
45class UserController(Controller[PydanticFastSerializer]):
46 async def post(
47 self,
48 parsed_body: Body[UserCreateModel],
49 ) -> UserResponseModel:
50 return UserResponseModel(uid=uuid.uuid4(), email=parsed_body.email)
51
52
53router = Router(
54 'api/',
55 [
56 path('user/', UserController.as_view(), name='users'),
57 ],
58)
59schema = build_schema(router)
60
61urlpatterns = [
62 router.to_urlpatterns(namespace='api'),
63 path('docs/openapi.json/', OpenAPIJsonView.as_view(schema), name='openapi'),
64 path('docs/swagger/', SwaggerView.as_view(schema), name='swagger'),
65]
66
67if __name__ == '__main__':
68 # Use `python THIS_FILE_NAME.py runserver` to run the example.
69 # Then visit `http://localhost:8000/docs/swagger` to view the docs.
70 execute_from_command_line(sys.argv)
Run result
$ curl http://127.0.0.1:8000/api/user/ -X POST -d '{"email": "djangomodernrest@wemake.services"}' -H 'Content-Type: application/json'
{"email":"djangomodernrest@wemake.services","uid":"b2655eac-4691-460b-9847-77f1319008a9"}
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"
},
"UserCreateModel": {
"properties": {
"email": {
"title": "Email",
"type": "string"
}
},
"required": [
"email"
],
"title": "UserCreateModel",
"type": "object"
},
"UserResponseModel": {
"properties": {
"email": {
"title": "Email",
"type": "string"
},
"uid": {
"format": "uuid",
"title": "Uid",
"type": "string"
}
},
"required": [
"email",
"uid"
],
"title": "UserResponseModel",
"type": "object"
}
}
},
"info": {
"title": "Django Modern Rest",
"version": "0.1.0"
},
"openapi": "3.2.0",
"paths": {
"/api/user/": {
"post": {
"operationId": "postUserControllerApiUser",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserCreateModel"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserResponseModel"
}
}
},
"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"
}
}
}
}
}
}
Microframeworks#
The single file example above is already a complete microframework app.
But if you are reaching for one of the popular microframework styles,
django-modern-rest drops into both without any special glue:
µDjango
is a demo of exactly this idea: a whole Django app in a single file,
exposed as a plain ASGIHandler and served by any ASGI server.
It is not a package you install, so there is nothing to integrate with.
It is a shape your own file can take, and django-modern-rest
fits into it without any glue code:
1import secrets
2import uuid
3
4import django
5import pydantic
6from django.conf import settings
7from django.core.handlers.asgi import ASGIHandler
8
9from dmr import Body, Controller
10from dmr.plugins.pydantic import PydanticFastSerializer
11from dmr.routing import path
12
13if not settings.configured:
14 settings.configure(
15 ROOT_URLCONF=__name__,
16 ALLOWED_HOSTS='*',
17 DEBUG=True,
18 INSTALLED_APPS=['dmr'],
19 # Secret key for tests, will be new on each run,
20 # in production it must be the same token, kept in secret:
21 SECRET_KEY=secrets.token_hex(),
22 )
23 django.setup()
24
25
26class UserCreateModel(pydantic.BaseModel):
27 email: str
28
29
30class UserResponseModel(UserCreateModel):
31 uid: uuid.UUID
32
33
34class UserController(Controller[PydanticFastSerializer]):
35 async def post(
36 self,
37 parsed_body: Body[UserCreateModel],
38 ) -> UserResponseModel:
39 return UserResponseModel(uid=uuid.uuid4(), email=parsed_body.email)
40
41
42urlpatterns = [
43 path('user/', UserController.as_view(), name='users'),
44]
45
46# Use `uvicorn udjango_style_asgi:app --reload` to run the example.
47app = ASGIHandler()
Run result
$ curl http://127.0.0.1:8000/user/ -X POST -d '{"email": "djangomodernrest@wemake.services"}' -H 'Content-Type: application/json'
{"email":"djangomodernrest@wemake.services","uid":"dc8029ef-19d8-43d3-b0a0-98fd186231ae"}
Run it with any ASGI server:
uvicorn udjango_style_asgi:app --reload
Note
µDjango itself is explicitly a demonstration project and is not meant for production. The pattern it shows, however, is just regular Django.
nanodjango is a real package that wraps the single file idea into a Flask-like API: it configures the settings for you and collects urls via decorators.
Two things are needed to use it with django-modern-rest:
Add
dmrtoEXTRA_APPS, so its templates and static files are found.EXTRA_APPSis appended to nanodjango’s ownINSTALLED_APPSdefaultsMount a
Routerwithapp.path()called as a function withinclude(), not as a decorator
1import secrets
2import uuid
3
4import pydantic
5from django.urls import include
6from nanodjango import Django
7
8from dmr import Body, Controller
9from dmr.openapi import build_schema
10from dmr.openapi.views import OpenAPIJsonView, SwaggerView
11from dmr.plugins.pydantic import PydanticFastSerializer
12from dmr.routing import Router, path
13
14app = Django(
15 # `dmr` must be in `INSTALLED_APPS` for its templates and static files:
16 EXTRA_APPS=['dmr'],
17 ALLOWED_HOSTS=['*'],
18 # Secret key for tests, will be new on each run,
19 # in production it must be the same token, kept in secret:
20 SECRET_KEY=secrets.token_hex(),
21)
22
23
24class UserCreateModel(pydantic.BaseModel):
25 email: str
26
27
28class UserResponseModel(UserCreateModel):
29 uid: uuid.UUID
30
31
32class UserController(Controller[PydanticFastSerializer]):
33 async def post(
34 self,
35 parsed_body: Body[UserCreateModel],
36 ) -> UserResponseModel:
37 return UserResponseModel(uid=uuid.uuid4(), email=parsed_body.email)
38
39
40router = Router(
41 'api/',
42 [
43 path('user/', UserController.as_view(), name='users'),
44 ],
45)
46schema = build_schema(router)
47
48# `app.path()` is called as a function here, not as a decorator,
49# because we mount whole url confs instead of single views:
50app.path(router.to_urlpatterns(namespace='api'))
51app.path(
52 'docs/',
53 include(
54 [
55 path('openapi.json/', OpenAPIJsonView.as_view(schema)),
56 path('swagger/', SwaggerView.as_view(schema)),
57 ],
58 ),
59)
60
61if __name__ == '__main__':
62 # Use `python nanodjango_app.py` to run the example.
63 # Then visit `http://localhost:8000/docs/swagger/` to view the docs.
64 app.run()
Single controllers can also be registered with the decorator form,
since app.path() handles class-based views:
@app.path('user/')
class UserController(Controller[PydanticFastSerializer]): ...
But you lose the OpenAPI schema this way,
because it is built from a Router.
Warning
app.run() picks uvicorn over Django’s runserver
only when nanodjango detects async views, and it detects them
by inspecting the functions passed to its own decorators.
Controllers mounted via include() are invisible
to that check, so an app with only async controllers
will still be served over WSGI.
Async views keep working, Django adapts them. But to actually serve them over ASGI, run the app directly:
uvicorn nanodjango_app:app.asgi --interface asgi3
Note that app.asgi serves in nanodjango’s production mode,
where static files are served by whitenoise from STATIC_ROOT.
Run app.manage(['collectstatic']) once beforehand,
otherwise the Swagger UI assets will 404.
Scaling it for real projects#
However, all projects tend to grow while they are alive. Microframeworks handle the scale poorly, because they are not designed for this task.
We recommend using https://github.com/wemake-services/wemake-django-template to set up production ready Django boilerplate code with all the best practices.
It can handle any scale!