Official Django REST Framework Tutorial - A Beginners Guide

Updated

Table of Contents

This is a beginner-friendly guide to the official Django Rest Framework tutorial that works for both Windows and macOS. If you have struggled to complete the official tutorial on your own, consider this guide a good place to start instead.

The final code is exactly the same and is available on GitHub. However I provide more detailed explanations of each step, use the Django admin rather than the Django shell for populating the models, emphasize class-based views over function-based views, and so on.

After you complete this tutorial, the official tutorial should make more sense and help you fully take advantage of the awesomeness that is the Django REST Framework.

NOTE: If you'd like to learn even more about APIs in Django, I've written an entire book on the subject, Django for APIs. The first few chapters are available for free.

Initial Set Up

You will need to have the latest version of Python installed on your computer. If you need help with this, go to Chapter 1: Set Up of the Django for APIs book for a step-by-step guide to configuring your computer for Django development.

Once that's done, create a dedicated directory for our code and set up a new Python virtual environment. This can live anywhere on your computer but in an easily accessible location. One option is the Desktop, whether you are on a Windows or Mac computer. That's what we'll do here.

Enter the following commands in your terminal:

# Windows
$ cd onedrive\desktop\code
$ mkdir drf
$ cd drf
$ python -m venv .venv
$ .venv\Scripts\Activate.ps1
(.venv) $ 

# macOS
$ cd desktop/desktop/code
$ mkdir drf
$ cd drf
$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $ 

Then, within the new directory, install django, djangorestframework, and pygments (which is used for code highlighting). Once all three packages are installed, make sure to activate the virtual environment shell.

(.venv) $ python -m pip install django~=5.0.0
(.venv) $ python -m pip install djangorestframework==3.15.2
(.venv) $ python -m pip install pygments==2.18.0

If you use the command pip freeze to output the virtual environment, you should see the following:

asgiref==3.8.1
black==22.12.0
click==8.1.3
Django==5.0.7
djangorestframework==3.15.2
mypy-extensions==0.4.3
pathspec==0.10.3
platformdirs==2.6.2
Pygments==2.18.0
pytz==2022.7.1
sqlparse==0.4.3

Our new project is called tutorial, and within it, there is an app called snippets for our web API. Adding a period . at the end of the command is optional but recommended as otherwise Django will create an additional directory with the startproject command.

(.venv) $ django-admin startproject tutorial .
(.venv) $ python manage.py startapp snippets

Now add the snippets app and rest_framework to the INSTALLED_APPS config in our tutorial/settings.py file. It's not enough to simply create new apps; they must also be explicitly added to INSTALLED_APPS.

# tutorial/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",  # new
    "snippets",  # new
]

Models

The model is a good place to start any new project. In the snippets/models.py file, create a new model called Snippet.

# snippets/models.py
from django.db import models
from pygments.lexers import get_all_lexers
from pygments.styles import get_all_styles

