DEV Community

Abigail Afi Gbadago for MongoDB

Posted on

Building a REST API with the Django REST Framework and MongoDB

REpresentational State Transfer (REST) APIs are fundamental in building software applications and services because they support efficient scaling and optimization of client-server interactions. REST is a software architectural style that retrieves and transmits user’s requests on the client side and information rendered on the server end in JavaScript Object Notation (JSON) or some other format.

Client requests are executed through Hypertext Transfer Protocol (HTTP) with HTTP verbs (methods):

  • GET: retrieve data sent by the server
  • POST: send and publish information to the server
  • PUT: update the server information
  • PATCH: partially modify an existing resource
  • DELETE: delete information from the server

REST APIs conform to:

  • Client-server architecture.
  • Statelessness.
  • Cacheable data.
  • Uniformity between systems.
  • A layered architecture that structures types of server.

REST APIs are heavily used in mobile and web app development since mobile applications can access them in the background and web apps can benefit from dynamic content that does not require a page reload. Also, third-party integrations such as payment gateways and microservices use REST APIs to interact with different services in larger systems.

Modern software applications using microservices rely on REST APIs that glue everything together. Instead of one application performing various concurrent tasks, which may not be optimal for high performance and scalability, REST APIs can be used with many smaller independent services like payment gateways, authentication, analytics, inventory and the like, with each service following the single-responsibility principle, executing tasks and communicating with each other.

This enables you to build, deploy, and scale each service separately without bringing the entire system down. Without REST APIs, services will be isolated with no means of interaction to execute tasks, and every application will exist in silos and system integrations may be difficult.

What is the Django REST framework?

Django developers have several options for building REST APIs, one of which is the Django REST framework (DRF)—a powerful and flexible toolkit for building web application programming interfaces (APIs). The Django REST framework OAuth package provides both OAuth1 and OAuth2 support, serialization support for both ORM and non-ORM data sources, extensive documentation, and other cool, customizable features.

In this tutorial, we will cover how to build a RESTful API using the official Django MongoDB Backend while highlighting the available features.

Prerequisites

Installation and setup

 python -m venv venv source venv/bin/activate 
Enter fullscreen mode Exit fullscreen mode
  • Install Django MongoDB Backend and DRF

pip install django-mongodb-backend djangorestframework

Configuration

Project structure

After creating your virtual environment (venv) in the previous step:

The django-mongodb-project template is similar to the default Django project template but it includes MongoDB-specific migrations and modifies the settings.py file to configure Django to use an ObjectId as each model's primary key.

After, the directory structure should look like this:

Expected Django project directory structure in VS Code

[Expected Django project directory structure in VS Code ]

  • Create the shipwrecks_api app by running this command:
python manage.py startapp shipwrecks_api --template https://github.com/mongodb-labs/django-mongodb-app/archive/refs/heads/5.1.x.zip 
Enter fullscreen mode Exit fullscreen mode

The django-mongodb-app template ensures that your app.py file includes the line

"default_auto_field = 'django_mongodb_backend.fields.ObjectIdAutoField'" 
Enter fullscreen mode Exit fullscreen mode
  • Then, add the name of the app (shipwrecks_api) , rest framework, and django_filters to the settings.py under the INSTALLED APPS section.

It should look like this:

Installed apps under settings.py
[Installed apps under settings.py]

The final directory structure should look like this:

Directory structure showing Django project and app
[Directory structure showing Django project and app]

Django’s MVC pattern overview

Now, let’s take a step back to understand the architecture pattern of Django which will be useful in the creation of the shipwrecks_api.

The Django project architecture follows the Model View Controller architectural pattern but has a slight variation where you have Models, Templates, and Views (MTV). The MVC pattern in Django consist of:

  • A model: The model is the data layer which defines how data will be structured, enforces validation, and handles interactions with the MongoDB database.
  • A view: The view describes the data that gets presented to the user. It’s not necessarily how the data looks, but which data is presented. The view describes which data you see, not how you see it.
  • A template: The template is the presentation layer and consists of HTML files that define how data should appear to users.

Next, let’s define how our data will be structured based on the sample geospatial dataset. Kindly take a look at the fields within the sample document on that page as we will be data modeling using them in the next step when creating our Django model. This dataset contains fields which hold GeoJSON data.

Defining models

With Django MongoDB Backend, the MongoDB _id field is mapped to Django’s traditional id field—meaning you won’t have both _id and id simultaneously.

This is achieved by the DEFAULT_AUTO_FIELD =django_mongodb_backend.fields.ObjectIdAutoField line in the settings.py file in the django project.

Based on the sample document in the sample geospatial database, our model will look like:

