Development
-
August 30, 2023

Automating Django REST APIs documentation made easy with DRF Spectacular - Part 2

In the previous blog post, I emphasized the significance of thoroughly documenting APIs. Introducing an exceptional tool, DRF Spectacular, which seamlessly generates high-quality documentation for Django REST APIs, I explored its installation and initial configuration. Additionally, I have showcased an example project to highlight the great documentation it produces.

Now, in this second part, I embark on a more in-depth journey, delving into the library's advanced configuration and customization options. Building on the example project from the previous part, I’ll provide practical examples, ensuring a hands-on learning experience. So, without further ado, let's dive right in and uncover the full potential of DRF Spectacular!

The example project

To continue with the practical examples, I will use the presented example project in the previous blog post. The reality of the project is about users that can be led by another user, and the system maintains this relationship in a tree structure. The explored features are:

  • Create users.
  • List and filter users. The users can be filtered by username.
  • List the users inside a leadership tree of a given user.
  • Set a user as the leader of another user.
  • List the users that are leaders.
  • Get calculated metrics of the users.
  • Delete a user.

If you want to know more about the user model, or the ViewSet and used serializers, please go ahead and take a look at the first part of this blog post where I explained the features of listing and creating users. I will assume you know the example project and reality well, so we can go deeper into the other features, showing amazing tools, tips, and the documentation’s appearance. Let’s begin.

Use extend_schema and OpenApiParameter

Imagine I want to add more information about the query parameter username in the listing users endpoint. I want to add a description for it, something like "Searches for the value in this query parameter returning all the users that have this value as substring. Ignores lowercase and uppercase". For this, I will use:

  • extend_schema decorator: Adding this decorator to the functions in the view, will allow me to customize the query parameters, request, response and more.
  • OpenApiParameter class: A class to define and customize parameters.

So, let’s override the list function in the UserViewSet and add this:

CODE: https://gist.github.com/brunomichetti/6a6dd97195dfccf91b9e3216fdcbae98.js?file=list_user_function.py

I added in the parameters a list of OpenApiParameter elements. I can add more if we want. Let’s see the updated documentation:

In the extend_schema decorator we can add more options to customize requests and responses. Let’s see more cases.

Custom actions

For the leader's features, I will add custom actions to the UserViewSet.

The tree of a user

For a given user, I want to get all the users that are inside its leadership tree. For this, I have to make a GET request to api/users/{id}/tree/ . This will get all the users below that leadership tree. This is the action:

CODE: https://gist.github.com/brunomichetti/0acb0c8e29ac86f5f3041a129b3f51ad.js?file=user_tree_function.py

Note: the get_tree function is a function defined in the CustomUser class. For the purpose of this blog, it doesn’t matter how it works. Just assume that the function returns the queryset with the users inside the tree. The docs look like this:

This is not good, because it says “No response body” but the endpoint actually returns a list of users. So I use again extend_schema to customize the response of this action:

CODE: https://gist.github.com/brunomichetti/d44e7021fba8931580417ce288ee0525.js?file=user_tree_extend_schema.py

I used the decorator to indicate that the response has the form of a list (many=True) with the fields of UserListSerializer . In the line of parameters I indicate that the query parameter username that is inherited from the ViewSet, won’t be used in this action. If I don’t add this line, the documentation will say that the endpoint has the username query parameter. Take a look at how the documentation looks now:

Set a leader and get the list of leaders

Now lets see the features of setting a user’s leader and listing the leaders of the system:

  • To set a user’s leader I need to make a POST request to api/users/leaders/ sending the ids of the user and leader in the body. The successful response will have a 200 status and return those ids.
  • To list the users that are leaders, I will need to make a GET request to api/users/leaders/ and every user that has at least one user in charge, will be part of the returned list.

This is the serializer to set a leader:

CODE: https://gist.github.com/brunomichetti/414964f7dee15855ce6b2a0fa4c9a2a0.js?file=set_leader_serializer.py

Now let’s see the action in the UserViewSet:

CODE: https://gist.github.com/brunomichetti/a29588689b21786e48d2110a6f2633ff.js?file=leaders_function.py

In the same way as the tree endpoint, I won’t see any information about the expected body in the POST request in the docs. I won’t see the format of the responses either. Let’s add some documentation, but what can we do if the same action has two methods? The extend_schema decorator allows me to specify different documentation in the same action with more than one method. This is one way:

CODE: https://gist.github.com/brunomichetti/b08ab7902fa08ab8a71f7916b5b6c0dc.js?file=leaders_extend_schema.py

I have added two decorators: one for the GET method and another one for the POST method. This way I customize each endpoint separately. On each method I customize the parameters, responses, description, etc. as I want. Let’s see the changes in the docs:

And that’s it! I have customized the action that has two methods. Now let’s see more examples.

Endpoint that doesn’t require serializers

Now let’s talk about the user metrics’ feature. Let’s assume the system generates metrics for the users, and returns a dictionary with three results: metric_1, metric_2, and metric_3. For the purpose of this article I don’t care about the metrics names nor the calculation of each one. All I care about is that the response of this endpoint doesn’t need a serializer, it’s just a dictionary where the keys are always the same, and the data that can change is the value of each key. This is the action:

