Django REST Framework – A Complete Guide

Table of Contents

  1. What is Django REST framework?
  2. Why should you use Django REST framework?
  3. Why shouldn’t you use Django REST framework
  4. Alternatives to Django REST framework
  5. Getting Started
    1. Installation
    2. Basic Example – Model CRUD API
  6. Django views and class based views
    1. Object update example
  7. Serialization
    1. Referencing other models
    2. Nested serializers
    3. Validation
  8. Authentication and authorization
    1. Authentication
    2. Authorization
  9. Backend filtering
    1. Filtering
    2. Generic filtering
    3. Generic ordering
  10. Pagination, caching, throttling, versioning and documentation
    1. Pagination
    2. Caching
    3. Throttling
    4. Versioning
    5. Documentation
  11. Final thoughts

What is Django REST framework?

Django REST framework is the de facto library for building REST APIs in Django. It’s been around since 2011, and in that time has been used in thousands of projects with over a thousand contributors. It’s currently used by many large companies, including Robindhood, Mozilla, Red Hat, and Eventbrite.

Why should you use Django REST framework?

If you are using Django as your web framework and you need to write a REST API, the Django REST framework is the default choice to get the job done. It’s by far the most popular Django library for writing REST APIs, it’s well maintained and supported. It also comes with many features out of the box to simplify API development:

  • A web browsable API – where you can browse and interact with your API
  • Built in authentication and authorization, including packages for OAuth1a and OAuth2
  • Built in serialization for Django models and other data, with input validation
  • Easy backend filtering and sorting of data
  • Support for throttling of requests
  • Support for easy pagination of results
  • Support for versioning
  • Support for API schemas and documentation
  • Lots of documentation and support of a large community

Why shouldn’t you use Django REST framework?

If you are using Django and REST APIs, it’s a no-brainer, you should use Django REST framework. But over the past few years another API type started gaining a lot of traction – GraphQL. If you are going to be writing a GraphQL API, it doesn’t make sense to use the Django REST framework, you should take a look at Graphene Django instead.

Alternatives to Django REST framework

Django REST framework has pretty much come to dominate Django REST API, but here are some other alternatives:

Django Tastypie

It looks like another complete Django REST API library. People that have used it seem to say lots of positive things about it. Unfortunately, the project stopped being maintained, and is not under active development anymore.

Django Restless

From the creator of Django Tastypie, this is a small, flexible REST API library. Where Django REST framework has evolved to be a big library that can accommodate pretty much everyone, Django Restless just tries to do a few things really well, without adding any bloat. If you like to tinker more and want something really fast and flexible, this might be the library for you.

Getting Started

Installation

To start using the Django REST Framework you need to install the djangorestframework package:

pip install djangorestframework

Add rest_framework to INSTALLED_APPS settings

INSTALLED_APPS = [
    ...
    'rest_framework',
]

That should be enough to get you started

Basic Example – Model CRUD API

Django REST framework makes it very easy to create a basic API that works with Django models. With a few lines of code we can create an API that can list our objects and do basic CRUD. Let’s take a look at an example with some basic models.

models.py

class Author(models.Model):
   name = models.CharField(max_length=255)

class Book(models.Model):
   author = models.ForeignKey(Author, on_delete=models.CASCADE)
   title = models.CharField(max_length=255)

Serializers allow the conversion of querysets and model instances into data types that can be rendered as Content Type (JSON, XML, etc) and the other way around.

serializers.py

class BookSerializer(serializers.ModelSerializer):
   class Meta:
       model = Book
       fields = ['author', 'title', 'num_pages']

views.py

from rest_framework import viewsets

from book.models import Book
from book.serializers import BookSerializer

class BookViewset(viewsets.ModelViewSet):
   queryset = Book.objects.all()
   serializer_class = BookSerializer

And we let the REST framework wire up the url routes based on common conventions.

urls.py

router = routers.DefaultRouter()
router.register(r'', views.BookViewset)

urlpatterns = [
   path('', include(router.urls)),
]

Going to http://127.0.0.1:8000/book gives us:

viewset list

Here we can see a list of books with a GET request and can create a new book with a POST request. The Browsable API gives us a nice human browsable display and forms that we can play around with.

If we go to http://127.0.0.1:8000/book/1/, we see that a GET request to this url will give us details about the book with ID 1. A PUT request will modify that book’s data. And a DELETE request will delete the book with ID `1`.

