Skip to content
Kyle Truong

Building an API with Django REST Framework and Class-Based Views

Tutorial, Backend, Python7 min read

What and Why Are We Building?

With the rise of Single-Page-Applications and the trend of separating monoliths into services with distinct front and backends, knowing how to make your own RESTful API for your backend is more important than ever.

Officially, a RESTful API is a Representational State Transfer-ful Application Programming Interface. Big words, but what it really boils down to is putting data onto your web server in a way that’s accessible to other servers and clients, and it works through HTTP requests and responses and carefully structured URL routes to represent specific resource(s).

It looks a lot like this:

HTTP Model

HTTP is short for Hypertext Transfer Protocol and it’s a set of rules that dictate how data is packaged and communicated throughout the web. There are other protocols that go along with HTTP, but HTTP will be the focus for now for simplicity.

An HTTP request looks something like this:

1POST /cgi-bin/process.cgi HTTP/1.1
2User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT)
4Content-Type: application/x-www-form-urlencoded
5Content-Length: length
6Accept-Language: en-us
7Accept-Encoding: gzip, deflate
8Connection: Keep-Alive

And an HTTP response looks something like this:

1HTTP/1.1 200 OK
2Date: Mon, 27 Jul 2009 12:28:53 GMT
3Server: Apache/2.2.14 (Win32)
4Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
5Content-Length: 88
6Content-Type: text/html
7Connection: Closed
11<h1>Hello, World!</h1>

Clients send HTTP requests to the server and the server sends back HTTP responses. Many things can fit within requests and responses, though most of the times the data is either metadata about the request/response or some kind of JSON string or both, when dealing with APIs.

This is a brief walkthrough of how to make a barebones API for a ‘Todo-list’ application using the Django Rest Framework.

This is not a comprehensive guide, so you should be somewhat familiar with making AJAX calls with JavaScript and Django itself. You can make an API in any modern language and the concepts behind it are similar but I am choosing Python because:

  • It’s clean and explicit
  • Django and Django Rest Framework are both mature, stable, and well-documented
  • Django and Django Rest Framework gives you a lot out of the box
  • Pluggable auth systems
  • Serializers
  • Views
  • Browseable client and admin panel
  • Auto-generated documentation for your API
  • An awesome ORM
  • Highly customizable on every level

In a nutshell, this is what we’ll be creating:

REST API endpoints

We structure our endpoints in accordance with common RESTful guidelines so that we have clear endpoints that return expected resources.


First, we use virtualenv and virtualenvwrapper to make a virtual environment to install Python packages in a way that doesn’t interfere with other projects and environments. In this environment, we install Django, Django Rest Framework, and coreapi (for Django Rest Framework)

1ktruong:auth-api ktruong$ which python3
3ktruong:auth-api ktruong$ mkvirtualenv --python=/usr/local/bin/python3 auth-api
5(auth-api) ktruong:auth-api ktruong$
7pip install django djangorestframework coreapi

Now, we create the django project, make folders, and adjust some project settings:

1(auth-api) ktruong:auth-api ktruong$ pwd
3(auth-api) ktruong:auth-api ktruong$ touch
4(auth-api) ktruong:auth-api ktruong$ startproject auth_api
5(auth-api) ktruong:auth-api ktruong$ cd auth_api
6(auth-api) ktruong:auth_api ktruong$ ls
8(auth-api) ktruong:auth_api ktruong$ python startapp users
9(auth-api) ktruong:auth_api ktruong$ python startapp todos
10(auth-api) ktruong:auth_api ktruong$ ls
11auth_api todos users
12(auth-api) ktruong:auth_api ktruong$
4 'django.contrib.admin',
5 'django.contrib.auth',
6 'django.contrib.contenttypes',
7 'django.contrib.sessions',
8 'django.contrib.messages',
9 'django.contrib.staticfiles',
10 'rest_framework',
11 'todos',
12 'users',
15AUTH_USER_MODEL = 'users.User'

Notice how we explicitly set our AUTH_USER_MODEL to a custom User model (we’ll write the actual model, users.User, later). We could use the default User model that comes from Django but it becomes unnecessarily complicated to change it down the road.