LEXERS = [item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
STYLE_CHOICES = sorted([(item, item) for item in get_all_styles()])


class Snippet(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=100, blank=True, default="")
    code = models.TextField()
    linenos = models.BooleanField(default=False)
    language = models.CharField(
        choices=LANGUAGE_CHOICES, default="python", max_length=100
    )
    style = models.CharField(choices=STYLE_CHOICES, default="friendly", max_length=100)

    class Meta:
        ordering = ["created"]

    def __str__(self):
        return self.title

Then create an initial migration file and sync the database for the first time.

(.venv) $ python manage.py makemigrations snippets
(.venv) $ python manage.py migrate

We need to add some data to our model to make it "real" now. At this point, the official tutorial goes on a lengthy tangent into the Django shell. However, to beginners—and even professional programmers—the graphical Django admin is often a more intuitive approach. That's what we'll use here.

But first, we need to update snippets/admin.py so the app will actually appear! Just as with the INSTALLED_APPS setting, apps must be explicitly added to the admin.

# snippets/admin.py
from django.contrib import admin
from .models import Snippet

admin.site.register(Snippet)

Now, create a superuser account to log in. Follow the prompts for setting a username, email, and password. I've used admin, ' [email protected], andtestpass123`.

(.venv) $ python manage.py createsuperuser

And start our local web server for the first time.

(.venv) $ python manage.py runserver

Navigate over to the Django homepage at http://127.0.0.1:8000/ to confirm everything is working.

Django welcome page

Then switch over to the Django admin at http://127.0.0.1:8000/admin/. Log in with your superuser account.

Django admin homepage

Click on the "+ Add" button next to Snippets. And create two new snippets.

Django snippet

Django snippet

Click the "Save" button in the lower right for each snippet. Both will be visible on the main Snippets page.

Django snippet page

Serialization

A Serializer transforms model instances into JSON. This is the real "magic" that Django Rest Framework provides for us. Consider that the end result of a traditional website is a page of HTML, CSS, and content from the database. But an API doesn't care about that: it's only raw data at endpoints, which means JSON and accompanying HTTP verbs that tell the API what actions can be taken (more on this shortly).

Create a new snippets/serializers.py file and update it as follows. We can extend DRF's ModelSerializer to create a SnippetSerializer class that uses our model and outputs the table fields.

# snippets/serializers
from rest_framework import serializers
from .models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES


class SnippetSerializer(serializers.ModelSerializer):
    class Meta:
        model = Snippet
        fields = (
            "id",
            "title",
            "code",
            "linenos",
            "language",
            "style",
        )

Next, we need a view that handles the logic of combining a model, serializer, and, eventually, URL together. Just as traditional Django ships with several class-based generic views to handle common functionality, so too does Django Rest Framework have its own set of powerful class-based generic views we can use.

Specifically we will use ListCreateAPIView to create a read-write endpoint that lists all available Snippet instances and then RetrieveUpdateDestroyAPIView for a read-write-delete endpoint for each individual Snippet.

# snippets/views.py
from rest_framework import generics
from .models import Snippet
from .serializers import SnippetSerializer


class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer


class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer

URLs

The final step is to configure our URLs. In the topmost, project-level tutorial/urls.py file, add include as an import for the snippets app URLs which will appear at the empty string ''.

# tutorial/urls.py
from django.contrib import admin
from django.urls import include, path  # new

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("snippets.urls")),  # new
]

Then create a snippets/urls.py file and add the following code.

# snippets/urls.py
from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views

urlpatterns = [
    path("snippets/", views.SnippetList.as_view()),
    path("snippets/<int:pk>/", views.SnippetDetail.as_view()),
]

urlpatterns = format_suffix_patterns(urlpatterns)

Including format_suffix_patterns is an optional choice that provides a simple, DRY way to refer to a specific file format for a URL endpoint. It means our API will be able to handle URls such as http://example.com/api/items/4.json rather than just http://example.com/api/items/4.

Browsable API

Django Rest Framework ships with a browsable API that we can now use. Make sure the local server is running.

(.venv) $ python manage.py runserver

Navigate to the Snippets List endpoint at http://127.0.0.1:8000/snippets/.

API Snippets List

We can also go to the detail view for each snippet. For example, the first snippet is at http://127.0.0.1:8000/snippets/1/.

API Snippets Detail

As a reminder, the id is automatically set by Django on each database entry.

Requests and Responses

Currently our API has no restrictions on who can edit or delete code snippets. In this section we will make sure that:

  • Code snippets are always associated with a creator
  • Only authenticated users may create snippets
  • Only the creator of a snippet may update or delete it
  • Unauthenticated requests should have full read-only access

Adding Information to Our Model

First up let's add two fields to our existing Snippet model class: owner which will represent the user who created the code snippet and highlighted to store the highlighted HTML representation of the code.

We also want to ensure that when the model is saved, we use the pygments code highlighting library to populate our highlighted field. So we'll need some additional imports as well as a .save() method.

# snippets/models.py
from django.db import models
from pygments import highlight  # new
from pygments.formatters.html import HtmlFormatter  # new
from pygments.lexers import get_all_lexers, get_lexer_by_name  # new
from pygments.styles import get_all_styles

LEXERS = [item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
STYLE_CHOICES = sorted((item, item) for item in get_all_styles())


class Snippet(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=100, blank=True, default="")
    code = models.TextField()
    linenos = models.BooleanField(default=False)
    language = models.CharField(
        choices=LANGUAGE_CHOICES, default="python", max_length=100
    )
    style = models.CharField(choices=STYLE_CHOICES, default="friendly", max_length=100)
    owner = models.ForeignKey(
        "auth.User", related_name="snippets", on_delete=models.CASCADE
    )  # new
    highlighted = models.TextField()  # new

    class Meta:
        ordering = ("created",)

    def save(self, *args, **kwargs):  # new
        """
        Use the `pygments` library to create a highlighted HTML
        representation of the code snippet.
        """
        lexer = get_lexer_by_name(self.language)
        linenos = "table" if self.linenos else False
        options = {"title": self.title} if self.title else {}
        formatter = HtmlFormatter(
            style=self.style, linenos=linenos, full=True, **options
        )
        self.highlighted = highlight(self.code, lexer, formatter)
        super(Snippet, self).save(*args, **kwargs)

    def __str__(self):
        return self.title

Normally we would create a migration and sync it to update our database tables. However since we have added an owner here and have existing content, it's simpler to just delete the database and start again. Make sure you have stopped the local server with Control+c.

(.venv) $ rm db.sqlite3
(.venv) $ rm -r snippets/migrations
(.venv) $ python manage.py makemigrations snippets
(.venv) $ python manage.py migrate

Re-create our steps from earlier to create a new superuser account. We'll want a second superuser account which is simplest to setup from the command line too. So run createsuperuser twice. I've called my users admin and testuser. Then start up the local server.

(.venv) $ python manage.py createsuperuser
(.venv) $ python manage.py createsuperuser
(.venv) $ python manage.py runserver

Go back into the Django admin at http://127.0.0.1:8000/admin/ and login with the admin account.

If you click on the Users link you will be redirected to the Users page which should show both users.

Once complete, you should see the two users on the Users page.

Admin Users Page

We need to recreate our snippets too since the initial database was just destroyed. Create a new snippet and specify the Owner as one of our users. I've chosen testuser here.

But there's a problem when we try to "Save".

Add Snippet Error

We are getting a ValidationError here. In the official tutorial the Django shell is used to input data, but we are using the admin here. So the existing code doesn't work as is. Recall that the highlighted field is automatically set by our custom save() method on the model, but the admin doesn't know this. It expects us to enter in a value here. To solve the problem update our admin.py file and set highlighted as a read-only field.

# snippets/admin.py
from django.contrib import admin
from .models import Snippet


class SnippetAdmin(admin.ModelAdmin):
    readonly_fields = ("highlighted",)


admin.site.register(Snippet, SnippetAdmin)

Try clicking the "Save" button again. It should work.

The final step is to click the Log out link in the upper right corner of the admin page.

Admin Logout Link

We will shortly be adding permissions to our API so that only authenticated (logged-in) users have access.

Admin Logged Out

Adding Endpoints to Our User Models

Now that we have some users to work with, let's add endpoints for them to our API. Add a new UserSerializer class to the snippets/serializers.py file.

# snippets/serializers.py
from django.contrib.auth.models import User
from rest_framework import serializers
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES


class SnippetSerializer(serializers.ModelSerializer):
    class Meta:
        model = Snippet
        fields = (
            "id",
            "title",
            "code",
            "linenos",
            "language",
            "style",
        )


class UserSerializer(serializers.ModelSerializer):
    snippets = serializers.PrimaryKeyRelatedField(
        many=True, queryset=Snippet.objects.all()
    )

    class Meta:
        model = User
        fields = ("id", "username", "snippets")

Because snippets is a reverse relationship on the default Django User model, it will not be included by default using the ModelSerializer class, we need to add an explicit field for it.

We also need to add two new read-only views for a list of all users and a detail view of individual users. Note that we use the generic class-based RetrieveAPIView for the read-only detail view. And that we import both User and UserSerializer at the top.

# snippets/views.py
from django.contrib.auth.models import User  # new
from rest_framework import generics

from .models import Snippet
from .serializers import SnippetSerializer, UserSerializer  # new


class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer


class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer


class UserList(generics.ListAPIView):  # new
    queryset = User.objects.all()
    serializer_class = UserSerializer


class UserDetail(generics.RetrieveAPIView):  # new
    queryset = User.objects.all()
    serializer_class = UserSerializer

Finally we need to add the new views to the API by configuring their URL routes. Add the following pattern to snippets/urls.py.

# snippets/urls.py
from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views

urlpatterns = [
    path("snippets/", views.SnippetList.as_view()),
    path("snippets/<int:pk>/", views.SnippetDetail.as_view()),
    path("users/", views.UserList.as_view()),  # new
    path("users/<int:pk>/", views.UserDetail.as_view()),  # new
]

urlpatterns = format_suffix_patterns(urlpatterns)

Associating Snippets with Users

Currently there is no way to automatically associate the logged-in user that created a snippet with the snippet instance. We can set this automatically by overriding .perform_create() method on our snippet views that lets us modify how an instance is saved.

Add the following method to our existing SnippetList view class.

# snippets/views.py
class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer

    def perform_create(self, serializer):  # new
        serializer.save(owner=self.request.user)

Updating Our Serializer

Now that snippets are associated with the user that created them, let's update SnippetSerializer with an owner to reflect that. Make sure to also include owner in the list of fields too.

# snippets/serializers.py
from django.contrib.auth.models import User
from rest_framework import serializers
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES


class SnippetSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source="owner.username")  # new

    class Meta:
        model = Snippet
        fields = (
            "id",
            "title",
            "code",
            "linenos",
            "language",
            "style",
            "owner",
        )  # new


class UserSerializer(serializers.ModelSerializer):
    snippets = serializers.PrimaryKeyRelatedField(
        many=True, queryset=Snippet.objects.all()
    )

    class Meta:
        model = User
        fields = ("id", "username", "snippets")

The source argument used here controls which attribute is used to populate a field and can point to any attribute on the serialized instance. Also note that we're using ReadOnlyField which is always read-only; it can not be used for updating model instances when they are serialized. We could have also used CharField(read_only=True) here to accomplish the same thing.

Adding Required Permissions To Views

Now that code snippets are associated with users, we want to make sure that only authenticated users are able to create, update, and delete code snippets.

Django Rest Framework ships with a number of permission classes we could use to restrict access to a given view. Here we will use IsAuthenticatedOrReadOnly to ensure that authenticated requests have read-write access and unauthenticated requests only have read-only access.

# snippets/views.py
from django.contrib.auth.models import User
from rest_framework import generics, permissions  # new

from .models import Snippet
from .serializers import SnippetSerializer, UserSerializer


class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)  # new

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)


class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)  # new


class UserList(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer


class UserDetail(generics.RetrieveAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

Adding Log In To The Browsable API

Now navigate to our browsable API at http://127.0.0.1:8000/snippets/.

API List Logged Out

Since we are logged out, notice that you are no longer able to create new code snippets. In order to do so you need to be logged in as a user.

We can add a login view to the browsable API by editing the URLconf in our project-level tutorial/urls.py file. Add rest_framework.urls to the route api-auth/.

# tutorial/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api-auth/", include("rest_framework.urls")),  # new
    path("", include("snippets.urls")),
]

Note that the actual route used does not matter. Instead of api-auth/ we could also have used something-else/. The important thing is that rest_framework.urls was included.

Now open up the browser again and refresh the page. You will see a Log in link in the top right of the page.

API Login Link

Log in with your testuser account. Then navigate to our http://127.0.0.1:8000/users/ endpoint and notice that snipped ids are associated with each user, as desired.

API Users List

We only have one snippet, made with our testuser account and containing the primary id of 1. If we added additional snippets for each user, they'd appear here as well. So things are working.

Object Level Permissions

Really what we'd like is for all code snippets to be visible to anyone, but only the user that created a code snippet can update or delete it.

Django Rest Framework gives us several options for setting permissions: at a project-level, view level, or object level. In this case we will implement the last option and create a custom permission we can add to our SnippetDetail view class.

Create a new snippets/permissions.py file and then add the following code which extends Django Rest Framework's existing permissions classes.

# snippets/permissions.py
from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow owners of an object to edit it.
    """

    def has_object_permission(self, request, view, obj):
        # Read permissions are allowed to any request,
        # so we'll always allow GET, HEAD or OPTIONS requests.
        if request.method in permissions.SAFE_METHODS:
            return True

        # Write permissions are only allowed to the owner of the snippet.
        return obj.owner == request.user

Next add the new custom permission to SnippetDetail by importing it at the top and including it in permission_classes.

# snippets/views.py
from django.contrib.auth.models import User
from rest_framework import generics, permissions

from .models import Snippet
from .permissions import IsOwnerOrReadOnly # new
from .serializers import SnippetSerializer, UserSerializer


class SnippetList(generics.ListCreateAPIView):
    ...

class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = (
        permissions.IsAuthenticatedOrReadOnly,
        IsOwnerOrReadOnly,
    )  # new

...

If you open the browser again to http://127.0.0.1:8000/snippets/1/ you will find that the 'DELETE' and 'PUT' actions appear on the snippet instance endpoint because we're logged in as testuser, the owner of the snippet.

API Detail Testuser

Now log out and log in with the admin account. The DELETE and PUT options are not available. Good, as expected.

API Detail Admin

Root API Endpoint

Currently there are endpoints for snippets and users, but we don't have a single entry point to our API. To create one, we'll use a regular function-based view and Django REST Framework's built-in @api_view decorator.

In snippets/views.py import api_view, Response, and reverse. Then use @api_view to set a GET for api_root.

# snippets/views.py
from django.contrib.auth.models import User
from rest_framework import generics, permissions
from rest_framework.decorators import api_view  # new
from rest_framework.response import Response  # new
from rest_framework.reverse import reverse  # new

from .models import Snippet
from .permissions import IsOwnerOrReadOnly
from .serializers import SnippetSerializer, UserSerializer


@api_view(["GET"])  # new
def api_root(request, format=None):
    return Response(
        {
            "users": reverse("user-list", request=request, format=format),
            "snippets": reverse("snippet-list", request=request, format=format),
        }
    )

class SnippetList(generics.ListCreateAPIView):
    ...
...

Next we need to add a URL at the empty string "" for api_root. And since we're using reverse we also must add named urls to each existing view.

# snippets/urls.py
from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views

urlpatterns = [
    path("snippets/", views.SnippetList.as_view(), name="snippet-list"),
    path("snippets/<int:pk>/", views.SnippetDetail.as_view(), name="snippet-detail"),
    path("users/", views.UserList.as_view(), name="user-list"),
    path("users/<int:pk>/", views.UserDetail.as_view(), name="user-detail"),
    path("", views.api_root),
]

urlpatterns = format_suffix_patterns(urlpatterns)

Now navigate to http://127.0.0.1:8000/ to see our new API Root page.

API Root

It lists both users and snippets as well as their respective API endpoints which can be clicked on.

Highlighted Snippets Endpoint

The other obvious thing that's still missing from our pastebin API is the code highlighting endpoints.

Unlike all our other API endpoints, we don't want to use JSON, but instead just present an HTML representation. REST framework has two HTML renderers: one for dealing with HTML rendered using templates and one for pre-rendered HTML (which is our case here).

Also, there's no existing generic view that will work so we'll need to create our own .get() method.

In your snippets/views.py import renderers at the top and then create a new class for SnippetHighlight.

# snippets/views.py
from django.contrib.auth.models import User
from rest_framework import generics, permissions, renderers  # new
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse

from .models import Snippet
from .permissions import IsOwnerOrReadOnly
from .serializers import SnippetSerializer, UserSerializer


class SnippetHighlight(generics.GenericAPIView):  # new
    queryset = Snippet.objects.all()
    renderer_classes = (renderers.StaticHTMLRenderer,)

    def get(self, request, *args, **kwargs):
        snippet = self.get_object()
        return Response(snippet.highlighted)


@api_view(["GET"]) 
...

Add the new view to the urls file. Make sure to include the name snippet-highlight!

# snippets/urls.py
from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views

urlpatterns = [
    path("snippets/", views.SnippetList.as_view(), name="snippet-list"),
    path("snippets/<int:pk>/", views.SnippetDetail.as_view(), name="snippet-detail"),
    path(
        "snippets/<int:pk>/highlight/",
        views.SnippetHighlight.as_view(),
        name="snippet-highlight",
    ),  # new
    path("users/", views.UserList.as_view(), name="user-list"),
    path("users/<int:pk>/", views.UserDetail.as_view(), name="user-detail"),
    path("", views.api_root),
]

urlpatterns = format_suffix_patterns(urlpatterns)

We only have one snippet in our database so the highlight will be located at http://127.0.0.1:8000/snippets/1/highlight/.

Snippet Highlight

Hyperlinking our API

One of the more challenging aspects of web API design is dealing with the relationships between entities. We could use primary key, hyperlinks, slugs, strings, nesting, or a custom representation.

REST framework supports all of these styles but here we'll use a hyperlinked style between entities. In order to do so, we'll modify our serializers to extend HyperlinkedModelSerializer instead of the existing ModelSerializer.

The HyperlinkedModelSerializer has the following differences from ModelSerializer:

  • It does not include the id field by default.
  • It includes a url field, using HyperlinkedIdentityField.
  • Relationships use HyperlinkedRelatedField, instead of PrimaryKeyRelatedField.

Let's rewrite our existing serializers to use hyperlinks.

# snippets/serializers.py
from django.contrib.auth.models import User
from rest_framework import serializers
from snippets.models import Snippet, LANGUAGE_CHOICES, STYLE_CHOICES


class SnippetSerializer(serializers.HyperlinkedModelSerializer):  # new
    owner = serializers.ReadOnlyField(source="owner.username")
    highlight = serializers.HyperlinkedIdentityField(  # new
        view_name="snippet-highlight", format="html"
    )

    class Meta:
        model = Snippet
        fields = (
            "url",
            "id",
            "highlight",
            "title",
            "code",
            "linenos",
            "language",
            "style",
            "owner",
        )  # new


class UserSerializer(serializers.HyperlinkedModelSerializer):  # new
    snippets = serializers.HyperlinkedRelatedField(  # new
        many=True, view_name="snippet-detail", read_only=True
    )

    class Meta:
        model = User
        fields = ("url", "id", "username", "snippets")  # new

Aside from swapping in HyperlinkedModelSerializer there is a new highlight field for snippets that points to the snippet-highlight url pattern, instead of the snippet-detail url pattern.

Also for the fields we add url to both and highlight to the snippet serializer.

API Hyperlinked Snippet

API Hyperlinked User

Pagination

Currently we only have the one code snippet but as others are added it makes sense to limit the number of snippets displayed per API endpoint. Let's paginate the results so that API clients can step through each of the individual pages.

REST Framework ships with a number of default settings which can be easily customized. We'll set a DEFAULT_PAGINATION_CLASS and PAGE_SIZE to 10 although we could easily customize things further as desired.

At the bottom of the tutorial/settings.py file add the following:

# tutorial/settings.py
REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 10,
}

At this point it's possible to click around the entire API just via links. Success!

Viewsets and Routers

Section 6 of the official tutorial has us switch over from views and URLs to viewsets and routers. This is an optional choice that is, in my opinion, better suited to larger API projects and for developers already comfortable with REST framework. Since neither applies here we will not update our code. The resulting API endpoints will still be exactly the same!

Conclusion

With an incredibly small amount of code, we've now got a complete pastebin Web API, which is fully web browsable, includes a schema-driven client library, and comes complete with authentication, per-object permissions, and multiple renderer formats.

We've walked through each step of the design process, and seen how if we need to customize anything we can gradually work our way down to simply using regular Django views.

You can review the final tutorial code on GitHub.

And if you'd like to learn even more about APIs in Django, check out my book Django for APIs.