This is a beginner-friendly guide to the official Django Rest Framework tutorial. We will build the exact same code-highlighting web API but include detailed explanations and complete code samples along the way. However we’ll skip sections on the Django shell, function-based views, and other areas that are hepful but also typically confusing for newcomers.

If you have struggled to complete the official tutorial on your own, consider this guide a good place to start instead.

Complete source code is available on Github.

NOTE: This tutorial is powered by the book REST APIs with Django which covers how to build and test multiple web APIs with Django and Django REST Framework.

Initial Setup

Go to Django For Beginners for a step-by-step guide to properly installing Python 3.6 and pipenv on Mac, Windows, or Linux computer.

Once that’s done, create a new virtual environment for our project that contains django, djangorestframework, and pygments which is used for code highlighting. Then make sure to activate the virtual environment in a new shell.

$ pipenv install django djangorestframework pygments
$ pipenv shell
(env) $

Note: The name of your virtual environment is listed as (env) here for convenience. In practice, the name will be some variation of your current directory name. To exit the virtual environment at any time, type exit.

Let’s create a new project to work with called tutorial and an app within it called snippets for our web API.

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

Add the snippets app and rest_framework to the INSTALLED_APPS config in our tutorial/settings.py file.

# 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.

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

We need to add some data into our model to make it “real” now. The official tutorial uses the Django shell for this however the Django admin is a more intuitive, visual approach for many developers. We’ll use that instead.

First however we need to update snippets/admin.py so the app will appear in the admin.

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

admin.site.register(Snippet)

Then create a superuser account we can login with. Follow all the prompts that follow the command below for setting a username, email, and password.

(env) $ python manage.py createsuperuser

Now start our local web server for the first time.

(env) $ python manage.py runserver

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

Django homepage

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 the real “magic” that Django Rest Framework provides for us since ultimately a web API endpoint returns JSON and available HTTP verbs.

Create a snippets/serializers.py file.

(env) $ touch snippets/serializers.py

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 Django Rest Framework has its own set of powerful class-based generic views we can use.

Specifically we will use ListCreateAPIView to create a read-only endpoint that lists all available Snippet instances and then RetrieveUpdateDestroyAPIView for a detail view of individual snippets which supports CRUD-like functionality.

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


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

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('snippets.urls')),
]

Then create a urls.py file with our snippets app.

(env) $ touch snippets/urls.py

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.

(env) $ 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.

(env) $ rm -f db.sqlite3
(env) $ rm -r snippets/migrations
(env) $ python manage.py makemigrations snippets
(env) $ 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.

(env) $ python manage.py createsuperuser
(env) $ python manage.py createsuperuser
(env) $ 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 our 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

...

Adding login 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('', include('snippets.urls')),
    path('api-auth/', include('rest_framework.urls')), # new
]

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/ 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 permissions.py file.

(env) $ touch snippets/permissions.py

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 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)
    })

...

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/views.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)

...

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!

Schemas

A schema is a machine-readable document that describes the available API endpoints, their URLS, and what operations they support.

Schemas can be a useful tool for auto-generated documentation, and can also be used to drive dynamic client libraries that can interact with the API.

In order to provide schema support REST framework uses Core API which can be quickly added to our project.

First stop the local server Control + c and install it.

(env) pipenv install coreapi

Then add a URL route for it. Import get_schema_view at the top, create a schema_view, and then a route at schema/.

# tutorial/urls.py
from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from rest_framework.schemas import get_schema_view # new
from snippets import views

schema_view = get_schema_view(title='Pastebin API') # new

urlpatterns = [
    path('schema/', schema_view), # new
    ...
]

urlpatterns = format_suffix_patterns(urlpatterns)

Now start up the local server again with python manage.py runserver and navigate to http://127.0.0.1:8000/schema/.

API Schema

Voila! An auto-generated schema of our API.

Command line client

Now that our API is exposing a schema endpoint, we can use a dynamic client library to interact with the API. To demonstrate this, let’s use the Core API command line client.

