Host API Docs in Django
Generate API docs from an OpenAPI spec and host them in Django

Search for a command to run...
Generate API docs from an OpenAPI spec and host them in Django

No comments yet. Be the first to comment.
In this series I show how to manage browsable API documentation using an OpenAPI specification, including the continuous generation of documentation in a CI pipeline with GitHub Actions.
In the last article I covered how to Host API Docs in Django. To recap, this included the following: Creating an OpenAPI spec Generating API docs with the redoc-cli Exposing docs in Django at a path of our choice Requiring authentication to acces...
Eliminate network dependencies until the last responsible moment

An API should operate as intended. This requires both an expressed intent and verification that, indeed, the API fulfills this intent.

Background When I worked for Arity (an Allstate startup), Allstate had contracted with Pivotal Labs to facilitate its agile transformation. Pivotal's model was one of enablement - they would augment a cross-functional product team with their own cros...

Setting up JSON logging for a production Django app requires the following steps: Configure JSON logging from the Django app Configure JSON logging from the application server (for example, uWSGI) In the last article I covered how to configure JSON...

What is observability? Improving an application's observability can be one of the most empowering endeavors as an engineer. Imagine tiptoeing across a messy child's room in the middle of the night with the goal of not stepping on a Lego. Now imagine ...

So you've found a reason to maintain an OpenAPI spec in your codebase, maybe to create agility among frontend and backend engineers. Whatever your reason may be, a helpful addition to your organization might be to host browsable API docs based on this spec.
In this article, I'll show you how to expose API docs in Django as an authenticated view, with content generated from an OpenAPI spec.
Let's start by creating a Django project with a separate api app:
pip install django==4.1.2
django-admin startproject django_api_docs
# If you prefer skewer-cased project names
mv django_api_docs django-api-docs
cd django-api-docs
python manage.py startapp api
At this point you should have the following project structure:
django-api-docs
├── api
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── django_api_docs
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── manage.py
Let's add an OpenAPI spec with an endpoint for fetching some resources. Our kiddo is 5 years old, so dinosaurs are on my mind. Let's document an endpoint to fetch all dinosaurs at GET /api/dinosaurs/. We'll store this spec at api/docs/openapi.yaml:
openapi: 3.1.0
info:
title: RESTful dinosaurs
version: 0.0.1
servers:
- url: http://localhost:8000
description: Local development environment
paths:
/api/dinsosaurs/:
get:
summary: Retrieve a list of dinosaurs
description: Retrieve a list of all dinosaurs
responses:
"200":
description: OK
content:
application/json:
schema:
type: array
items:
type: object
properties:
common_name:
type: string
scientific_name:
type: string
To explore the OpenAPI specification, checkout this tutorial.
redoc-cliBefore we integrate generated documentation with Django, let's first take a peek at those docs. For this we'll use the redoc-cli from Redocly. This is a node-based tool that we'll use to generate documentation from the OpenAPI spec we created above.
To get started, you'll need the following installed:
node (I prefer to use nvm for this)
redoc-cli
Assuming you now have node installed, let's install redoc-cli:
npm i -g redoc-cli
To quickly experiment with the tool, let's have redoc-cli serve our docs:
redoc-cli serve api/docs/openapi.yaml
After doing this and navigating to http://localhost:8080, you should see the following (after expanding "200 OK"):

