Build CRUD API with Django REST framework
PythonIntroduction to Building CRUD API with Django REST Framework for a Simple Blog:
Welcome to this tutorial on building a CRUD API with Django REST Framework for a simple blog application. Django REST Framework is a powerful toolkit that allows us to create robust and scalable APIs quickly. In this tutorial, we will explore how to develop a RESTful API that enables basic CRUD operations (Create, Read, Update, Delete) on blog post.
Setup Django
The first step in creating any project is to create the project’s work directory. So navigate to your Desktop or any location on your machine and create a folder named Django_Crud_Project
. After that, open the newly-created folder in an IDE or code editor (I will use Visual Studio Code in this project)
mkdir Django_Crud_Project
cd Django_Crud_Project && code .
Now let’s create a virtual environment to isolate dependencies. To do this, open the integrated terminal in your IDE and run this command.
- Windows OS –
python -m venv venv
- Mac or Linux OS –
python3 -m venv venv
After that, run the command below to activate the virtual environment in the current workspace.
- Windows OS (Command Prompt ) –
venv\Scripts\activate.bat
. - Windows OS (Git Bash) –
venv/Scripts/activate.bat
. - Mac or Linux OS –
source venv/bin/activate
Because we are using both Django and Django REST framework, run this command in the console of the root directory to install them in the virtual environment.
pip install django djangorestframework
django
– A Python web framework that follows the model–template–views architectural pattern.djangorestframework
– A powerful and flexible toolkit for building Web APIs.
django
– A Python web framework that follows the model–template–views architectural pattern.djangorestframework
– A powerful and flexible toolkit for building Web APIs.At this point, we are ready to create the Django project. Run this command to create a Django project named notes
in the current workspace folder. The dot (.) after the project’s name will tell Django-admin to generate the project in the current directory.
django-admin startproject notes .
After the project has been generated, run this command to activate the initial database migrations of the built-in user model.
python manage.py migrate
Now that we’ve generated the Django project and migrated the database schema, let’s start the dev server to make sure we did everything correctly. So, start the Django development server with this command.
python manage.py runserver
This will start the HTTP server on http://127.0.0.1:8000/
. When you visit http://127.0.0.1:8000/ in a new tab, you will see the Django welcome page.
Django provides a reusability feature that we can leverage to organize our code into reusable apps. That means we will create an app to handle every request made to the “notes” endpoints. So, run this command to create a Django app called note_api
.
django-admin startapp node_api
Next, add the rest_framework and note_api apps to the INSTALLED_APPS list inside the notes/settings.py
file:
notes/settings.py
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'note_api', 'rest_framework']
In this section, you’ll create a database model that the API will use to perform the CRUD operations and a serializer model that the Django REST framework will use to convert the models into serialized JSON objects.
Database Model
In Django, a model is a built-in feature that Django uses to generate the underlying database tables, their columns, relationships between the tables, and various constraints.
So, create a models.py
file in the note_api app and add the following model definitions. Instead of using incremental integers for the primary key of the table, we’ll use UUIDs to prevent attackers from scanning the table with a range of integers to explore leakages.
note_api/models.py
import uuidfrom django.db import modelsclass NoteModel(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) title = models.CharField(max_length=255, unique=True) content = models.TextField() category = models.CharField(max_length=100, null=True, blank=True) createdAt = models.DateTimeField(auto_now_add=True) updatedAt = models.DateTimeField(auto_now_add=True) class Meta: db_table = "notes" ordering = ['-createdAt']
def __str__(self) -> str: return self.title
The models.UUIDField()
attribute will tell Django to generate a UUID for the ID field by evoking the uuid4() function.
We added a unique constraint on the title field to ensure that no two records in the table end up with the same title.
The db_table
property provided in the Meta Options will tell Django to change the table name to the specified value. Also, the ordering
property will tell Django to order the lists of objects in ascending order of the createdAt timestamp.
Model Serializer
Serializers in Django REST framework convert the object or list of objects returned by the database into data types that frontend frameworks and API testing tools can understand. Also, serializers can be used to deserialize data into complex types after the incoming request data has been validated.
Django REST framework comes with a handful of serializers but in this tutorial, we’ll only use the ModelSerializer. The ModelSerializer class provides a shortcut to create a serializer whose fields correspond to the Model fields.
In the note_api app directory, create a serializers.py
file and add the serializer below.
note_api/serializers.py
from rest_framework import serializersfrom note_api.models import NoteModelclass NoteSerializer(serializers.ModelSerializer): class Meta: model = NoteModel fields = '__all__'
Inheriting the ModelSerializer class will:
- Automatically generate a set of fields for the NoteSerializer
- Automatically generate validators for the serializer
- Create default implementations of
.create()
and.update()
Create the CRUD API Views in Django
In this section, you’ll create five API views to implement the CRUD (Create, Read, Update, and Delete) functionalities. In Django, views are divided into two major types; function-based views (FBVs) and class-based views (CBVs).
function-based views
– Are functions that leverage the@api_view
decorator and return HTTP responses after processing the business logic of the API.class-based views
– Python objects that allow us to create views from inherited classes.
These views handle the application logic of the API and return information as responses to the client or frontend application. As a matter of best practice, we always keep the application logic in a views.py
file.
To simplify the process, we’ll use the GenericAPIView class provided by the Django REST framework to create the views. Using the GenericAPIView class is quite similar to the regular View class since the incoming request will be delegated to an appropriate handler method such as .get()
or .post()
.
We’ll group the API views under two classes, Notes and NoteDetail. The Notes API view class will handle requests made to the /api/notes
endpoint and the NoteDetail API view class will handle requests made to the api/notes/<str:pk>
endpoint.
Before the CRUD implementation, create a views.py
file inside the note_api app directory and add the following module imports.
note_api/views.py
from rest_framework.response import Responsefrom rest_framework import status, genericsfrom note_api.models import NoteModelfrom note_api.serializers import NoteSerializerimport mathfrom datetime import datetime
Here, you’ll create a Notes class that inherits the GenericAPIView
class to handle POST and GET requests. The Notes view will have two handler methods:
get()
– The get() handler will return a paginated list of records to the client. By default, the handler will only return the first 10 records to the client if the limit and page parameters are absent in the request URL.post()
– This handler will be evoked by the REST framework to add the new record to the database when a POST request hits the server at/api/notes
.
note_api/views.py
class Notes(generics.GenericAPIView): serializer_class = NoteSerializer queryset = NoteModel.objects.all()
def get(self, request): page_num = int(request.GET.get("page", 1)) limit_num = int(request.GET.get("limit", 10)) start_num = (page_num - 1) * limit_num end_num = limit_num * page_num search_param = request.GET.get("search") notes = NoteModel.objects.all() total_notes = notes.count() if search_param: notes = notes.filter(title__icontains=search_param) serializer = self.serializer_class(notes[start_num:end_num], many=True) return Response({ "status": "success", "total": total_notes, "page": page_num, "last_page": math.ceil(total_notes / limit_num), "notes": serializer.data })
def post(self, request): serializer = self.serializer_class(data=request.data) if serializer.is_valid(): serializer.save() return Response({"status": "success", "data": {"note": serializer.data}}, status=status.HTTP_201_CREATED) else: return Response({"status": "fail", "message": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
Here, you’ll create a NoteDetail class that inherits the GenericAPIView
class to handle GET, PATCH, and DELETE requests. The NoteDetail view will have three handler methods:
get()
– This handler will be called to find a record by ID and return the found record to the client or frontend app.patch()
– This handler will be evoked by the REST framework to find a record that matches an ID and update the fields of the found record.delete()
– This handler will be evoked to find a record by ID and delete the found record from the database.
note_api/views.py
Complclass NoteDetail(generics.GenericAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)ete APIViews
note_api/views.py
from rest_framework.response import Responsefrom rest_framework import status, genericsfrom note_api.models import NoteModelfrom note_api.serializers import NoteSerializerimport mathfrom datetime import datetime
class Notes(generics.GenericAPIView): serializer_class = NoteSerializer queryset = NoteModel.objects.all()
def get(self, request): page_num = int(request.GET.get("page", 1)) limit_num = int(request.GET.get("limit", 10)) start_num = (page_num - 1) * limit_num end_num = limit_num * page_num search_param = request.GET.get("search") notes = NoteModel.objects.all() total_notes = notes.count() if search_param: notes = notes.filter(title__icontains=search_param) serializer = self.serializer_class(notes[start_num:end_num], many=True) return Response({ "status": "success", "total": total_notes, "page": page_num, "last_page": math.ceil(total_notes / limit_num), "notes": serializer.data })
def post(self, request): serializer = self.serializer_class(data=request.data) if serializer.is_valid(): serializer.save() return Response({"status": "success", "data": {"note": serializer.data}}, status=status.HTTP_201_CREATED) else: return Response({"status": "fail", "message": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
class NoteDetail(generics.GenericAPIView): queryset = NoteModel.objects.all() serializer_class = NoteSerializer
def get_note(self, pk): try: return NoteModel.objects.get(pk=pk) except: return None
def get(self, request, pk): note = self.get_note(pk=pk) if note == None: return Response({"status": "fail", "message": f"Note with Id: {pk} not found"}, status=status.HTTP_404_NOT_FOUND)
serializer = self.serializer_class(note) return Response({"status": "success", "data": {"note": serializer.data}})
def patch(self, request, pk): note = self.get_note(pk) if note == None: return Response({"status": "fail", "message": f"Note with Id: {pk} not found"}, status=status.HTTP_404_NOT_FOUND)
serializer = self.serializer_class( note, data=request.data, partial=True) if serializer.is_valid(): serializer.validated_data['updatedAt'] = datetime.now() serializer.save() return Response({"status": "success", "data": {"note": serializer.data}}) return Response({"status": "fail", "message": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk): note = self.get_note(pk) if note == None: return Response({"status": "fail", "message": f"Note with Id: {pk} not found"}, status=status.HTTP_404_NOT_FOUND)
note.delete() return Response(status=status.HTTP_204_NO_CONTENT)
Add the CRUD Routes
A request in Django first comes to urls.py
in the main project and then goes to the urls.py
of an app before the matching function in views.py
will be called to process the request.
So, to make users interact with the API views defined in the note_api app, we need to create URLs that map to them.
Add the CRUD API URLs
To add the CRUD API views to the middleware stack, create a urls.py
file in the note_api app directory and add the code below.
note_api/urls.py
from django.urls import pathfrom note_api.views import Notes, NoteDetail
urlpatterns = [ path('', Notes.as_view()), path('<str:pk>', NoteDetail.as_view())]
Add the Base URL of the CRUD App to the Project
Now that we’ve defined the CRUD API routes in the note_api/urls.py
file, let’s add a base URL that references the URLs defined in the note_api app to the notes project.
from django.contrib import adminfrom django.urls import path, include
urlpatterns = [ path('admin/', admin.site.urls), path('api/notes/', include('note_api.urls'))]
Setup CORS in Django
Now that we’ve implemented the CRUD functionalities, let’s configure the Django project to enable CORS on the server. Adding CORS will ensure that the server accepts requests from specified cross-origin domains. Follow the steps below to enable CORS on the Django server.
First, install the django-cors-headers
library.
pip install django-cors-headers
Add the corsheaders
package to the list of INSTALLED_APPS
notes/settings.py
INSTALLED_APPS = [
...
'corsheaders',
...
]
Add this middleware class to listen in on responses. Note: The CorsMiddleware should be placed before any middleware that can generate responses.
notes/settings.py
MIDDLEWARE = [
...,
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
...,
]
Now let’s configure the CORS middleware’s behavior by specifying the domains for CORS. Also, add CORS_ALLOW_CREDENTIALS = True
so that cookies can be allowed in cross-site HTTP requests.
notes/settings.py
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000"
]
CORS_ALLOW_CREDENTIALS = True
After all the above configurations, your notes/settings.py file should look somewhat like this.
notes/settings.py
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!SECRET_KEY = 'django-insecure-zt8*frb&#-fs&qe&x+qr!*z@9$ig)3w!^d#uie-q56@$ny^ap$'
# SECURITY WARNING: don't run with debug turned on in production!DEBUG = True
ALLOWED_HOSTS = []
CORS_ALLOWED_ORIGINS = [ "http://localhost:3000"]CORS_ALLOW_CREDENTIALS = True
# Application definition
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'corsheaders', 'note_api', 'rest_framework']
MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',]
ROOT_URLCONF = 'notes.urls'
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, },]
WSGI_APPLICATION = 'notes.wsgi.application'
# Database# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', }}
# Password validation# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', },]
# Internationalization# https://docs.djangoproject.com/en/4.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)# https://docs.djangoproject.com/en/4.1/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
Create the Migration File and Start the Server
Let’s create a database migration file for the NoteModel. The migration files will instruct Django on how to create the database tables. Run this command to generate the migration file.
python manage.py makemigrations
Now run the migrate command to push the migrations to the database
python manage.py migrate
After Django has pushed the schema to the database and synced the database schema with our model, run this command to start the HTTP server.
python manage.py runserver
Test the Django CRUD API
It’s now time to test the Django CRUD API. To do this, we’ll use an API testing tool like Postman, Insomnia, or Thunder client VS Code extension. You can import the collection I used in testing the API into Thunder client or Postman by following the steps below.
These steps are for Postman but the process should be similar for the Thunder client extension.
Step 1: Click on the Import button and then click the Choose Files button.
Step 2: In the file explorer, navigate to the Note App.postman_collection.json
file in the Django project and choose it.
Step 3: Click on the Import button in the Import Elements tab to add the collection.
Create Note
To create a new note, make a POST request to the http://localhost:8000/api/notes/
endpoint with the JSON object.
{
"title": "You'll learn how to build a CRUD API with Django",
"content": "Django REST framework is really easy to use",
"category": "Python"
}
The Django API will then validate the request body, add the new record to the database, and return the newly-created record to the API client or frontend app.
Update Note
To edit an existing note in the database, make a PATCH request to the http://localhost:8000/api/notes/{noteId}
endpoint with the new data provided in the JSON object.
The Django API will validate the request payload, query the database to find the record that matches the provided ID, and update the fields in the found record.
To retrieve a list of records, make a GET request to the http://localhost:8000/api/notes/
endpoint. The Django API has a pagination feature so you can add a limit and page parameters in the URL to get a selected number of records.
If you don’t provide the page and limit parameters in the URL, the Django API will only return the first 10 records.
To delete a note item in the database, make a DELETE request to the http://localhost:8000/api/notes/{noteId}
endpoint.
The Django API will query the database to find the record that matches the provided ID and remove the found record from the database.
After that, a 204 status code will be returned to the client or frontend app to indicate that the record was deleted.
Create Documentation for the CRUD API
It’s a good practice to provide documentation for an API to help other people understand the various endpoints of the API and how to interact with it. We’ll use the drf-yasg
package to automatically generate Swagger/OpenAPI 2.0 schemas from the Django REST framework code.
Install drf-yasg
and coreapi
libraries with this command. The coreapi
package will add schema support for the Django REST framework.
pip install -U drf-yasg[validation] coreapi
In the notes/settings.py
file, add the drf_yasg
package to the list of installed apps.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'corsheaders',
'note_api',
'rest_framework',
'drf_yasg',
To add the Swagger UI and ReDoc URLs to the project, open the notes/urls.py
file and replace its content with the following code.
from django.contrib import admin
from django.urls import path, include, re_path
from rest_framework import permissions
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
schema_view = get_schema_view(
openapi.Info(
title="Notes API",
default_version='v1',
description="Note API built by CodevoWeb",
terms_of_service="https://www.google.com/policies/terms/",
contact=openapi.Contact(email="contact@snippets.local"),
license=openapi.License(name="BSD License"),
),
public=True,
permission_classes=[permissions.AllowAny],
)
urlpatterns = [
path('admin/', admin.site.urls),
path('api/notes/', include('note_api.urls')),
re_path(r'^swagger(?P<format>\.json|\.yaml)$',
schema_view.without_ui(cache_timeout=0), name='schema-json'),
re_path(r'^swagger/$', schema_view.with_ui('swagger',
cache_timeout=0), name='schema-swagger-ui'),
re_path(r'^redoc/$', schema_view.with_ui('redoc',
cache_timeout=0), name='schema-redoc'),
]
After that, start the Django HTTP server and visit http://localhost:8000/swagger/ to see the CRUD API documentation in Swagger docs.
Conclusion
Oops, quite a lot of code. If you made it this far, am proud of you. In this article, you learned how to build a CRUD API with Django REST framework. You also learned how to generate API documentation for the API endpoints.
Build CRUD API with Django REST framework
May 24, 2023