#models.py from django.db import models from django_mongodb_backend.fields import ArrayField from django_mongodb_backend.managers import MongoManager class ShipwreckFeature(models.Model): recrd = models.CharField(max_length=200, blank=True) vesslterms = models.CharField(max_length=200, blank=True) feature_type = models.CharField(max_length=200, blank=True) chart = models.CharField(max_length=200, blank=True) latdec = models.FloatField(null=True, blank=True) londec = models.FloatField(null=True, blank=True) gp_quality = models.CharField(max_length=200, blank=True) depth = models.CharField(max_length=200, blank=True) sounding_type = models.CharField(max_length=200, blank=True) history = models.TextField(blank=True) quasou = models.CharField(max_length=200, blank=True) watlev = models.CharField(max_length=200, blank=True) coordinates = ArrayField( base_field=models.FloatField(), null=True, blank=True ) objects = MongoManager() class Meta: db_table = "shipwrecks_api" managed = False def __str__(self): return f"{self.feature_type} at ({self.latdec}, {self.londec})" 
Enter fullscreen mode Exit fullscreen mode

Learn more about document modeling with Django MongoDB Backend.

Serializers

Introduction to DRF serializers

Serializers allow complex data, such as querysets and model instances, to be converted to native Python datatypes that can then be easily rendered into JSON, XML, or other content types. Serializers also provide deserialization, allowing parsed data to be converted back into complex types, after first validating the incoming data.

The serializers in the REST framework work similarly to Django's Form and ModelForm classes. The DRF provides a serializer class which gives you a powerful and generic way to control the output of your responses, as well as a ModelSerializer class which provides a useful shortcut for creating serializers that deal with model instances and querysets.

In the serializer class below, we use two serializer fields.

  • The SerializerMethodField is a read-only field which gets its value by calling a method on the serializer class it is attached to.
  • The HyperlinkedIdentityField can be applied as an identity relationship, such as the url field on a HyperlinkedModelSerializer (introduces the use of a hyperlinked style between entities). It can also be used for an attribute on the object.

Create serializers

#serializers.py from rest_framework import serializers from .models import ShipwreckFeature class ShipwreckFeatureSerializer(serializers.ModelSerializer): id = serializers.SerializerMethodField(read_only=True) url = serializers.HyperlinkedIdentityField( view_name='shipwreck-detail' ) def get_id(self, obj): """Convert ObjectId to string for API response""" return str(obj.pk) if obj.pk else None class Meta: model = ShipwreckFeature fields = [ 'id', 'url', 'recrd', 'vesslterms', 'feature_type', 'chart', 'latdec', 'londec', 'gp_quality', 'depth', 'sounding_type', 'history', 'quasou', 'watlev', 'coordinates' ] read_only_fields = ['id'] 
Enter fullscreen mode Exit fullscreen mode

Views and URL configuration

Now, let’s create function-based views to display the shipwreck data.

#views.py from rest_framework import viewsets, filters, permissions from rest_framework.response import Response from rest_framework import status from django.shortcuts import get_object_or_404 from bson import ObjectId from bson.errors import InvalidId from .models import ShipwreckFeature from .serializers import ShipwreckFeatureSerializer from django_filters.rest_framework import DjangoFilterBackend class ShipwreckFeatureViewSet(viewsets.ModelViewSet): queryset = ShipwreckFeature.objects.all() serializer_class = ShipwreckFeatureSerializer permission_classes = [permissions.AllowAny] filter_backends = [DjangoFilterBackend, filters.OrderingFilter] lookup_field = 'pk' filterset_fields = ['recrd', 'vesslterms', 'feature_type', 'latdec', 'londec'] ordering_fields = ['latdec', 'londec'] ordering = ['latdec'] http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options'] def get_object(self): """ Override get_object to handle ObjectId validation """ lookup_value = self.kwargs[self.lookup_field] # Validate ObjectId try: if not ObjectId.is_valid(lookup_value): raise InvalidId("Invalid ObjectId format") object_id = ObjectId(lookup_value) except (InvalidId, ValueError): from django.http import Http404 raise Http404("Invalid ObjectId format") # Get the object queryset = self.filter_queryset(self.get_queryset()) obj = get_object_or_404(queryset, pk=object_id) self.check_object_permissions(self.request, obj) return obj def destroy(self, request, *args, **kwargs): """ Override destroy to add better error handling """ try: instance = self.get_object() self.perform_destroy(instance) return Response(status=status.HTTP_204_NO_CONTENT) except Exception as e: return Response( {'error': f'Failed to delete shipwreck: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) 
Enter fullscreen mode Exit fullscreen mode
  • Url configuration to map views in the shipwrecks_api
#urls.py - shipwrecks_api from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import ShipwreckFeatureViewSet router = DefaultRouter() router.register(r'shipwrecks', ShipwreckFeatureViewSet, basename='shipwreck') urlpatterns = [ path('', include(router.urls)), ] 
Enter fullscreen mode Exit fullscreen mode
  • Url configuration to map views in the djangoproject
