Host API Docs in Django
Generate API docs from an OpenAPI spec and host them in Django
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.
Create a Django Project
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
Create an OpenAPI Spec
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.
Preview the Generated Docs with redoc-cli
Before 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 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.
Configure Django to Serve Docs
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.
Generate the documentation as a static HTML file
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
Add a Django view that serves our static HTML content
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.
Add a URL to route requests to our new Django view
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")),
]
Testing the API Docs
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 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.
Requiring Authentication to View Docs
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
Create a superuser
From the command line, run the following, which will prompt you for a username, email and password:
python manage.py createsuperuser
Add Django's default auth views to our URL patterns
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 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.
Next Steps
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!