viewset instance

Since we are requesting Content-Type text/html we are receiving the Browsable API, human friendly template. If we were to ask for Content-Type application/json we would just be getting the JSON. You can also set the format explicitly in your browser like so:

http://127.0.0.1:8000/book/1/?format=json

Response:

{"author":2,"title":"To Kill a Mockingbird","num_pages":281}

As you can see Django REST framework makes it very easy for us to create a basic model CRUD API.

Django views and class based views

As we saw in the basic example, Django REST framework makes model CRUD really simple. How do we go about writing some custom API calls? Let’s say we wanted to search the books from the basic example by author and title.

Here’s a basic Django view method for searching books:

views.py

@api_view(['GET'])
def book_search(request):
   author = request.query_params.get('author', None)
   title = request.query_params.get('title', None)

   queryset = Book.objects.all()
   if author:
       queryset = queryset.filter(author__name__contains=author)
   if title:
       queryset = queryset.filter(title__contains=title)

   serializer = BookSerializer(queryset, many=True)
   return Response(serializer.data)

urls.py

urlpatterns = [
   path('book-search', views.book_search, name='book_search'),
]

The code overall looks pretty similar to the standard Django view, with just a few modifications. It’s wrapped in the api_view decorator. This decorator passes a REST framework Request object and modifies the context of the returned REST framework Response object. We are using request.query_params instead of request.GET, and would use request.data instead of request.POST. And finally it uses a serializer to return a response, which will return the right content type to the client.

If we wanted to use class based views to facilitate code reuse we could modify the above code like so:

views.py

class BookSearch(APIView):

   def get(self, request, format=None):
       author = self.request.query_params.get('author', None)
       title = self.request.query_params.get('title', None)

       queryset = Book.objects.all()
       if author:
           queryset = queryset.filter(author__name__contains=author)
       if title:
           queryset = queryset.filter(title__contains=title)

       serializer = BookSerializer(queryset, many=True)
       return Response(serializer.data)

urls.py

urlpatterns = [
   path('book-search-view', views.BookSearch.as_view()),
]

Of course the REST framework has a bunch of reusable view classes and mixins you can use. For example, for the above example you might want to use ListAPIView. If you wanted to customize the Book CRUD code, instead of using the ViewSet from the basic example, you might want to combine a variation of ListModelMixin, CreateModelMixin, RetrieveModelMixin, UpdateModelMixin, and DestroyModelMixin

GenericAPIView is a common view that adds some common functionality and behavior to the base REST framework APIView class. With this class you can override some attributes to get the desired behavior:

  • queryset or override get_queryset() to specify the objects that should come back from the view
  • serializer_class or override get_serializer_class() to get the serializer class to use for the object
  • pagination_class to specify how pagination will be used
  • filter_backends – backends to use to filter the request, we go over backend filtering below

Here we use ListAPIView (which extends GenericAPIView and ListModelMixin) to create our book search:

views.py

class BookSearch(ListAPIView):

    serializer_class = BookSerializer

    def get_queryset(self):
        author = self.request.query_params.get('author', None)
        title = self.request.query_params.get('title', None)
        queryset = Book.objects.all()
        if author:
            queryset = queryset.filter(author__name__contains=author)
        if title:
            queryset = queryset.filter(title__contains=title)

        return queryset

Object update example

Let’s say that we had to write an API where we had to have someone update the status of a book’s read state:

model.py

class UserBook(models.Model):
   STATUS_UNREAD = 'u'
   STATUS_READ = 'r'
   STATUS_CHOICES = [
       (STATUS_UNREAD, 'unread'),
       (STATUS_READ, 'read'),
   ]

   book = models.ForeignKey(Book, on_delete=models.CASCADE)
   user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
   state = models.CharField(max_length=1, choices=STATUS_CHOICES, default=STATUS_UNREAD)

serializers.py

class UserBookSerializer(serializers.ModelSerializer):
   class Meta:
       model = UserBook
       fields = ['book', 'user', 'status']

We want to limit the field that they can change to just the status. Ideally, we would validate that the user has permission to change this specific book, but we’ll get to that in the authentication/authorization section.

views.py