We need two command line consoles at this point. Our existing one should still be running the local server. But we need a second, new console to execute our commands. So open a second command line console, navigate to our current directory, and then make sure to activate the virtual environment with pipenv shell so that you see (env) or the equivalent on the command line.

Now install coreapi-cli in this new, second console.

(env) $ pipenv install coreapi-cli

To make sure it is working, type coreapi and hit Return. You should see the following.

(env) $ coreapi
Usage: coreapi [OPTIONS] COMMAND [ARGS]...
  Command line client for interacting with CoreAPI services.

  Visit http://www.coreapi.org for more information.
Options:
  --version  Display the package version number.  --help     Show this message and exit.

Commands:  action       Interact with the active document.
  bookmarks    Add, remove and show bookmarks.
  clear        Clear the active document and other state.  codecs       Manage the installed codecs.
  credentials  Configure request credentials.
  describe     Display description for link at given PATH.  dump         Dump a document to console.
  get          Fetch a document from the given URL.
  headers      Configure custom request headers.  history      Navigate the browser history.
  load         Load a document from disk.
  reload       Reload the current document.  show         Display the current document.

Ok good. Now load the API schema by performing a get request on our schema/ endpoint.

(env) $ coreapi get http://127.0.0.1:8000/schema/
<Pastebin API "http://127.0.0.1:8000/schema/">
    : {        list()
    }
    snippets: {        highlight: {
            list(id, [page])
        }        list([page])
        read(id)
        read_0(format, id)        read_1(format, id)
    }
    users: {        list([page])
        read(id)
        read_0(format, id)    }
    read(format)
    read_0(format)    read_1(format)

We haven’t authenticated yet, so right now we’re only able to see the read only endpoints, in line with how we’ve set up the permissions on the API.

Let’s try the existing snippets:

(env) $ coreapi action snippets list
{
    "count": 1,    "next": null,
    "previous": null,
    "results": [        {
            "url": "http://127.0.0.1:8000/snippets/1/",
            "id": 1,            "highlight": "http://127.0.0.1:8000/snippets/1/highlight/",
            "title": "Hello world",
            "code": "print(\"Hello, World!\")",            "linenos": false,
            "language": "python",
            "style": "friendly",            "owner": "testuser"
        }
    ]
}

Now let’s try a highlight endpoint that returns HTML.

(env) coreapi get http://127.0.0.1:8000/snippets/1/highlight/
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"   "http://www.w3.org/TR/html4/strict.dtd">

<html><head>
  <title>Hello world</title>
  <meta http-equiv="content-type" content="text/html; charset=None">  <style type="text/css">td.linenos { background-color: #f0f0f0; padding-right: 10px; }
...

The result is the pre-formatted HTML of the page.

Authenticating our client

If we want to be able to create, edit and delete snippets, we’ll need to authenticate as a valid user. REST Framework ships with multiple authentication options but in this case we’ll just use basic auth which is the default setting.

Make sure to replace the <username> and <password> below with your actual username and password. Mine are admin and testpass123.

(env) coreapi credentials add 127.0.0.1 admin:testpass123 --auth basic
Added credentials
127.0.0.1 "Basic YWRtaW46d2lsbGlhbTE="

Now that we are authenticated we should be able to interact with all API endpoints.

For example to create a new snippet:

(env) coreapi action snippets create --param title="Example" --param co
de="print('hello, world')"
{
    "url": "http://127.0.0.1:8000/snippets/2/",
    "id": 2,
    "highlight": "http://127.0.0.1:8000/snippets/2/highlight/",
    "title": "Example",
    "code": "print('hello, world')",
    "linenos": false,
    "language": "python",
    "style": "friendly",
    "owner": "admin"
}

And to then delete that snippet:

(env) coreapi action snippets delete --param id=2

In the real world, many developers rely on a third-party client library like Postman to fully interact with an API.

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.


Want to learn how to build RESTful APIs with Python and Django? Check out my book REST APIs with Django.