If you aren't able to expand the response, or you don't see the response properties listed, then you may have an indentation problem in your spec. If the command fails altogether, check to see that you've saved the spec at api/docs/openapi.yaml and that you've accurately referenced this path in the redoc-cli serve command.
To configure Django to serve our generated documentation at a request path of our choosing, we'll need to do the following:
Generate documentation as a static HTML file
Add a Django view that serves our static HTML content
Add a URL to route requests to our new Django view
Let's get started.
Generating static content is as easy as swapping out the redoc-cli serve command for the redoc-cli bundle command, while also specifying where we want the generated content to be saved:
redoc-cli bundle api/docs/openapi.yaml -o api/docs/generated-docs.html
After running this command, your project structure should look like this:
django-api-docs
├── api
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── docs
│ │ ├── generated-docs.html <= NEW DOCS!
│ │ └── openapi.yaml
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ ├── urls.py
│ └── views.py
├── django_api_docs
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── manage.py
Let's keep it simple and use a function-based Django view. Replace the contents of api/views.py with the following:
import os
from django.http import Http404, HttpResponse
def docs(request):
current_dir = os.path.dirname(os.path.realpath(__file__))
docs_path = os.path.join(current_dir, "docs/generated-docs.html")
try:
with open(docs_path, "r") as f:
content = f.read()
except OSError:
raise Http404("Page not found")
else:
return HttpResponse(content)
This view looks in the same directory as the views.py file for a file at docs/generated-docs.html. If it exists, its contents will be served. If not, a 404 response will be served.
In order to expose our docs in Django, the last step is to add to the urlpatterns of our api app, as well as to the django_api_docs app. Edit api/urls.py to contain the following:
from django.urls import path
from api import views
app_name = "api"
urlpatterns = [
path("docs/", views.docs)
]
Because the DJANGO_SETTINGS_MODULE is set to django_api_docs.settings (see your generated manage.py file at the root of your project), only the URLs in django_api_docs/urls.py will be exposed. We need to add an entry in that file so that all paths beginning with api/ are routed to the api app. To accomplish this, edit django_api_docs/urls.py to contain the following:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("api.urls", namespace="api")),
]
At this point, we're ready to see the results:
python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
October 21, 2022 - 18:20:50
Django version 4.1.2, using settings 'django_api_docs.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Upon navigating to http://localhost:8000/api/docs/, you should see the same content we saw in our preview from above. If you don't, make sure the paths you added to both api/urls.py and django_api_docs/urls.py are accurate.
If the API we're exposing is not meant for public consumption, as a practice of the principle of least privilege, we should ensure that our API docs are accessible only by intended consumers of the API we're documenting.
The change to require authentication is quick: decorate the docs view function with @login_required in api/views.py:
import os
from django.contrib.auth.decorators import login_required
from django.http import Http404, HttpResponse
@login_required
def docs(request):
current_dir = os.path.dirname(os.path.realpath(__file__))
docs_path = os.path.join(current_dir, "docs/generated-docs.html")
try:
with open(docs_path, "r") as f:
content = f.read()
except OSError:
raise Http404("Page not found")
else:
return HttpResponse(content)
Because we're now adding authentication to our Django app, we have a little more setup to ensure we have a user we can use for authentication:
Create a superuser
Add Django's default auth views to our URL patterns
From the command line, run the following, which will prompt you for a username, email and password:
python manage.py createsuperuser
So that we don't have to create our own login form, we'll use Django's default form. In order to expose the login form and a way to logout, we need to add those URL patterns to django_api_docs/urls.py:
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path('accounts/login/', auth_views.LoginView.as_view(template_name="admin/login.html")),
path('accounts/logout/', auth_views.LogoutView.as_view()),
path("api/", include("api.urls", namespace="api")),
]
At this point we're ready to test our authenticated view!
Let's start the app again:
python manage.py runserver
Navigate to the docs at http://localhost:8000/api/docs/, which should now redirect to http://localhost:8000/accounts/login/?next=/api/docs/:

After entering the credentials you provided when running the createsuperuser management command, you should see the API docs!
Adding @login_required validates only that a client is an authenticated user. If you want finer-grained control, you'll need to modify this approach, possibly leveraging the @permission_required decorator.
In this article we've covered how to host API docs in Django with content generated with the redoc-cli from an OpenAPI spec. In my next article, I'll cover how to inject these docs into a Docker image using a CI pipeline with GitHub Actions.
Enjoy!