Using the Django REST Framework to Develop APIs
In this article, we'll talk about building APIs and see how to do it step by step.
Join the DZone community and get the full member experience.
Join For FreeIf you know the basics of the field you work in, you can master any technology, reach higher levels as a developer, and create better products. In this article, we'll talk about building APIs and see how to do it step by step. I believe this is going to be useful for all beginner Django developers, as REST APIs are used in every application or piece of software to connect the backend and frontend parts. If you master this, you can build all kinds of products.
The Step-by-Step Guide to Your API
In this Django REST framework tutorial, we'll go through all the stages of building an API in great detail.
You might also be interested in: Create a Simple API Using Django REST Framework in Python
Set up Your Development Environment
First of all, you have to install Python dependencies for your OS. If you're using Windows, you can easily install Linux as your secondary OS using this manual or VirtualBox.
To proceed, use pyenv, a simple yet effective Python management tool. It's our go-to helper. It allows us to change the global Python version, install multiple Python versions, set project-specific Python versions, and manage virtual Python environments.
If you're using Mac OS or Linux, it should be easy to install:
$ git clone https://github.com/pyenv/pyenv.git ~/.pyenv
Define the environment variable PYENV_ROOT
to point to the path where pyenv repo is cloned. Then, add $PYENV_ROOT/bin
to your $PATH
for access to the pyenv command-line utility.
$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
Add the pyenv unit to your shell to enable shims and autocompletion.
$ echo -e 'if command -v pyenv 1>/dev/null 2>&1; then\n eval "$(pyenv init -)"\nfi' >> ~/.bashrc
Restart your shell so the path changes take effect. Now, you can begin to use pyenv.
At this point, install the latest stable version of Python. Now this...
$ pyenv install 3.7.4
... and create an environment for your project.
$ pyenv virtualenv 3.7.4 blog
Create a Django Project
Now, we'll create a project folder and navigate to the newly-created directory.
$ mkdir projects && cd projects
The next step is to create a Django project. There are several ways to do it.
Personally, I prefer to start projects from a template and use the Django Stars backend skeleton that has been implemented in our company.
Here's the command that will help you start a project from a template:
$ curl https://be.skeletons.djangostars.com/startproject | bash
At the start, you will be offered several Python versions to work with. Select the 3.7 version of Python.
Set the project name to django_blog.
Since we're going to use the Django Rest Framework, we should select the 3rd option:
If you know you'll need Celery for your project, you can also choose the first option and press OK.
Anyway, what we just did was create a project from a template. Let's now navigate to the project name directory:
$ cd django_blog
This is what our project's structure looks like:
django_blog
├── api
│ ├── django_blog
│ │ ├── apps
│ │ │ ├── account
│ │ │ │ ├── admin.py
│ │ │ │ ├── apps.py
│ │ │ │ ├── forms.py
│ │ │ │ ├── __init__.py
│ │ │ │ ├── managers.py
│ │ │ │ ├── migrations
│ │ │ │ │ ├── 0001_initial.py
│ │ │ │ │ └── __init__.py
│ │ │ │ └── models.py
│ │ │ ├── common
│ │ │ │ ├── apps.py
│ │ │ │ ├── __init__.py
│ │ │ │ ├── management
│ │ │ │ │ ├── commands
│ │ │ │ │ │ ├── generate_secretkey.py
│ │ │ │ │ │ ├── __init__.py
│ │ │ │ │ │ └── startapp.py
│ │ │ │ │ └── __init__.py
│ │ │ │ └── models
│ │ │ │ ├── core.py
│ │ │ │ └── __init__.py
│ │ │ └── __init__.py
│ │ ├── __init__.py
│ │ ├── settings
│ │ │ ├── contrib.py
│ │ │ ├── django_blog.py
│ │ │ ├── django.py
│ │ │ ├── environment.py
│ │ │ └── __init__.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ ├── Makefile
│ ├── manage.py
│ ├── pylama.ini
│ ├── pyproject.toml
│ ├── pytest.ini
│ └── requirements
│ ├── common.in
│ ├── common.txt
│ ├── dev.in
│ └── dev.txt
├── build
│ ├── ci
│ │ └── circle.yml
│ ├── docker-compose-api.yml
│ ├── docker-compose-dev.yml
│ ├── docker-entrypoint-api.sh
│ └── Dockerfile.api
├── circle.yml -> build/ci/circle.yml
└── README.md
Install Packages With Requirements
To run the project, we need to install some packages, which are defined in the requirements/dev.txt.
Thus, we install packages with requirements for local development.
Navigate to the API directory:
$ cd api
and install the dependencies.
If you need to add any other packages, just add your package to the dev.txt file for local development or to common.txt if you need it to be installed on the server.
Next, we will have to add the Pillow package that isn't included by default in the package we installed. Open therequirements/common.txt
file and add Pillow==6.1.0 to the end. Then run:
$ pip install -r requirements/dev.txt
This is the file that contains a list of all the packages that will be installed.
Bootstrap the Database
Our project needs a database. Most Django developers prefer to use PostgreSQL for all environments — i.e., the development, staging, and production systems. Some people use SQLite for local development and PostgreSQL in production. Django ORM allows you to deal with both databases. I strongly recommend that you keep your development station and production as similar as possible and use the same database.
There are several ways to bootstrap a database:
Using SQLite
To make the database bootstrapping easier, we can use SQLite. It's an in-process library that implements a self-contained, serverless, zero-configuration, transactional SQL database engine.
All you need to do is open the .env file and replace:
DJANGO_BLOG_DATABASE_URL=psql://django_blog@127.0.0.1:5432/django_blog
with
DJANGO_BLOG_DATABASE_URL=sqlite:///db.sqlite3
Here, we use the django-environ package, which allows you to define the configuration depending on your environment. You can find out more about this approach through The Twelve Factor App.
PostgreSQL is a powerful, open-source object-relational database system. It uses and extends the SQL language, and has many features that allow you to safely store and scale the most complicated data workloads. This system is a bit more complicated than SQLite, so you might want to learn more about it.
It's much easier to install PostgreSQL with Docker — an open-source tool that automates the deployment of an application inside a software container. If you're not familiar with Docker yet, feel free to check out some instructions on how to install it and work with docker-compose.
Then just open a new tab and run:
$ docker-compose -f build/docker-compose-dev.yml up
Next, we should apply initial migrations:
$ python manage.py migrate
This will apply all previously unapplied migrations to the database which comes with the Django installation.
https://github.com/olegkovalov/django_blog/commit/4f9b20bafc69fb686026d112592c549bd8b6e858
Create a Blog Application
At this point, we've already created a Django project, installed packages with requirements, and bootstrapped the database. Which means we can now finally create a blog application using the following command:
$ python manage.py startapp blog
By doing this, you've also created a new directory. It will be available in apps/blog.
Now, we should register our application in 'INSTALLED_APPS'
.
Add 'blog' to your 'INSTALLED_APPS'
settings placed in 'api/django_blog/settings/django.py'
INSTALLED_APPS = [
....
# our apps
"django_blog.apps.common.apps.CommonConfig",
"django_blog.apps.account.apps.AccountConfig",
"django_blog.apps.blog.apps.BlogConfig",
]
Test-Driven Development (TDD) of APIs
As you may have already learned, test-driven development is an approach that focuses on writing tests before you start implementing the business logic of your application. Writing tests first requires you to really consider what do you want from the code. But apart from this, TDD has numerous other benefits:
- Fast feedback and detailed specification;
- Reduced time spent on reworking and time spent in the debugger;
- Maintainable, flexible, and easily extensible code;
- Shorter development time to market;
- Increased developer's productivity;
- SOLID code; and
- A clean interface.
Also, TDD tells you whether your last change (or refactoring) broke previously working code. Moreover, it forces radical simplification of the code - you will only write code in response to the requirements of the tests. Also, you're forced to write small classes focused on one thing only. Further on, it allows the design to evolve and adapt to your changing understanding of the problem.
The resulting unit tests are simple and act as documentation for the code. Since TDD use cases are written as tests, other developers can view the tests as examples of how the code is supposed to work.
Before we write tests for our API, let's take a step back and look at HTTP and how it interacts with the Django REST Framework.
The Hypertext Transfer Protocol (HTTP) is an application protocol for distributed, collaborative, hypermedia information systems. HTTP defines methods like GET, POST, PUT, DELETE, OPTIONS, and HEAD to indicate the desired action that should be performed on the resource you identify. These methods can be defined by the characteristics of safety and idempotency (you can find out more about this here). By agreement, REST APIs rely on these methods, so feel free to use the appropriate HTTP method for each type of action.
Let's apply it to the resource 'post'.
How to Write Tests
With the unittests framework, writing tests becomes very easy.
Open the 'api/django_blog/apps/blog/tests/test_models.py'
file and add the following code:
from django.test import TestCase
from django_blog.apps.blog.models import Post, Tag
class PostTestCase(TestCase):
def test_post(self):
self.assertEquals(
Post.objects.count(),
0
)
Post.objects.create(
title='active', text='text', is_active=True
)
Post.objects.create(
title='inactive', text='text', is_active=False
)
self.assertEquals(
Post.objects.count(),
2
)
active_posts = Post.objects.active()
self.assertEquals(
active_posts.count(),
1
)
inactive_posts = Post.objects.inactive()
self.assertEquals(
inactive_posts.count(),
1
)
class TagTestCase(TestCase):
def test_tag(self):
self.assertEquals(
Tag.objects.count(),
0
)
Tag.objects.create(name='name')
self.assertEquals(
Tag.objects.count(),
1
)
Open the 'api/django_blog/apps/blog/models/post.py'
file, and define models for Post and Tags:
from django.db import models
from django_blog.apps.account.models import User
from django_blog.apps.common.models import CoreModel
class Tag(models.Model):
name = models.CharField(max_length=100, unique=True)
class Post(CoreModel):
title = models.CharField(max_length=100)
text = models.TextField()
tags = models.ManyToManyField(Tag, related_name='posts', blank=True)
author = models.ForeignKey(User, related_name='posts', on_delete=models.CASCADE, blank=True, null=True)
image = models.ImageField(null=True, blank=True)
Our Post model inherits from 'CoreModel'
, so fields and methods defined in 'CoreModel'
can be added to the 'Post'
model.
Now, let's run our first test on the models we've created:
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.005s
OK
This means that the test was run successfully.
Open 'api/django_blog/apps/blog/tests/tests.py'
and add tests for your API:
from django.urls import reverse
from rest_framework.test import APITestCase
from rest_framework.views import status
from django_blog.apps.blog.models import Post, Tag
class PostListCreateAPIView(APITestCase):
def setUp(self) -> None:
self.url = reverse('api-post-list', kwargs={'version': 'v1'})
def test_create_post(self):
self.assertEquals(
Post.objects.count(),
0
)
data = {
'title': 'title',
'text': 'text'
}
response = self.client.post(self.url, data=data, format='json')
self.assertEquals(response.status_code, status.HTTP_201_CREATED)
self.assertEquals(
Post.objects.count(),
1
)
post = Post.objects.first()
self.assertEquals(
post.title,
data['title']
)
self.assertEquals(
post.text,
data['text']
)
def test_get_post_list(self):
tag = Tag(name='tag_name')
tag.save()
post = Post(title='title1', text='text1')
post.save()
post.tags.add(tag)
response = self.client.get(self.url)
response_json = response.json()
self.assertEquals(
response.status_code,
status.HTTP_200_OK
)
self.assertEquals(
len(response_json),
1
)
data = response_json[0]
self.assertEquals(
data['title'],
post.title
)
self.assertEquals(
data['text'],
post.text
)
self.assertEquals(
data['tags'][0]['name'],
tag.name
)
class PostDetailsAPIViewTest(APITestCase):
def setUp(self) -> None:
self.post = Post(title='title2', text='text2')
self.post.save()
self.url = reverse('api-post-details', kwargs={'version': 'v1', 'pk': self.post.pk})
def test_get_post_details(self):
response = self.client.get(self.url)
self.assertEquals(
response.status_code,
status.HTTP_200_OK
)
data = response.json()
self.assertEquals(
data['pk'],
str(self.post.pk)
)
self.assertEquals(
data['title'],
self.post.title
)
self.assertEquals(
data['text'],
self.post.text
)
def test_update_post(self):
response = self.client.get(self.url)
self.assertEquals(
response.status_code,
status.HTTP_200_OK
)
data = response.json()
data['title'] = 'new_title'
data['text'] = 'new_text'
response = self.client.put(self.url, data=data, format='json')
self.assertEquals(
response.status_code,
status.HTTP_200_OK
)
self.post.refresh_from_db()
self.assertEquals(
self.post.title,
data['title']
)
self.assertEquals(
self.post.text,
data['text']
)
def test_delete_post(self):
self.assertEquals(
Post.objects.count(),
1
)
response = self.client.delete(self.url)
self.assertEquals(
response.status_code,
status.HTTP_204_NO_CONTENT
)
self.assertEquals(
Post.objects.count(),
0
)
Serializers
A serializer is a framework that allows complex data such as query sets and model instances to be converted to native Python data types. Then, these can then be easily rendered into JSON, XML or other content types. Serializers also work in the opposite direction - deserializing allows parsed data to be converted back into complex types after having validated the incoming data. The serializers in the REST framework work in a way similar to Django's Form and ModelForm classes.
Declare Serializers
Open 'api/django_blog/apps/blog/rest_api/serializers/post.py'
and add:
from rest_framework import serializers
from django_blog.apps.account.models import User
from django_blog.apps.blog.models import Tag, Post
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('pk', 'email', 'first_name', 'last_name',)
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ('pk', 'name',)
class PostSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True, required=False, read_only=True)
author = UserSerializer(required=False, read_only=True)
serializers.ImageField(use_url=True, required=False, allow_null=True)
class Meta:
model = Post
fields = ('pk', 'title', 'text', 'tags', 'author', 'image',)
Now, we can use the PostSerializer
to serialize a post or list of posts.
from django_blog.apps.blog.models import Post
from django_blog.apps.blog.rest_api.serializers.post import PostSerializer
post = Post.objects.create(title='First post', text='This is a first post')
print(PostSerializer(post).data)
# {'pk': '4670511f-4a03-455e-a160-18c396fa743d', 'title': 'First post', 'text': 'This is a first post', 'tags': [], 'author': None, 'image': None}
Open 'api/django_blog/apps/blog/rest_api/views/blog_views.py'
and add the following code to define API view:
from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView
from django_blog.apps.blog.models import Post
from django_blog.apps.blog.rest_api.serializers.post import PostSerializer
class PostListCreateAPIView(ListCreateAPIView):
"""
API view to retrieve list of posts or create new
"""
serializer_class = PostSerializer
queryset = Post.objects.active()
class PostDetailsAPIView(RetrieveUpdateDestroyAPIView):
"""
API view to retrieve, update or delete post
"""
serializer_class = PostSerializer
queryset = Post.objects.active()
Add the URLs of your endpoints to
‘api/django_blog/apps/blog/rest_api/urls.py’
. This will be the URL where your API will be available.
from django.urls import path
from .views import blog_views
urlpatterns = [
path('posts/', blog_views.PostListCreateAPIView.as_view(), name='api-post-list'),
path('posts/<uuid:pk>/', blog_views.PostDetailsAPIView.as_view(), name='api-post-details'),
]
Open 'api/urls.py'
and include the blog's application URLs:
urlpatterns = [
path("admin/", admin.site.urls),
re_path(r'api/(?P<version>[v1|v2]+)/', include('django_blog.apps.blog.rest_api.urls')),
]
Run Tests
Now, you can test your API using the following command:
$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.......
----------------------------------------------------------------------
Ran 7 tests in 0.036s
OK
Destroying test database for alias 'default'...
Voila! The test has been completed successfully.
Browsable API
API may stand for Application Programming Interface, but humans have to be able to read the APIs, too - someone has to do the programming, hence the browsable API. With the Django REST Framework, you can generate a human-friendly HTML output for each resource when an HTML format is requested. These pages allow you to easily browse through resources, as well as build in forms to submit data to the resources using POST, PUT, and DELETE.
Let's test our API with the browsable REST Framework interface. First, start the development server:
$ python manage.py runserver_plus
Then, open your browser of choice and visit this url:
Create a new post with an image file. Fill out the fields "Title" and "Text", choose an image file in the form at the bottom, and press "POST". The page will reload with the new content.
Versioning
Versioning is a vital part of API design that allows you to improve the representation for the resources of your API and alter behavior between different clients. The REST framework provides a number of different versioning schemes. Versioning is determined by the incoming client request and may either be based on the request URL or the request headers.
In this example, we use the URL path versioning approach.
Open 'api/django_blog/settings/django.py'
and add:
REST_FRAMEWORK = {
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.URLPathVersioning'
}
If the response was changed and you decide to define a different API version, you might handle it as follows:
def get_serializer_class(self):
if self.request.version == 'v2':
return PostSerializerV2
else:
return super().get_serializer_class()
Documenting Your API
The REST framework provides built-in support for generating OpenAPI schemas. These can be used with tools that allow you to build API documentation.
The browsable API that the REST framework provides allows your API to be entirely self- describing. You can get the documentation for each API endpoint by simply visiting the URL in your browser.
However, there's also a number of great third-party documentation packages available. For instance, let's try to integrate drf-yasg. Its goal is to implement as much of the OpenAPI specification as possible (nested schemas, named models, response bodies, enum/pattern/min/max validators, form parameters, etc.) and generate documents that can be used with code-generation tools.
Open the terminal, and type in:
$ pip install -U drf-yasg
Open 'api/django_blog/settings/django.py'
and add the INSTALLED_APPS 'drf_yasg'
package:
INSTALLED_APPS = [
....
"‘drf_yasg’",
]
Also, we should include URLs, for generated documentation:
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from rest_framework import permissions
schema_view = get_schema_view(
openapi.Info(
title="Blog API",
default_version='v1',
description="Test description",
),
public=True,
permission_classes=(permissions.AllowAny,),
)
urlpatterns = [
path('admin/', admin.site.urls),
path('doc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
re_path(r'api/(?P<version>[v1|v2]+)/', include('django_blog.apps.blog.rest_api.urls')),
]
You can find the documentation on your API in this document.
If you want to implement new features or play around, feel free to clone or fork my GitHub repo.
If you still have questions and want to learn more, there's plenty of great Django REST API tutorials and resources for self-improvement. Here's a reading list our software development team Django Stars highly recommends. And if by any chance you have to deal with nested objects, here's a useful library that might come in handy. Also, feel free to visit the django_blog repository for ideas and inspiration. But as I said before, if you know your basics well, you can build upon this knowledge. And considering how universal APIs are, you'll be able to work with any kind of product, which will make you an indispensable team member and a universal soldier of software development.
This guide was originally posted on Django Stars blog.
Further Reading
Python Django Tutorial for Beginners
Understanding Security for Django Web Services, Part 1 — JSON Web Token
Published at DZone with permission of Oleg Kovalyov. See the original article here.
Opinions expressed by DZone contributors are their own.
Trending
-
File Upload Security and Malware Protection
-
Logging Best Practices Revisited [Video]
-
Cypress Tutorial: A Comprehensive Guide With Examples and Best Practices
-
Merge GraphQL Schemas Using Apollo Server and Koa
Comments