A solution is to write our own User model that subclasses the same AbstractUser model that Django’s User model subclasses. Doing this will give us the same functionality but allow us to easily customize our User model down the road.

Project Level URL Routing

We’ll be building the features in this order:

URLs → views → serializers → models

This means we’ll be referencing some files before we make them, which may feel strange, but I feel doing it this way is more intuitive because it follows the path of the HTTP request more closely.

When the request first comes into our server, we need to decide where to route that request. Kind of like a receptionist to our web server, our top-most URL routes will route the request to the proper modules and views.

We’ll use the innermost auth_api folder to store the top-most url routes along with our project-wide settings:

1// auth_api/
3from django.conf.urls import url, include
4from django.contrib import admin
5from rest_framework.documentation import include_docs_urls
7from auth_api import views
9urlpatterns = [
10 url(r'^admin/',,
11 url(r'^docs/', include_docs_urls(title='Todo API', description='RESTful API for Todo')),
13 url(r'^$', views.api_root),
14 url(r'^', include('users.urls', namespace='users')),
15 url(r'^', include('todos.urls', namespace='todos')),

When Django parses the incoming request, it will use regex to match the URL to the urlpatterns we write and forward the request to the place we want, which could contain more URL routes or a view.

We set up routes for our Django admin, documentation (automatically generated thanks to DRF), routes for users, routes for todos, and our api root.

Making the Root View of the API

One of the routes we defined earlier routed to views.api_root. Though not necessary, making a root view that acts as a table of contents to other routes in your api when requests matches your domain name exactly is easy to implement and improves developer experience.

The urlpatterns we wrote route requests and the views we will write will handle those requests and return HTTP responses. To write our api_root, we can use DRF’s built in decorator, @api_view, to wrap our view with some nice utilities:

  • Specifies which HTTP methods we allow the view to respond to
  • Wraps normal HTTP request and response objects to provide a more uniform interface to work with HTTP data.
1// auth_api/
3from rest_framework.decorators import api_view
4from rest_framework.response import Response
5from rest_framework.reverse import reverse
8def api_root(request, format=None):
9 return Response({
10 'users': reverse('users:user-list', request=request, format=format),
11 'todos': reverse('todos:todo-list', request=request, format=format),
12 })

Configuring URLs for Users and Todos

We need to create routes for when the request matches /todos/ or /users/ (as per the table we made earlier) and route them to the proper views to be handled:

1// todos/
3from django.conf.urls import url
4from rest_framework.urlpatterns import format_suffix_patterns
5from todos import views
7urlpatterns = [
8 url(r'^todos/$', views.TodoList.as_view(), name='todo-list'),
9 url(r'^todos/(?P<pk>[0-9]+)/$', views.TodoDetail.as_view(), name='todo-detail'),
1// users/
3from django.conf.urls import url
4from rest_framework.urlpatterns import format_suffix_patterns
5from users import views
7urlpatterns = [
8 url(r'^users/$', views.UserList.as_view(), name='user-list'),
9 url(r'^users/(?P<pk>[0-9]+)/$', views.UserDetail.as_view(), name='user-detail'),

Writing the Views

Views handle the request and return a response and by handling, I mean we can do anything we want with it - from hitting the database, modifying the request, structuring the response, or injecting our own logic into it.

With respect to APIs, all the core views often do the same thing conceptually:

  • Parse the request for data and the HTTP method
  • Query the database (DB) to fetch the model object(s), if needed
  • Serialize the data (we’ll discuss this more later)
  • Do something with the object/data (create, read, update, delete)
  • Return an HTTP response

Views are most often written as functions and an example of a function-based view that handles our API request looks something like this:

1@api_view(['GET', 'POST'])
2def snippet_list(request):
3 """
4 List all snippets, or create a new snippet.
5 """
6 if request.method == 'GET':
7 snippets = Snippet.objects.all()
8 serializer = SnippetSerializer(snippets, many=True)
9 return Response(
11 elif request.method == 'POST':
12 serializer = SnippetSerializer(
13 if serializer.is_valid():
15 return Response(, status=status.HTTP_201_CREATED)
16 return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
19@api_view(['GET', 'PUT', 'DELETE'])
20def snippet_detail(request, pk):
21 """
22 Retrieve, update or delete a snippet instance.
23 """
24 try:
25 snippet = Snippet.objects.get(pk=pk)
26 except Snippet.DoesNotExist:
27 return Response(status=status.HTTP_404_NOT_FOUND)
29 if request.method == 'GET':
30 serializer = SnippetSerializer(snippet)
31 return Response(
33 elif request.method == 'PUT':
34 serializer = SnippetSerializer(snippet,
35 if serializer.is_valid():
37 return Response(
38 return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
40 elif request.method == 'DELETE':
41 snippet.delete()
42 return Response(status=status.HTTP_204_NO_CONTENT)

Examples from

So again, we see that the things that views essentially do are:

  • Parse the request for data and the HTTP method
  • Query the database (DB) to fetch the model object(s), if needed
  • Serialize the data (we’ll discuss this more later)
  • Do something with the object/data (create, read, update, delete)
  • Return an HTTP response

It’s relatively straight-forward and explicit, but imagine having to write our views likes this for:

  • Todo_detail
  • Todo_list
  • User_detail
  • User_list

There would be a lot of logic duplication. Instead, we could use an object-oriented approach and use classes and inheritance to reuse common blocks of logic. Using class-based views, we could write our views like this:

1from todos.models import Todo
2from rest_framework import generics
3from rest_framework.response import Response
4from rest_framework.reverse import reverse
6from todos.serializers import TodoSerializer
9class TodoList(generics.ListCreateAPIView):
10 queryset = Todo.objects.all()
11 serializer_class = TodoSerializer
13 def perform_create(self, serializer):
17class TodoDetail(generics.RetrieveUpdateDestroyAPIView):
18 serializer_class = TodoSerializer
20 def get_queryset(self):
21 return Todo.objects.all().filter(user=self.request.user)

With that, we accomplish the exact same functionality as the example above that uses function-based views, only with significantly less code.

But less code is not always better, so you’ll have to decide where to draw the line in terms of how explicit or terse you want your code to be. Personally, I feel class-based views strike that perfect balance, plus they force you to grow as a developer by exposing you to classes, inheritance, and object-oriented programming, which is probably the bulk of the code you’ll be reading and/or writing as a developer.

In the above classes, we subclass generic classes provided by DRF and the generic classes provide methods and properties that encapsulate the common logic so we don’t have to keep rewriting it, but we can still access, overwrite, and customize the logic if needed.

Blocks of logic like the ones that parse the DB for the correct model instance, or carry out error handling, or determining which serializer to use and how to instantiate it, or structure the response, or parsing the request, are already written for you and are readily available to the classes that you write if you subclass the generic ones.

There is honestly a lot of cleverly written code hidden behind these generic views with a lot of concepts and techniques to learn from that are beyond the scope of this guide, but the best way to really learn and understand is to download the source code, read it, and tinker. I also recommend reading the documentation on these topics:

Try not to overthink it too much and remember that the 10 or so lines of code using class-based views does the exact same thing as the many more lines of codes using function-based views in the above example, just more concise.

Repeat for Users:

1// users/
3from users.models import User
4from rest_framework import generics
5from rest_framework.response import Response
6from rest_framework.reverse import reverse
8from users.serializers import UserSerializer
11class UserList(generics.ListCreateAPIView):
12 queryset = User.objects.all()
13 serializer_class = UserSerializer
16class UserDetail(generics.RetrieveUpdateDestroyAPIView):
17 serializer_class = UserSerializer
19 def get_queryset(self):
20 return User.objects.all().filter(username=self.request.user)

Writing the Serializers

DRF serializers provide the service of serialization and deserialization. Serialization is the process of translating data structures into a format that can be stored, which in this case means turning querysets and model instances into native Python datatypes and then into JSON. Deserialization is the opposite, taking JSON and turning it into native Python datatypes and then into model instances.

Serializers are not magic.

If you’ve made an API in any language then you’ve used the same concepts serializers use. Serializers just give you a convenient interface to take data in one form and convert it into another. DRF serializers use an interface similar to that of Django forms in that you define the fields of the model you wish to serialize/deserialize and when you instantiate the serializer with data it will do the field validation for you.

Let’s write our serializers for Todos and Users:

1// Todos/
3from rest_framework import serializers
5from todos.models import Todo
7class TodoSerializer(serializers.HyperlinkedModelSerializer):
8user = serializers.ReadOnlyField(source='user.username')
10 class Meta:
11 model = Todo
12 fields = ('url', 'id', 'created', 'name', 'user')
13 extra_kwargs = {
14 'url': {
15 'view_name': 'todos:todo-detail',
16 }
17 }
19// users/
21from rest_framework import serializers
23from users.models import User
26class UserSerializer(serializers.HyperlinkedModelSerializer):
27 todos = serializers.HyperlinkedRelatedField(
28 many=True,
29 view_name='todos:todo-detail',
30 read_only=True
31 )
32 password = serializers.CharField(write_only=True)
34 def create(self, validated_data):
35 user = User(
36 username=validated_data.get('username', None)
37 )
38 user.set_password(validated_data.get('password', None))
40 return user
42 def update(self, instance, validated_data):
43 for field in validated_data:
44 if field == 'password':
45 instance.set_password(validated_data.get(field))
46 else:
47 instance.__setattr__(field, validated_data.get(field))
49 return instance
51 class Meta:
52 model = User
53 fields = ('url', 'id', 'username',
54 'password', 'first_name', 'last_name',
55 'email', 'todos'
56 )
57 extra_kwargs = {
58 'url': {
59 'view_name': 'users:user-detail',
60 }
61 }

Notice that UserSerializer is much more complex than TodoSerializer. Serializers, like our class-based views, come in many varieties and can inherit from many generic base classes. Within those base classes are methods and properties we can overwrite to customize what happens at certain hooks, like when a serializer updates or saves data.

we customize UserSerializer because we want to use the hashing feature that comes with Django’s AbstractUser when updating and saving users for security reasons.

Writing the Models

From client to url routes to views to serializers, and now to the last part in our little app, the models.

Our models are what define our Data. Django models get written in normal Python classes and they get mapped to SQL DBs, each attribute on the model getting mapped to a field in its respective table.

We’ll create two models, Todo and User, ad we’ll create a many-to-one relationship (foreign key) from our Todoes to our Users, so a User can have many Todos but a Todo will only have one User.

1// todos/
3from django.db import models
4from users.models import User
7class Todo(models.Model):
8 created = models.DateTimeField(auto_now_add=True)
9 name = models.CharField(max_length=100, unique=True, blank=False, null=False)
10 user = models.ForeignKey('users.User', related_name='todos', on_delete=models.CASCADE, null=False)
12 class Meta:
13 ordering = ('created',)
1// users/
3from django.db import models
4from django.contrib.auth.models import AbstractUser
7class User(AbstractUser):
8 pass

We touched a bit on this earlier, but we don’t do anything extra with our User model. We use the same AbstractUser that the default Django User model uses, but defining our own User model gives us the ability to easily edit it later on while still retaining the same features as the default Django User.

Bringing It All Together

We’ve got all the code we need to make this API work, and now we just need to migrate our models and create a superuser.

1ktruong$ python makemigrations todos users
2ktruong$ python migrate
3ktruong$ python createsuperuser
4ktruong$ python runserver

Now go browse localhost:8000, login to the admin if needed, and play around with your new API.

The browsable API is just another out-of-the-box feature from DRF and it allows you to browse your API interactively in a browser. You can click through links, see relationships, see models and objects, perform any CRUD (create, read, update, delete) request, and do pretty much anything you can do with an API, but in a visible and interactive way.

Django REST Framework admin example


We covered some fundamental concepts of building a RESTful API by creating a basic Todo API, with complete CRUD endpoints. Though very barebones, the instructions in this guide should serve as another step in the ladder to help you become a better developer and understand how to make your own API.

There’s still a lot more one can do to improve your API, such as adding authentication and authorization with JSON Web Tokens, adding unit tests, and more, which are also topics I plan on covering in the future.

© 2021 by Kyle Truong. All rights reserved.