0. Introduction
A like, favorite, or bookmark system is one of the most common features in modern web applications. You see it everywhere: users like blog posts, favorite products, bookmark tutorials, save articles for later, star repositories, and collect content they want to revisit. At first sight, this feature may appear small, almost trivial. It is often represented by a heart icon, a star, or a bookmark symbol with a counter beside it. But technically and architecturally, it teaches some very important ideas in Django: user-to-content relationships, many-to-many logic, efficient querying, authentication, toggling actions, database constraints, and user experience design.
This is why building this kind of system is such a useful exercise. It is not just about adding a heart button to a page. It is about understanding how a user interacts with content in a persistent and structured way. Once you understand this topic well, you can build features such as saved posts, wishlists, read-later systems, upvotes, reaction systems, and personalized dashboards much more confidently.
In this tutorial, we will go deep into the design and implementation of a like, favorite, and bookmark system in Django. We will explain what each variation means, how to model the data properly, how to build views and templates, how to protect the logic with authentication, how to avoid duplicates, how to count interactions efficiently, and how to think about scaling and good design choices. The goal is not only to make the feature work, but to understand the reasoning behind each approach.
1. What Is the Difference Between Like, Favorite, and Bookmark?
These three words are often used together, but they do not always mean the same thing.
Like
A like usually means a quick positive reaction. It is public or semi-public in many systems, and it often contributes to popularity metrics. For example, a blog post may show “25 likes.”
A like is generally lightweight and often used for engagement.
Favorite
A favorite often means stronger preference than a like. It may indicate that the user especially values the content. In some systems, “favorite” is almost the same as “save,” while in others it behaves more like a personal collection.
A favorite is often more personal and sometimes used to create curated lists.
Bookmark
A bookmark usually means “save this for later.” It is often private and more utility-oriented than emotional. A bookmark system is very useful in content-heavy platforms because users want to return to something later.
A bookmark is often less about approval and more about organization.
So although these three features can be implemented with similar technical structures, their product meaning can differ. That meaning affects naming, UI, privacy, and business logic.
2. Why These Systems Are Great Django Features
A like, favorite, or bookmark system is valuable in Django because it teaches a specific kind of relationship: a user interacts with an object, and that interaction should be stored and retrievable later.
This means the application must answer questions like:
- Has this user liked this post?
- How many users liked this tutorial?
- Which articles has this user bookmarked?
- Can the user remove a favorite later?
- How do we show the right icon state in the template?
- How do we prevent the same user from liking the same post twice?
These questions lead us naturally into model design, efficient queries, and permission logic.
3. The Core Idea: A Relationship Between User and Content
At the heart of all these systems is one idea:
A user has a relationship with a content object.
That content object might be:
- a blog post
- a product
- a tutorial
- a lesson
- a video
- a document
The relationship might be:
- liked
- favorited
- bookmarked
So technically, what we need is not just a button. We need a data model that stores the relationship between the user and the target object.
This is why these systems are usually implemented with:
- a ManyToManyField
- or an explicit intermediate model with
ForeignKeys
Each choice has advantages.
4. The Simplest Case: Likes on Blog Posts
Let us begin with a blog post example.
models.py
from django.db import models
from django.conf import settings
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
content = models.TextField()
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.titleNow we want users to like posts.
5. First Approach: Using ManyToManyField
A simple way to implement likes is to add a many-to-many field directly on the Post model.
models.py
from django.db import models
from django.conf import settings
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
content = models.TextField()
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
likes = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name='liked_posts',
blank=True
)
def __str__(self):
return self.title
def total_likes(self):
return self.likes.count()6. Deep Explanation of ManyToManyField
This field means:
- one post can be liked by many users
- one user can like many posts
That is exactly the relationship we need.
The related_name='liked_posts' lets us access all posts liked by a user with:
request.user.liked_posts.all()The blank=True means a post can exist with no likes yet, which is perfectly normal.
This is a clean and simple design when you only need the fact that a like exists. If you do not need extra metadata such as when the like happened or what type of reaction it was, this approach is often enough.
7. Why ManyToManyField Is a Natural Fit
A like is fundamentally a many-to-many relationship. The same user can like multiple posts, and the same post can be liked by multiple users.
Instead of manually building this relationship from scratch, Django gives us a high-level abstraction with ManyToManyField. Behind the scenes, Django creates an intermediate table to store the relationships.
So while the model code looks simple, Django is actually managing a join table for you.
This is one of the strengths of Django’s ORM: it lets you work with complex relationships in a very readable way.
8. Running Migrations
After adding the likes field, run:
python manage.py makemigrations
python manage.py migrateThis creates the required database structure.
Even though you do not see the join table explicitly in your model, Django creates it automatically.
9. Building the Like Toggle View
A like button usually behaves like a toggle:
- if the user has not liked the post yet, clicking adds the like
- if the user already liked the post, clicking removes it
This is more user-friendly than having separate “like” and “unlike” actions.
views.py
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect
from .models import Post
@login_required
def toggle_like(request, slug):
post = get_object_or_404(Post, slug=slug)
if request.user in post.likes.all():
post.likes.remove(request.user)
else:
post.likes.add(request.user)
return redirect('post_detail', slug=post.slug)10. Deep Explanation of the Toggle Logic
This logic is simple but very powerful.
if request.user in post.likes.all():
This checks whether the current user already belongs to the set of users who liked the post.
If yes, the user is removing their like.
post.likes.remove(request.user)
This removes the relationship from the many-to-many table.
post.likes.add(request.user)
This creates the relationship.
So the system behaves as a toggle. This pattern is very common for likes, favorites, and bookmarks because it matches how users expect these buttons to behave.
11. Why login_required Is Important
Only authenticated users should usually be able to like, favorite, or bookmark content.
Why?
Because these features represent a personal relationship between a user account and a content item. If the user is anonymous, there is no persistent identity to associate with the action.
Using:
@login_requiredensures that only logged-in users can interact.
This also prevents ambiguity and helps with personalization later.
12. The Post Detail View with Like State
To render the button correctly, the template often needs to know whether the current user already liked the post.
views.py
from django.shortcuts import render, get_object_or_404
from .models import Post
def post_detail(request, slug):
post = get_object_or_404(Post, slug=slug)
user_has_liked = False
if request.user.is_authenticated:
user_has_liked = post.likes.filter(id=request.user.id).exists()
context = {
'post': post,
'user_has_liked': user_has_liked,
}
return render(request, 'blog/post_detail.html', context)13. Why .exists() Is Better Than Loading Everything
Notice this line:
post.likes.filter(id=request.user.id).exists()This is better than doing:
request.user in post.likes.all()inside the detail view, because .exists() generates a more efficient database query. It asks only whether at least one matching record exists, instead of loading the full liked-users set into Python.
This kind of small optimization matters more as your database grows.
It also shows a good professional instinct: when you only need a yes/no answer, use .exists().
14. The Template for the Like Button
post_detail.html
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
<p>{{ post.total_likes }} Like{{ post.total_likes|pluralize }}</p>
{% if user.is_authenticated %}
<form method="post" action="{% url 'toggle_like' post.slug %}">
{% csrf_token %}
<button type="submit">
{% if user_has_liked %}
Unlike
{% else %}
Like
{% endif %}
</button>
</form>
{% else %}
<p><a href="{% url 'login' %}">Log in</a> to like this post.</p>
{% endif %}
15. Deep Explanation of the Template
This template does three things:
It shows the total number of likes
{{ post.total_likes }}That gives social proof and engagement feedback.
It changes the button label based on the current user state
If the user already liked the post, it shows “Unlike.” Otherwise, it shows “Like.”
This gives clear feedback and avoids confusion.
It shows a login message for anonymous users
This is a good UX detail. Even though the backend already protects the action with login_required, the template still explains what is happening visually.
This is a good example of combining backend correctness with frontend clarity.
16. The Same Structure for Favorites and Bookmarks
Now that we understand likes, favorites and bookmarks become much easier.
Technically, they can be implemented almost the same way.
For example:
Favorites
favorites = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name='favorite_posts',
blank=True
)Bookmarks
bookmarks = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name='bookmarked_posts',
blank=True
)Then you create toggle views just like for likes.
This is why these features are often discussed together: the underlying model pattern is the same.
17. Why Product Meaning Still Matters
Even if likes, favorites, and bookmarks are technically similar, you should still name them according to product meaning.
For example:
- likes may be visible publicly and contribute to popularity
- bookmarks may be private and shown only in the user dashboard
- favorites may be used to build curated collections
So you should not choose the word only based on code convenience. You should choose it based on what the feature means for the user.
Good software design includes domain vocabulary, not just working code.
18. Creating a Bookmark System Properly
Suppose your platform has tutorials, and users want to save them for later.
models.py
from django.db import models
from django.conf import settings
class Tutorial(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
body = models.TextField()
bookmarks = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name='bookmarked_tutorials',
blank=True
)
def __str__(self):
return self.title
def total_bookmarks(self):
return self.bookmarks.count()This gives you a simple saved-for-later system.
19. Listing a User’s Saved Items
One of the biggest benefits of bookmark or favorite systems is that they allow personalized dashboards.
Example view:
views.py
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
@login_required
def bookmarked_tutorials(request):
tutorials = request.user.bookmarked_tutorials.all()
return render(request, 'tutorials/bookmarked_list.html', {
'tutorials': tutorials
})This lets users see all items they saved.
This is where personalization starts becoming visible in the application.
20. Why Personal Collections Are Powerful
A bookmark or favorite feature does more than record an action. It creates a personal content collection.
That makes the platform feel much more useful, because the user is no longer just consuming content. They are organizing it.
This is a big product improvement because it increases return visits, user engagement, and personal relevance.
In many cases, a bookmark system is even more practically valuable than a like system.
21. Second Approach: Explicit Intermediate Model
So far, we have used ManyToManyField, which is simple and elegant. But sometimes you need more control.
For example, you may want to store:
- when the action happened
- whether it is private
- the type of reaction
- additional metadata
In those cases, an explicit model is better.
Example Like model
from django.db import models
from django.conf import settings
class Like(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
post = models.ForeignKey('Post', on_delete=models.CASCADE, related_name='like_objects')
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = ('user', 'post')
def __str__(self):
return f"{self.user} liked {self.post}"22. Why Use an Explicit Model?
An explicit model gives you more power.
With ManyToManyField, Django manages the relationship table automatically, but you do not easily attach your own extra fields.
With an explicit model, the relationship becomes a first-class object in your system.
That means you can store things like:
- timestamp
- source of the action
- visibility rules
- note or folder category for bookmarks
- soft-delete flags
- analytics data
So the explicit model is better when the relationship itself has meaning beyond simple existence.
23. Why unique_together Is Important
This line matters a lot:
unique_together = ('user', 'post')It prevents the same user from liking the same post multiple times.
This is extremely important for data integrity.
Even if your view logic already tries to avoid duplicates, database-level protection is still valuable. Application logic can fail or be bypassed, but database constraints are stronger.
This is a good general rule:
If duplicates would be invalid, protect against them at the database level too.
24. Toggle Logic with an Explicit Model
views.py
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect
from .models import Post, Like
@login_required
def toggle_like(request, slug):
post = get_object_or_404(Post, slug=slug)
like, created = Like.objects.get_or_create(
user=request.user,
post=post
)
if not created:
like.delete()
return redirect('post_detail', slug=slug)25. Deep Explanation of get_or_create()
get_or_create() is one of Django’s most useful ORM helpers.
Here it means:
- if a like already exists for this user and post, retrieve it
- otherwise create it
The returned created boolean tells us what happened.
If created is True, the user has just liked the post.
If created is False, the relationship already existed, so we delete it to perform the “unlike.”
This creates a clean toggle flow with minimal code.
26. Which Approach Is Better?
This is an important design question.
Use ManyToManyField when:
- the relationship is simple
- you only need existence and count
- you want easy and readable code
- no extra metadata is needed
Use an explicit intermediate model when:
- you need timestamps or other fields
- you want stronger control over the relationship
- the relationship itself has business meaning
- future expansion is likely
For small and medium projects, ManyToManyField is often enough.
For more serious systems, especially bookmarks and favorites with extra data, an explicit model can be the better long-term choice.
27. Bookmarks with Extra Metadata
Suppose you want users not only to bookmark articles, but also to organize them into folders later. That suggests the relationship is more than a simple yes/no state.
An explicit bookmark model becomes a better design.
models.py
from django.db import models
from django.conf import settings
class Bookmark(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
article = models.ForeignKey('Article', on_delete=models.CASCADE, related_name='bookmark_objects')
created_at = models.DateTimeField(auto_now_add=True)
note = models.CharField(max_length=255, blank=True)
class Meta:
unique_together = ('user', 'article')
def __str__(self):
return f"{self.user} bookmarked {self.article}"
This gives you room to grow later.28. Counting Efficiently
A like or favorite count is often displayed publicly. For example:
- “15 likes”
- “42 users saved this”
With ManyToManyField, you can count like this:
post.likes.count()With an explicit model:
post.like_objects.count()This is fine for many situations, but if you display many items on a listing page, repeated counts can become expensive.
That is where annotation can help.
Example with annotation
from django.db.models import Count
posts = Post.objects.annotate(num_likes=Count('likes'))Then in the template:
{{ post.num_likes }}This is often more efficient when rendering lists of many posts.
29. Avoiding N+1 Query Problems
Suppose you list many posts and for each one you show:
- number of likes
- whether the current user liked it
If you are not careful, this can generate many queries.
Some optimization ideas:
- use
annotate()for counts - prefetch related users if needed
- use efficient existence checks
- avoid calling
.count()repeatedly inside loops without thinking
Performance may not matter much in a toy project, but once you have more data, this feature can become a hot spot.
That is why building this system is educational: it forces you to think beyond raw correctness.
30. AJAX and Better UX
So far, our toggle views redirect back to the detail page. That is simple and perfectly valid. But in modern interfaces, users often expect the heart or bookmark icon to update instantly without a full page reload.
That is where AJAX, HTMX, or JavaScript fetch requests can improve the experience.
The backend logic stays almost the same, but instead of redirecting, the server returns JSON or a small HTML fragment.
For example, a JSON response could include:
- whether the item is now liked
- the new count
This makes the interface feel smoother and more modern.
For learning, though, it is better to first master the normal Django request/response version before adding asynchronous behavior.
31. Privacy Considerations
Likes, favorites, and bookmarks do not always have the same privacy expectations.
Likes
Often public or semi-public. Users may expect popularity metrics.
Favorites
May be public or private depending on the platform.
Bookmarks
Usually private, because they are personal utility tools.
So when designing the UI and dashboard, think carefully:
- Should other people see my bookmarked items?
- Should the like count be visible?
- Should user profiles show favorite items publicly?
These are product questions, but they influence the Django design too.
32. Showing the Right Button State
A feature like this succeeds or fails partly on small UX details.
For example, if the user bookmarked an article, the button should visually show that state.
This may mean:
- filled heart vs empty heart
- “Saved” vs “Save”
- highlighted star vs normal star
- bookmark icon in active color
Technically, this comes from the boolean you pass to the template:
user_has_bookmarked = article.bookmarks.filter(id=request.user.id).exists()Then the template adjusts the button.
This is a good example of backend data directly supporting frontend clarity.
33. Common Beginner Mistakes
Here are some very common mistakes in like/favorite/bookmark systems.
Mistake 1: Forgetting authentication protection
Then anonymous users may try to perform actions that should belong only to accounts.
Mistake 2: Allowing duplicates
If the same user can like the same object multiple times, your counts become meaningless.
Mistake 3: Doing everything in templates
The template should display state, not contain the real business logic.
Mistake 4: Choosing the wrong model design
A simple ManyToManyField may be enough now, but if you already know the relationship needs metadata, an explicit model is better.
Mistake 5: Confusing public engagement with private saving
Likes and bookmarks may need different UI and privacy treatment.
Mistake 6: Inefficient queries on listing pages
Counting or checking state repeatedly without optimization can hurt performance.
These are common because the feature looks small, but it touches data integrity, UX, and performance all at once.
34. Adding a Favorite Products System Example
Let us imagine an e-commerce-style example where users favorite products.
models.py
from django.db import models
from django.conf import settings
class Product(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
description = models.TextField()
price = models.DecimalField(max_digits=10, decimal_places=2)
favorited_by = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name='favorite_products',
blank=True
)
def __str__(self):
return self.name
def total_favorites(self):
return self.favorited_by.count()views.py
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404, redirect
from .models import Product
@login_required
def toggle_favorite_product(request, slug):
product = get_object_or_404(Product, slug=slug)
if product.favorited_by.filter(id=request.user.id).exists():
product.favorited_by.remove(request.user)
else:
product.favorited_by.add(request.user)
return redirect('product_detail', slug=slug)This is structurally almost identical to likes, but semantically it means something different for the user.
35. User Dashboard Pages
One of the best reasons to build bookmarks or favorites is to create personalized user pages.
Examples:
- My liked posts
- My bookmarked tutorials
- My favorite products
Example dashboard view
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
@login_required
def my_favorite_products(request):
products = request.user.favorite_products.all()
return render(request, 'shop/my_favorites.html', {
'products': products
})This is where the system becomes truly useful for the user.
A personal dashboard turns a simple interaction into long-term value.
36. Should You Use Signals?
Sometimes developers ask whether signals should be used for these systems.
Usually, the core like/bookmark action itself should stay explicit in the view or service layer. It is a direct user action, and keeping it explicit makes the system easier to understand.
However, signals might be useful for side effects such as:
- logging the event
- sending a notification
- updating analytics
- clearing caches
So the main relationship toggle is usually clearer in the view, while signals can help with secondary consequences.
37. Advanced Idea: One Generic Saved-Items System
In larger projects, you may eventually want one general “saved items” concept that works across many content types.
For example, users may save:
- posts
- tutorials
- products
- videos
This can be built with Django’s generic relations, but that is a more advanced topic. The important idea is that once you understand the simple version, you can generalize it later.
For now, it is better to build one clear system per model before moving to fully generic cross-model saving.
38. Testing the System
Like, favorite, and bookmark systems should be tested.
Example test for likes:
from django.test import TestCase
from django.contrib.auth import get_user_model
from .models import Post
User = get_user_model()
class PostLikeTest(TestCase):
def test_user_can_like_post(self):
user = User.objects.create_user(username='alice', password='test123')
post = Post.objects.create(
title='Test Post',
slug='test-post',
content='Body',
author=user
)
post.likes.add(user)
self.assertEqual(post.likes.count(), 1)
self.assertTrue(post.likes.filter(id=user.id).exists())Testing matters because it ensures:
- toggles behave correctly
- duplicates are prevented
- counts remain accurate
- refactors do not break behavior
39. A Clean Complete Example
Here is a solid minimal like system using ManyToManyField.
models.py
from django.db import models
from django.conf import settings
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
content = models.TextField()
likes = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name='liked_posts',
blank=True
)
def __str__(self):
return self.title
def total_likes(self):
return self.likes.count()views.py
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, get_object_or_404, redirect
from .models import Post
def post_detail(request, slug):
post = get_object_or_404(Post, slug=slug)
user_has_liked = False
if request.user.is_authenticated:
user_has_liked = post.likes.filter(id=request.user.id).exists()
return render(request, 'blog/post_detail.html', {
'post': post,
'user_has_liked': user_has_liked,
})
@login_required
def toggle_like(request, slug):
post = get_object_or_404(Post, slug=slug)
if post.likes.filter(id=request.user.id).exists():
post.likes.remove(request.user)
else:
post.likes.add(request.user)
return redirect('post_detail', slug=slug)urls.py
from django.urls import path
from .views import post_detail, toggle_like
urlpatterns = [
path('post/<slug:slug>/', post_detail, name='post_detail'),
path('post/<slug:slug>/like/', toggle_like, name='toggle_like'),
]
post_detail.html
<h1>{{ post.title }}</h1>
<p>{{ post.content }}</p>
<p>{{ post.total_likes }} Like{{ post.total_likes|pluralize }}</p>
{% if user.is_authenticated %}
<form method="post" action="{% url 'toggle_like' post.slug %}">
{% csrf_token %}
<button type="submit">
{% if user_has_liked %}
Unlike
{% else %}
Like
{% endif %}
</button>
</form>
{% else %}
<p><a href="{% url 'login' %}">Log in</a> to like this post.</p>
{% endif %}This is already a strong practical foundation.
40. What You Learned in This Tutorial
In this tutorial, you learned that a like, favorite, or bookmark system is fundamentally a structured relationship between users and content. You saw how Django’s ManyToManyField provides a clean way to represent simple interactions such as likes or saves, and how toggle views allow users to add or remove those relationships easily. You also learned when an explicit intermediate model is better, especially when the relationship needs metadata such as timestamps or notes. Along the way, you explored authentication protection, efficient existence checks, counting strategies, personalized dashboard pages, privacy considerations, UX state rendering, and common mistakes such as duplicate relationships or inefficient queries.
Most importantly, you learned to think of these features not as simple buttons, but as meaningful application behaviors with both technical and product implications.
41. Conclusion
A like, favorite, or bookmark system is one of the best Django exercises because it sits exactly at the intersection of user interaction, database design, and user experience. It looks simple on the screen, but behind it is a rich structure of relationships, permissions, counts, and personalized state. Once you understand how to build this feature properly, you are much better prepared to create wishlists, saved searches, reaction systems, reading lists, and many other personalized content features.
The best way to approach this topic is to start with the simplest correct design. If you only need a yes/no relationship, a ManyToManyField is often enough. If the relationship itself carries meaning and metadata, use an explicit model. That balance between simplicity and future flexibility is one of the most important lessons in Django design.
💬 Comments
No comments yet. Be the first to comment!
Login to comment.