class BookStatusUpdate(UpdateAPIView):
   queryset = UserBook.objects.all()
   serializer_class = UserBookSerializer
   permission_classes = (permissions.IsAuthenticated,)

   def update(self, request, *args, **kwargs):
       instance = self.get_object()
       data = {'status': request.data.get('status')}
       serializer = self.get_serializer(instance, data, partial=True)
       serializer.is_valid(raise_exception=True)
       self.perform_update(serializer)

       return Response(serializer.data)

Serialization

So far we used very simple, automatic serialization by just listing the fields. REST Framework serializers are similar to Django Forms and give us a lot of control by specifying attributes and overriding various methods.

For our BookSerializers we could have listed out the fields with type, requirement, max_length, etc.

serializers.py

class BookSerializer(serializers.ModelSerializer):
   title = serializers.CharField(required=True, max_length=100)
   num_pages = serializers.IntegerField(read_only=True)

   class Meta:
       model = Book
       fields = ['author', 'title', 'num_pages']

We could also override create() and update() methods to be able to execute some custom functionality:

serializers.py

class BookSerializer(serializers.ModelSerializer):
   title = serializers.CharField(required=True, allow_blank=True, max_length=100)
   num_pages = serializers.IntegerField(read_only=True)

   def create(self, validated_data):

       # Custom code

       return Book.objects.create(**validated_data)

   def update(self, instance, validated_data):

       # Custom Code

       instance.title = validated_data.get('title', instance.title)
       instance.code = validated_data.get('num_pages', instance.code)
       instance.save()
       return instance


   class Meta:
       model = Book
       fields = ['author', 'title', 'num_pages']

Working with serializers is very similar to how we work with Django forms, we validate the serializer and then call save(), saving the instance. Here’s a serializer that validates the title of the book.

serializers.py

class BookSerializer(serializers.ModelSerializer):
   title = serializers.CharField(max_length=100)

   def validate_title(self, value):
       if len(value) < 4:
           raise serializers.ValidationError("Title is too short")

       return value

   class Meta:
       model = Book
       fields = ['author', 'title', 'num_pages']

views.py

serializer = BookSerializer(data=data)
if serializer.is_valid():
	serializer.save()

Referencing other models

You can reference other entities in various ways:

  • Using the primary key: PrimaryKeyRelatedField
  • Using hyperlinking (the api endpoint url for the other entity): HyperlinkedRelatedField
  • Using the string representation of the object: StringRelatedField
  • Using an identifying slug field on the related entity: SlugRelatedField
  • Nesting the related entity inside the parent representation: We’ll discuss that more below

For example, here’s how an Author on our Book might look if we were to just use PrimaryKeyRelatedField

{
    "author": 2,
    "title": "To Kill a Mockingbird",
    "num_pages": 281
}

Nested serializers

Serializers can be nested, this way we can work on multiple objects in one operation, like getting all the information about the Book as well as the Author in a GET request:

serializers.py

class AuthorSerializer(serializers.ModelSerializer):
   name = serializers.CharField(max_length=255)

   class Meta:
       model = Author
       fields = ['name']

class BookSerializer(serializers.ModelSerializer):
   title = serializers.CharField(max_length=255)
   author = AuthorSerializer()

   class Meta:
       model = Book
       fields = ['author', 'title', 'num_pages']

http://127.0.0.1:8000/book/1/ returns

{
    "author": {
        "name": "Harper Lee"
    },
    "title": "To Kill a Mockingbird",
    "num_pages": 281
}

To be able to create and update a nested relationship in one request you will need to modify create() and update(), they will not work with nested fields out of the box. The reason for this is that the relationship between models is complicated and based on specific application requirements. It’s not something that can be set up automatically; your logic will have to deal with deletions, None objects, and so on. 

Here’s how you might handle create() in our simple example:

serializers.py

class BookSerializer(serializers.ModelSerializer):
   title = serializers.CharField(max_length=255)
   author = AuthorSerializer()

   def create(self, validated_data):
       author_data = validated_data.pop('author')
       author = Author.objects.create(**author_data)
       book = Book.objects.create(author=author, **validated_data)
       return book


   class Meta:
       model = Book
       fields = ['author', 'title', 'num_pages']

Doing a POST to http://127.0.0.1:8000/book with

{
    "author": {
        "name": "John1"
    },
    "title": "Book by John1",
    "num_pages": 10
}

