How to Create API Endpoint With Django Rest Framework
As a component of our work to make sharp web applications at Caktus, we every now and again make API endpoints that permit other programming to connect with a server. As a rule this implies utilizing a frontend application (React, Vue, or Angular), however it could likewise mean associating some other bit of programming to collaborate with a server. A great deal of our API endpoints, across ventures, wind up working in comparable ways, so we have gotten productive at keeping in touch with them, and this blog entry gives a case of how to do as such.
An ordinary solicitation for an API endpoint might be something like: 'the front end application should have the option to peruse, make, and update organizations through the API'. Here is a rundown of making a model, a serializer, and a view for such a situation, including tests for each part:
Model
For this example, we’ll assume that a Company model doesn’t currently exist in Django, so we will create one with some basic fields:
# models.py
from django.db import models
class Company(models.Model):
name = models.CharField(max_length=255)
description = models.TextField(blank=True)
website = models.URLField(blank=True)
street_line_1 = models.CharField(max_length=255)
street_line_2 = models.CharField(max_length=255, blank=True)
city = models.CharField(max_length=80)
state = models.CharField(max_length=80)
zipcode = models.CharField(max_length=10)
def __str__(self):
return self.name
Writing tests is important for making sure our app works well, so we add one for the __str__() method. Note: we use the factory-boy and Faker libraries for creating test data:
# tests/factories.py
from factory import DjangoModelFactory, Faker
from ..models import Company
class CompanyFactory(DjangoModelFactory):
name = Faker('company')
description = Faker('text')
website = Faker('url')
street_line_1 = Faker('street_address')
city = Faker('city')
state = Faker('state_abbr')
zipcode = Faker('zipcode')
class Meta:
model = Company
# tests/test_models.py
from django.test import TestCase
from ..models import Company
from .factories import CompanyFactory
class CompanyTestCase(TestCase):
def test_str(self):
"""Test for string representation."""
company = CompanyFactory()
self.assertEqual(str(company), company.name)
With a model created, we can move on to creating a serializer for handling the data going in and out of our app for the Company model.
Serializer
Django Rest Framework utilizes serializers to deal with changing over information between JSON or XML and local Python objects. There are various useful serializers we can import that will make serializing our items simpler. The most widely recognized one we use is a ModelSerializer, which advantageously can be utilized to serialize information for Company objects:
# serializers.py
from rest_framework.serializers import ModelSerializer
from .models import Company
class CompanySerializer(ModelSerializer):
class Meta:
model = Company
fields = (
'id', 'name', 'description', 'website', 'street_line_1', 'street_line_2',
'city', 'state', 'zipcode'
)
That is all that’s required for defining a serializer, though a lot more customization can be added, such as:
- outputting fields that don’t exist on the model (maybe something like is_new_company, or other data that can be calculated on the backend)
- custom validation logic for when data is sent to the endpoint for any of the fields
- custom logic for creates (POST requests) or updates (PUT or PATCH requests)
It’s also beneficial to add a simple test for our serializer, making sure that the values for each of the fields in the serializer match the values for each of the fields on the model:
# tests/test_serializers.py
from django.test import TestCase
from ..serializers import CompanySerializer
from .factories import CompanyFactory
class CompanySerializer(TestCase):
def test_model_fields(self):
"""Serializer data matches the Company object for each field."""
company = CompanyFactory()
for field_name in [
'id', 'name', 'description', 'website', 'street_line_1', 'street_line_2',
'city', 'state', 'zipcode'
]:
self.assertEqual(
serializer.data[field_name],
getattr(company, field_name)
)
View
The view is the layer wherein we attach a URL to a queryset, and a serializer for each article in the queryset. Django Rest Framework again gives supportive articles that we can use to characterize our view. Since we need to make an API endpoint for perusing, making, and refreshing Company objects, we can utilize Django Rest Framework mixins for such activities. Django Rest Framework provides a ModelViewSet which as a matter of course permits treatment of POST, PUT, PATCH, and DELETE demands, however since we don't have to deal with DELETE demands, we can utilize the important mixins for every one of the activities we need:
# views.py
from rest_framework.mixins import (
CreateModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin
)
from rest_framework.viewsets import GenericViewSet
from .models import Company
from .serializers import CompanySerializer
class CompanyViewSet(GenericViewSet, # generic view functionality
CreateModelMixin, # handles POSTs
RetrieveModelMixin, # handles GETs for 1 Company
UpdateModelMixin, # handles PUTs and PATCHes
ListModelMixin): # handles GETs for many Companies
serializer_class = CompanySerializer
queryset = Company.objects.all()
And to hook up our viewset to a URL:
# urls.py
from django.conf.urls import include, re_path
from rest_framework.routers import DefaultRouter
from .views import CompanyViewSet
router = DefaultRouter()
router.register(company, CompanyViewSet, base_name='company')
urlpatterns = [
re_path('^', include(router.urls)),
]
Now we have an API endpoint that allows making GET, POST, PUT, and PATCH requests to read, create, and update Company objects. In order to make sure it works just as we expect, we add some tests:
# tests/test_views.py
from django.test import TestCase
from django.urls import reverse
from rest_framework import status
from .factories import CompanyFactory, UserFactory
class CompanyViewSetTestCase(TestCase):
def setUp(self):
self.user = UserFactory(email='testuser@example.com')
self.user.set_password('testpassword')
self.user.save()
self.client.login(email=self.user.email, password='testpassword')
self.list_url = reverse('company-list')
def get_detail_url(self, company_id):
return reverse(self.company-detail, kwargs={'id': company_id})
def test_get_list(self):
"""GET the list page of Companies."""
companies = [CompanyFactory() for i in range(0, 3)]
response = self.client.get(self.list_url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
set(company['id'] for company in response.data['results']),
set(company.id for company in companies)
)
def test_get_detail(self):
"""GET a detail page for a Company."""
company = CompanyFactory()
response = self.client.get(self.get_detail_url(company.id))
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['name'], company.name)
def test_post(self):
"""POST to create a Company."""
data = {
'name': 'New name',
'description': 'New description',
'street_line_1': 'New street_line_1',
'city': 'New City',
'state': 'NY',
'zipcode': '12345',
}
self.assertEqual(Company.objects.count(), 0)
response = self.client.post(self.list_url, data=data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Company.objects.count(), 1)
company = Company.objects.all().first()
for field_name in data.keys():
self.assertEqual(getattr(company, field_name), data[field_name])
def test_put(self):
"""PUT to update a Company."""
company = CompanyFactory()
data = {
'name': 'New name',
'description': 'New description',
'street_line_1': 'New street_line_1',
'city': 'New City',
'state': 'NY',
'zipcode': '12345',
}
response = self.client.put(
self.get_detail_url(company.id),
data=data
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# The object has really been updated
company.refresh_from_db()
for field_name in data.keys():
self.assertEqual(getattr(company, field_name), data[field_name])
def test_patch(self):
"""PATCH to update a Company."""
company = CompanyFactory()
data = {'name': 'New name'}
response = self.client.patch(
self.get_detail_url(company.id),
data=data
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
# The object has really been updated
company.refresh_from_db()
self.assertEqual(company.name, data['name'])
def test_delete(self):
"""DELETEing is not implemented."""
company = CompanyFactory()
response = self.client.delete(self.get_detail_url(company.id))
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
As the application turns out to be increasingly entangled, we include greater usefulness (and more tests) to deal with things like consents and required fields. For a speedy method to constrain consents to validated clients, we add the accompanying to our settings document:
# settings file
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAuthenticated',)
}
And add a test that only permissioned users can access the endpoint:
# tests/test_views.py
from django.test import TestCase
from django.urls import reverse
from rest_framework import status
from .factories import CompanyFactory, UserFactory
class CompanyViewSetTestCase(TestCase):
...
def test_unauthenticated(self):
"""Unauthenticated users may not use the API."""
self.client.logout()
company = CompanyFactory()
with self.subTest('GET list page'):
response = self.client.get(self.list_url)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
with self.subTest('GET detail page'):
response = self.client.get(self.get_detail_url(company.id))
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
with self.subTest('PUT'):
data = {
'name': 'New name',
'description': 'New description',
'street_line_1': 'New street_line_1',
'city': 'New City',
'state': 'NY',
'zipcode': '12345',
}
response = self.client.put(self.get_detail_url(company.id), data=data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
# The company was not updated
company.refresh_from_db()
self.assertNotEqual(company.name, data['name'])
with self.subTest('PATCH):
data = {'name': 'New name'}
response = self.client.patch(self.get_detail_url(company.id), data=data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
# The company was not updated
company.refresh_from_db()
self.assertNotEqual(company.name, data['name'])
with self.subTest('POST'):
data = {
'name': 'New name',
'description': 'New description',
'street_line_1': 'New street_line_1',
'city': 'New City',
'state': 'NY',
'zipcode': '12345',
}
response = self.client.put(self.list_url, data=data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
with self.subTest('DELETE'):
response = self.client.delete(self.get_detail_url(company.id))
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
# The company was not deleted
self.assertTrue(Company.objects.filter(id=company.id).exists())
As our venture develops, we can alter these authorizations, make them progressively explicit, and keep on including greater unpredictability, however for the time being, these are sensible defaults to begin with.
Also Read:-How To Build a Text-to-Speech App with Web Speech API
Conclusion
Adding an API endpoint to an undertaking can take a lot of time, yet with the Django Rest Framework instruments, it tends to be accomplished all the more rapidly, and be very much tried. Django Rest Framework gives supportive apparatuses that we've utilized at Caktus to make numerous endpoints, so our procedure has become significantly increasingly effective, while as yet keeping up great coding rehearses. In this manner, we've had the option to center our endeavors in different submits in request to extend our capacities to develop sharp web applications.