#urls.py - shipwrecks_api from django.contrib import admin from django.urls import path, include from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView urlpatterns = [ path('admin/', admin.site.urls), path('', include('shipwrecks_api.urls')), # Swagger Docs path('api/schema/', SpectacularAPIView.as_view(), name='schema'), path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), ] 
Enter fullscreen mode Exit fullscreen mode

Using the Python shell to create objects

#sw1 = ShipwreckFeature object for testing sw1 = ShipwreckFeature.objects.create( recrd="R001", vesslterms="RMS Titanic", feature_type="Wrecks - Submerged, not dangerous", chart="US,U1,graph,DNC H140984", latdec=10.123456, londec=-80.654321, gp_quality="A", depth="3840 meters", sounding_type="approximate", history="The RMS Titanic sank on April 15, 1912, after hitting an iceberg, and its wreck lies in the North Atlantic Ocean about 400 nautical miles southeast of the coast of Newfoundland, Canada.", quasou="depth known", watlev="always under water/submerged", coordinates=[-80.654321, 10.123456] ) 
Enter fullscreen mode Exit fullscreen mode
#sw2 = ShipwreckFeature object for testing sw2 = ShipwreckFeature.objects.create( recrd="R002", vesslterms="MV Explorer", feature_type="Wrecks - Submerged, navigational hazard", chart="US,U2,graph,DNC H1409880", latdec=-5.987654, londec=150.4321, gp_quality="B", depth="45 meters", sounding_type="sonar", history="Collided with reef in 1955", quasou="depth approximate", watlev="always under water/submerged", coordinates=[150.4321, -5.987654] ) 
Enter fullscreen mode Exit fullscreen mode
  • Using the Python shell python manage.py shell

Creating a shipwreck object via the Python shell
[Creating a shipwreck object via the Python shell]

Using the Django REST framework web browsable API

  • Run python manage.py runserver and fill the fields of the object via the raw data tab or HTML form.

Examples of raw JSON data to create an object via the raw data option on the DRF web browsable API :

Example 1:

{ "recrd": "R001", "vesslterms": "RMS Titanic", "feature_type": "Wrecks - Submerged, not dangerous", "chart": "US,U1,graph,DNC H140984", "latdec": 10.123456, "londec": -80.654321, "gp_quality": "A", "depth": "3840 meters", "sounding_type": "approximate", "history": "The RMS Titanic sank on April 15, 1912, after hitting an iceberg, and its wreck lies in the North Atlantic Ocean about 400 nautical miles southeast of the coast of Newfoundland, Canada.", "quasou": "depth known", "watlev": "always under water/submerged", "coordinates": "[-80.654321, 10.123456]" } 
Enter fullscreen mode Exit fullscreen mode

Example 2:

{ "recrd": "R002", "vesslterms": "MV Explorer", "feature_type": "Wrecks - Submerged, navigational hazard", "chart": "US,U2,graph,DNC H1409880", "latdec": -5.987654, "Londec": 150.4321, "gp_quality": "B", "depth": "45 meters", "sounding_type": "sonar", "history": "Collided with reef in 1955", "quasou": "depth approximate", "watlev": "always under water/submerged", "coordinates": "[150.4321, -5.987654]" } 
Enter fullscreen mode Exit fullscreen mode

CRUD operations:

The CRUD methods — CREATE, GET/RETRIEVE, UPDATE, and DELETE—can be executed via the DRF web browsable API, Postman, and via the Python shell. Since this tutorial covers the DRF, the examples shown will be done via the DRF web browsable API.

DRF web browsable API page listing all shipwreck features
[DRF web browsable API page listing all shipwreck features]

DRF web browsable API page listing a shipwreck feature by an id
[DRF web browsable API page listing a shipwreck feature by an id]

Filling the fields to create a shipwreck object

DRF web browsable API page after successfully creating a shipwreck
[DRF web browsable API page after successfully creating a shipwreck]

  • PUT/PATCH: /api/shipwrecks/{id}/

http://127.0.0.1:8000/shipwrecks/

In this example, the gp_quality of changed from "A" to "C" and the latdec is changed to 20.83546.

Creating an Shipwreck Feature that will be updated via a PUT/PATCH method
[Updating a Shipwreck Feature via a PATCH method]

Successfully updated the Shipwreck object via PATCH request
[Successfully updated the Shipwreck object via PATCH request]

  • DELETE /api/shipwrecks/{id}/

(Bulk delete is possible by adding it as a custom operation)

DRF web browsable API page deleting a shipwreck feature based on an id
[Deleting a shipwreck feature via an id on the DRF web browsable API]

After clicking the DELETE button on the DRF web browsable API

Successfully deleted object via id on the DRF web browsable API
[Successfully deleted object via id on the DRF web browsable API]