Will now create both an author and a book.

Here’s how we might handle a simple update()

serializers.py

def update(self, instance, validated_data):
   author_data = validated_data.pop('author')
   author = instance.author

   instance.title = validated_data.get('title', instance.title)
   instance.num_pages = validated_data.get('num_pages', instance.num_pages)
   instance.save()

   author.name = author_data.get('name', author.name)
   author.save()

   return instance

A PATCH or PUT call to http://127.0.0.1:8000/book/8/ (that’s the id of this particular book), with

{
    "author": {
        "name": "John1_mod"
    },
    "title": "Book by John1_mod",
    "num_pages": 20
}

Will modify our book with the new author, title, and num_pages.

Validation

Validation in the REST framework is done on the serializer. Just like with Django forms you can set some basic validation on the fields themselves. In our example above, we had:

serializers.py

class AuthorSerializer(serializers.ModelSerializer):

   class Meta:
       model = Author
       fields = ['name', 'email']

We can add individual fields with various requirements to enforce various rules:

serializers.py

class AuthorSerializer(serializers.ModelSerializer):
   name = serializers.CharField(max_length=255, required=True)
   email = serializers.EmailField(read_only=True,
                               validators=[UniqueValidator(queryset=Author.objects.all())])


   class Meta:
       model = Author
       fields = ['name', 'email']

Now name is required field, email is read only and unique.

Just like with forms, before saving a serializer, you should call is_valid() on it. If there are validation errors they will show up in serializer.errors as a dictionary.

serializer.errors
# {'email': ['Enter a valid e-mail address.'], 'created': ['This field is required.']}

When writing your serializer, you can do field level and object level validation. Field-level validation can be done like this:

class AuthorSerializer(serializers.ModelSerializer):

   def validate_email(self, value):
       if value.find('@mail.com') >= 0:
           raise serializers.ValidationError("The author can't have a mail.com address")
       return value

   class Meta:
       model = Author
       fields = ['name', 'email']

Object level validation can be done like this:

class AuthorSerializer(serializers.ModelSerializer):

   def validate(self, data):
       if data['email'].find(data['name']) >= 0:
           raise serializers.ValidationError("The author's email can't contain his name")

       return data

   class Meta:
       model = Author
       fields = ['name', 'email']

Authentication and authorization

Authentication

The default authentication scheme can be set globally with DEFAULT_AUTHENTICATION_CLASSES setting:

settings.py

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    ]
}

Or on a per view basis with authentication_classes:

views.py

class BookSearch(ListAPIView):
   queryset = Book.objects.all()
   serializer_class = BookSerializer
   authentication_classes = [SessionAuthentication, BasicAuthentication]
   permission_classes = [IsAuthenticated]


@api_view(['GET'])
@authentication_classes([SessionAuthentication, BasicAuthentication])
@permission_classes([IsAuthenticated])
def book_search(request):
	pass

There are four types of authentication schemes:

  • BasicAuthentication: Where the client sends the username and password in the request, not really suitable for production environments
  • TokenAuthentication: When the client authenticates, receives a token, and that token is then used to authenticate the client. This is good for separate clients and servers
  • SessionAuthentication: This is the standard django authentication scheme, where there is a server side session and the client passes the session id to the server
  • RemoteUserAuthentication: This scheme has the web server deal with authentication

For APIs, especially where the client is a separate application from the server, token authentication is the most interesting. To do token authentication with Django REST framework, you have to add rest_framework.authtoken to your INSTALLED_APPS.

settings.py

INSTALLED_APPS = [
    ...
    'rest_framework.authtoken'
]

Run migrations after adding this app.

In your application you will have to create a token for the user after they authenticate with a username and password, you can do it with this call:

views.py

token = Token.objects.create(user=...)

And then pass that token back to the client. The client will then include that Token in the HTTP headers like so:

Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b

REST framework already has a built-in view for obtaining an auth token, obtain_auth_token. If the defaults work for you, you can wire this view in urls, and don’t have to write any of your own logic.

urls.py

from rest_framework.authtoken import views
urlpatterns += [
    path('api-token-auth/', views.obtain_auth_token)
]

Authorization

For authorization you can also set global and view level policies. For global you would set it in settings.py:

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated', # Allow only authenticated requests
        # 'rest_framework.permissions.AllowAny', # Allow anyone
    ]
}

