Documenting the API with drf-spectacular and OpenAPI
The browsable API (part 18) documents this project informally, one endpoint at a time, by actually being it. OpenAPI is a formal, machine-readable specification of the same information — every endpoint, every field, every possible response — that other tools (client code generators, API testing tools, a proper docs site) can consume directly.
Installing drf-spectacular
pip install drf-spectacular# taskapi/settings.py
INSTALLED_APPS = [
# ...
"drf_spectacular",
]
REST_FRAMEWORK = {
# ... existing settings ...
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}
SPECTACULAR_SETTINGS = {
"TITLE": "Task API",
"DESCRIPTION": "A task management API built across the DRF from scratch series.",
"VERSION": "1.0.0",
}drf-spectacular is the current standard schema generator for DRF — it introspects TaskSerializer's fields, TaskViewSet's actions, and every permission and pagination setting already configured, and builds an OpenAPI schema from what's already there. Nothing about the API itself changes; this is purely additive.
Serving the schema and interactive docs
# taskapi/urls.py
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
urlpatterns = router.urls + [
path("schema/", SpectacularAPIView.as_view(), name="schema"),
path("docs/", SpectacularSwaggerView.as_view(url_name="schema"), name="docs"),
]/api/schema/ serves the raw OpenAPI document (YAML by default) — the machine-readable file other tools consume. /api/docs/ serves Swagger UI, an interactive page built from that same schema: every endpoint, every field, with a "Try it out" button that sends real requests, generated entirely from code already written in parts 1 through 20.
What it picks up automatically
- Every serializer field, including
read_only_fieldsfrom part 3 and the custom validation from part 11 - Every ViewSet action, including the
@action-decoratedmark_completefrom part 9 - Permission requirements, reflected as which operations need authentication
- Pagination and filtering parameters —
page,search,ordering, and everyfilterset_fieldsentry from parts 13 and 14, all appearing as documented query parameters without writing a single one by hand
Adding detail the schema can't infer
from drf_spectacular.utils import extend_schema
class TaskViewSet(ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
@extend_schema(description="Marks a task as completed. Idempotent — calling it twice is safe.")
@action(detail=True, methods=["post"])
def mark_complete(self, request, pk=None):
# ... unchanged from part 9 ...@extend_schema adds documentation the code itself can't express — why an endpoint exists, or a behavioral guarantee like idempotency, isn't something a schema generator can infer purely from type signatures.
A separate, hand-written API reference document drifts out of sync with the actual code the moment anyone changes a serializer field or forgets to update it — a familiar failure mode from any project with docs maintained separately from the implementation. A generated schema can't drift the same way, since it's read directly from the same TaskSerializer and TaskViewSet classes serving real requests.
Last part: getting this from runserver on a laptop to a real, deployed API.