- Published on
Generating API documentation with Django REST Framework and drf-spectacular
- Authors
- Name
- Jeongwon Park
Introduction
Documentation is a crucial part of any API. It helps developers understand how to use the API and what to expect from it. When I first built the backend system for Earthmera, I was writing the API documentation for each endpoint manually. This was a time-consuming process, and I wasn’t able to keep pace with all the changes in the API across different versions.
To address this issue, I decided to use drf-spectacular to generate the API documentation automatically. drf-spectacular is a library that lets you generate OpenAPI (formerly known as Swagger) documentation for your Django REST Framework APIs. OpenAPI provides a standardized way to describe, produce, consume, and visualize RESTful web APIs.
In this post, we’ll look at how I set up and use drf-spectacular to generate API documentation for a Django REST Framework application for Earthmera.
What is drf-spectacular?
drf-spectacular is a library that generates OpenAPI documentation for your Django REST Framework APIs. It automatically generates documentation for each endpoint based on your DRF configuration and Python code. It’s highly customizable using decorators such as @extend_schema and @extend_schema_view. With drf-spectacular, you can create and maintain your API documentation in a single place, and it will be updated automatically whenever you make changes to your API.
Setting up drf-spectacular
First, install the library using pip:
pip install drf-spectacular
Next, add the library to your INSTALLED_APPS in settings.py and configure SPECTACULAR_SETTINGS:
INSTALLED_APPS = [
...
'drf_spectacular',
...
]
SPECTACULAR_SETTINGS = {
'TITLE': 'Earthmera API',
'DESCRIPTION': 'Earthmera API',
'VERSION': API_DEFAULT_VERSION,
'SERVE_INCLUDE_SCHEMA': False,
'AUTHENTICATION_WHITELIST': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
'common.auth.backends.CustomJWTAuthentication',
'Ticketeer.auth.backends.TicketeerJWTAuthentication',
],
}
TITLE
: The title of the API.DESCRIPTION
: The description of the API.VERSION
: The version of the API, which is defined in theAPI_DEFAULT_VERSION
variable in mysettings.py
file.SERVE_INCLUDE_SCHEMA
: Whether to include the schema in the response, which is set toFalse
because I didn't want to include the default schema in the response.AUTHENTICATION_WHITELIST
: The list of authentication classes to use for the API.
Last, I added the schema_view
to my urls.py
file:
# ...
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
from config.settings.base import API_ALLOWED_VERSION
urlpatterns = [
# ...
]
for version in API_ALLOWED_VERSION:
urlpatterns.extend([
path(f'api/schema/{version}/', SpectacularAPIView.as_view(
api_version=version,
custom_settings={'VERSION': version}
), name=f'schema-{version}'),
path(f'api/schema/{version}/swagger-ui/',
SpectacularSwaggerView.as_view(url_name=f'schema-{version}'),
name=f'swagger-ui-{version}'
),
path(f'api/schema/{version}/redoc/',
SpectacularRedocView.as_view(url_name=f'schema-{version}'),
name=f'redoc-{version}'
),
])
I dynamically allocated the routes for each version of the API because I want to support multiple versions of the API. This code creates three routes for each version of the API:
/api/schema/{version}/
: The schema for the API./api/schema/{version}/swagger-ui/
: The Swagger UI for the API./api/schema/{version}/redoc/
: The Redoc for the API.
Generating API documentation
Generating the schema
drf-spectacular automatically reads ViewSets, Serializers, and Models to generate basic API documentation. Although it provides a certain level of documentation without additional decorators, detailed elements—such as request/response schemas, examples, permissions, or tags—must be added using features like @extend_schema
.
@extend_schema decorator
The @extend_schema
decorator lets you add extra details to the API documentation. This can include request/response schemas, examples, permissions, or tags.
from drf_spectacular.utils import extend_schema
@extend_schema(
summary='Retrieve user list',
description='Get a list of all registered users',
responses=UserSerializer(many=True),
tags=['User'],
# security=[{'Bearer': []}], # (drf-spectacular 0.20+)
)
def list(self, request, *args, **kwargs):
...
Although the decorator supports many more parameters, I utilized the following to generate our API documentation:
operation_id
: The operation ID of the API.summary
: The summary of the API.description
: The description of the API. Not using it in this way but just used docstring which is automatically added to the API documentation.request
: The request of the API.responses
: The response of the API.tags
: The tags of the API.parameters
: The parameters of the API.
Use cases
For the Earthmera API, I used @extend_schema
to add detailed documentation. Because I use class-based views, I attach tags at the class level to group the APIs and add other details at the method level.
Tag
@extend_schema(tags=['Common.Badge.Admin'])
@permission_classes([IsAdminUser])
class BadgeManageView(APIView):
pagination_class = CustomPageNumberPagination
# ...
def post(self, request, *args, **kwargs):
# ...
@extend_schema(tags=['Common.Badge'])
@permission_classes([IsAuthenticated])
class BadgeDetailView(APIView):
# ...
def put(self, request, badge_title, *args, **kwargs):
# ...
def delete(self, request, badge_title, *args, **kwargs):
# ...
I follow a naming scheme for tags: {Category}.{Subcategory}.({Constraint})
.
Category
: The category of the API.Subcategory
: The subcategory of the API.Constraint
: The constraint of the API, if it's for admin or user.
This setup makes it easy to find the relevant API documentation by searching for tags.
Other details for each HTTP method
Depending on the API, I add more details using @extend_schema
for each method. If the API requires a request body, I specify the request serializer under the request parameter. If it needs query or path parameters, I add them under parameters. Likewise, if the API returns a response body, I set a serializer for the responses parameter.
To format the request body, I use serializers. These serializers also help define the request data format for the API documentation:
class BadgeObtainSerializer(serializers.Serializer):
userId = serializers.IntegerField(help_text='User ID')
badgeTitle = serializers.CharField(help_text='Badge Title')
def validate_userId(self, value):
if not User.objects.filter(id=value).exists():
raise serializers.ValidationError("User not found")
return value
def validate_badgeTitle(self, value):
if not Badge.objects.filter(title=value).exists():
raise serializers.ValidationError("Badge not found")
return value
This serializer can then be referenced in the API documentation:
@extend_schema(
operation_id='...',
# ...
request=BadgeObtainSerializer, # single json format
)
@extend_schema(
operation_id='...',
# ...
responses=BadgeObtainSerializer(many=True), # list of json format
)
For pagination, simply assigning the pagination class to pagination_class
in the view typically auto-formats the API documentation. If you have custom pagination, you must override the get_paginated_response
method in your pagination class:
class CustomPageNumberPagination(PageNumberPagination):
def get_paginated_response(self, data):
return Response({
'count': self.page.paginator.count,
'totalPagesCount': self.page.paginator.num_pages,
'results': data
})
def get_paginated_response_schema(self, schema):
return {
'type': 'object',
'properties': {
'count': {'type': 'integer', 'example': 100},
'totalPagesCount': {'type': 'integer', 'example': 10},
'results': schema
}
}
class BadgeListView(APIView):
pagination_class = CustomPageNumberPagination
@extend_schema(
operation_id='badge_list_get',
summary='List all badges',
# ...
responses={
200: BadgeSerializer(many=True)
}
)
Similarly, I use response serializers to define the structure of the response body. One challenge has been dealing with cases where serializers contain method fields, require a nested JSON format, or incorporate pagination. This includes both the default DRF pagination and custom pagination logic.
Handling Nested JSON Fields and Custom Method Fields
To manage nested JSON formats and custom method fields, I use the @extend_schema_field
decorator to ensure the response body is accurately documented. The decorator helps make fields (like nested JSON or custom method fields) readable in the documentation.
class GroupPreviewSerializer(serializers.ModelSerializer):
groupId = serializers.IntegerField(source='id')
groupName = serializers.CharField(source='group_name')
isOrganizer = serializers.SerializerMethodField()
class Meta:
model = Group
fields = ['groupId', 'groupName', 'isOrganizer']
@extend_schema_field(bool) # this helps to format the isOrganizer field as boolean
def get_isOrganizer(self, obj):
try:
user = self.context.get('request').user
return obj.group_leader == user
except:
return False
class UserInfoSerializer(serializers.Serializer):
id = serializers.IntegerField()
name = serializers.CharField()
age = serializers.IntegerField()
@extend_schema_field(UserInfoSerializer) # This helps to format the userInfo field as UserInfoSerializer (nested json format)
def get_userInfo(self, obj):
return UserInfoSerializer(obj.user).data
Defining Query Parameters
For query parameters, I add them to the parameters list in the @extend_schema
decorator. Path parameters are often auto-generated, so I usually only define query parameters manually.
@extend_schema(
operation_id='...',
# ...
parameters=[
OpenApiParameter(
name='pageSize',
description='The number of badges to be displayed per page.',
required=False,
type=int,
default=10 # not required if required is True
),
OpenApiParameter(
name='badgeType',
description='Type of badges to choose. it can be either EVENT_BADGE or LEVEL_BADGE'',
required=True,
type=str,
default='EVENT_BADGE' # not required if required is True
),
]
)
With all those decorators, I was able to format the API documentation for the Earthmera API. To view the API documentation, first I had to generate the schema based on the current settings.
python manage.py spectacular --color --file schema_{api_version}.yml
I set the api_version
to the version of the API I want to generate the schema for.
Now, I could view the API documentation by accessing the Swagger UI or Redoc UI.
http://localhost:8000/api/schema/{api_version}/swagger-ui/
http://localhost:8000/api/schema/{api_version}/redoc/
Redoc

Swagger UI

Conclusion
By creating a well-structured API documentation system, I’ve managed to keep it consistently updated and ensure that the APIs function as expected. This also improved my team’s understanding of the APIs and allowed for more seamless communication between teams.
I considered setting up a CI pipeline to automatically update the schema whenever there are code changes or when APIs are modified/added and merged into the dev branch. However, at this stage, implementing this process might introduce unnecessary overhead. For now, I’m updating the schema manually and applying the changes as needed. In the future, as the team grows and the project becomes more complex, I plan to revisit and implement an automated schema update process.