And for views, you would use permission_classes:

views.py

class BookSearch(ListAPIView):
   queryset = Book.objects.all()
   serializer_class = BookSerializer
   authentication_classes = [SessionAuthentication, BasicAuthentication]
   permission_classes = [IsAuthenticated]


@api_view(['GET'])
@authentication_classes([SessionAuthentication, BasicAuthentication])
@permission_classes([IsAuthenticated])
def book_search(request):
	Pass

You can have a view that’s authenticated or read only like this:

permission_classes = [IsAuthenticated|ReadOnly]

For a full list of permissions take a look at the API Reference

You can also create custom permissions by extending permissions.BasePermission:

class CustomPermission(permissions.BasePermission):
   def has_permission(self, request, view):
       ip_addr = request.META['REMOTE_ADDR']
       blocked = Blocklist.objects.filter(ip_addr=ip_addr).exists()
       return not blocked

And then include it in your permission_classes:

views.py

class BookSearch(ListAPIView):
   queryset = Book.objects.all()
   serializer_class = BookSerializer
   authentication_classes = [SessionAuthentication, BasicAuthentication]
   permission_classes = [CustomPermission]

And finally, Django REST framework supports object level permissioning by calling check_object_permissions, it will then determine if the user has permissions on the Model itself.

Backend filtering

Filtering

Most of the time you want to filter the queryset that comes back. If you are using GenericAPIView, the simplest way to do that is to override get_queryset(). One common requirement is to filter out the queryset by the current user, here is how you would do that:

views.py

class UserBookList(ListAPIView):
   serializer_class = UserBookSerializer

   def get_queryset(self):
       user = self.request.user
       return UserBook.objects.filter(user=user())

Our BookSearch above, actually used query parameters (query_param) to do the filtration by overriding get_queryset().

Generic filtering

Django REST framework also lets you configure a generic filtering system that will use fields on the models to determine what to filter.

To get that set up, you need to first install django-filter

pip install django-filter

Then add django_filters to INSTALLED_APPS

INSTALLED_APPS = [
    ...
    'django_filters',
    ...

Then you can either add backend filters globally in your settings.py file

REST_FRAMEWORK = {
    'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
}

Or add it to individual class views

views.py

from django_filters.rest_framework import DjangoFilterBackend

class BookSearch(generics.ListAPIView):
    ...
    filter_backends = [DjangoFilterBackend]

Let’s modify our BookSearch example above with Django Backend Filtering. What we had above:

views.py

class BookSearch(APIView):

   def get(self, request, format=None):
       author = self.request.query_params.get('author', None)
       title = self.request.query_params.get('title', None)

       queryset = Book.objects.all()
       if author:
           queryset = queryset.filter(author__name__contains=author)
       if title:
           queryset = queryset.filter(title__contains=title)

       serializer = BookSerializer(queryset, many=True)
       return Response(serializer.data)

Let’s modify it to use Backend Filtering:

class BookSearch(ListAPIView):
   queryset = Book.objects.all()
   serializer_class = BookSerializer
   filter_backends = [DjangoFilterBackend]
   filterset_fields = ['author__name', 'title']

This gets us exact matches though, which isn’t exactly the same functionality. We can change it to SearchFilter filter to get us the same functionality as above:

class BookSearch(ListAPIView):
   queryset = Book.objects.all()
   serializer_class = BookSerializer
   filter_backends = [SearchFilter]
   filterset_fields = ['author__name', 'title']

Now we just call it with

http://127.0.0.1:8000/book/book-search-view?search=harper

And get back all the books that have “harper” in the title or author’s name.

Generic ordering

We can also order against specific fields like so:

views.py

class BookSearch(ListAPIView):
   queryset = Book.objects.all()
   serializer_class = BookSerializer
   filter_backends = [OrderingFilter]
   ordering_fields = ['title', 'author__name']

Letting someone order with a query like this:

http://127.0.0.1:8000/book/book-search-view?ordering=-title

Note, that if you don’t specify ordering_fields or set it to ‘__all__’ it will potentially expose fields that you don’t want someone to filter by, like passwords.

Pagination, caching, throttling, versioning and documentation

Pagination

Pagination can be set globally and per view level. To set it globally add it to the settings file:

settings.py

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
    'PAGE_SIZE': 25

To set it on a view you can use the pagination_class attribute. You can create a custom pagination class by extending PageNumberPagination:

class StandardResultsSetPagination(PageNumberPagination):
    page_size = 100
    page_size_query_param = 'page_size'
    max_page_size = 1000

Caching

Caching is done by Django with method_decorator, cache_page and vary_on_cookie:

# Cache requested url for 2 hours
@method_decorator(cache_page(60*60*2), name='dispatch')
class BookSearch(ListAPIView):
   queryset = Book.objects.all()
   serializer_class = BookSerializer
   authentication_classes = [SessionAuthentication, BasicAuthentication]
   permission_classes = [CustomPermission]

vary_on_cookie can be used to cache the request for a user

Throttling

You can throttle (control the rate of requests) your API. To do it at a global state add these settings:

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle'
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/day',
        'user': '1000/day'
    }
}

