Putting Django into Practice: A Step‑by‑Step Backend Project


Hello again👋
So… this article should have been released about a month ago — but the past few weeks have basically been:
Anyway, we’re back!
In the last few articles, we explored the foundations of building a Django backend: models, serializers, views, and how they fit together. But it’s one thing to know the pieces — it’s another to see them come alive in a real project.
Instead of building yet another generic blog or page-based CMS, let’s do something a little different: we’ll build a domain‑specific CMS for tailors.
Think of it as the Tailor’s Toolkit: an API that lets tailors:
Save customer details and measurements
Upload and track inventory data e.g. fabrics and how many yards they have
Record orders, prices, and currencies
Upload catalogue images of finished styles
And view useful metrics: total orders, total revenue, fabric usage over time
At first glance, this might feel niche — but under the hood, it’s a classic content management system:
And this project doesn’t stop here. Over time, we’ll use it to explore deeper backend engineering topics:
🐳 Docker: containerize the app and run supporting services like Redis locally
🏃♂️ Celery: send emails and run tasks in the background
⏰ Scheduled jobs: e.g., notify tailors when they hit sales milestones
🗄 Redis caching: speed up expensive endpoints like metrics
🔌Websockets (Django Channels): e.g. facilitate messaging between Tailor and Customer
☁️ Production deployment: from local dev to a real host
Step by step, you’ll see how a real backend project starts simple, then evolves as needs grow — just like in real life.
In this first part, though, we will keep it focused. We’ll design the models, build the API, and add simple metrics.
Plan & design your backend
Before jumping into code, it’s important always to figure out exactly what your backend needs to do. Good architecture starts with a few simple questions:
What features do we want?
What data (models & fields) do we need to store?
What actions should users be able to perform?
Who is allowed to do what?
Let’s answer them.
What are the necessities?
Our CMS should let tailors:
Save information about their customers: names, contact details, measurements
Keep track of fabric inventory: how many yards they have left, plus a photo
Record orders: how much the customer paid, in which currency, and when
Upload photos of finished styles into a catalogue
See quick business metrics: total revenue this month, number of orders, yards used, etc.
To keep our codebase tidy, we’ll split these into Django apps:
customers
for customer profiles & measurementsinventory
for fabricsorders
for order recordscatalogue
for style photos(and possibly)
core
for shared utilities, metrics, and future features like notifications
What models and fields do we need?
We’ll mainly have:
User (Tailor):
Each tailor logs in to manage only their own data.
We can use Django’s built-in User model or extend it, but the key idea:
every customer, fabric, order, and catalogue item belongs to a single tailor.
Customer:
Stores name, phone number, email, address, measurement unit, and a flexible JSON field for measurements (e.g., chest, waist, sleeve).
Each customer is linked to the tailor who created them.
Inventory:
Represents a piece of fabric (could expand on this later on to include others) the tailor owns, with its name, how many yards are left, and a photo.
Also linked to the tailor (and optionally to a specific customer, if needed).
Order:
Records when a customer paid for work: amount, down payment, currency, date, optional notes.
Linked to both the customer and the tailor.
CatalogueItem:
Stores a photo of a finished style the tailor wants to showcase, with a title and optional description.
Also belongs to the tailor.
Metrics:
Not a model, but an endpoint that aggregates data from orders and fabrics.
What actions do we want to support?
For each model:
Create a new record
Retrieve a single record
Update it
Delete it
List all records (filtered by the current logged-in tailor)
For metrics:
- A read-only endpoint that shows data like total revenue, total number of orders, total yards of fabric remaining.
Later on, we can add more advanced actions, like:
Bulk uploads (multiple catalogue items at once)
Search and filtering
Nested routes (e.g., list all orders for a single customer)
Who can access what?
From the very start, we’ll make it multi-tenant:
Each tailor must be logged in
Each tailor can only see, create, update, or delete their own customers, fabrics, orders, and catalogue items
Tailor A shouldn’t even know Tailor B’s data exists
Later, we could:
Add roles (admin vs. staff)
Make some data public (e.g., read-only public catalogue)
Build the app
Alrighty. Now we know what we are building, as well as how we want it to work - time to actually build the thing.
We'll start by creating a new Django project and the separate apps that will handle each part of our CMS.
If you’ve followed my earlier foundational series, you probably remember how to do this. But here’s a quick refresher just in case.
1. Create and activate a virtual environment
python -m venv venv
source venv/Scripts/activate
2. Install Django and Django REST Framework
pip install django djangorestframework
3. Start a new Django project
Replace tailors_toolkit
with whatever project name you like:
django-admin startproject tailors_toolkit . # . keeps it in the current folder instead of making a subfolder.
4. Create the apps
We decided to split the project into four domain apps right from the start:
python manage.py startapp customers
python manage.py startapp inventory
python manage.py startapp orders
python manage.py startapp catalogue
python manage.py startapp users
You might also create a core
app later for metrics or shared utilities.
5. Register the apps
Open tailors_toolkit/settings.py
and add them to INSTALLED_APPS
:
INSTALLED_APPS = [
...
'rest_framework',
'customers',
'inventory',
'orders',
'catalogue',
'users',
]
Defining our models
Now that we’ve set up our Django project and created our apps, let’s define the actual models that will power our tailor’s CMS.
This is where the database structure comes to life: we’ll write Python classes that describe the things our backend manages, and Django will turn them into database tables.
Remember, each model:
Belongs to the currently logged‑in user (tailor)
Has fields that store the data we planned earlier
Can later be extended with methods, validations, or special behaviors
Now, let’s go through each model step by step.
1. User
In users/models.py
:
from django.db import models
from django.contrib.auth.models import AbstractUser
# Create your models here.
class User(AbstractUser):
role_choices = (
('tailor', 'Tailor'), # the tailor / service worker who owns catalogue, customers, etc.
('viewer', 'viewer'), # regular public user who can sign up to browse catalogues or posts
('admin', 'Admin'), # platform admin with full power
('Support', 'support'), # can moderate flagged posts, etc.
)
username = models.CharField(
max_length=150,
unique=True,
null=True, # <- allow null in DB
blank=True # <- allow empty in forms
)
email = models.EmailField(unique=True, null=False, blank=False)
first_name = models.CharField(max_length=60)
last_name = models.CharField(max_length=60, blank=True)
role = models.CharField(max_length=20, choices=role_choices, default='tailor', help_text='Role of the user in the system')
date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.first_name} {self.last_name} ({self.role})"
The other roles will become important in later iterations of this product.
Ensure to tell Django to use this user model. In your settings.py
:
AUTH_USER_MODEL = 'users.User'
2. Customer
The customer model holds the tailor’s client information and their measurements.
from django.db import models
from django.db import models
from django.conf import settings
from phonenumber_field.modelfields import PhoneNumberField
class Customer(models.Model):
MEASUREMENT_UNIT_CHOICES = [
("metric", "Metric (cm)"),
("imperial", "Imperial (inches)"),
]
GENDER_CHOICES = [("male", "Male"), ("female", "Female")]
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="customers"
)
first_name = models.CharField(max_length=60)
last_name = models.CharField(max_length=60, blank=True)
gender = models.CharField(max_length=6, choices=GENDER_CHOICES, blank=True)
email = models.EmailField(blank=True)
address = models.CharField(max_length=255, blank=True)
note = models.TextField(blank=True)
phone_number = PhoneNumberField(blank=True)
measurements = models.JSONField(default=dict, blank=True)
measurement_unit = models.CharField(
max_length=10, choices=MEASUREMENT_UNIT_CHOICES, default="metric"
)
date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=True)
def __str__(self):
return f"{self.first_name} {self.last_name}"
For the customer’s phone number, we won’t just use a simple CharField
.
Instead, we’ll use PhoneNumberField
from the django-phonenumber-field
package.
This helps us validate that the number is real, keep it in a consistent format (like +234...
), and makes it easier to display or filter later.
To install, run:
pip install "django-phonenumber-field[phonenumberslite]"
3. Inventory
Each tailor can track fabrics they own, including photos.
from django.db import models
from django.conf import settings
from media_file.models import MediaFile
# Create your models here.
class Inventory(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="inventory"
)
name = models.CharField(max_length=255)
description = models.TextField(blank=True)
yards = models.FloatField()
date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=True)
def __str__(self):
return self.name
yards
is a float to allow partial yards (e.g., 2.5).
3. Catalogue
from django.db import models
from django.conf import settings
from media_file.models import MediaFile
# Create your models here.
class CatalogueItem(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="catalogue_items",
)
title = models.CharField(max_length=255)
description = models.TextField(blank=True)
date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
4. Order
from django.db import models
from django.conf import settings
from customers.models import Customer
# Create your models here.
class Order(models.Model):
currency_choices = (("NGN", "NGN"), ("USD", "USD"))
status = (
("in_progress", "In progress"),
("completed", "Completed"),
("not_started", "Not started"),
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="orders"
)
customer = models.ForeignKey(
Customer, on_delete=models.CASCADE, related_name="orders"
)
amount = models.DecimalField(max_digits=10, decimal_places=2)
downpayment = models.DecimalField(
max_digits=10,
decimal_places=2,
default=0,
help_text="Amount paid as downpayment",
)
currency = models.CharField(max_length=10, default="NGN")
due_date = models.DateField(help_text="When the order is meant to be delivered")
notes = models.TextField(blank=True)
is_fully_paid = models.BooleanField(
default=False, help_text="Has the full payment been completed?"
)
date_downpayment_paid = models.DateField(
null=True, blank=True, help_text="Date the downpayment was actually paid"
)
date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=True)
def __str__(self):
return f"Order for {self.customer.name} on {self.date_created}"
To better help tailors manage orders, we added:
downpayment
: how much the client paid upfront.date_downpayment_paid
: when they actually paid.due_date
: when the outfit should be ready.is_fully_paid
: shows whether the full amount has been received.
This keeps payment tracking simple, yet flexible.
5. MediaFile
Instead of saving image URLs directly on every model (like User
, CatalogueItem
, Fabric
), we’ll introduce a dedicated MediaFile model.
This is a common real‑world pattern:
Keeps media storage centralized
Lets us track metadata (like upload date, uploader, file type)
Makes it easy to reuse the same media file across different parts of the app
Keeps your database design clean and scalable
To do this:
Create a new app
python manage.py startapp media_file
In media_file/models.py
:
from django.db import models
from django.conf import settings
# Create your models here.
class MediaFile(models.Model):
url = models.URLField()
uploaded_by = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True
)
file_type = models.CharField(max_length=50, blank=True)
date_created = models.DateTimeField(auto_now_add=True)
date_updated = models.DateTimeField(auto_now=True)
def __str__(self):
return self.url
Example: linking a user’s profile picture to a MediaFile
In your users/models.py
:
from media.models import MediaFile
class User(AbstractUser):
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default=ARTISAN)
profile_picture = models.ForeignKey(
MediaFile,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='profile_users'
)
# ... rest of the User class
Now replicate the same idea in:
CatalogueItem
→ add a field likepicture = models.ForeignKey(MediaFile, ...)
Inventory
→ addpicture = models.ForeignKey(MediaFile, ...)
This keeps all images stored consistently — and your backend stays DRY, maintainable, and professional.
Tip: Remember to add “media_file” to INSTALLED_APPS
Apply your new models to the database
Now that we’ve defined all our models (User
, Customer
, Order
, Inventory
, CatalogueItem
, and MediaFile
), we need to create the migrations and apply them so Django updates the actual database.
Run:
python manage.py makemigrations
python manage.py migrate
This tells Django to build the database tables that match the models we just wrote.
Tip: if you add new fields or models later, repeat the same commands to keep your database schema up to date.
Connect DB
Now, we need to connect our Postgres DB (using pgAdmin) to Django. Rather than hardcoding database credentials, we keep them in a .env
file (which we never commit to git; update .gitignore
file).
We load them in settings.py
using python-decouple
, so we can safely change DB config for local, staging, or production.
Step 1: create an .env
file
In your project root (same folder as manage.py
), create a file called, .env
Inside, add your database config:
DB_NAME=mytailor_db
DB_USER=postgres
DB_PASSWORD=supersecretpassword
DB_HOST=localhost
DB_PORT=5432
(change values to match what you actually created in pgAdmin)
Step 2: load env vars in Django settings
Install python-decouple (simple & popular):
pip install python-decouple
In your settings.py
:
from decouple import config
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': config('DB_NAME'),
'USER': config('DB_USER'),
'PASSWORD': config('DB_PASSWORD'),
'HOST': config('DB_HOST'),
'PORT': config('DB_PORT'),
}
}
This reads your real credentials from .env
.
Step 3: tell Django to use PostgreSQL
Make sure you have psycopg2
installed:
pip install psycopg2
(or psycopg2-binary
for local/dev: pip install psycopg2-binary
)
Now your project is connected to your PostgreSQL DB (the one you can manage in pgAdmin)🙌
After this step, you can safely run:
python manage.py migrate
Now, if you get this error (cause I did): FATAL: database "tailor_toolkit" does not exist, you know you need to create a database on the pgAdmin GUI called tailors_toolkit (under a server).
And in your DB,
Alrighty. We’re halfway there.
Next: wiring up URLs and views
Now that our models are in place and migrated, it’s time to expose them through an API.
This is what lets the frontend (or tools like Postman) talk to our backend: create users, update inventory, upload catalogue items, and so on.
In Django REST Framework (DRF), we usually do this in two steps:
Views – where we define what actions can happen (list, create, update, delete)
URLs – which connect HTTP endpoints (like
/api/users/
or/api/orders/
) to those views
A great tip for structure is that for each app (like users
, inventory
, catalogue
):
add a
views.py
for your DRF views or viewsetsadd a
urls.py
to register the routes
Then in your project’s main urls.py
, include those app URLs under something like /api/
.
users/urls.py
from rest_framework.routers import DefaultRouter
from users.views import UserViewSet
router = DefaultRouter()
router.register(r'users', UserViewSet, basename='user')
urlpatterns = router.urls
Do the same as above for catalogue, customers, inventory, orders, and media_file, making sure to update the route, viewset and base names.
main project urls.py (e.g., myproject/urls.py
)
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('users.urls')),
path('api/', include('catalogue.urls')),
path('api/', include('customers.urls')),
path('api/', include('inventory.urls')),
path('api/', include('orders.urls')),
path('api/', include('media_file.urls')),
]
All our app routes are now grouped under /api/
.
For example: /api/users/
, /api/catalogue/
, /api/fabrics/
etc.
Now, draft your views
In each app’s views.py
, start simple with DRF ModelViewSet
.
Using ModelViewSet
from DRF automatically gives you all CRUD operations out of the box:
✅ List (GET /api/things/)
✅ Retrieve (GET /api/things/{id}/)
✅ Create (POST)
✅ Update / partial update (PUT/PATCH)
✅ Delete (DELETE)
users/views.py
from rest_framework import viewsets
from users.models import User
from users.serializers import UserSerializer
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
Do the same as above for catalogue, customers, inventory, orders, and media_file, making sure to update the route, viewset and base names.
Draft your serializers
Now, here are the serializers for all the models we defined — clean, starter code so your API works right away.
users/serializers.py
from rest_framework import serializers
from users.models import User
from media.serializers import MediaFileSerializer
class UserSerializer(serializers.ModelSerializer):
profile_picture = MediaFileSerializer(read_only=True)
class Meta:
model = User
fields = [
'id', 'first_name', 'last_name', 'email', 'role',
'date_created', 'date_updated',
'profile_picture',
]
Using nested MediaFileSerializer
as read-only, so it shows the S3 URL & metadata.
orders/serializers.py
from rest_framework import serializers
from orders.models import Order
class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = "__all__"
Do the same as above for catalogue, customers, inventory, and media_file, making sure to update the class and model and names, as well as the fields to be returned.
Authentication & Permissions
This answers part of the “Who is allowed to do what?” question when we were designing our backend system.
We need:
Auth endpoints: register (signup) & login
To add
IsAuthenticated
permission globally so only logged‑in users can access the APIs
Let’s do it!
Since it’s an API, DRF’s TokenAuthentication is simple & perfect for now.
Later on, in another article, we will replace with JWT or OAuth or Cookies.
Step 1: install & setup tokens
Install DRF’s token auth (usually already installed with DRF):
Add to INSTALLED_APPS
in settings.py
:
INSTALLED_APPS = [
# ...,
'rest_framework',
'rest_framework.authtoken',
]
Then run migrations to create the token table:
python manage.py migrate
Spot authtoken_token✅
Step 2: create auth endpoints
Create a new app:
python manage.py startapp authentication
authentication/views.py
from django.shortcuts import render
# Create your views here.
from rest_framework.authtoken.models import Token
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from django.contrib.auth import authenticate
from users.models import User
from django.utils.crypto import get_random_string
@api_view(["POST"])
@permission_classes([AllowAny])
def register(request):
VALID_ROLES = [choice[0] for choice in User.role_choices]
first_name = request.data.get("first_name")
last_name = request.data.get("last_name")
password = request.data.get("password")
email = request.data.get("email")
role = request.data.get("role", "tailor")
username = request.data.get("username") # optional
if not first_name or not password or not email:
return Response(
{"error": "First name, password and email are required."}, status=400
)
if role not in VALID_ROLES:
return Response(
{"error": f"Invalid role. Must be one of: {VALID_ROLES}"}, status=400
)
# if username not provided, generate one
if not username:
while True:
username = get_random_string(10)
if not User.objects.filter(username=username).exists():
break
user = User.objects.create_user(
username=username,
first_name=first_name,
last_name=last_name,
password=password,
email=email,
role=role,
)
token, created = Token.objects.get_or_create(user=user)
return Response(
{
"token": token.key,
"user": {
"id": user.id,
"username": user.username,
"email": user.email,
"role": user.role,
"first_name": user.first_name,
"last_name": user.last_name,
},
}
)
@api_view(["POST"])
@permission_classes([AllowAny])
def login(request):
email = request.data.get("meail")
password = request.data.get("password")
user = authenticate(email=email, password=password)
if user:
token, created = Token.objects.get_or_create(user=user)
return Response(
{
"token": token.key,
"user": {
"id": user.id,
"username": user.username,
"email": user.email,
"role": user.role,
"first_name": user.first_name,
"last_name": user.last_name,
},
}
)
else:
return Response({"error": "Invalid credentials"}, status=400)
authentication/urls.py
from django.urls import path
from authentication.views import register, login
urlpatterns = [
path('register/', register, name='register'),
path('login/', login, name='login'),
]
Step 3: protect all APIs with IsAuthenticated
In settings.py
:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
Now, all our viewsets (users, catalogue, inventory, orders, media) automatically require the user to be logged in and send their token in the header:
Authorization: Token <token_value>
Step 4: include auth URLs in main urls.py
In the main urls.py
:
Then run migrations to create the token table:
urlpatterns = [
# ... other paths
path('api/auth/', include('authentication.urls')),
]
Now we have:
/api/auth/register/
→ create user & get token/api/auth/login/
→ login & get tokenAll other APIs protected by
IsAuthenticated
Testing the API
Now that our models, views, serializers, and auth endpoints are wired up, it’s time to test everything end‑to‑end.
You can use:
✅ Postman
✅ Django GUI
✅ Or even curl
if you like the terminal
I’m using the GUI. Why? I closed my eyes and pointed.
1. Start your server
python manage.py runserver
You should see:
Starting development server at http://127.0.0.1:8000/
2. Register a user
Method:
POST
URL:
http://127.0.0.1:8000/api/auth/register/
Body (JSON):
{
"first_name": "john",
"last_name": "doe",
"password": "supersecret",
"email": "johndoe@example.com",
"role": "tailor" // optional. tailor is default
}
You should get back:
{
"token": "abcdef123456...",
"user": {
"user_id": 1,
"username": "xxxxx",
"role": "artisan",
"first_name": "john",
"last_name": "doe",
"date_created": "2025-07-27T18:46:27.328025Z",
"date_updated": "2025-07-27T18:46:27.328025Z",
}
3. Log in
POST to
http://127.0.0.1:8000/api/auth/login/
Body:
{
"email": "johndoe@example.com",
"password": "supersecret"
}
You’ll get the same kind of token back.
4. Access your protected APIs
For Postman or —curl, add the Token you get after login to your headers.
Authorization: Token your_token_here
Without this, you’ll get:
{"detail":"Authentication credentials were not provided."}
If you’re on the GUI, like me, add 'rest_framework.authentication.SessionAuthentication'
to your DEFAULT_AUTHENTICATION_CLASSES
:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
And in your view
from django.contrib.auth import get_user_model, login as django_login
def login(request):
email = request.data.get("email")
password = request.data.get("password")
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
return Response({"error": "Invalid credentials"}, status=400)
if user.check_password(password):
# ✅ Create session
django_login(request, user)
# ✅ Create/get token
token, created = Token.objects.get_or_create(user=user)
# ...
Then when you login via Django admin → your browser has a session cookie → browsable API shows you as authenticated.
We’re calling django_login(request, user)
so Django creates a session cookie.
That way, when we use the DRF browsable API in the browser, it sees us as logged in at the top‑right corner — without needing to open Postman or manually add token headers.
This is perfectly fine for development because it makes testing easier and keeps us lazy & happy.
In production, your frontend should instead always send the Authorization: Token <token>
header.
5. Try CRUD
Try:
POST
to/api/catalogue/
→ create a catalogue itemGET
to/api/catalogue/
→ list your itemsPATCH
to/api/catalogue/{id}/
→ updateDELETE
→ delete
Same for inventory, orders, etc.
Example: Create a customer
{
"first_name": "velma",
"last_name": "thorfinn",
"gender": "female",
"email": "velmathorfinn@yahoo.com",
"address": "",
"note": "a note on velma",
"phone_number": "+2349090909090",
"measurements": {
"waist": 100
},
"user": 1
}
6. Check your DB
Open pgAdmin → see tables:
users_user
catalogue_catalogueitem
inventory_inventory
orders_order
media_file_mediafile
authtoken_token
You should see real data inside.
From my example:
Add dashboard metrics
For now, we’ll keep it simple and add this metrics view inside the users
app (or the core
app like I’d originally planned. Oops!).
Later, if our analytics get bigger (e.g., weekly reports, charts, time‑based metrics), we could split them into a dedicated metrics
or analytics
app.
In users/views.py
(or core/views.py
):
# ...other imports
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from orders.models import Order
from inventory.models import Inventory
from catalogue.models import Catalogue
from customers.models import Customer
# class
@api_view(["GET"])
@permission_classes([IsAuthenticated])
def user_metrics(request):
user = request.user
total_orders = Order.objects.filter(user=user).count()
total_revenue = (
Order.objects.filter(user=user, is_fully_paid=True).aggregate(
total=models.Sum("amount")
)["total"]
or 0
)
total_inventory_yards = (
Inventory.objects.filter(user=user).aggregate(total=models.Sum("yards"))[
"total"
]
or 0
)
catalogue_items = Catalogue.objects.filter(user=user).count()
total_customers = Customer.objects.filter(user=user).count()
return Response(
{
"total_orders": total_orders,
"total_revenue": total_revenue,
"total_inventory_yards": total_inventory_yards,
"catalogue_items": catalogue_items,
"total_customers": total_customers,
}
)
In users/urls.py:
from rest_framework.routers import DefaultRouter
from users.views import UserViewSet, user_metrics
from django.urls import path
router = DefaultRouter()
router.register(r"users", UserViewSet, basename="user")
urlpatterns = [
*router.urls,
path("dashboard/", user_metrics, name="dashboard"),
]
That’s it!
From defining our models, to wiring up serializers, views, auth, and even setting up media uploads, we now have a solid, working Django REST Framework backend for our tailor toolkit.
Of course, there’s always room for improvement — and that’s the fun part. For instance:
When creating a customer, the backend should automatically set the
user
field from the logged‑in user (request.user
) instead of relying on the client to send it.We could generate usernames in a cleaner, more meaningful way.
Our permissions could be refined so only owners can modify their own resources.
This project is a solid foundation you can build on — and that’s exactly what we’ll do.
Later, we can explore adding:
Celery for background jobs (like sending email notifications)
Redis for caching
Docker to containerize everything
Even exposing parts of the app publicly so real customers can view catalogs
And yes — you probably noticed: while we prepared the
MediaFile
model for S3 storage, we didn’t actually wire it up to S3 yet. That’s definitely something to explore next.
Remember: building a backend is an iterative process. Start simple, get it working, then make it better.
Thanks for following along — and happy building! 🚀
Subscribe to my newsletter
Read articles from Tito Adeoye directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
