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

Tito AdeoyeTito Adeoye
18 min read

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 & measurements

  • inventory for fabrics

  • orders for order records

  • catalogue 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 like picture = models.ForeignKey(MediaFile, ...)

  • Inventory → add picture = 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:

  1. Views – where we define what actions can happen (list, create, update, delete)

  2. 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 viewsets

  • add 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 token

  • All 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 item

  • GET to /api/catalogue/ → list your items

  • PATCH to /api/catalogue/{id}/ → update

  • DELETE → 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! 🚀

0
Subscribe to my newsletter

Read articles from Tito Adeoye directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tito Adeoye
Tito Adeoye