CODE: https://gist.github.com/brunomichetti/065ed6ec6dd06d34260b4af4c93edeaa.js?file=metrics_endpoint.py

As with the other custom actions, the documentation won’t show anything in the response body. For this, I will use extend_schema and two other tools:

  • inline_serializer: The inline serializer allows us to define a schema for an endpoint without the need of defining a serializer class. We want that because we don’t want to create a new serializer just for documentation.
  • OpenApiExample: A class to define examples for open API.

This is how it looks:

CODE: https://gist.github.com/brunomichetti/0626aaf41b2abc0b42e5fefbaa73227d.js?file=metrics_extend_schema.py

The inline_serializer is required by extend_schema. I define an inline serializer with an example field, just to force the decorator to take the examples list of OpenApiExample. There I define a dictionary example with example values. I use request_only in False and response_only in True just to indicate that I want the example only for the response. But I can change those booleans as I need. Let’s see the docs:

Endpoint with authorization

Let’s discuss the delete user endpoint. Imagine that for security reasons I want only authenticated users to be able to delete their own account. That means, unauthenticated users can’t access the endpoint and authorized users can access but deleting only their own account. For this, I use the token authentication from Django REST framework. The login flow is out of the scope for this article.

Now let’s see how to indicate the UserViewSet to ask for authentication in the delete method. First, I use the DestroyModelMixin class, so I add it as a parent class of UsersViewSet. Then I override the get_permissions function to indicate that the destroy endpoint requires the user to be authenticated. The rest of the endpoints don't require it. Here are the overwritten functions:

CODE: https://gist.github.com/brunomichetti/7a71062d654fff12618dfd7f55cdd60b.js?file=destroy_function.py

Now, let’s check the docs:

An important thing to notice in the docs is this:

The endpoints that require authorization, have a different lock icon (The gray one). This is good because this way the person that reads the documentation, can easily identify the endpoints that require authorization.

Adding pagination

Now let’s suppose that the system has a lot of users, and I want to paginate the response of the listing feature. DRF has tools to do this very easily, I’m going to choose the LimitOffsetPagination, adding this to the REST_FRAMEWORK settings:

CODE: https://gist.github.com/brunomichetti/8fa9fce27a0c1bb2cc26e25ae4af7e85.js?file=rest_framework_dict.py

This automatically adds pagination to the endpoints that return listed objects. DRF Spectacular recognizes this, and updates the docs. These are now the query parameters of the endpoint that lists users:

This is because when adding pagination, now I can add offset and limit query parameters to the request to filter the list of users. The response has also changed:

That is great! The documentation already shows the pagination! But wait, what happens with the custom actions? The documentation shows them also paginated. The response and query parameters of the tree endpoint are equal to these shown in the previous image. This is not true, because in the custom actions we return a serialized queryset, not a paginated response. How can I fix that? An option is to set in the action decorator that I don’t want to paginate the response. Let’s add it:

CODE: https://gist.github.com/brunomichetti/d70a5d6fb142f9a01ed0af128f41ff59.js?file=pagination_none.py

After this, the only pagination reflected in the docs will be in the listing users endpoint.

Bonus track: centralize the schemas

Finally, I would like to talk about something that is totally optional but I think it improves the maintainability of your project. Imagine you have a big project with big views and a lot of actions. If you want to customize most of them, your views will grow even more, because you are going to add extend_schema decorators everywhere.

What I like to do, is to create on each app in the Django project, a file called custom_schemas.py. There I define all the schemas, something like:

CODE: https://gist.github.com/brunomichetti/7bd17e0725120121ff829eb76d8f04cd.js?file=list_users_schema.py

After that, I import them in the views files, and use the extend_schema_view decorator. This decorator can be added above the ViewSet, and then I can map the endpoint with the defined schema. This is how it looks like:

CODE: https://gist.github.com/brunomichetti/cee1ed5e77e6e37a1da30c509048546f.js?file=schemas_file.py

Now I add that to the UserViewSet:

CODE: https://gist.github.com/brunomichetti/4d3865e7d6ad7bb6f7729f315885b376.js?file=extende_schema_view.py

And then of course I remove the override of the list function and the extend_schema decorator above.

I can add more actions and their custom schemas inside the extend_schema_view decorator as I want. I can even combine the use of extend_schema_view with some functions above the ViewSet, and the use of extend_schema decorator with other functions inside the ViewSet.

Again, this is totally optional, I use it because I like to centralize the customizations to increase the maintainability.

Summary

This was the second and final part of a blog post of APIs documentation. I focused on the crucial role of documentation for Django REST APIs and introduced the tool DRF Spectacular as a game-changer in this domain. I shared an example project to help readers grasp the key functionalities of this tool and understand its significance in API development. The article also provided valuable tips on how to create clear and user-friendly documentation. One standout feature of DRF Spectacular that I highlighted was its ability to automatically generate documentation, simplifying the process and saving developers valuable time and effort. With a genuine hope that my article resonates with readers and fosters a greater appreciation for the importance of thorough API documentation, I'm excited to share these insights with the wider community.