Pagination

Pagination allows you to modify how large result sets are split into individual pages of data to improve readability and performance. To use the default pagination class, add the following lines of code to your settings.py, indicating that you want to display twenty(20) results per page.

REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 20 } 
Enter fullscreen mode Exit fullscreen mode

Django Admin

Create an admin user via the Django Admin interface.

Testing

from django.test import TestCase from rest_framework.test import APITestCase from rest_framework import status from django.urls import reverse from .models import ShipwreckFeature class ShipwreckFeatureModelTest(TestCase): def setUp(self): self.shipwreck = ShipwreckFeature.objects.create( record="R001", vesslterms="test_vessel", feature_type="wreck", chart="test_chart", latdec=40.7128, londec=-74.0060, history="Test shipwreck for testing", coordinates=[-74.0060, 40.7128] ) def test_shipwreck_creation(self): """Test that a shipwreck can be created""" self.assertEqual(self.shipwreck.recrd, "R001") self.assertEqual(self.shipwreck.vesslterms, "test_vessel") self.assertEqual(self.shipwreck.feature_type, "wreck") def test_shipwreck_str_method(self): """Test the string representation of shipwreck""" expected = "wreck at (40.7128, -74.006)" self.assertEqual(str(self.shipwreck), expected) class ShipwreckFeatureAPITest(APITestCase): """Simple tests for the ShipwreckFeature API""" def setUp(self): self.shipwreck = ShipwreckFeature.objects.create( recrd="R002", vesslterms="api_test_vessel", feature_type="wreck", latdec=41.0, londec=-75.0, history="API test shipwreck" ) self.list_url = reverse('shipwreck-list') def test_get_shipwreck_list(self): """Test retrieving list of shipwrecks""" response = self.client.get(self.list_url) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data['results']), 1) def test_create_shipwreck(self): """Test creating a new shipwreck""" data = { 'recrd': 'R003', 'vesslterms': 'new_vessel', 'feature_type': 'wreck', 'latdec': 42.0, 'londec': -76.0, 'history': 'Newly created test shipwreck' } response = self.client.post(self.list_url, data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(ShipwreckFeature.objects.count(), 2) def test_get_single_shipwreck(self): """Test retrieving a single shipwreck""" detail_url = reverse('shipwreck-detail', kwargs={'pk': self.shipwreck.pk}) response = self.client.get(detail_url) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['recrd'], 'R002') 
Enter fullscreen mode Exit fullscreen mode

Documentation

With REST APIs, we can document the endpoints with tools such as Swagger UI, Postman, OpenAPI Generator, and the like. With Django, a popular go-to is Swagger which describes the structure of your APIs so that machines can read them.

To implement documentation with Swagger UI:

  • Run pip install drf-spectacular in your terminal.
  • Add 'drf_spectacular' under INSTALLED_APPS in settings.py .
SPECTACULAR_SETTINGS = { 'TITLE': 'Shipwrecks API', 'DESCRIPTION': 'API for managing shipwreck data with MongoDB backend', 'VERSION': '1.0.0', 'SERVE_INCLUDE_SCHEMA': False, 'COMPONENT_SPLIT_REQUEST': True, 'SCHEMA_PATH_PREFIX': '/api/', } 
Enter fullscreen mode Exit fullscreen mode
  • Add the following to urls.py:
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, path('api/schema/', SpectacularAPIView.as_view(), name='schema'), path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), 
Enter fullscreen mode Exit fullscreen mode

Now, we can access the Shipwreck API docs by running the following command http://127.0.0.1:8000/api/docs/. This gives a visualization of the CRUD endpoints which we can interact with just as with the DRF web browsable API.

Swagger API page listing all CRUD methods for the Shipwreck API
[Swagger API page listing all CRUD methods for the Shipwreck API]

Example: Read method to retrieve all ShipwreckFeature objects.

Swagger API page listing ShipwreckFeature objects
[Swagger API page listing ShipwreckFeature objects]

Example
Creating a Shipwreck Feature object via Swagger.

Swagger API page showing the fields to create a ShipwreckFeature object

Swagger API page showing the created ShipwreckFeature object

[Swagger API page showing the created ShipwreckFeature object]

Wrap-up and further resources

In this tutorial, we looked at REST API architecture, HTTP methods/verbs, the Django REST framework (DRF), how to set up and install dependencies for the API, configuration, structuring the Django project, Django’s MTV pattern, creating the Django app, defining models, URLS, views, tests, CRUD examples with the Shipwreck feature database, and documenting the Shipwreck API using Swagger.

At this point, you have the essentials to create a REST API with the Django REST framework and Django MongoDB Backend. Give it a shot and comment on your experience—looking forward to it!

If you liked this tutorial or have questions or feedback, kindly leave a comment and give this tutorial a reaction up top!

References

Top comments (0)