Or set it at the view level with throttle_classes, for example:

views.py

class BookSearch(ListAPIView):
   queryset = Book.objects.all()
   serializer_class = BookSerializer
   authentication_classes = [SessionAuthentication, BasicAuthentication]
   throttle_classes = [UserRateThrottle]

For throttling, clients are by default identified with the X-Forwarded-For HTTP header, and if not present then by the REMOTE_ADDR HTTP header.

Versioning

By default versioning is not enabled. You can set up versioning by adding this to your settings file:

REST_FRAMEWORK = {
    'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning'
}

If DEFAULT_VERSIONING_CLASS is None, which is the default, then request.version will be None.

It’s possible to set versioning on a specific view with versioning_class, but usually versioning is set globally.

You can control versioning with the following settings:

  • DEFAULT_VERSION: sets the version when no version is provided, defaults to None. default_version attribute on the view.
  • ALLOWED_VERSIONS: specifies the set of versions that are allowed, if not in the set it will raise an error. allowed_versions attribute on the view.
  • VERSION_PARAM: The parameter to use for versioning, defaults to version. version_param attribute on the view.

You have a few options for versioning classes:

  • AcceptHeaderVersioning: Version is passed in the Accept header
  • URLPathVersioning: Version is passed as part of the url structure
  • NamespaceVersioning: Similar to URLPathVersioning but uses url namespacing in Django. Take a look at how it differs from URLPathVersioning here
  • HostNameVersioning: Uses the hostname url to determine the version
  • QueryParameterVersioning: Uses a query parameter to determine the version

You can also create your own custom versioning scheme

How you deal with different versions in your code is up to you. One possible example is to just use different serializers:

def get_serializer_class(self):
    if self.request.version == 'v1':
        return BookSerializerV1
    return BookSerializerV2

Documentation

To generate documentation for your API you will have to generate an OpenAPI Schema. You will install pyyaml and uritemplate packages.

pip install pyyaml uritemplate

You can dynamically generate a schema with get_schema_view(), like so:

urlpatterns = [
    # ...
    # Use the `get_schema_view()` helper to add a `SchemaView` to project URLs.
    #   * `title` and `description` parameters are passed to `SchemaGenerator`.
    #   * Provide view name for use with `reverse()`.
    path('openapi', get_schema_view(
        title="Your Project",
        description="API for all things …",
        version="1.0.0"
    ), name='openapi-schema'),
    # ...

Going to http://127.0.0.1:8000/openapi should show you the full OpenAPI schema of your API.

You can customize how your schema is generated, to learn how to do that, check out the official documentation

You can set descriptions on your views that will then be shown in both the browsable API and in the generated schema. The description uses markdown. For example:

@api_view(['GET'])
def book_search(request):
   """
   The book search
   """
   …

For viewset and view based classes you have to describe the methods and actions:

class BookViewset(viewsets.ModelViewSet):
   """
   retrieve:
   Return the given book

   create:
   Create a new book.
   """
   queryset = Book.objects.all()
   serializer_class = BookSerializer

Final thoughts

As we saw above, Django REST Framework is an incredibly complex and all encompassing REST API framework. I tried to distill some of the main concepts into this guide, so that you can start working with the framework. The reality is that there is still a lot that I wasn’t able to cover in this guide as far as the types of customizations you can make, the options you can set, and the various classes for every type of scenario. If you ever get stuck, you can always reference the API Guide at https://www.django-rest-framework.org/.