Complete Guide to the Django Services and Repositories Design Pattern with the Django REST Framework
How to Structure and Maintain Your Web Application Efficiently and Scalably using RESTful APIs and Best Practices
Photo by Kenny Eliason on Unsplash
Introduction to the Django Services and Repositories Design Pattern with Django REST Framework
In the world of software development, code organisation and maintainability are crucial to the long-term success of any project. In particular, when working with frameworks like Django and the Django REST Framework to build robust web applications and APIs, it's essential to follow design patterns that help us keep our code clean, modular and easy to scale.
In this blog, we will explore one of these widely used design patterns: Services and Repositories. This pattern allows us to separate the concerns of data access and business logic, improving the structure and clarity of our code. Through this approach, we not only make our applications easier to maintain and test, but also more flexible and future-proof.
Join us as we break down this pattern step-by-step, from initial project setup to service and repository deployment, and discover how it can transform the way you develop your Django apps.
The realisation of the project is divided into three main sections: the structuring and design of the project; the coding of the structured project; testing.
You can find the complete code and structure of the project in the following GitHub link:
Also, now you can find the second part of this project where we do the testing of the complete app:
Creation and design of the project
In this first section, we will see how to create a new Django project with DRF (Django REST Framework) and we will analyse how the main parts of the project and REST APIs will be structured.
Start and creation of the Django project
Before starting the project, you must install Django and Django REST Framework if you don't already have it:
pip install django djangorestframework
Now that we've installed the main tools we're going to use for the project, we'll create a new Django project using the
django-admin startproject
command:django-admin startproject my_project_blog
This command generates the basic structure of a Django project, including configuration files and a project directory.
Creation of Django applications
For this small blog project, we will have two main functionalities which we will divide into two Django apps:
Posts
andComments
.For this, in the terminal, inside the main project directory (my_project_blog), create two applications:
Posts
andComments
using the python commandmanage.py startapp
:python manage.py startapp posts python manage.py startapp comments
Apps are modular Django components that group related functionalities. In this case,
Posts
will handle blog posts andComments
will handle comments.File settings:
Add the created apps and the DRF to the INSTALLED_APPS
section in the project settings file: /my_project_blog/my_project_blog/settings.py
:
INSTALLED_APPS = [
"rest_framework",
"apps.posts",
"apps.comments",
...
Project structure:
Next I will show you how the project will be structured, you can create all the directories/folders and files (ignore README.md
, .env
, .gitignore
), and then we will fill them with code as we learn.
Main structure:
Structure of the Comments app:
Structure of the Posts app:
System design:
For the realisation of the project we opted for an architecture based on layers and classic design patterns such as Repository and Services.
The Services and Repositories pattern divides business logic (services) and data access logic (repositories) into separate layers:
Layer | Description |
Web Layer | It presents the data to users. In this case, the views are for both REST APIs (using DRF) and web views (using Django Views). Note: REST APIs views handle all the API logic (CRUD, etc), while web views only handle sending data to the user via templates. |
Service layer | Contains the business logic. It communicates with the Repository Layer to retrieve data and performs additional operations (without interacting directly with the database) before returning data to the views or controllers. |
Repository Layer | It is responsible for interacting directly with the database. This layer is responsible for basic CRUD operations. Note: This layer is the only one in charge of interacting directly with the database. |
Model Layer | Data models representing the database tables. |
We will delve a little deeper into each layer later as we code.
Design of REST APIs
REST APIs allow communication between the frontend and the backend. We use the Django REST Framework to create RESTful APIs. This is the structure we have for their respective naming and functions:
HTTP Method | Endpoint | Description |
GET | /posts/ | Gets a list of all posts. |
POST | /posts/ | Create a new post in the database. |
GET | /posts/{post_id}/ | Gets the details of a specific post. |
PUT | /posts/{post_id}/ | Update a specific post. |
DELETE | /posts/{post_id}/ | Delete a specific post. |
GET | /posts/{post_id}/comments/ | Gets a list of comments for a post. |
POST | /posts/{post_id}/comments/ | Create a new comment for a post. |
GET | /posts/{post_id}/comments/{comment_id}/ | Gets the details of a comment on a post. |
PUT | /posts/{post_id}/comments/{comment_id}/ | Update a comment on a post. |
DELETE | /posts/{post_id}/comments/{comment_id}/ | Remove a comment from a post. |
You can learn more about naming your REST Endpoints here:
Coding and deepening
Now that we are clear about the layout of the project, for the second section, we will code our blogging project by delving into several of the layers we talked about earlier.
Model:
The Model represents the data. It defines the database tables. In Django, models are classes that inherit from
models.Model
.For this project we will create two models, one for each Django app:
Modelo | Fields | Description |
Post | 'title', 'content' | It represents a blog post. |
Comment | 'post', 'content' | Represents a comment associated with a post. |
- In the archive:
/my_project_blog/apps/posts/models.py
:
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
- In the archive:
/my_project_blog/apps/comments/models.py
:
from apps.posts.models import Post
class Comment(models.Model):
post = models.ForeignKey(Post, related_name="comments", on_delete=models.CASCADE)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.content[:20]
Views
The View handles user requests and returns responses. Django offers both class-based views (CBV) and function-based views (FBV).
CommentListView
and CommentDetailView
are examples of Class-Based Views (CBV).
Separation of concerns is a design principle that promotes the separation of a program into distinct sections, with each section addressing a separate concern.
As we saw earlier for this project, we separated the views into two:
API REST Views: Handle API-specific logic, such as serialisation, validation and returning JSON responses.
Traditional Django Views (Web Views): They handle rendering templates, session management and other web-specific logic.
For the Posts app:
- In the archive:
/my_project_blog/apps/posts/views/api_views.py
:
from rest_framework import generics
from ..serializers import PostSerializer
from ..services.post_service import PostService
from rest_framework.exceptions import NotFound, ValidationError
from django.core.exceptions import ObjectDoesNotExist
class PostListCreateAPIView(generics.ListCreateAPIView):
"""
API view for listing all posts and creating a new post.
Utilizes Django REST Framework's ListCreateAPIView for listing and creating resources.
"""
serializer_class = PostSerializer # Defines the serializer class used for converting model instances to JSON and vice versa.
def get_queryset(self):
"""
Fetch all posts from the database.
`get_queryset` method specifies the queryset for listing posts.
:return: QuerySet of all Post objects.
"""
return PostService.get_all_posts() # Delegates the database query to the PostService layer.
def perform_create(self, serializer):
"""
Handle the creation of a new post.
`perform_create` is called after validation of the serializer.
:param serializer: Validated serializer containing data for creating a new post.
:raises ValidationError: If the creation of the post fails.
"""
try:
PostService.create_post(serializer.validated_data) # Use PostService to handle creation logic.
except ValidationError as e:
raise ValidationError({"detail": str(e)})
class PostRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView):
"""
API view for retrieving, updating, and deleting a specific post.
Extends RetrieveUpdateDestroyAPIView for detailed operations on a single resource.
"""
serializer_class = PostSerializer # Specifies the serializer class for retrieving, updating, and deleting resources.
def get_object(self):
"""
Retrieve a post object based on the provided post_id.
`get_object` method returns the post instance for the specified post_id.
:return: Post object if found.
:raises NotFound: If the post does not exist.
"""
post_id = self.kwargs.get("post_id") # Extract post_id from the URL kwargs.
try:
post = PostService.get_post_by_id(post_id) # Fetch the post using PostService.
return post
except ObjectDoesNotExist:
raise NotFound("Post not found") # Raise a 404 error if the post does not exist.
except ValidationError as e:
raise NotFound({"detail": str(e)})
def perform_update(self, serializer):
"""
Update an existing post instance.
`perform_update` is called after the serializer's data is validated.
:param serializer: Validated serializer containing data for updating the post.
:raises ValidationError: If the update fails.
"""
post_id = self.kwargs["post_id"] # Extract post_id from the URL kwargs.
try:
PostService.update_post(serializer.validated_data, post_id) # Delegate the update logic to PostService.
except ObjectDoesNotExist:
raise NotFound("Post not found")
except ValidationError as e:
raise ValidationError({"detail": str(e)})
def perform_destroy(self, instance):
"""
Delete a post instance.
`perform_destroy` is called to delete the specified post.
:param instance: The post instance to delete.
:raises NotFound: If the post does not exist.
"""
post_id = self.kwargs["post_id"] # Extract post_id from the URL kwargs.
try:
PostService.delete_post(post_id) # Delegate the deletion logic to PostService.
except ObjectDoesNotExist:
raise NotFound("Post not found")
except ValidationError as e:
raise ValidationError({"detail": str(e)})
- In the archive:
/my_project_blog/apps/posts/views/web_views.py
:
from django.views.generic import ListView, DetailView
from django.http import Http404
from ..models import Post
from ..services.post_service import PostService
from django.core.exceptions import ValidationError
class PostListView(ListView):
"""
Class-based view for listing all posts in the web interface.
Utilizes Django's ListView to handle displaying a list of posts.
"""
model = Post # Specifies the model to be used in the view.
template_name = "posts/post_list.html" # Path to the template for rendering the list of posts.
context_object_name = "posts" # Context variable name to be used in the template.
def get_queryset(self):
"""
Overrides the default get_queryset method to fetch all posts from the service layer.
:return: QuerySet of all Post objects.
"""
return PostService.get_all_posts() # Delegates the database query to the PostService.
class PostDetailView(DetailView):
"""
Class-based view for displaying the details of a single post.
Utilizes Django's DetailView to handle displaying detailed information of a single post.
"""
model = Post
template_name = "posts/post_detail.html" # Path to the template for rendering post details.
context_object_name = "post" # Context variable name to be used in the template.
def get_object(self, queryset=None):
"""
Overrides the default get_object method to fetch a specific post based on post_id.
:param queryset: Optional queryset to filter the object (default is None).
:return: Post object if found.
:raises Http404: If the post does not exist.
"""
post_id = self.kwargs.get("post_id") # Extract post_id from the URL kwargs.
if not post_id:
raise Http404("Post ID not provided.")
try:
post = PostService.get_post_by_id(post_id) # Fetches the post using the PostService.
if post is None:
raise Http404("Post not found.")
return post
except ValidationError as e:
raise Http404(f"Invalid request: {str(e)}")
For the Comments app:
- In the archive:
/my_project_blog/apps/comments/views/api_views.py
:
from rest_framework import generics
from rest_framework.exceptions import NotFound, ValidationError, APIException
from ..serializers import CommentSerializer
from ..services.comment_service import CommentService
class CommentListCreateAPIView(generics.ListCreateAPIView):
serializer_class = CommentSerializer
def get_queryset(self):
"""
Retrieve the 'post_id' from the URL kwargs and fetch comments related to the given post ID using the CommentService.
:return: QuerySet of Comment objects.
:raises: ValidationError if 'post_id' is not provided.
"""
post_id = self.kwargs.get("post_id")
if not post_id:
raise APIException("Post ID is required to fetch comments.")
try:
return CommentService.get_comments_by_post_id(post_id)
except ValueError:
raise APIException("Invalid Post ID format.")
def perform_create(self, serializer):
"""
Create a new comment for the specified post using the CommentService.
:param serializer: Serializer instance with validated data.
:raises: ValidationError if 'post_id' is not provided.
"""
post_id = self.kwargs.get("post_id")
if not post_id:
raise APIException("Post ID is required to create a comment.")
try:
CommentService.create_comment(serializer.validated_data, post_id)
except ValueError:
raise APIException("Invalid Post ID format.")
class CommentRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = CommentSerializer
def get_object(self):
"""
Retrieve the specific comment for the given post ID and comment ID using the CommentService.
:return: Comment object.
:raises: NotFound if the comment does not exist.
:raises: ValidationError if 'post_id' or 'comment_pk' is not provided.
"""
post_id = self.kwargs.get("post_id")
comment_id = self.kwargs.get("comment_pk")
if not post_id or not comment_id:
raise APIException("Post ID and Comment ID are required to fetch the comment.")
try:
comment = CommentService.get_comment_by_post_and_id(post_id, comment_id)
if comment is None:
raise NotFound("Comment not found")
return comment
except ValueError:
raise APIException("Invalid Post or Comment ID format.")
def perform_update(self, serializer):
"""
Update the specified comment using the CommentService.
:param serializer: Serializer instance with validated data.
:raises: ValidationError if 'comment_pk' is not provided.
"""
comment_id = self.kwargs.get("comment_pk")
if not comment_id:
raise APIException("Comment ID is required to update the comment.")
try:
CommentService.update_comment(serializer.validated_data, comment_id)
except ValueError:
raise APIException("Invalid Comment ID format.")
def perform_destroy(self, instance):
"""
Delete the specified comment using the CommentService.
:param instance: Comment instance to be deleted.
:raises: ValidationError if 'comment_pk' is not provided.
"""
comment_id = self.kwargs.get("comment_pk")
if not comment_id:
raise APIException("Comment ID is required to delete the comment.")
try:
CommentService.delete_comment(comment_id)
except ValueError:
raise APIException("Invalid Comment ID format.")
- In the archive:
/my_project_blog/apps/comments/views/web_views.py
:
from django.views.generic import ListView, DetailView
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.exceptions import ValidationError
from ..models import Comment
from ..services.comment_service import CommentService
class CommentListView(ListView):
model = Comment
template_name = "comments/comment_list.html"
context_object_name = "comments"
def get_queryset(self):
"""
Extract 'post_id' from URL parameters to fetch comments associated with a specific post.
:return: QuerySet of Comment objects.
:raises: ValidationError if 'post_id' is not provided.
"""
post_id = self.kwargs.get("post_id")
if not post_id:
raise ValidationError("Post ID is required to fetch comments.")
try:
return CommentService.get_comments_by_post_id(post_id)
except Exception as e:
raise e
def get_context_data(self, **kwargs):
"""
Get the default context from the parent class and add additional context for 'post_id'.
:param kwargs: Additional context parameters.
:return: Context dictionary.
"""
context = super().get_context_data(**kwargs)
context["post_id"] = self.kwargs.get("post_id")
return context
class CommentDetailView(DetailView):
model = Comment
template_name = "comments/comment_detail.html"
context_object_name = "comment"
def get_object(self, queryset=None):
"""
Extract 'post_id' and 'comment_id' from URL parameters to retrieve a specific comment for a post.
:return: Comment object.
:raises: ValidationError if 'post_id' or 'comment_id' is not provided.
:raises: ObjectDoesNotExist if the comment does not exist.
"""
post_id = self.kwargs.get("post_id")
comment_id = self.kwargs.get("comment_id")
if not post_id or not comment_id:
raise ValidationError("Post ID and Comment ID are required to fetch the comment.")
try:
comment = CommentService.get_comment_by_post_and_id(post_id, comment_id)
if comment is None:
raise ObjectDoesNotExist(f"Comment with post_id {post_id} and comment_id {comment_id} not found.")
return comment
except ObjectDoesNotExist as e:
raise e
except Exception as e:
raise e
Repositories
Repositories handle data persistence and encapsulate data access logic. They are defined in separate files (repositories.py
) within each application.
For the Post app:
- In the archive:
/my_project_blog/apps/posts/repositories/post_repository.py
:
from ..models import Post
from django.core.exceptions import ObjectDoesNotExist, ValidationError
class PostRepository:
"""
Repository class for handling data operations related to the Post model.
"""
@staticmethod
def get_all_posts():
"""
Fetch all posts from the database.
:return: QuerySet of all Post objects.
"""
return Post.objects.all()
@staticmethod
def get_post_by_id(post_id):
"""
Fetch a specific post by its primary key (ID).
:param post_id: Primary key of the post to fetch.
:return: Post object if found, None otherwise.
:raises: ValidationError if 'post_id' is not provided.
"""
if not post_id:
raise ValidationError("Post ID is required to fetch the post.")
try:
return Post.objects.get(pk=post_id)
except Post.DoesNotExist:
return None
@staticmethod
def create_post(data):
"""
Create a new post with the provided data.
:param data: Dictionary of data to create the post.
:return: Newly created Post object.
"""
return Post.objects.create(**data)
@staticmethod
def update_post(data, post_id):
"""
Update an existing post with the provided data.
:param data: Dictionary of data to update the post.
:param post_id: Primary key of the post to update.
:return: Updated Post object.
:raises: ValidationError if 'post_id' is not provided.
:raises: ObjectDoesNotExist if the post is not found.
"""
if not post_id:
raise ValidationError("Post ID is required to update the post.")
try:
post = Post.objects.get(pk=post_id)
for attr, value in data.items():
setattr(post, attr, value) # Dynamically update each attribute
post.save()
return post
except Post.DoesNotExist:
raise ObjectDoesNotExist(f"Post with ID {post_id} does not exist.")
@staticmethod
def delete_post(post_id):
"""
Delete a post by its primary key (ID).
:param post_id: Primary key of the post to delete.
:raises: ValidationError if 'post_id' is not provided.
:raises: ObjectDoesNotExist if the post is not found.
"""
if not post_id:
raise ValidationError("Post ID is required to delete the post.")
try:
post = Post.objects.get(pk=post_id)
post.delete()
except Post.DoesNotExist:
raise ObjectDoesNotExist(f"Post with ID {post_id} does not exist.")
For the Comments app:
- In the archive:
/my_project_blog/apps/comments/repositories/comment_repository.py
:
from ..models import Comment
from django.core.exceptions import ObjectDoesNotExist
from django.db.utils import DatabaseError
class CommentRepository:
@staticmethod
def get_comments_by_post_id(post_id):
"""
Retrieve all comments associated with a specific post_id.
:param post_id: The ID of the post to retrieve comments for.
:return: QuerySet of Comment objects.
"""
try:
return Comment.objects.filter(post_id=post_id)
except DatabaseError as e:
# Log the exception (if logging is configured)
# logger.error(f"Database error when retrieving comments for post_id {post_id}: {e}")
raise e
@staticmethod
def get_comment_by_post_and_id(post_id, comment_id):
"""
Retrieve a specific comment by post_id and comment_id.
:param post_id: The ID of the post the comment is associated with.
:param comment_id: The ID of the comment to retrieve.
:return: Comment object if found, None otherwise.
"""
try:
return Comment.objects.get(post_id=post_id, id=comment_id)
except Comment.DoesNotExist:
return None
except DatabaseError as e:
# Log the exception (if logging is configured)
# logger.error(f"Database error when retrieving comment {comment_id} for post_id {post_id}: {e}")
raise e
@staticmethod
def create_comment(data, post_id):
"""
Create a new comment associated with a specific post_id.
:param data: Dictionary containing the data for the new comment.
:param post_id: The ID of the post the comment is associated with.
:return: The newly created Comment object.
"""
try:
return Comment.objects.create(post_id=post_id, **data)
except DatabaseError as e:
# Log the exception (if logging is configured)
# logger.error(f"Database error when creating comment for post_id {post_id}: {e}")
raise e
@staticmethod
def update_comment(data, comment_id):
"""
Update an existing comment identified by comment_id.
:param data: Dictionary containing the data to update the comment with.
:param comment_id: The ID of the comment to update.
:return: The updated Comment object.
"""
try:
comment = Comment.objects.get(pk=comment_id)
for attr, value in data.items():
setattr(comment, attr, value)
comment.save()
return comment
except Comment.DoesNotExist:
return None
except DatabaseError as e:
# Log the exception (if logging is configured)
# logger.error(f"Database error when updating comment {comment_id}: {e}")
raise e
@staticmethod
def delete_comment(comment_id):
"""
Delete an existing comment identified by comment_id.
:param comment_id: The ID of the comment to delete.
:return: True if the comment was successfully deleted, False otherwise.
"""
try:
comment = Comment.objects.get(pk=comment_id)
comment.delete()
return True
except Comment.DoesNotExist:
return False
except DatabaseError as e:
# Log the exception (if logging is configured)
# logger.error(f"Database error when deleting comment {comment_id}: {e}")
raise e
Services
Services contain the business logic and act as intermediaries between views and repositories. They are defined in separate files (services.py
) within each application.
For the Post app:
- In the archive:
/my_project_blog/apps/posts/services/post_service.py
:
from ..repositories.post_repository import PostRepository
from django.core.exceptions import ValidationError, ObjectDoesNotExist
class PostService:
"""
Service class for handling business logic related to the Post model.
"""
@staticmethod
def get_all_posts():
"""
Retrieve all posts from the repository.
:return: QuerySet of all Post objects.
"""
return PostRepository.get_all_posts()
@staticmethod
def get_post_by_id(post_id):
"""
Retrieve a post by its ID from the repository.
:param post_id: Primary key of the post to fetch.
:return: Post object if found, None otherwise.
:raises: ValidationError if 'post_id' is not provided.
:raises: ObjectDoesNotExist if the post does not exist.
"""
if not post_id:
raise ValidationError("Post ID is required to fetch the post.")
post = PostRepository.get_post_by_id(post_id)
if post is None:
raise ObjectDoesNotExist(f"Post with ID {post_id} does not exist.")
return post
@staticmethod
def create_post(data):
"""
Create a new post with the given data.
:param data: Dictionary of data to create the post.
:return: Newly created Post object.
:raises: ValidationError if required data fields are missing.
"""
# Example validation: Ensure title and content are provided
if not data.get("title") or not data.get("content"):
raise ValidationError("Title and content are required to create a post.")
return PostRepository.create_post(data)
@staticmethod
def update_post(data, post_id):
"""
Update an existing post with the given data.
:param data: Dictionary of data to update the post.
:param post_id: Primary key of the post to update.
:return: Updated Post object.
:raises: ValidationError if 'post_id' is not provided.
:raises: ObjectDoesNotExist if the post does not exist.
"""
if not post_id:
raise ValidationError("Post ID is required to update the post.")
if not data:
raise ValidationError("Data is required to update the post.")
return PostRepository.update_post(data, post_id)
@staticmethod
def delete_post(post_id):
"""
Delete a post by its ID from the repository.
:param post_id: Primary key of the post to delete.
:raises: ValidationError if 'post_id' is not provided.
:raises: ObjectDoesNotExist if the post does not exist.
"""
if not post_id:
raise ValidationError("Post ID is required to delete the post.")
return PostRepository.delete_post(post_id)
For the Comments app:
- In the archive:
/my_project_blog/apps/comments/services/comment_service.py
:
from ..repositories.comment_repository import CommentRepository
from django.core.exceptions import ObjectDoesNotExist
from django.db.utils import DatabaseError
class CommentService:
@staticmethod
def get_comments_by_post_id(post_id):
"""
Retrieve all comments associated with a specific post_id.
:param post_id: The ID of the post to retrieve comments for.
:return: QuerySet of Comment objects.
:raises: DatabaseError if there is an error accessing the database.
"""
try:
return CommentRepository.get_comments_by_post_id(post_id)
except DatabaseError as e:
# Log the exception (if logging is configured)
# logger.error(f"Database error when retrieving comments for post_id {post_id}: {e}")
raise e
@staticmethod
def get_comment_by_post_and_id(post_id, comment_id):
"""
Retrieve a specific comment by post_id and comment_id.
:param post_id: The ID of the post the comment is associated with.
:param comment_id: The ID of the comment to retrieve.
:return: Comment object if found, None otherwise.
:raises: DatabaseError if there is an error accessing the database.
"""
try:
return CommentRepository.get_comment_by_post_and_id(post_id, comment_id)
except DatabaseError as e:
# Log the exception (if logging is configured)
# logger.error(f"Database error when retrieving comment {comment_id} for post_id {post_id}: {e}")
raise e
@staticmethod
def create_comment(data, post_id):
"""
Create a new comment associated with a specific post_id.
:param data: Dictionary containing the data for the new comment.
:param post_id: The ID of the post the comment is associated with.
:return: The newly created Comment object.
:raises: DatabaseError if there is an error accessing the database.
"""
try:
return CommentRepository.create_comment(data, post_id)
except DatabaseError as e:
# Log the exception (if logging is configured)
# logger.error(f"Database error when creating comment for post_id {post_id}: {e}")
raise e
@staticmethod
def update_comment(data, comment_id):
"""
Update an existing comment identified by comment_id.
:param data: Dictionary containing the data to update the comment with.
:param comment_id: The ID of the comment to update.
:return: The updated Comment object.
:raises: ObjectDoesNotExist if the comment does not exist.
:raises: DatabaseError if there is an error accessing the database.
"""
try:
comment = CommentRepository.update_comment(data, comment_id)
if comment is None:
raise ObjectDoesNotExist(f"Comment with id {comment_id} does not exist.")
return comment
except ObjectDoesNotExist as e:
# Log the exception (if logging is configured)
# logger.warning(f"Attempted to update non-existent comment {comment_id}: {e}")
raise e
except DatabaseError as e:
# Log the exception (if logging is configured)
# logger.error(f"Database error when updating comment {comment_id}: {e}")
raise e
@staticmethod
def delete_comment(comment_id):
"""
Delete an existing comment identified by comment_id.
:param comment_id: The ID of the comment to delete.
:return: True if the comment was successfully deleted, False otherwise.
:raises: DatabaseError if there is an error accessing the database.
"""
try:
success = CommentRepository.delete_comment(comment_id)
if not success:
raise ObjectDoesNotExist(f"Comment with id {comment_id} does not exist.")
return success
except ObjectDoesNotExist as e:
# Log the exception (if logging is configured)
# logger.warning(f"Attempted to delete non-existent comment {comment_id}: {e}")
raise e
except DatabaseError as e:
# Log the exception (if logging is configured)
# logger.error(f"Database error when deleting comment {comment_id}: {e}")
raise e
Serializers
Serializers convert complex data into native data formats (JSON) and vice versa. Serialisers are an important part of creating REST APIs with DRF which are not usually used in a normal Django project.
For the Post app:
- In the archive:
/my_project_blog/apps/posts/serializers.py
from rest_framework import serializers
from .models import Post
# Serializer for the Post model
# This class is responsible for converting Post instances into JSON data and validating incoming data for creating or updating posts.
class PostSerializer(serializers.ModelSerializer):
# Meta class specifies the model and fields to be used by the serializer.
class Meta:
model = Post # The model associated with this serializer. It tells DRF which model the serializer will be handling.
fields = "__all__" # Specifies that all fields from the model should be included in the serialization and deserialization process.
For the Comments app:
- In the archive:
/my_project_blog/apps/comments/serializers.py
:
from rest_framework import serializers
from .models import Comment
# CommentSerializer is a ModelSerializer that automatically creates fields and methods for the Comment model.
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment # The model that this serializer will be based on.
fields = "__all__" # Automatically include all fields from the Comment model.
Configuration of URLs
URLs are organised into two separate files for the API and web views, allowing a clear separation between accessing data via the API and presenting data via web views. This organisation facilitates code management and maintenance by distinguishing between paths that serve JSON data and paths that present HTML views.
API routes are defined only in the Posts
application to centralise the management of resources related to posts and their comments. By not having its own urls.py
file in the Comments
application, redundancy is reduced and the project structure is simplified by grouping all related API paths in one place. This makes it easier to understand the data flow and the interrelationship between posts and comments, promoting a more coherent and maintainable architecture, and the correct use of REST best practices.
URLs API REST:
- In the archive:
/my_project_blog/apps/posts/urls/api_urls.py
:
from django.urls import path
from ..views.api_views import PostListCreateAPIView, PostRetrieveUpdateDestroyAPIView
from ...comments.views.api_views import (
CommentListCreateAPIView,
CommentRetrieveUpdateDestroyAPIView,
)
urlpatterns = [
# Route for listing all posts or creating a new post
path("", PostListCreateAPIView.as_view(), name="post-list-create"),
# Route for retrieving, updating, or deleting a specific post by post_id
path(
"<int:post_id>/",
PostRetrieveUpdateDestroyAPIView.as_view(),
name="post-retrieve-update-destroy",
),
# Route for listing all comments for a specific post or creating a new comment for that post
path(
"<int:post_id>/comments/",
CommentListCreateAPIView.as_view(),
name="post-comment-create",
),
# Route for retrieving, updating, or deleting a specific comment by comment_pk for a specific post
path(
"<int:post_id>/comments/<int:comment_pk>/",
CommentRetrieveUpdateDestroyAPIView.as_view(),
name="post-comment-retrieve-update-destroy",
),
]
URLs web:
- In the archive:
/my_project_blog/apps/posts/urls/web_urls.py
:
from django.urls import path
from ..views.web_views import PostListView, PostDetailView
from ...comments.views.web_views import CommentListView, CommentDetailView
urlpatterns = [
# Route for listing all posts.
# The URL is "web/posts/", and it maps to PostListView to display a list of all posts.
path("", PostListView.as_view(), name="post-list"),
# Route for displaying details of a specific post identified by post_id.
# The URL is "web/posts/<post_id>/", and it maps to PostDetailView to display details of a single post.
path("<int:post_id>/", PostDetailView.as_view(), name="post-detail"),
# Route for listing all comments for a specific post identified by post_id.
# The URL is "web/posts/<post_id>/comments", and it maps to CommentListView to display a list of comments for the given post.
path(
"<int:post_id>/comments",
CommentListView.as_view(),
name="post-comments",
),
# Route for displaying details of a specific comment identified by comment_id for a specific post.
# The URL is "web/posts/<post_id>/comments/<comment_id>/", and it maps to CommentDetailView to display details of a single comment.
path(
"<int:post_id>/comments/<int:comment_id>/",
CommentDetailView.as_view(),
name="post-comment-detail",
),
]
General project URLs:
- In the archive:
/my_project_blog/my_project_blog/urls.py
:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("posts/", include("apps.posts.urls.web_urls")),
path("api/posts/", include("apps.posts.urls.api_urls")),
]
Web application and API testing
Now, in the third section of this blog, we will create some posts and add several comments to each of the posts to verify the correct functioning of the web application and the API and web URLs.
Execute the project
- Creates Migrations:
python manage.py makemigrations
- Apply migrations:
python manage.py migrate
- Run the Django development server:
python manage.py runserver
Note: Open your browser and navigate to
http://127.0.0.1:8000/
, from now on the urls that I will give you will be after the domain (in this case ourlocalhost:8000
or127.0.0.1:8000
), example: If I give you the url/api/posts/
, in the browser you will have to puthttp://127.0.0.1:8000/api/posts/
orhttp://localhost:8000/api/posts/
.Creation of posts
Access the Django REST API interface:
URL:
/api/posts/
Action: Fill in the form with the details of the post to be created and click on the "POST" button.
Add comments to posts:
Access the Django REST API interface:
URL:
/api/posts/{post_id}/comments/
Action: Fill in the form with the details of the comment to be created for a specific post and click on the "POST" button.
You can repeat these steps to create as many posts and comments as you want.
Also, in this same Django REST API interface you can edit posts or comments or delete them.
Verify the creation of posts and comments:
Having already created several posts and comments for some of these via the REST API, we can see these in the web URLs that interact with the HTML templates.
So that when you enter the given URLs you can see the same design, you can see the HTML code of each template in the HTML files of the project.
Full project code on GitHub:
See the list of posts:
- URL:
/posts/
- URL:
See the details of a specific post:
- URL:
/posts/{post_id}/
- URL:
See comments on a specific post:
- URL:
/posts/{post_id}/comments/
- URL:
View the details of a comment on a specific post:
- URL:
/posts/{post_id}/comments/{comment_id}/
- URL:
Conclusions
Benefits of the Services and Repositories Pattern
Separation of Responsibilities:
Separation of responsibilities is one of the fundamental principles in software design. In the
Services
andRepositories
pattern, we clearly separate data access concerns (repositories) from business logic (services).Benefits:
Clarity and Organisation: The code is organised so that each class and function has a single, clearly defined responsibility. This makes the code easier to understand and maintain.
Maintainability: When the business logic changes, only the services need to be modified, while the repositories remain unchanged. This reduces the number of changes and the risk of introducing errors.
Team Collaboration: In a development team, different members can work at different layers (e.g. one on services and one on repositories) without interfering with each other, facilitating collaboration and integration.
Re-use of the Code:
The pattern encourages code reuse by centralising business logic and data access in specific classes. This avoids code duplication and facilitates code reuse in different parts of the application.
Benefits:
Duplication Reduction: By having business logic in services and data access in repositories, we avoid repeating code in multiple places, making the code more DRY (Don't Repeat Yourself).
Consistency: Reusing the same code in different parts of the application ensures that the same logic and rules are followed everywhere, which increases consistency and reduces errors.
Ease of Update: If you need to change the way data is accessed or business logic is implemented, you only need to update the corresponding repository or service. The other parts of the application that depend on them will automatically benefit from the changes.
Ease of Testing:
The separation of business logic and data access logic facilitates the creation of unit and integration tests. Services and repositories can be tested in isolation, which simplifies the testing process.
Benefits:
Unit testing: By having business logic in services and data access in repositories, it is easier to create unit tests for each component independently. This makes it possible to quickly detect errors and ensure that each part works correctly.
Mocks and Stubs: During testing, it is easy to use mocks and stubs to simulate the behaviour of repositories or services. This allows testing business logic without relying on the database or other external services.
Reduced Testing Complexity: By having clear and well-defined responsibilities, testing becomes less complex and more specific. This improves test coverage and software reliability.
Maintainability and Extensibility:
The
Services
andRepositories
pattern makes code more maintainable and extensible. This is especially important in long-term projects, where requirements may change over time.Benefits:
Code Evolution: When you need to add new functionality, it is easy to extend existing services and repositories without affecting the rest of the application. This allows the code to evolve in a controlled and safe way.
Easy Refactoring: If you identify an improvement in the code structure or business logic implementation, it is easy to refactor services and repositories without great risk. The separation of responsibilities makes it easy to identify which parts of the code need to be changed.
Adaptability to New Technologies: If at some point you decide to change the persistence technology (for example, from a SQL database to a NoSQL one), you only need to modify the repositories without affecting the business logic implemented in the services.
Finally, in closing, I thank you for your time and attention in following this guide to the Services and Repositories design pattern in a Django project with the Django REST Framework. I hope this detailed explanation has given you a solid understanding of how to structure your applications in an efficient and maintainable way.
Implementing this pattern will not only improve the organization of your code, but also facilitate its evolution and scalability as your project grows. If you have any additional questions or opinions on the topic, feel free to post in the comments, they are left open for discussion.
Good luck with your development and best of luck with your future projects!
See you next time!