Let’s use drf-spectacular to simplify creation of beautiful docs for your Django application according to the OpenAPI Specification version 3. Sounds easy? It wasn’t for me…
Since I’m still a relatively newbie programmer, I constantly struggle with different topics and almost always Googling helps. But not in this case. I decided to add the Django REST framework into my toolchain and combine it with technologies like OAS 3.0. I saw how cool API documentation might look when I first discovered FastAPI and wanted to implement Swagger UI in my own app.
The most popular option, in my opinion, was drf-yasg but it explicitly says that "drf-yasg is unlikely to soon, if ever, get support for OpenAPI 3.0". Well, the second-best option in this direction is drf-spectacular, but I couldn't figure out how to document my API just by following the provided examples (by the way I did it for the first time).
The solution I came up with is to create documentation with standard DRF tools (easiest way), then generate OpenAPI 2 schemas with drf-yasg (since it has a richer description of the process) and finally tune the configuration to use drf-spectacular.
Here I’m going to shorten this path for you by using quickstart example from the DRF documentation and sprinkle some customization on top of that to implement Swagger UI with version 3 of the specification. So, without further ado, be brave and follow me!
We start with a couple of steps that are slightly different from the official DRF documentation. I’ll omit some details specific to the framework itself and if you feel lack of information for your own implementation, just explore the official site or ask a question in the comment section below.
1. Project setup
First things first, create the project directory and enter it.
mkdir tutorial
cd tutorial
Then check if pipenv is installed in order to create a virtual environment and isolate your package dependencies.
pip3 install pipenv
Install Django and Django REST framework into the virtual environment.
pipenv install django
pipenv install djangorestframework
Activate virtual environment.
pipenv shell
Now, after a little preparation, it's time to set up a new project with a single application. Note the trailing "." character – it’s not a typo.
django-admin startproject config .
django-admin startapp quickstart
As an intermediate check, let’s compare our project's layout which at this point should look like this.
Now sync the database for the first time which will add a new SQLite file to the aforementioned layout.
python manage.py migrate
After successful migration, create an initial user admin with password password123. You’ll be prompted to enter the password blindly.
python manage.py createsuperuser --email [email protected] --username admin
2. Serializers
Roll up your sleeves because we're starting to code more!
Inside your tutorial directory, run the following command to create a new
serializers.py
file.touch quickstart/serializers.py
Open this newly created file with your favorite IDE and fill it with the following content.
from django.contrib.auth.models import User, Group
from rest_framework import serializers
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ['url', 'username', 'email', 'groups']
class GroupSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Group
fields = ['url', 'name']
3. Views
After that, open
quickstart/views.py
and get typing or just copy and paste – as you wish, no judgment 😉from django.contrib.auth.models import User, Group
from rest_framework import viewsets
from rest_framework import permissions
from .serializers import UserSerializer, GroupSerializer
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = User.objects.all().order_by("-date_joined")
serializer_class = UserSerializer
permission_classes = [permissions.IsAuthenticated]
class GroupViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows groups to be viewed or edited.
"""
queryset = Group.objects.all()
serializer_class = GroupSerializer
permission_classes = [permissions.IsAuthenticated]
Instead of writing multiple views, all common behavior is grouped into classes called ViewSets. For future purposes, you can easily break them down into individual views if needed, but using viewsets keeps the view logic nicely organized and very concise.
4. URLs
It’s time to make API urls available. Navigate to the config folder and change
urls.py
content to the following.from django.urls import include, path
from rest_framework import routers
from quickstart import views
router = routers.DefaultRouter()
router.register(r"users", views.UserViewSet)
router.register(r"groups", views.GroupViewSet)
# Wire up our API using automatic URL routing.
# Additionally, login URLs included for the browsable API.
urlpatterns = [
path("", include(router.urls)),
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
]
5. Pagination
Pagination allows you to control how many objects per page are returned. To enable it, add the following lines at the bottom of
config/settings.py
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 10,
}
Actually, we don’t really need this setting unless you decide to populate the database with lots of data further down the road.
6. Settings
Within the same
settings.py
file, for the test purposes, enable all hosts by adding "*" to ALLOWED_HOSTS.ALLOWED_HOSTS = ["*"]
Also add "rest_framework" to INSTALLED_APPS.
INSTALLED_APPS = [
...
"rest_framework",
]
7. Testing the API
It’s time to test what we’ve accomplished so far. Let’s launch the server.
python manage.py runserver
Now in your browser open the URL http://127.0.0.1:8000/users/. You're probably seeing the page like this, which means you need to log in (top right corner) with the superuser credentials you've created at the end of step 1.
After successful login, the page should look like this. If yes, great job!
So far so good and let’s move on to the next step: changing this fairly classy UI to the awesome Swagger!
First, stop the server by pressing CONTROL-C. After this, you need to install drf-spectacular in your virtual environment.
pipenv install drf-spectacular
Then, in
settings.py
add drf-spectacular to the installed apps. By now the result should be like this.INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"drf_spectacular",
]
Finally, register AutoSchema with DRF by adding an extra line to the REST_FRAMEWORK settings also inside
settings.py
file so that the result looks like this.REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 10,
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}
At the time of writing this text, the most recent version of drf-spectacular is 0.11.1. So, bear in mind that there might be some breaking changes in the code as it’s still below 1.x.x and under active development.
And now the trickiest part, which was especially difficult for me. Hopefully, you'll avoid the misery I went through and jump straight to the happy part 🎊
1. URLs
Switch to the
urls.py
inside config folder and modify content as follows.from django.urls import include, path
from rest_framework import routers
from quickstart import views
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
router = routers.DefaultRouter()
router.register(r"users", views.UserViewSet)
router.register(r"groups", views.GroupViewSet)
# Wire up our API using automatic URL routing.
# Additionally, login URLs included for the browsable API.
urlpatterns = [
path("", include(router.urls)),
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
# OpenAPI 3 documentation with Swagger UI
path("schema/", SpectacularAPIView.as_view(), name="schema"),
path(
"docs/",
SpectacularSwaggerView.as_view(
template_name="swagger-ui.html", url_name="schema"
),
name="swagger-ui",
),
]
Basically, you've just added a new import and two paths required for the Swagger UI.
2. Template
Now let's create a folder for a custom API documentation template with a new file inside. I assume that your terminal stays in the same root directory this whole time – tutorial.
mkdir templates
touch templates/swagger-ui.html
Then open
swagger-ui.html
and paste the following code. Just a quick note: I ran into difficulties even with this piece of code taken from the DRF documentation, so it's changed a bit although it may seem familiar.<!DOCTYPE html>
<html>
<head>
<title>Swagger</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@3/swagger-ui.css">
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
<script>
const ui = SwaggerUIBundle({
url: "{% url 'schema' %}",
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
layout: "BaseLayout",
requestInterceptor: (request) => {
request.headers['X-CSRFToken'] = "{{ csrf_token }}"
return request;
}
})
</script>
</body>
</html>
3. Settings
And again go back to
settings.py
inside config folder. Find TEMPLATES and change the line "DIRS" so that the whole block is as follows in order to tell Django where to look for our custom template.TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
Final check of the project layout just to make sure we are on the same page.
After all those configurations, let's spin up the server and open the url http://127.0.0.1:8000/docs/
Lo and behold! Your Swagger UI documentation implemented in the OpenAPI 3.0 standard 🎉
Further enhancements are only up to you, but there are a couple of ideas: