# Host API Docs 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](https://www.linkedin.com/posts/chris-ramacciotti_frontend-backend-api-activity-6987803634747027456-4SAZ?utm_source=share&utm_medium=member_desktop). 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:

```bash
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:

```plaintext
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`:

```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](https://support.smartbear.com/swaggerhub/docs/tutorials/openapi-3-tutorial.html).

## 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](https://redocly.com/docs/redoc/deployment/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](https://github.com/nvm-sh/nvm) for this)
    
* redoc-cli
    

Assuming you now have node installed, let's install `redoc-cli`:

```bash
npm i -g redoc-cli
```

To quickly experiment with the tool, let's have `redoc-cli` serve our docs:

```bash
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"):

![Screen Shot 2022-10-21 at 12.51.04 PM.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1666374672093/aacP6mFss.png align="left")

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:

1. Generate documentation as a static HTML file
    
2. Add a Django view that serves our static HTML content
    
3. 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:

```bash
redoc-cli bundle api/docs/openapi.yaml -o api/docs/generated-docs.html
```

After running this command, your project structure should look like this:

```plaintext
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:

```python
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:

```python
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:

```python
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:

```bash
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.

## 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](https://en.wikipedia.org/wiki/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`:

```python
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:

1. Create a superuser
    
2. 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:

```bash
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`:

```python
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:

```bash
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/`:

![Screen Shot 2022-10-21 at 4.07.34 PM.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1666386473833/U8OVSTsdl.png align="left")

After entering the credentials you provided when running the `createsuperuser` management command, you should see the API docs!

> Adding [@login\_required](https://docs.djangoproject.com/en/4.1/topics/auth/default/#django.contrib.auth.decorators.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](https://docs.djangoproject.com/en/4.1/topics/auth/default/#the-permission-required-decorator) 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!
