<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Mari - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Mari - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Fri, 09 Oct 2026 06:33:37 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/Techgirlll/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Prevent Race Conditions in Django
 ]]>
                </title>
                <description>
                    <![CDATA[ Let's say you have enough credit left to generate one more image in an AI app. You submit a request in one browser tab, then submit another in a second tab before the first finishes. The app accepts b ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-prevent-race-conditions-in-django/</link>
                <guid isPermaLink="false">6aa477b6e81d2fc1cc117115</guid>
                
                    <category>
                        <![CDATA[ Django ]]>
                    </category>
                
                    <category>
                        <![CDATA[ PostgreSQL ]]>
                    </category>
                
                    <category>
                        <![CDATA[ backend ]]>
                    </category>
                
                    <category>
                        <![CDATA[ race-condition ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Mari ]]>
                </dc:creator>
                <pubDate>Fri, 11 Sep 2026 21:50:46 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/560a0578-8b5d-4109-b1ba-9728e8476d6a.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Let's say you have enough credit left to generate one more image in an AI app. You submit a request in one browser tab, then submit another in a second tab before the first finishes.</p>
<p>The app accepts both.</p>
<p>Behind the scenes, each request passed the credit check. But the application accepted more work than your balance could pay for. What you just experienced is called a race condition.</p>
<p>For a developer, this raises two questions:</p>
<ul>
<li><p>How can both requests pass the check when there's only enough credit for one?</p>
</li>
<li><p>How do you prevent them from spending the same credit?</p>
</li>
</ul>
<p>In this guide, we’ll build a small Django credit system to explore those questions. We’ll reproduce the bug, fix it with database transactions and row locks, and test what happens when two requests compete for the last credit.</p>
<h2 id="heading-what-well-cover">What We’ll Cover</h2>
<ul>
<li><p><a href="#what-concurrency-and-race-conditions-mean">Concurrency, race conditions, and the credit check</a></p>
</li>
<li><p><a href="#how-to-set-up-the-django-project">How to set up the Django project</a></p>
</li>
<li><p><a href="#how-to-reproduce-the-race-condition">How to reproduce the race condition</a></p>
</li>
<li><p><a href="#how-to-protect-the-balance">How to protect the balance</a></p>
</li>
<li><p><a href="#how-to-accept-and-test-image-requests">How to accept and test image requests</a></p>
</li>
<li><p><a href="#common-mistakes-and-next-steps">Common mistakes and next steps</a></p>
</li>
</ul>
<h2 id="heading-who-this-guide-is-for">Who This Guide Is For</h2>
<p>This guide is for developers who understand basic Django but are new to concurrency.</p>
<p>Familiarity with models, migrations, and views will help you follow the examples. You’ll need Python 3.12 and Docker with Compose for the setup shown here. We’ll use Django 5.2, Django REST Framework 3.16, and PostgreSQL 17 because SQLite doesn't implement the row lock we’ll use.</p>
<p>I'll explain the concurrency concepts before we apply them to the code.</p>
<p>Image generation will be simulated throughout the tutorial.</p>
<p>Our API will accept a prompt and return a simulated result. You won't need an AI provider account or a paid API key. This keeps the exercise focused on the credit decision and its database changes.</p>
<p>You can follow the entire example without paying for an image request.</p>
<h2 id="heading-what-concurrency-and-race-conditions-mean">What Concurrency and Race Conditions Mean</h2>
<h3 id="heading-what-is-concurrency">What Is Concurrency?</h3>
<p>Concurrency refers to when two or more tasks make progress during overlapping periods of time.</p>
<p>For example, a server can start processing Request B while Request A waits for a database response. Their instructions don't have to execute at the same instant. An event loop can switch between tasks while one waits, which is one way Python supports concurrent work.</p>
<p>The important detail is that another operation can make progress before the first one finishes.</p>
<h3 id="heading-what-is-a-race-condition">What Is a Race Condition?</h3>
<p>A race condition is a flaw where the correctness of a result depends on the timing or order of concurrent operations.</p>
<p>It can occur when operations share data, and the application doesn't coordinate their access adequately. Each operation may appear correct on its own, yet one can act on information another has already changed. A different execution order can then produce a different, incorrect outcome.</p>
<p>Concurrency creates the opportunity for overlap, and a race condition is a bug that can arise from how the application handles that overlap.</p>
<h3 id="heading-where-else-can-race-conditions-happen">Where Else Can Race Conditions Happen?</h3>
<p>Race conditions can affect counters, user accounts, background jobs, and the results displayed in a browser.</p>
<p>The participants can be two requests from one person, two different users, or automated tasks with no user action at all. The shared resource can be a database row, a file, an in-memory value, or the current state of a page. The examples below illustrate several ways the order can matter.</p>
<p>Look for operations that share state and can interfere before either finishes.</p>
<h4 id="heading-a-view-counter-loses-an-update">A View Counter Loses an Update</h4>
<p>Imagine two requests try to increase a post’s view count from 100.</p>
<p>Both read 100 before either saves a change. Each adds one and writes 101. The counter should have reached 102, so one update disappears.</p>
<p>This is a lost update, and no purchase or limited stock is involved.</p>
<h4 id="heading-two-signups-claim-the-same-username">Two Signups Claim the Same Username</h4>
<p>A registration form can race if it relies only on a preliminary username check.</p>
<p>Two requests check the same name, and both find it available. Each then attempts to create an account with that name. Without a database uniqueness rule, the application may create duplicate usernames.</p>
<p>The protection here includes enforcing uniqueness in the database rather than trusting the earlier check.</p>
<h4 id="heading-two-workers-pick-the-same-job">Two Workers Pick the Same Job</h4>
<p>Background workers can accidentally process the same pending job.</p>
<p>Both workers read its status before either claims it. Each decides the job is available and begins the work. The application may then send a notification twice or generate the same report twice.</p>
<p>A job-claim mechanism needs to coordinate ownership before the work begins.</p>
<h4 id="heading-an-older-search-response-replaces-a-newer-one">An Older Search Response Replaces a Newer One</h4>
<p>A browser can display the wrong search results because responses arrive out of order.</p>
<p>You type “Django”, then change the search to “Django transactions” before the first response returns. The second response arrives first and displays the results you now want. If the first response arrives later and replaces them without a check, the page shows results for the old query.</p>
<p>A request identifier or stale-response check addresses this case, so a database row lock wouldn't be the relevant fix.</p>
<h2 id="heading-how-the-credit-check-can-fail">How the Credit Check Can Fail</h2>
<p>First, let’s define the rule for our example from the beginning of this tutorial: each image request costs one credit. Credits represent the app’s usage allowance. They're separate from the tokens a model processes.</p>
<p>This is a rule we’re choosing for this tutorial. If an account starts with ten credits, nine accepted requests leave enough for one more.</p>
<p>To accept a request, the backend must:</p>
<ol>
<li><p>Read the account’s balance.</p>
</li>
<li><p>Check whether at least one credit remains.</p>
</li>
<li><p>Deduct a credit and save the balance.</p>
</li>
<li><p>Record the accepted request.</p>
</li>
</ol>
<p>When you test one request at a time, this process can appear correct. The first request saves a balance of zero. The next reads zero and stops.</p>
<p>The next step is to examine the same operations when their execution overlaps.</p>
<h3 id="heading-what-happens-when-two-requests-overlap">What Happens When Two Requests Overlap</h3>
<p>Suppose Request A reads the account and finds one credit. Before it updates the database, Request B reads the same account. It also finds one credit.</p>
<p>Each request now has its own copy of the balance. Both pass the check, and both calculate <code>1 - 1 = 0</code>. Request A saves zero and records a generation. Request B then saves zero and records another generation.</p>
<p>The final balance is zero, but the application accepted two requests. A check for negative balances would miss this particular failure.</p>
<p>The mistake is trusting a value after another request has had an opportunity to change it. To see this in practice, we’ll first build the version with that mistake.</p>
<h2 id="heading-how-to-set-up-the-django-project">How to Set Up the Django Project</h2>
<p>Create a project directory and a virtual environment. The activation command below is for Linux and macOS:</p>
<pre><code class="language-shell">mkdir django-credit-demo
cd django-credit-demo
python3.12 -m venv .venv
source .venv/bin/activate
</code></pre>
<p>These commands give our example its own directory and Python environment.</p>
<p><code>mkdir</code> creates the directory, and <code>cd</code> moves you into it. The <code>venv</code> command creates an isolated environment named <code>.venv</code>. The <code>source</code> command activates it so subsequent package installations belong to this project.</p>
<p>Keep this environment active while you run the commands below.</p>
<p>On Windows, use <code>py -3.12 -m venv .venv</code> to create the environment and <code>.venv\Scripts\Activate.ps1</code> to activate it in PowerShell.</p>
<p>Create <code>requirements.txt</code>:</p>
<pre><code class="language-plaintext">Django&gt;=5.2,&lt;5.3
djangorestframework&gt;=3.16,&lt;3.17
psycopg[binary]&gt;=3.2,&lt;3.3
</code></pre>
<p>This file lists the three packages the project needs.</p>
<p>Django supplies the models and database tools, while Django REST Framework handles the endpoint. Psycopg provides the PostgreSQL connection, and <code>[binary]</code> requests its prebuilt implementation. Each version range allows updates within the chosen release series while excluding the next series.</p>
<p>Using one requirements file makes the dependencies explicit for anyone who follows the guide.</p>
<p>Install the packages, create the project, and add an app named <code>credits</code>:</p>
<pre><code class="language-plaintext">python -m pip install -r requirements.txt
python -m django startproject config .
python manage.py startapp credits
</code></pre>
<p>These commands install the dependencies and create the application structure.</p>
<p><code>pip install -r</code> reads the package list from <code>requirements.txt</code>. The <code>startproject</code> command creates the <code>config</code> package and <code>manage.py</code>, with the final dot selecting the current directory. The <code>startapp</code> command creates the <code>credits</code> package where our models, service functions, views, and tests will live.</p>
<p>We now have a Django project ready to connect to a database.</p>
<h3 id="heading-start-postgresql">Start PostgreSQL</h3>
<p>Create <code>compose.yaml</code> beside <code>manage.py</code>:</p>
<pre><code class="language-plaintext">services:
  db:
    image: postgres:17
    environment:
      POSTGRES_DB: credit_demo
      POSTGRES_USER: credit_demo
      POSTGRES_PASSWORD: local-demo-only
    ports:
      - "127.0.0.1:5433:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U credit_demo -d credit_demo"]
      interval: 2s
      timeout: 5s
      retries: 15
</code></pre>
<p>This configuration describes a PostgreSQL container for local development.</p>
<p><code>image</code> chooses PostgreSQL 17, and the environment values set up the database and local credentials. The port mapping exposes the database only on your computer’s loopback address at port <code>5433</code>. The health check runs <code>pg_isready</code> every two seconds, allows five seconds per check, and permits fifteen retries before marking the container unhealthy.</p>
<p>Django will use these same connection details in its settings.</p>
<p>Start it with:</p>
<pre><code class="language-plaintext">docker compose up -d --wait
</code></pre>
<p>This command starts the database defined in <code>compose.yaml</code>.</p>
<p><code>up</code> creates and starts the service. The <code>-d</code> flag lets it run in the background so you can continue using the terminal. The <code>--wait</code> flag waits for the service to become healthy according to the health check.</p>
<p>Keep the database container running throughout the tutorial.</p>
<h3 id="heading-configure-django">Configure Django</h3>
<p>Replace <code>config/settings.py</code> with this minimal configuration:</p>
<pre><code class="language-python">import os

SECRET_KEY = "local-tutorial-only-do-not-use-in-production"
DEBUG = True
ALLOWED_HOSTS = ["localhost", "127.0.0.1", "testserver"]
INSTALLED_APPS = [
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "rest_framework",
    "credits",
]
MIDDLEWARE = []
ROOT_URLCONF = "config.urls"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
USE_TZ = True
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ.get("DB_NAME", "credit_demo"),
        "USER": os.environ.get("DB_USER", "credit_demo"),
        "PASSWORD": os.environ.get("DB_PASSWORD", "local-demo-only"),
        "HOST": os.environ.get("DB_HOST", "127.0.0.1"),
        "PORT": os.environ.get("DB_PORT", "5433"),
        "OPTIONS": {"options": "-c lock_timeout=5000 -c statement_timeout=10000"},
    }
}
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.BasicAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
}
</code></pre>
<p>This settings file connects the parts of our small application.</p>
<p><code>INSTALLED_APPS</code> enables Django’s user support, Django REST Framework, and our <code>credits</code> app. <code>ROOT_URLCONF</code> points to the route definitions, while <code>DEFAULT_AUTO_FIELD</code> and <code>USE_TZ</code> configure automatic identifiers and timezone-aware dates. The empty <code>MIDDLEWARE</code> list keeps this API example minimal, and <code>ALLOWED_HOSTS</code> accepts the local addresses and test client host.</p>
<p>These settings are tailored to this tutorial’s endpoint.</p>
<p>The <code>DATABASES</code> section tells Django how to reach PostgreSQL.</p>
<p><code>ENGINE</code> selects the PostgreSQL backend. Each <code>os.environ.get()</code> reads an optional environment variable and falls back to the matching container value. The options set a five-second lock timeout and a ten-second statement timeout, so a stalled operation raises an error instead of waiting indefinitely.</p>
<p>A timeout is an error path, and this small endpoint doesn't provide a custom response for it.</p>
<p>The <code>REST_FRAMEWORK</code> section requires an authenticated user.</p>
<p><code>BasicAuthentication</code> reads credentials supplied with the request. <code>IsAuthenticated</code> rejects anonymous callers before the view accepts their prompt. The sample secret key and <code>DEBUG = True</code> are local development settings.</p>
<p>For deployment, configure production secrets, appropriate authentication, and encrypted connections.</p>
<p>Replace <code>config/urls.py</code> with an empty route list for now. We’ll add the endpoint after we fix the credit logic:</p>
<pre><code class="language-plaintext">urlpatterns = []
</code></pre>
<p>This empty list temporarily gives Django no application routes.</p>
<p>Django reads <code>urlpatterns</code> from the module named in <code>ROOT_URLCONF</code>. We removed the generated admin route because this minimal configuration doesn't enable the admin app. Database commands can still run before an endpoint exists.</p>
<p>We’ll replace this list when the protected view is ready.</p>
<h3 id="heading-create-the-models">Create the Models</h3>
<p>Add these models to <code>credits/models.py</code>:</p>
<pre><code class="language-python">from django.conf import settings
from django.db import models


class CreditAccount(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    balance = models.PositiveIntegerField(default=0)


class Generation(models.Model):
    account = models.ForeignKey(CreditAccount, on_delete=models.CASCADE)
    prompt = models.CharField(max_length=500)
    status = models.CharField(max_length=20, default="reserved")
    created_at = models.DateTimeField(auto_now_add=True)
</code></pre>
<p><code>CreditAccount</code> stores the balance associated with a user.</p>
<p><code>settings.AUTH_USER_MODEL</code> refers to the project’s configured user model. The <code>OneToOneField</code> allows at most one account per user, and <code>on_delete=models.CASCADE</code> tells Django to remove the account when it deletes that user. <code>PositiveIntegerField(default=0)</code> stores a nonnegative whole-number balance with an initial value of zero.</p>
<p>The balance field represents our allowance, but its type alone can't enforce one generation per credit.</p>
<p><code>Generation</code> stores the work the application has accepted.</p>
<p>The foreign key links each record to its paying account and allows an account to have many generations. <code>prompt</code> holds up to 500 characters, while <code>created_at</code> records when the row is created. <code>status</code> starts as <code>reserved</code>, meaning our service has recorded the request but hasn't completed the simulated image work.</p>
<p>Keeping a generation record lets us check what the deducted credit actually paid for.</p>
<p>Create and apply the migrations:</p>
<pre><code class="language-shell">python manage.py makemigrations credits
python manage.py migrate
</code></pre>
<p>These commands turn the model definitions into database tables.</p>
<p><code>makemigrations credits</code> creates migration files describing the model changes. <code>migrate</code> applies pending migrations, including Django’s user tables and our two new tables. The connection comes from the database settings we just configured.</p>
<p>The database is now ready to store an account and its generation requests.</p>
<h2 id="heading-how-to-reproduce-the-race-condition">How to Reproduce the Race Condition</h2>
<p>Create <code>credits/services.py</code> and add this deliberately unsafe function:</p>
<pre><code class="language-python">from .models import CreditAccount, Generation


class InsufficientCredits(Exception):
    pass


def reserve_generation_unsafe(user_id, prompt):
    account = CreditAccount.objects.get(user_id=user_id)

    if account.balance &lt; 1:
        raise InsufficientCredits

    account.balance -= 1
    account.save(update_fields=["balance"])

    return Generation.objects.create(account=account, prompt=prompt)
</code></pre>
<p>This function implements the credit check without concurrency protection.</p>
<p><code>objects.get()</code> retrieves the account for<code>user_id</code>, and <code>raise InsufficientCredits</code> stops the function if the balance is below one. The subtraction changes the Python object, then <code>save(update_fields=["balance"])</code> writes that value to the database. Finally, <code>Generation.objects.create()</code> inserts the accepted request and returns its model object.</p>
<p>The read and write remain separate operations, so another request can act between them.</p>
<p><code>InsufficientCredits</code> gives the caller a specific failure to handle.</p>
<p>It's a custom exception class derived from Python’s <code>Exception</code>. The <code>pass</code> statement means we don't add any behaviour to that class. Later, the view will catch this exception and return a useful response.</p>
<p>Keep the deliberately unsafe function available for comparison, but don't route the endpoint through it.</p>
<h3 id="heading-make-both-reads-happen-before-either-write">Make Both Reads Happen Before Either Write</h3>
<p>Opening two browser tabs isn't a reliable way to reproduce the bug. One request might finish before the other reads the balance. Instead, we’ll use two Django shells and pause after each has read the account.</p>
<p>Open a terminal in your project directory, activate the virtual environment, and start the shell:</p>
<pre><code class="language-plaintext">python manage.py shell
</code></pre>
<p>This command opens a Python shell with the Django project loaded.</p>
<p>It uses the settings associated with <code>manage.py</code>. You can import the models and query the configured database directly. Each terminal you open provides a separate shell for our experiment.</p>
<p>We’ll use those shells to control the order of the database operations.</p>
<p>Create a fresh user and an account with one credit:</p>
<pre><code class="language-python">from django.contrib.auth import get_user_model
from credits.models import CreditAccount, Generation

user = get_user_model().objects.create_user(username="race-demo")
CreditAccount.objects.create(user=user, balance=1)

account_a = CreditAccount.objects.get(user__username="race-demo")
print(account_a.balance)
</code></pre>
<p>This block prepares the first operation with a balance of one.</p>
<p><code>get_user_model()</code> retrieves the configured user class, and <code>create_user()</code> inserts our demonstration user. The account creation assigns that user one credit. The lookup uses <code>user__username</code> to follow the user relationship and stores the resulting account object in <code>account_a</code>.</p>
<p>The print should show<code>1</code>. Leave this shell open without updating the account.</p>
<p>In a second terminal, activate the same environment and run <code>python manage.py shell</code> again. Read the account there too:</p>
<pre><code class="language-python">from credits.models import CreditAccount, Generation

account_b = CreditAccount.objects.get(user__username="race-demo")
print(account_b.balance)
</code></pre>
<p>The second shell reads the same database row into a different Python object.</p>
<p><code>account_b</code> belongs to this shell and is separate from <code>account_a</code>. The first shell hasn't saved a deduction, so this lookup should also return a balance of one. Later changes in the other shell won't automatically refresh this object.</p>
<p>Both operations now have a copy of the credit they intend to spend.</p>
<p>Return to the first shell and run:</p>
<pre><code class="language-python">if account_a.balance &gt;= 1:
    account_a.balance -= 1
    account_a.save(update_fields=["balance"])
    Generation.objects.create(account=account_a, prompt="A garden")
</code></pre>
<p>The first shell now checks and spends its copy of the balance.</p>
<p>The <code>if</code> condition passes because <code>account_a.balance</code> is one. The subtraction changes it to zero, and <code>save()</code> writes zero to the account row. The final line creates a generation for the garden prompt.</p>
<p>Press Enter on a blank line to finish the block before switching terminals.</p>
<p>Then run the corresponding block in the second shell:</p>
<pre><code class="language-python">if account_b.balance &gt;= 1:
    account_b.balance -= 1
    account_b.save(update_fields=["balance"])
    Generation.objects.create(account=account_b, prompt="A beach")
</code></pre>
<p>The second shell makes its decision using the object it loaded earlier.</p>
<p>Its <code>if</code> condition still sees one because we have not refreshed <code>account_b</code>. It subtracts one and saves zero, overwriting the balance with the same value the first operation wrote. It then creates a separate generation for the beach prompt.</p>
<p>The second operation has accepted work using a credit the first operation already spent.</p>
<p>Finally, check the database from the second shell:</p>
<pre><code class="language-python">account_b.refresh_from_db()
print(account_b.balance)
print(Generation.objects.filter(account=account_b).count())
</code></pre>
<p>This block checks the stored result of both operations.</p>
<p><code>refresh_from_db()</code> reloads the account so the print reflects the database value. The filtered <code>count()</code> counts only generations linked to that account. You should see zero credits and two generation records.</p>
<p>The generation count reveals the failure that the balance alone would hide.</p>
<p>The two shells let us reproduce the unsafe sequence deliberately.</p>
<p>We paused after each read and then allowed both writes. A server can produce the same order when requests overlap, even though it won't do so on every attempt. To repeat the experiment, create a new username and account so previous records don't affect the count.</p>
<p>We can now build protection around the exact gap we observed.</p>
<h2 id="heading-how-to-protect-the-balance">How to Protect the Balance</h2>
<p>There are two database concerns in this function. The credit deduction and generation record should succeed together. Competing requests also need a coordinated way to check and update the account.</p>
<p>We’ll address them in that order.</p>
<h3 id="heading-keep-related-changes-in-one-transaction">Keep Related Changes in One Transaction</h3>
<p>A <strong>database transaction</strong> groups operations into a unit of work. Django’s <code>transaction.atomic()</code> commits the changes when the block completes successfully and rolls them back if an exception leaves the block.</p>
<p>This matters because the unsafe function saves the balance before it creates the generation record. If record creation fails, the deduction can remain without an accepted generation.</p>
<p>For illustration, wrapping the operations looks like this:</p>
<pre><code class="language-python">from django.db import transaction


@transaction.atomic
def reserve_generation_atomic_only(user_id, prompt):
    account = CreditAccount.objects.get(user_id=user_id)

    if account.balance &lt; 1:
        raise InsufficientCredits

    account.balance -= 1
    account.save(update_fields=["balance"])

    return Generation.objects.create(account=account, prompt=prompt)
</code></pre>
<p>The decorator places the function’s database operations inside an atomic transaction.</p>
<p><code>from django.db import transaction</code> provides Django’s transaction tools. The function still reads the account, checks the balance, saves the deduction, and creates the generation in that order. If an exception escapes during record creation, the transaction rolls back the deduction too.</p>
<p>This protects the relationship between the deduction and its generation record.</p>
<p>The plain account lookup still leaves the credit check exposed.</p>
<p>PostgreSQL uses Read Committed as its default isolation level. Under it, a plain read inside a transaction doesn't make another transaction wait before reading the row. Both functions can therefore read one credit before either updates it.</p>
<p>We need to coordinate access before making the balance decision.</p>
<h3 id="heading-lock-the-account-before-the-balance-check">Lock the Account Before the Balance Check</h3>
<p>Row locking is a database mechanism <strong>that restricts conflicting operations on selected rows while a transaction holds a lock.</strong></p>
<p>In this example, a row is the stored record for one credit account. A <code>SELECT FOR UPDATE</code> lock makes competing updates and conflicting lock requests wait until the lock is released. Ordinary reads can still proceed, and transactions can work on other account rows.</p>
<p>This lets us protect one account while its balance is checked and changed.</p>
<p>Add <code>from django.db import transaction</code> at the top of <code>credits/services.py</code>. Keep the unsafe function for comparison, then add this protected version:</p>
<pre><code class="language-python">@transaction.atomic
def reserve_generation(user_id, prompt):
    account = CreditAccount.objects.select_for_update().get(user_id=user_id)

    if account.balance &lt; 1:
        raise InsufficientCredits

    account.balance -= 1
    account.save(update_fields=["balance"])

    return Generation.objects.create(account=account, prompt=prompt)
</code></pre>
<p>This function acquires the account’s row lock before it checks the balance.</p>
<p><code>select_for_update()</code> requests the lock, and <code>.get(user_id=user_id)</code> executes the query for this account inside the transaction. Once it holds the lock, the function checks the balance and raises <code>InsufficientCredits</code> if necessary. Otherwise, it saves the deduction and creates the generation before the transaction completes.</p>
<p>The decision and its database changes now happen while the account is protected.</p>
<p>A competing call to this function must wait at the locking query.</p>
<p>If the first call commits its deduction, the waiting call checks the updated balance under our Read Committed setup. If the first call rolls back, its deduction doesn't remain. Our configured timeout can also stop the wait with an error.</p>
<p>For one credit and a successful first commit, the second call reads zero and rejects the request.</p>
<p>This protection needs to cover every path that spends the balance.</p>
<p>An older function could still read an account without requesting a lock. Its later update would wait while our lock is held, but it could then overwrite the balance using its stale value. Administrative adjustments and background jobs therefore need a safe update strategy, too.</p>
<p>One protected function can't correct an unsafe writer elsewhere.</p>
<h3 id="heading-keep-image-generation-outside-the-transaction">Keep Image Generation Outside the Transaction</h3>
<p>The transaction should cover the credit reservation. A remote image request could take much longer than those database operations, so placing it inside the transaction would keep competing requests waiting unnecessarily.</p>
<p>For our demo, add this function to <code>credits/services.py</code>:</p>
<pre><code class="language-python">def simulate_generation(generation):
    # No external AI request is made in this tutorial.
    generation.status = "completed"
    generation.save(update_fields=["status"])
    return "Simulated image generation completed."
</code></pre>
<p>This function simulates completion of an accepted generation.</p>
<p>It changes the supplied generation object’s <code>status</code> to <code>completed</code>. The <code>save()</code> call persists only that field. The return value is a message, so the function produces no image and makes no external request.</p>
<p>We’ll call it after the reservation function returns.</p>
<p>The call order keeps image work outside the reservation transaction.</p>
<p>With these settings and no enclosing transaction, the reservation commits before the simulation starts. A failure after that point wouldn't automatically restore the credit. A real provider integration needs its own retry or refund policy.</p>
<p>We’ll return to those limits after we test the reservation itself.</p>
<h2 id="heading-how-to-accept-and-test-image-requests">How to Accept and Test Image Requests</h2>
<p>Now we can connect the protected function to an endpoint. Add this code to <code>credits/views.py</code>:</p>
<pre><code class="language-python">from rest_framework import serializers, status
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import CreditAccount
from .services import InsufficientCredits, reserve_generation, simulate_generation


class GenerationInput(serializers.Serializer):
    prompt = serializers.CharField(max_length=500)


class GenerateView(APIView):
    def post(self, request):
        serializer = GenerationInput(data=request.data)
        serializer.is_valid(raise_exception=True)
        try:
            generation = reserve_generation(
                request.user.pk, serializer.validated_data["prompt"]
            )
        except CreditAccount.DoesNotExist:
            return Response({"detail": "Credit account not found."}, status=404)
        except InsufficientCredits:
            return Response({"detail": "Not enough credits."}, status=409)

        result = simulate_generation(generation)
        return Response(
            {"id": generation.pk, "status": generation.status, "result": result},
            status=status.HTTP_201_CREATED,
        )
</code></pre>
<p>The serializer checks the prompt before the view spends a credit.</p>
<p><code>GenerationInput</code> declares a required text field with a maximum length of 500 characters. <code>is_valid(raise_exception=True)</code> rejects missing, blank, or invalid input with a validation response. The view then reads the cleaned value from <code>validated_data</code> and passes it with <code>request.user.pk</code>, the authenticated user’s database identifier, to the reservation function.</p>
<p>The client supplies a prompt while the server chooses the account from the authenticated user.</p>
<p>The view translates the reservation outcome into a response.</p>
<p>A missing account produces<code>404</code>, and <code>InsufficientCredits</code> produces <code>409</code>, our chosen response for the balance conflict. On success, the view calls the simulation after the reservation returns. It sends <code>201</code> with the record identifier, completion status, and simulated result.</p>
<p>These branches let the caller distinguish accepted work from a rejected request.</p>
<p>Replace <code>config/urls.py</code> with:</p>
<pre><code class="language-python">from django.urls import path
from credits.views import GenerateView

urlpatterns = [
    path("api/generate/", GenerateView.as_view()),
]
</code></pre>
<p>This route connects the request address to our view.</p>
<p><code>path()</code> matches the <code>api/generate/</code> part of the address. <code>GenerateView.as_view()</code> turns the class-based view into a callable Django can dispatch to. The view’s <code>post()</code> method handles a POST request at that route.</p>
<p>The endpoint is now available at <code>/api/generate/</code> when the server runs.</p>
<h3 id="heading-try-one-request-at-a-time">Try One Request at a Time</h3>
<p>Open the Django shell and create a separate user for the endpoint demonstration:</p>
<pre><code class="language-python">from django.contrib.auth import get_user_model
from credits.models import CreditAccount

user = get_user_model().objects.create_user(
    username="api-demo",
    password="local-example-password",
)
CreditAccount.objects.create(user=user, balance=1)
</code></pre>
<p>This block creates a user for the authenticated endpoint example.</p>
<p><code>create_user()</code> saves the username and hashes the supplied password. <code>CreditAccount.objects.create()</code> gives the new user one credit. A separate username keeps this check independent of the account used in the two-shell experiment.</p>
<p>The credentials below belong only to this local demonstration account.</p>
<p>Exit the shell and start the development server:</p>
<pre><code class="language-plaintext">python manage.py runserver
</code></pre>
<p>This command starts Django’s development server.</p>
<p>With no address argument, it listens at <code>127.0.0.1:8000</code>. Requests at that address pass through the route configuration we just added. Keep this terminal open while you send the request from another terminal.</p>
<p>This server is for the local demonstration.</p>
<p>In another terminal, send a request:</p>
<pre><code class="language-python">curl -i -u api-demo:local-example-password \
  -H "Content-Type: application/json" \
  -d '{"prompt": "A garden at sunrise"}' \
  http://127.0.0.1:8000/api/generate/
</code></pre>
<p>This command submits a prompt to the endpoint with the demonstration user’s credentials.</p>
<p><code>-i</code> includes response headers, and <code>-u</code> supplies the username and password for Basic authentication. <code>-H</code> declares JavaScript Object Notation (<strong>JSON</strong>) as the request body format. <code>-d</code> supplies that body and makes curl send a POST request to the address shown.</p>
<p>The first request should return <code>201</code> with the simulated completion result.</p>
<p>Sending the command again checks the account after the first deduction.</p>
<p>The next request should find zero credits. The view should return <code>409</code> with <code>"detail": "Not enough credits."</code>. Because we waited between requests, this verifies the ordinary sequence without exercising concurrency.</p>
<p>Next, we’ll test overlapping operations.</p>
<h3 id="heading-set-up-the-automated-tests">Set Up the Automated Tests</h3>
<p>Replace <code>credits/tests.py</code> with the following imports and helper class. We’ll add the test methods in the next steps.</p>
<pre><code class="language-python">from concurrent.futures import ThreadPoolExecutor
from threading import Barrier
from unittest.mock import patch

from django.contrib.auth import get_user_model
from django.db import (
    OperationalError, close_old_connections, connection, connections, transaction,
)
from django.test import TransactionTestCase
from rest_framework.test import APIClient

from .models import CreditAccount, Generation
from .services import InsufficientCredits, reserve_generation, reserve_generation_unsafe


class ConcurrencyTests(TransactionTestCase):
    def setUp(self):
        if connection.vendor != "postgresql":
            self.skipTest("Run these concurrency tests on PostgreSQL.")

        self.user = get_user_model().objects.create_user(username="parallel-reader")
        self.account = CreditAccount.objects.create(user=self.user, balance=1)

    def run_two(self, action):
        def worker():
            close_old_connections()
            try:
                return action()
            finally:
                connections.close_all()

        with ThreadPoolExecutor(max_workers=2) as pool:
            futures = [pool.submit(worker) for _ in range(2)]
            return [future.result(timeout=15) for future in futures]
</code></pre>
<p>The test class prepares a fresh account before each concurrency test.</p>
<p><code>setUp()</code> skips the test when the connection isn't PostgreSQL. It then creates a user and an account with one credit. <code>TransactionTestCase</code> allows actual transaction boundaries, unlike regular<code>TestCase</code>, whose enclosing transactions can hide lock-usage mistakes.</p>
<p>A skipped test on SQLite doesn't verify PostgreSQL’s lock behaviour.</p>
<p>The <code>run_two()</code> helper gives the same action to two worker threads.</p>
<p><code>ThreadPoolExecutor(max_workers=2)</code> provides the workers, and each <code>pool.submit(worker)</code> schedules one call. A future represents that call’s eventual result, which <code>future.result(timeout=15)</code> retrieves or raises an error for. Each worker clears unusable old connections before the action and closes its own connections, including when the action fails.</p>
<p>This lets the two operations reach the same database through separate connections.</p>
<h3 id="heading-confirm-the-unsafe-behaviour">Confirm the Unsafe Behaviour</h3>
<p>Add this method inside<code>ConcurrencyTests</code>, at the same indentation level as <code>run_two()</code>:</p>
<pre><code class="language-python"> def test_reproduce_unsafe_spending(self):
        both_have_read = Barrier(2)
        original_get = CreditAccount.objects.get

        def read_then_wait(*args, **kwargs):
            account = original_get(*args, **kwargs)
            both_have_read.wait(timeout=5)
            return account

        with patch(
            "credits.services.CreditAccount.objects.get",
            side_effect=read_then_wait,
        ):
            self.run_two(
                lambda: reserve_generation_unsafe(self.user.pk, "A garden").pk
            )

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, 0)
        self.assertEqual(Generation.objects.count(), 2)
</code></pre>
<p>This test forces both unsafe operations to read before either proceeds.</p>
<p><code>original_get</code> keeps the real lookup, and <code>read_then_wait()</code> calls it before waiting at <code>Barrier(2)</code>. The barrier releases the workers only after both arrive, or raises an error if its wait times out. <code>patch()</code> temporarily replaces the service’s lookup with this wrapper for the duration of the with block.</p>
<p>The database read stays real while the test controls the pause after it.</p>
<p>The final assertions document the deliberately incorrect result.</p>
<p>The small lambda calls the unsafe function and returns the created record’s identifier for each worker. After both return, <code>refresh_from_db()</code> reloads the account. The assertions expect a zero balance and two generation records in this isolated test.</p>
<p>A pass here confirms reproduction of the bug, not correctness of the unsafe function.</p>
<h3 id="heading-check-the-protected-endpoint">Check the Protected Endpoint</h3>
<p>Add these methods inside the same class:</p>
<pre><code class="language-python">def concurrent_api_requests(self):
        ready = Barrier(2)

        def send_request():
            client = APIClient()
            client.force_authenticate(self.user)
            ready.wait(timeout=5)
            return client.post(
                "/api/generate/",
                {"prompt": "A garden"},
                format="json",
            ).status_code

        return self.run_two(send_request)

    def test_last_credit_accepts_only_one_request(self):
        self.assertEqual(sorted(self.concurrent_api_requests()), [201, 409])
        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, 0)
        self.assertEqual(Generation.objects.count(), 1)

    def test_two_credits_accept_both_requests(self):
        self.account.balance = 2
        self.account.save(update_fields=["balance"])

        self.assertEqual(self.concurrent_api_requests(), [201, 201])
        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, 0)
        self.assertEqual(Generation.objects.count(), 2)
</code></pre>
<p>The request helper sends two authenticated requests through the protected view.</p>
<p>Each worker creates its own<code>APIClient</code>, and <code>force_authenticate()</code> supplies the test user without a password exchange. The barrier sits before <code>post()</code> so both workers reach the request start together. Each call returns its response code to <code>run_two()</code>.</p>
<p>This tests the endpoint’s credit behaviour without also testing the authentication mechanism.</p>
<p>The two test methods cover both insufficient and sufficient shared credit.</p>
<p>The one-credit test sorts the codes because either worker may finish first, then expects one <code>201</code> and one <code>409</code>. It also checks zero remaining credits and exactly one generation record. The two-credit test changes the starting balance and expects two successes, two records, and a zero balance.</p>
<p>Checking the records as well as the responses helps catch an incorrectly accepted generation.</p>
<p>The barrier coordinates request starts without controlling every database operation.</p>
<p>One request could still progress faster than the other. Moving the barrier after lock acquisition would make the first worker wait for a second worker that can't acquire its lock. The current placement avoids that artificial deadlock but doesn't prove every possible execution order.</p>
<p>Use this regression test alongside the controlled reproduction and documented database behaviour.</p>
<h3 id="heading-verify-the-service-encounters-a-held-lock">Verify the Service Encounters a Held Lock</h3>
<p>We can also test whether the reservation function waits for a lock before it checks the balance.</p>
<p>The test below starts with zero credits so the balance check would immediately reject an unprotected read. The main connection acquires the account lock before it starts a worker. That worker calls the actual reservation function with a one-second lock timeout.</p>
<p>Add this method inside <code>ConcurrencyTests</code>:</p>
<pre><code class="language-python">    def test_reservation_waits_for_account_lock(self):
        self.account.balance = 0
        self.account.save(update_fields=["balance"])
        user_id = self.user.pk

        def attempt_reservation():
            close_old_connections()
            try:
                try:
                    with transaction.atomic():
                        with connection.cursor() as cursor:
                            cursor.execute("SET LOCAL lock_timeout = '1s'")
                        reserve_generation(user_id, "A garden")
                except OperationalError as error:
                    return error.__cause__.sqlstate
                except InsufficientCredits:
                    return "insufficient_credits"
                return "accepted"
            finally:
                connections.close_all()

        with ThreadPoolExecutor(max_workers=1) as pool:
            with transaction.atomic():
                CreditAccount.objects.select_for_update().get(pk=self.account.pk)
                result = pool.submit(attempt_reservation).result(timeout=5)
                self.assertEqual(result, "55P03")

            result = pool.submit(attempt_reservation).result(timeout=5)
            self.assertEqual(result, "insufficient_credits")

        self.account.refresh_from_db()
        self.assertEqual(self.account.balance, 0)
        self.assertEqual(Generation.objects.count(), 0)
</code></pre>
<p>This test controls when the competing lock exists.</p>
<p>The main connection keeps its transaction open while it waits for the worker’s result. Inside the worker’s transaction, <code>SET LOCAL</code> temporarily shortens the lock timeout. The worker should return PostgreSQL’s <code>55P03</code> error code, which means <code>lock_not_available</code>, after the lock wait times out.</p>
<p>A balance rejection at this point would show that the service didn't wait for the account lock before its check.</p>
<p>The second call checks the same operation after the main transaction releases its lock.</p>
<p>This time, the worker can acquire the account lock and read the zero balance. It should return <code>insufficient_credits</code> rather than a database error. The final assertions confirm no credit or generation record changed during either attempt.</p>
<p>Together, the two calls check contention and release without relying on two requests happening to overlap.</p>
<p>The zero balance is deliberate and makes the test more specific.</p>
<p>If we used one credit, an unprotected function might still encounter the lock when it eventually tried to update the row. With zero credits, that function would reject the request before any update and fail our expected timeout assertion. The test therefore checks the service’s lock-before-check behaviour, while the preceding endpoint tests check its spending outcomes.</p>
<p>This controlled lock test passed as part of the companion project’s PostgreSQL test suite.</p>
<h3 id="heading-check-rollback-after-a-failure">Check Rollback After a Failure</h3>
<p>Outside <code>ConcurrencyTests</code> add another class:</p>
<pre><code class="language-python">class CreditRollbackTests(TransactionTestCase):
    def test_failed_record_creation_restores_credit(self):
        user = get_user_model().objects.create_user(username="rollback-reader")
        account = CreditAccount.objects.create(user=user, balance=1)

        with patch(
            "credits.services.Generation.objects.create",
            side_effect=RuntimeError("Simulated record creation failure"),
        ):
            with self.assertRaises(RuntimeError):
                reserve_generation(user.pk, "A garden")

        account.refresh_from_db()
        self.assertEqual(account.balance, 1)
        self.assertEqual(Generation.objects.count(), 0)
</code></pre>
<p>This test deliberately raises an exception during generation-record creation.</p>
<p>It first creates an account with one credit. The patch makes <code>Generation.objects.create()</code> raise <code>RuntimeError</code>, and <code>assertRaises()</code> confirms that the error leaves the reservation function. After the transaction rolls back, the refreshed account should still have one credit and no generation record.</p>
<p>This checks that a failed reservation doesn't leave a deduction behind.</p>
<p>Run the tests with PostgreSQL still available:</p>
<pre><code class="language-python">python manage.py test credits -v 2
</code></pre>
<p>This command runs the tests in the <code>credits</code> app.</p>
<p><code>-v 2</code> asks Django to show each test and its outcome. Django creates a separate test database, so the database user needs permission to create it. The user from our local container has that permission, but an existing database setup may need configuration.</p>
<p>Expect five passes on the intended PostgreSQL setup. Confirm them by execution before relying on the example.</p>
<h2 id="heading-common-mistakes-and-next-steps">Common Mistakes and Next Steps</h2>
<p>The example now protects the credit decision, but a few details are easy to miss when you adapt it.</p>
<h3 id="heading-read-the-account-inside-the-lock">Read the Account Inside the Lock</h3>
<p>Fetching an account before the transaction and continuing to use that object can leave you with an old balance. The protected function deliberately retrieves the account through <code>select_for_update()</code> before making its decision.</p>
<p>Keep this order when you move the code into another service or endpoint. Passing in a user identifier makes the function responsible for its own fresh, locked read.</p>
<h3 id="heading-use-the-database-as-the-shared-point-of-coordination">Use the Database as the Shared Point of Coordination</h3>
<p>Disabling a submit button can reduce accidental clicks, but a second tab or another client can still send a request. A Python thread lock also coordinates only the code sharing that lock in the same process.</p>
<p>If you run multiple application workers, the credit decision still needs protection at the shared database. Review other balance updates, including administrative adjustments, rather than assuming this endpoint is the only writer.</p>
<h3 id="heading-consider-a-conditional-update-for-simpler-counters">Consider a Conditional Update for Simpler Counters</h3>
<p>Row locks are one approach. For a simple deduction, Django can also express the balance condition and subtraction in one database update using an <code>F()</code> expression.</p>
<p>The condition matters: an unconditional subtraction doesn't enforce sufficient credit. If you also create a generation record, keep the deduction and record creation in one transaction.</p>
<p>We used an explicit lock here because it makes the read, decision, and update easy to follow. You can explore conditional updates once you understand what the operation must protect.</p>
<h3 id="heading-separate-concurrent-requests-from-retries">Separate Concurrent Requests from Retries</h3>
<p>Our example treats two submissions as two distinct requests. If the user has two credits, both should succeed.</p>
<p>A retry introduces a different requirement. The app might accept a generation but lose the response before the client receives it. If the client resends the same logical request, you may want to return the original result without another charge.</p>
<p>That requires <strong>idempotency</strong>: a way to identify a repeated operation and avoid applying its effect again. A typical design uses a request key, a database uniqueness rule, and a stored result. A balance lock alone doesn't identify duplicate intent.</p>
<h3 id="heading-plan-for-provider-failures">Plan for Provider Failures</h3>
<p>Once the reservation commits, a later provider failure doesn't automatically restore the credit. You need a policy for whether to retry the generation, refund it, or leave it pending for recovery.</p>
<p>A real implementation also needs to recover if the application stops after it reserves the credit but before it starts the job. The <code>reserved</code> record gives you something to track, but this tutorial doesn't implement a durable job queue or recovery worker.</p>
<p>Keep those concerns visible when you extend the example. Preventing concurrent overspending is one part of a complete credit system.</p>
<h2 id="heading-summary-and-next-steps">Summary and Next Steps</h2>
<p>At the start of this guide, two requests could each pass the balance check and spend the same credit. The balance ended at zero, which made the failure easy to overlook.</p>
<p>We reproduced the sequence, then protected the decision with a transaction and a row lock. We also checked the generation count, tested the case where both requests had enough credit, and added a rollback test for record creation failure.</p>
<p>When you review a similar feature in your own application:</p>
<ol>
<li><p>State the rule the data must satisfy, such as one credit per accepted generation.</p>
</li>
<li><p>Identify where separate requests can read and change the same value.</p>
</li>
<li><p>Protect the decision and its related database changes.</p>
</li>
<li><p>Test overlapping operations and failures on the database you actually use.</p>
</li>
</ol>
<p>The same reasoning applies to the last item in an online store or any shared allowance. A successful check is only useful if the application can safely act on it.</p>
<h2 id="heading-references">References</h2>
<p>For a deeper look at retries, see <a href="https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/">Making retries safe with idempotent APIs</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Optimize Django REST APIs for Performance: Profiling, Caching, and Scaling. ]]>
                </title>
                <description>
                    <![CDATA[ Performance problems in APIs rarely start as performance problems. They usually start as small design decisions that worked perfectly when the application had ten users, ten records, or a single developer testing locally. Over time, as traffic increa... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-optimize-django-rest-apis-for-performance/</link>
                <guid isPermaLink="false">6994b1d13e0696149c7c229c</guid>
                
                    <category>
                        <![CDATA[ Django ]]>
                    </category>
                
                    <category>
                        <![CDATA[ django rest framework ]]>
                    </category>
                
                    <category>
                        <![CDATA[ REST API ]]>
                    </category>
                
                    <category>
                        <![CDATA[ caching ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Performance Optimization ]]>
                    </category>
                
                    <category>
                        <![CDATA[ scalability ]]>
                    </category>
                
                    <category>
                        <![CDATA[ backend ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Mari ]]>
                </dc:creator>
                <pubDate>Tue, 17 Feb 2026 18:22:09 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1771352481135/11be538b-aaf5-4c1e-8ee2-99deea5f180e.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Performance problems in APIs rarely start as performance problems. They usually start as small design decisions that worked perfectly when the application had ten users, ten records, or a single developer testing locally. Over time, as traffic increases and data grows, those same decisions begin to slow everything down.</p>
<p>In this article, we’ll walk step by step through how performance issues arise in Django REST APIs, how to see them clearly using profiling tools, and how to fix them using query optimization, caching, pagination, and basic scaling strategies.</p>
<p>This article will be most useful for developers who already understand Django, the Django REST Framework, and REST concepts, but are new to performance optimization.</p>
<h3 id="heading-what-well-cover">What we’ll cover:</h3>
<ul>
<li><p><a class="post-section-overview" href="#heading-why-django-rest-apis-become-slow">Why Django REST APIs Become Slow</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-profiling-finding-the-real-bottlenecks">Profiling: Finding the Real Bottlenecks</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-logging-sql-queries">Logging SQL Queries</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-summary-and-next-steps">Summary and Next Steps</a></p>
</li>
</ul>
<h2 id="heading-why-django-rest-apis-become-slow">Why Django REST APIs Become Slow</h2>
<p>Before optimizing anything, it’s important to understand why APIs become slow in the first place.</p>
<p>Most performance issues in Django REST APIs come from three main sources:</p>
<ol>
<li><p>Too many database queries</p>
</li>
<li><p>Doing expensive work repeatedly</p>
</li>
<li><p>Returning more data than necessary</p>
</li>
</ol>
<p>Django is fast by default, but it does exactly what you ask it to do. If your API endpoint triggers 300 database queries, Django will happily run all 300.</p>
<p>Now let’s look at some common causes of performance issues in Django REST APIs.</p>
<h3 id="heading-1-n1-query-problems-in-serializers">1. N+1 Query Problems in Serializers</h3>
<p>This happens when you loop over objects and access related fields, causing a separate query for each object.</p>
<pre><code class="lang-python"><span class="hljs-comment"># models.py</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Author</span>(<span class="hljs-params">models.Model</span>):</span>
    name = models.CharField(max_length=<span class="hljs-number">100</span>)

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Post</span>(<span class="hljs-params">models.Model</span>):</span>
    title = models.CharField(max_length=<span class="hljs-number">200</span>)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)

<span class="hljs-comment"># views.py (naive approach)</span>
posts = Post.objects.all()
<span class="hljs-keyword">for</span> post <span class="hljs-keyword">in</span> posts:
    <span class="hljs-comment"># This triggers a query per post to fetch the author</span>
    print(post.author.name)
</code></pre>
<p>If you have 100 posts, this runs 101 queries: 1 for posts and 100 for authors. Django lazily loads related objects by default, so without intervention, your API performs repetitive database work that slows response times.</p>
<h3 id="heading-2-fetching-related-objects-inefficiently">2. Fetching Related Objects Inefficiently</h3>
<pre><code class="lang-python"><span class="hljs-comment"># Naive queryset fetching all related objects separately</span>
posts = Post.objects.all()
authors = [post.author <span class="hljs-keyword">for</span> post <span class="hljs-keyword">in</span> posts]  <span class="hljs-comment"># triggers extra queries per post</span>
</code></pre>
<p>Each access to <code>post.author</code> triggers a new query. Even though you already fetched all posts, Django lazily loads related objects by default. This creates many extra queries, slowing down your API.</p>
<h3 id="heading-3-serializing-large-datasets-without-pagination">3. Serializing Large Datasets Without Pagination</h3>
<p>Returning large query sets all at once can slow down your API and increase memory usage.</p>
<pre><code class="lang-python"><span class="hljs-comment"># views.py</span>
<span class="hljs-keyword">from</span> rest_framework.response <span class="hljs-keyword">import</span> Response
<span class="hljs-keyword">from</span> rest_framework.decorators <span class="hljs-keyword">import</span> api_view
<span class="hljs-keyword">from</span> .models <span class="hljs-keyword">import</span> Post
<span class="hljs-keyword">from</span> .serializers <span class="hljs-keyword">import</span> PostSerializer

<span class="hljs-meta">@api_view(['GET'])</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">all_posts</span>(<span class="hljs-params">request</span>):</span>
    posts = Post.objects.all()  <span class="hljs-comment"># retrieves all posts at once</span>
    serializer = PostSerializer(posts, many=<span class="hljs-literal">True</span>)
    <span class="hljs-keyword">return</span> Response(serializer.data)
</code></pre>
<p>If your database has thousands of posts, this endpoint fetches everything in memory, serializes it, and sends it over the network. It’s slow and can crash under load. Later, we’ll learn to paginate results efficiently.</p>
<h3 id="heading-4-recomputing-expensive-work-repeatedly">4. Recomputing Expensive Work Repeatedly</h3>
<p>Some endpoints calculate the same values on every request instead of caching or precomputing.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">expensive_view</span>(<span class="hljs-params">request</span>):</span>
    <span class="hljs-comment"># Simulate expensive computation</span>
    result = sum([i**<span class="hljs-number">2</span> <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">1000000</span>)])
    <span class="hljs-keyword">return</span> JsonResponse({<span class="hljs-string">"result"</span>: result})
</code></pre>
<p>Even if the data doesn’t change often, this computation happens on every request, consuming CPU time unnecessarily.  </p>
<p>Performance optimization is about reducing unnecessary work.  </p>
<p>At this point, it might be tempting to jump straight into fixes like caching responses or optimizing database queries. But doing that without evidence often leads to wasted effort or even new problems.</p>
<p>Before changing anything, you need to understand where your API is actually spending time. Is it the database? Is it serialization? Is it Python code running repeatedly on every request? This is where profiling becomes essential.</p>
<h2 id="heading-profiling-finding-the-real-bottlenecks">Profiling: Finding the Real Bottlenecks</h2>
<p>Optimizing without profiling is guessing. Profiling helps you answer one question:</p>
<blockquote>
<p>Where is my API actually spending time?</p>
</blockquote>
<p>In practice, profiling means observing an API while it runs and collecting data about what it’s doing. This includes how many database queries are executed, how long those queries take, and how much time is spent in Python code, such as serializers or business logic.</p>
<p>By profiling first, you avoid making assumptions and can focus on fixing the parts of your API that are truly slowing things down.</p>
<h3 id="heading-measuring-query-count-in-a-view">Measuring Query Count in a View</h3>
<p>During development, Django keeps track of all executed queries. You can inspect them directly:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> django.db <span class="hljs-keyword">import</span> connection
<span class="hljs-keyword">from</span> rest_framework.decorators <span class="hljs-keyword">import</span> api_view
<span class="hljs-keyword">from</span> rest_framework.response <span class="hljs-keyword">import</span> Response
<span class="hljs-keyword">from</span> .models <span class="hljs-keyword">import</span> Post
<span class="hljs-keyword">from</span> .serializers <span class="hljs-keyword">import</span> PostSerializer

<span class="hljs-meta">@api_view(["GET"])</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">post_list</span>(<span class="hljs-params">request</span>):</span>
    posts = Post.objects.all()
    serializer = PostSerializer(posts, many=<span class="hljs-literal">True</span>)

    response = Response(serializer.data)

    print(<span class="hljs-string">f"Total queries executed: <span class="hljs-subst">{len(connection.queries)}</span>"</span>)

    <span class="hljs-keyword">return</span> response
</code></pre>
<p>If this prints 101 queries for 100 posts, you likely have an N+1 problem. This simple check confirms whether the database layer is the bottleneck.</p>
<p>One of the easiest ways to profile Django applications during development is by using tools that expose this information directly while requests are being processed.</p>
<h3 id="heading-using-the-django-debug-toolbar">Using the Django Debug Toolbar</h3>
<p>The Django Debug Toolbar is one of the simplest ways to understand performance during development. It acts as a lightweight profiling tool that shows what happens behind the scenes when a request is handled.</p>
<p>It shows you:</p>
<ul>
<li><p>How many SQL queries were executed</p>
</li>
<li><p>How long each query took</p>
</li>
<li><p>whether queries are duplicated</p>
</li>
<li><p>Which parts of the request lifecycle are slow</p>
</li>
</ul>
<h4 id="heading-how-to-install-and-enable-the-django-debug-toolbar">How to Install and Enable the Django Debug Toolbar</h4>
<p>First, install it:</p>
<pre><code class="lang-bash">pip install django-debug-toolbar
</code></pre>
<p>In settings.py:</p>
<pre><code class="lang-bash">INSTALLED_APPS = [
    ...
    <span class="hljs-string">"debug_toolbar"</span>,
]

MIDDLEWARE = [
    ...
    <span class="hljs-string">"debug_toolbar.middleware.DebugToolbarMiddleware"</span>,
]

INTERNAL_IPS = [
    <span class="hljs-string">"127.0.0.1"</span>,
]
</code></pre>
<p>In urls.py:</p>
<pre><code class="lang-bash">import debug_toolbar
from django.urls import path, include

urlpatterns = [
    ...
    path(<span class="hljs-string">"__debug__/"</span>, include(debug_toolbar.urls)),
]
</code></pre>
<p>When you load an endpoint in the browser during development, the toolbar displays total SQL queries, execution time, and duplicate queries. This makes inefficiencies immediately visible.</p>
<p>When you load an API endpoint and see 150 SQL queries for a single request, that’s a strong signal that something is wrong, often an N+1 query problem or inefficient serializer behavior.</p>
<h3 id="heading-logging-sql-queries">Logging SQL Queries</h3>
<p>Django allows you to log all executed SQL queries. This is especially useful when debugging API views.</p>
<p>Seeing the raw SQL makes inefficiencies obvious, such as repeated <code>SELECT</code> statements for the same table.</p>
<h4 id="heading-how-to-enable-sql-query-logging">How to Enable SQL Query Logging</h4>
<p>You can configure Django to log all SQL queries in settings.py:</p>
<pre><code class="lang-bash">LOGGING = {
    <span class="hljs-string">"version"</span>: 1,
    <span class="hljs-string">"handlers"</span>: {
        <span class="hljs-string">"console"</span>: {
            <span class="hljs-string">"class"</span>: <span class="hljs-string">"logging.StreamHandler"</span>,
        },
    },
    <span class="hljs-string">"loggers"</span>: {
        <span class="hljs-string">"django.db.backends"</span>: {
            <span class="hljs-string">"handlers"</span>: [<span class="hljs-string">"console"</span>],
            <span class="hljs-string">"level"</span>: <span class="hljs-string">"DEBUG"</span>,
        },
    },
}
</code></pre>
<p>With this configuration, every SQL query will be printed to the console when your API runs. Repeated SELECT statements or unexpected queries become obvious.</p>
<h3 id="heading-profiling-api-response-time">Profiling API Response Time</h3>
<p>Database queries are only one part of API performance. Beyond queries, it’s also important to measure the total response time of an endpoint.</p>
<p>Profiling response time helps you understand whether delays are caused by database access or by other parts of the request lifecycle. For example, if an endpoint takes 1.2 seconds to respond but only 50 milliseconds are spent on database queries, the bottleneck is likely in serialization, business logic, or repeated computations in Python.</p>
<p>By comparing query time and total response time, profiling helps you identify what to fix first instead of optimizing the wrong layer of the system.</p>
<h4 id="heading-how-to-measure-total-response-time">How to Measure Total Response Time</h4>
<pre><code class="lang-bash">import time
from rest_framework.decorators import api_view
from rest_framework.response import Response

@api_view([<span class="hljs-string">"GET"</span>])
def example_view(request):
    start_time = time.time()

    <span class="hljs-comment"># Simulate work</span>
    data = {<span class="hljs-string">"message"</span>: <span class="hljs-string">"Hello world"</span>}

    response = Response(data)

    end_time = time.time()
    <span class="hljs-built_in">print</span>(f<span class="hljs-string">"Response time: {end_time - start_time:.4f} seconds"</span>)

    <span class="hljs-built_in">return</span> response
</code></pre>
<p>If database queries are fast but the total response time is high, the bottleneck may be serialization or expensive Python logic.  </p>
<p>Once you’ve identified that database access is a significant contributor to slow response times, the next step is to look more closely at how Django retrieves related data.</p>
<h3 id="heading-sql-query-optimization-in-django-rest-apis">SQL Query Optimization in Django REST APIs</h3>
<p>One of the most common reasons Django REST APIs become slow is inefficient access to related objects. This often manifests as the N+1 query problem, where fetching related objects triggers a separate database query for each item. Identifying and fixing this problem can significantly reduce the number of queries and improve API performance.</p>
<h4 id="heading-understanding-the-n1-query-problem">Understanding the N+1 Query Problem</h4>
<p>Consider a simple example:</p>
<ul>
<li><p>You fetch a list of posts</p>
</li>
<li><p>Each post has an author</p>
</li>
<li><p>For every post, Django fetches the author separately</p>
</li>
</ul>
<p>If you have 100 posts, this results in 101 queries: 1 for the posts and 100 for the authors. This happens because Django lazily loads related objects by default. Without intervention, your API performs repetitive database work that slows down response times.</p>
<h4 id="heading-solving-the-problem-with-selectrelated-and-prefetchrelated">Solving the Problem with <code>select_related</code> and <code>prefetch_related</code></h4>
<p>Django provides built-in tools to control how related objects are loaded efficiently: <code>select_related</code> and <code>prefetch_related</code>.</p>
<p><strong>1. Using</strong> <code>select_related</code></p>
<p><code>select_related</code> is designed for foreign key and one-to-one relationships. It performs an SQL join and retrieves related objects in a single query.</p>
<p>Use it when:</p>
<ul>
<li><p>You know you will access related objects</p>
</li>
<li><p>The relationship is one-to-one or many-to-one</p>
</li>
</ul>
<pre><code class="lang-bash">posts = Post.objects.select_related(<span class="hljs-string">"author"</span>)

<span class="hljs-keyword">for</span> post <span class="hljs-keyword">in</span> posts:
    <span class="hljs-built_in">print</span>(post.author.name)  <span class="hljs-comment"># No additional queries</span>
</code></pre>
<p>This performs a SQL JOIN and retrieves posts and authors in a single query, eliminating the N+1 problem.</p>
<p>It reduces multiple queries into just one, avoiding repeated database hits.</p>
<p><strong>2. Using</strong> <code>prefetch_related</code></p>
<p><code>prefetch_related</code> is used for many-to-many and reverse foreign key relationships. It performs separate queries for each related table but combines the results in Python.</p>
<p>Use it when:</p>
<ul>
<li><p>A SQL join would produce too much duplicated data</p>
</li>
<li><p>You are dealing with collections of related objects</p>
</li>
</ul>
<h4 id="heading-example-how-to-optimize-a-many-to-many-relationship">Example: How to Optimize a Many-to-Many Relationship</h4>
<p>Consider a blog application where posts can have multiple tags:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># models.py</span>
class Tag(models.Model):
    name = models.CharField(max_length=50)

class Post(models.Model):
    title = models.CharField(max_length=200)
    tags = models.ManyToManyField(Tag)
</code></pre>
<p>Now imagine fetching posts and accessing their tags:</p>
<pre><code class="lang-bash">posts = Post.objects.all()

<span class="hljs-keyword">for</span> post <span class="hljs-keyword">in</span> posts:
    <span class="hljs-built_in">print</span>(post.tags.all())  <span class="hljs-comment"># Triggers additional queries</span>
</code></pre>
<p>If you have 100 posts, Django may execute:</p>
<ul>
<li><p>1 query to fetch posts</p>
</li>
<li><p>1 query per post to fetch related tags</p>
</li>
</ul>
<p>This results in many unnecessary database hits.</p>
<p>You can optimize this using <code>prefetch_related</code>:</p>
<pre><code class="lang-bash">posts = Post.objects.prefetch_related(<span class="hljs-string">"tags"</span>)

<span class="hljs-keyword">for</span> post <span class="hljs-keyword">in</span> posts:
    <span class="hljs-built_in">print</span>(post.tags.all())  <span class="hljs-comment"># Uses prefetched data</span>
</code></pre>
<p>With this approach, Django performs one query for posts and one query for all related tags. It then matches them in Python, eliminating repeated database queries.</p>
<p>Together, these tools allow you to optimize your queries and eliminate the N+1 problem efficiently.</p>
<h4 id="heading-common-beginner-mistakes">Common Beginner Mistakes</h4>
<p>Even after applying these optimizations, it’s easy to make mistakes. Watch out for:</p>
<ul>
<li><p>Forgetting that serializers can trigger additional queries</p>
</li>
<li><p>Using <code>select_related</code> on many-to-many relationships</p>
</li>
<li><p>Assuming Django automatically optimizes queries</p>
</li>
<li><p>Not checking the query count after adding serializers</p>
</li>
</ul>
<p>Paying attention to these pitfalls ensures your API remains fast and scalable.</p>
<h3 id="heading-caching-in-django-rest-apis">Caching in Django REST APIs</h3>
<p>Even after optimizing database queries, API performance can still suffer if the same computations or database lookups are performed repeatedly. This is where caching comes in. Caching is a technique for storing the results of expensive operations so they can be retrieved more quickly the next time they are needed.</p>
<p>At its core, caching exists because computers have multiple layers of memory with different speeds:</p>
<ul>
<li><p>CPU registers (fastest)</p>
</li>
<li><p>L1, L2, L3 caches</p>
</li>
<li><p>Main memory (RAM)</p>
</li>
<li><p>SSD storage</p>
</li>
<li><p>HDD storage (slowest)</p>
</li>
</ul>
<p>Each layer trades speed for size: the closer the data is to the CPU, the faster it can be accessed. Software systems use the same principle; by storing frequently accessed data in a “closer” or faster location, applications can respond more quickly.</p>
<h4 id="heading-cache-eviction">Cache Eviction</h4>
<p>Caches are limited in size, so when a cache is full, some data must be removed to make room for new data. This process is called cache eviction.</p>
<p>Common eviction strategies include:</p>
<ul>
<li><p><strong>Least Recently Used (LRU):</strong> removes the data that hasn’t been accessed for the longest time</p>
</li>
<li><p><strong>Random Replacement:</strong> removes a random item from the cache</p>
</li>
</ul>
<p>The goal is to keep the data that is most likely to be requested again while freeing space for new data. Understanding this helps developers use caching effectively.</p>
<h4 id="heading-caching-in-application-architectures">Caching in Application Architectures</h4>
<p>Caching exists at several levels in modern software systems:</p>
<ul>
<li><p><strong>Client-side caching:</strong> Web browsers cache HTTP responses to reduce the need for repeated network requests. This is controlled with HTTP headers like <code>Cache-Control</code>.</p>
</li>
<li><p><strong>CDN caching:</strong> Content Delivery Networks store static assets closer to users, reducing latency and server load.</p>
</li>
<li><p><strong>Backend caching:</strong> Backend services cache results from database queries, computed values, or API responses. This is where Django caching is most commonly applied.</p>
</li>
</ul>
<p>By applying caching strategically at the backend, APIs can serve data faster while reducing computation and database load.</p>
<h4 id="heading-caching-in-django">Caching in Django</h4>
<p>Django provides a flexible caching framework that supports multiple backends, including in-memory, file-based, database-backed, and third-party stores like Redis. The main types of caching in Django are:</p>
<ol>
<li><p><strong>Per-view caching:</strong> caches the entire output of a view. Ideal for endpoints where responses rarely change.</p>
<pre><code class="lang-python"> <span class="hljs-keyword">from</span> django.views.decorators.cache <span class="hljs-keyword">import</span> cache_page

<span class="hljs-meta"> @cache_page(60 * 15)  # cache for 15 minutes</span>
 <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">my_view</span>(<span class="hljs-params">request</span>):</span>
</code></pre>
<ol start="2">
<li><p>Template fragment caching: caches specific parts of a template to avoid repeated rendering.</p>
</li>
<li><p>Low-level caching: gives full control over what is cached and for how long, making it ideal for API responses.</p>
</li>
</ol>
</li>
</ol>
<p>    By combining these approaches, you can reduce repeated work in your API, lower database load, and speed up response times.</p>
<h3 id="heading-when-to-use-redis">When to Use Redis</h3>
<p>While Django’s built-in caching backends are sufficient for many projects, high-traffic APIs often require a shared, in-memory cache. This is where Redis excels. Redis is designed for fast access, low latency, and can handle frequent reads across multiple servers.</p>
<p>You should consider using Redis when:</p>
<ul>
<li><p>Data is read frequently but changes infrequently</p>
</li>
<li><p>Low latency is important for API responses</p>
</li>
<li><p>You need cache expiration and eviction policies</p>
</li>
<li><p>You want a shared cache across multiple servers or services</p>
</li>
</ul>
<p>Redis is particularly effective for API endpoints that serve the same data to many users, such as frequently accessed lists or computed results.</p>
<h3 id="heading-common-beginner-mistakes-1">Common Beginner Mistakes</h3>
<p>Caching is powerful, but it’s easy to misuse. Some common pitfalls include:</p>
<ul>
<li><p><strong>Caching everything blindly:</strong> not all data benefits from caching</p>
</li>
<li><p><strong>Forgetting cache invalidation:</strong> stale data can lead to incorrect responses</p>
</li>
<li><p><strong>Using cache where query optimization would suffice:</strong> sometimes optimizing database queries is a better solution than caching.</p>
</li>
</ul>
<p>Remember: caching should complement good database design, not replace it.</p>
<h3 id="heading-pagination-and-limiting-expensive-datasets">Pagination and Limiting Expensive Datasets</h3>
<p>Even with caching, returning large datasets in a single request can slow down your API and increase memory usage. Pagination is a simple and effective way to limit the amount of data returned at once.</p>
<p>Pagination helps by reducing:</p>
<ul>
<li><p>Database load</p>
</li>
<li><p>Memory usage</p>
</li>
<li><p>Serialization time</p>
</li>
<li><p>Network transfer size</p>
</li>
</ul>
<p>Django REST Framework provides built-in pagination classes that make it easy to paginate endpoints. As a rule of thumb, always paginate list endpoints unless there is a strong reason not to.</p>
<h3 id="heading-load-testing-and-measuring-improvement">Load Testing and Measuring Improvement</h3>
<p>Optimizations are only meaningful if you can measure their impact. Load testing simulates multiple users accessing your API simultaneously, helping you answer key questions:</p>
<ul>
<li><p>How many requests per second can my API handle?</p>
</li>
<li><p>Where does the API start to break under load?</p>
</li>
<li><p>Did caching, query optimization, and pagination actually improve performance?</p>
</li>
</ul>
<p>By running load tests before and after optimization, you can validate that your changes have the desired effect and avoid optimizing the wrong parts of your system.</p>
<h2 id="heading-summary-and-next-steps">Summary and Next Steps</h2>
<p>Optimizing Django REST APIs isn’t about chasing every tiny micro-optimization. It’s about reducing unnecessary work and focusing on the parts of your API that actually slow down performance.</p>
<h4 id="heading-key-takeaways">Key Takeaways</h4>
<ul>
<li><p><strong>Profile before optimizing:</strong> Identify the real bottlenecks before making changes.</p>
</li>
<li><p><strong>Reduce database queries:</strong> Use techniques like <code>select_related</code>, <code>prefetch_related</code>, and avoid N+1 queries.</p>
</li>
<li><p><strong>Cache frequently accessed data:</strong> Use Django caching and Redis to reduce repeated computations.</p>
</li>
<li><p><strong>Paginate large datasets:</strong> Limit memory usage and network load by returning data in chunks.</p>
</li>
<li><p><strong>Measure performance changes:</strong> Always verify that your optimizations have a real impact.</p>
</li>
</ul>
<h4 id="heading-next-steps-for-your-apis">Next Steps for Your APIs</h4>
<ol>
<li><p><strong>Add profiling to your existing APIs</strong> to understand where time is spent.</p>
</li>
<li><p><strong>Identify one slow endpoint</strong> and focus on optimizing it first.</p>
</li>
<li><p><strong>Optimize database queries</strong> using Django ORM best practices.</p>
</li>
<li><p><strong>Introduce caching carefully</strong>; avoid caching everything blindly.</p>
</li>
<li><p><strong>Measure the results</strong> with load testing and performance metrics.</p>
</li>
</ol>
<p>Remember: Performance optimization is not a one-time task. It’s a habit built by continuously observing how your system works, testing improvements, and applying changes where they make the most impact.</p>
<h2 id="heading-read-more">Read More</h2>
<ol>
<li><p><a target="_blank" href="https://www.django-rest-framework.org/topics/performance/">DRF Performance</a></p>
</li>
<li><p><a target="_blank" href="https://docs.djangoproject.com/en/stable/topics/db/optimization/">Django ORM Optimization</a></p>
</li>
<li><p><a target="_blank" href="https://docs.djangoproject.com/en/stable/topics/db/optimization/#select-related">Understanding N+1 queries</a></p>
</li>
</ol>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use the Django REST Framework - Build Backend APIs with DRF ]]>
                </title>
                <description>
                    <![CDATA[ When you click on most backend development tutorials, they often teach you what to do, not how to think.That’s why many developers only realize their mistakes after they start building. So, how does one actually think like a backend developer? Before... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-the-django-rest-framework/</link>
                <guid isPermaLink="false">6920d5f802099ac646b401c5</guid>
                
                    <category>
                        <![CDATA[ Django ]]>
                    </category>
                
                    <category>
                        <![CDATA[ backend ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Mari ]]>
                </dc:creator>
                <pubDate>Fri, 21 Nov 2025 21:13:28 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763759552021/cc57d91b-c2b9-4a40-8bb9-52c517dbbc35.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When you click on most backend development tutorials, they often teach you <em>what</em> to do, not <em>how to think</em>.<br>That’s why many developers only realize their mistakes after they start building.</p>
<p>So, how does one actually think like a backend developer? Before answering that, let’s start with the basics: what exactly is backend development?</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-backend-development">What is Backend Development?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-django-rest-framework">Why Django REST Framework?</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-flask">Flask</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-fastapi">FastAPI</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-django-rest-framework">Django REST Framework</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-think-like-a-backend-developer">How to Think Like a Backend Developer</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-1-think-in-systems-not-lines-of-code">1. Think in Systems, Not Lines of Code</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-2-separate-concerns-keep-things-organized">2. Separate Concerns — Keep Things Organized</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-3-anticipate-problems-before-they-happen">3. Anticipate Problems Before They Happen</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-4-make-your-code-predictable-and-readable">4. Make Your Code Predictable and Readable</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-5-think-in-the-request-logic-response-cycle">5. Think in the Request → Logic → Response Cycle</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-6-practice-thinking-like-a-backend-developer">6. Practice Thinking Like a Backend Developer</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-install-the-django-rest-framework">How to Install the Django REST Framework</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-step-1-install-python">Step 1: Install Python</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-create-a-project-folder">Step 2: Create a Project Folder</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-create-a-virtual-environment">Step 3: Create a Virtual Environment</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-4-install-django">Step 4: Install Django</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-5-create-a-django-project">Step 5: Create a Django Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-6-install-django-rest-framework">Step 6: Install Django REST Framework</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-7-add-drf-to-installed-apps">Step 7: Add DRF to Installed Apps</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-8-run-initial-migrations">Step 8: Run Initial Migrations</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-9-start-the-server">Step 9: Start the Server</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-10-verify-drf-installation">Step 10: Verify DRF Installation</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-the-backend-developers-mindset">The Backend Developer’s Mindset</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-common-mistakes-beginners-make">Common Mistakes Beginners Make</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-further-reading">Further Reading</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-backend-development">What is Backend Development?</h2>
<p>Backend development is the foundation of most web and mobile applications. It focuses on everything that happens behind the scenes, from processing logic and handling data to connecting with databases and APIs.</p>
<p>While it’s true that backend developers build APIs that communicate with the frontend, the job goes far beyond that. The backend is where data is validated, protected, stored, and retrieved.</p>
<p>In short: backend development is about building systems that ensure data integrity, performance, and scalability.</p>
<p>Backend developers are the ones responsible for designing and maintaining those systems. They ensure that every user request is processed efficiently and securely.</p>
<p>Now, how does the Django REST Framework (DRF) fit into all this?</p>
<h2 id="heading-why-django-rest-framework">Why Django REST Framework?</h2>
<p>A beginner-friendly tutorial must use a tool that:</p>
<ul>
<li><p>Teaches good structure</p>
</li>
<li><p>Encourages best practices</p>
</li>
<li><p>Hides unnecessary complexity</p>
</li>
<li><p>Helps you learn backend fundamentals correctly</p>
</li>
</ul>
<p>That’s why this guide uses the <strong>Django REST Framework (DRF)</strong>. Here’s how it compares to other popular Python frameworks.</p>
<h3 id="heading-flask">Flask</h3>
<p>Flask is a lightweight and flexible microframework. It is great for small projects, but:</p>
<ul>
<li><p>You have to set up everything manually (routing, JSON handling, database handling).</p>
</li>
<li><p>You need extra libraries for authentication, validation, or serialization.</p>
</li>
<li><p>Beginners often create unstructured projects because Flask doesn’t enforce architecture.</p>
</li>
</ul>
<p>Flask teaches freedom, not structure.</p>
<h3 id="heading-fastapi">FastAPI</h3>
<p>FastAPI is modern, fast, and async-first. However:</p>
<ul>
<li><p>It assumes you already understand APIs.</p>
</li>
<li><p>It requires understanding Python type hints deeply.</p>
</li>
<li><p>The ecosystem is still growing.</p>
</li>
<li><p>Beginners may not understand its underlying concepts (dependency injection, async IO).</p>
</li>
</ul>
<p>FastAPI teaches speed, not fundamentals.</p>
<h3 id="heading-django-rest-framework">Django REST Framework</h3>
<p>DRF is ideal for beginners because:</p>
<ul>
<li><p>It sits on top of Django, a very stable full-stack framework.</p>
</li>
<li><p>It encourages good architecture from day one.</p>
</li>
<li><p>It handles serialization, authentication, routing, validation, and permissions for you.</p>
</li>
<li><p>It gives you structure instead of chaos.</p>
</li>
</ul>
<p><strong>Bottom line:</strong> DRF can help you learn how backend systems work from scratch.</p>
<h2 id="heading-how-to-think-like-a-backend-developer">How to Think Like a Backend Developer</h2>
<p>Thinking like a backend developer is not about memorizing code. It’s about learning to see the bigger picture, how data moves, how logic flows, and how to build systems that work reliably and can grow.</p>
<p>Backend thinking can be summarized into six main principles:</p>
<h3 id="heading-1-think-in-systems-not-lines-of-code">1. Think in Systems, Not Lines of Code</h3>
<p>Many beginners focus on writing code that works for one feature. A backend developer thinks about the entire system.</p>
<p><strong>Analogy:</strong> Imagine a factory. Each machine (function or endpoint) does one task, but the factory only works efficiently if every machine is arranged correctly and communicates properly.</p>
<p><strong>Example:</strong> When a user submits a form to create a task:</p>
<ul>
<li><p>The request reaches the server.</p>
</li>
<li><p>The backend validates the data.</p>
</li>
<li><p>The backend stores it in the database.</p>
</li>
<li><p>The backend sends a response to the user.</p>
</li>
</ul>
<p>A backend developer doesn’t just write a function to save data. They ask:</p>
<ul>
<li><p>Where should this logic live — view, serializer, or service layer?</p>
</li>
<li><p>How will the data be validated and cleaned?</p>
</li>
<li><p>How will the system scale if thousands of users submit tasks at the same time?</p>
</li>
</ul>
<p>Seeing the system first makes code predictable, maintainable, and scalable.</p>
<h3 id="heading-2-separate-concerns-keep-things-organized">2. Separate Concerns — Keep Things Organized</h3>
<p>Backend thinking is about <strong>structure</strong>. Every piece of code should have a clear responsibility:</p>
<ul>
<li><p><strong>Models</strong>: Store and define your data</p>
</li>
<li><p><strong>Serializers</strong>: Convert data to a format the client understands (like JSON)</p>
</li>
<li><p><strong>Views</strong>: Apply the business logic and respond to requests</p>
</li>
</ul>
<p><strong>Why this matters:</strong> Without separation, code becomes messy and hard to debug. You might find yourself mixing database queries with validation or formatting, which leads to errors later.</p>
<p><strong>Simple analogy:</strong> Think of a restaurant.</p>
<ul>
<li><p>The <strong>chef</strong> prepares the food (model/data).</p>
</li>
<li><p>The <strong>waiter</strong> delivers the food to customers in a presentable way (serializer).</p>
</li>
<li><p>The <strong>manager</strong> decides who gets what and handles special requests (view/logic).</p>
</li>
</ul>
<p>Each role is separate but connected. This is exactly how backend developers structure code.</p>
<h3 id="heading-3-anticipate-problems-before-they-happen">3. Anticipate Problems Before They Happen</h3>
<p>Backend developers don’t just code for today. They <strong>think ahead</strong>:</p>
<ul>
<li><p>What if the user sends invalid data?</p>
</li>
<li><p>What if two users try to edit the same record at the same time?</p>
</li>
<li><p>How will the system handle millions of requests in the future?</p>
</li>
</ul>
<p><strong>Example:</strong> If a user tries to create a task without a title, a beginner might just let it crash. A backend developer writes validation rules to catch this and return a clear error message.</p>
<p><strong>Rule of thumb:</strong> Always ask, <em>“What could go wrong here?”</em> and design your code to handle it gracefully.</p>
<h3 id="heading-4-make-your-code-predictable-and-readable">4. Make Your Code Predictable and Readable</h3>
<p>Backend development is about <strong>writing code for humans, not just computers</strong>.</p>
<ul>
<li><p>Use clear variable names (<code>task_title</code> instead of <code>x</code>).</p>
</li>
<li><p>Keep functions short and focused.</p>
</li>
<li><p>Document your code.</p>
</li>
</ul>
<p>This way, <strong>anyone can pick up your code and understand it</strong>, including your future self.</p>
<p><strong>Tip:</strong> A backend system that is easy to read and predict is easier to debug, extend, and scale.</p>
<h3 id="heading-5-think-in-the-request-logic-response-cycle">5. Think in the Request → Logic → Response Cycle</h3>
<p>Every backend action fits into this pattern:</p>
<ul>
<li><p><strong>Request</strong>: The client sends data.</p>
</li>
<li><p><strong>Logic</strong>: The server validates, processes, and decides what to do.</p>
</li>
<li><p><strong>Response</strong>: The server sends data back in a structured way.</p>
</li>
</ul>
<p><strong>Example:</strong> User creates a task:</p>
<ul>
<li><p>Request: <code>{ "title": "Learn DRF" }</code></p>
</li>
<li><p>Logic: Check title is not empty → save to database → mark completed as <code>False</code></p>
</li>
<li><p>Response: <code>{ "id": 1, "title": "Learn DRF", "completed": false }</code></p>
</li>
</ul>
<p>Thinking in this cycle makes debugging and designing systems intuitive.</p>
<h3 id="heading-6-practice-thinking-like-a-backend-developer">6. Practice Thinking Like a Backend Developer</h3>
<ul>
<li><p><strong>Ask questions before coding:</strong> “Where should this logic live? How will this affect other parts of the system?”</p>
</li>
<li><p><strong>Break down problems into steps:</strong> Don’t just code the solution; code the process.</p>
</li>
<li><p><strong>Visualize data flow:</strong> Draw diagrams if necessary, from user request to database and back.</p>
</li>
<li><p><strong>Learn by doing:</strong> Build small projects and reflect on each component’s role.</p>
</li>
</ul>
<p>Check out Andy Harris’s video on how to think like a programmer.</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/azcrPFhaY9k" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<p> </p>
<p>Now that you understand how backend developers think, let’s walk through setting up a real backend environment using Django REST Framework.</p>
<h2 id="heading-how-to-install-the-django-rest-framework">How to Install the Django REST Framework</h2>
<p>Here’s how to get the Django REST framework running on your machine from scratch.</p>
<h3 id="heading-step-1-install-python">Step 1: Install Python</h3>
<p>Make sure you have <strong>Python 3.8+</strong> installed. You can check if Python is installed with this command:</p>
<pre><code class="lang-bash">python --version
</code></pre>
<p>If it’s not installed, download it from the <a target="_blank" href="https://docs.python.org/3/">official Python documentation</a>.</p>
<h3 id="heading-step-2-create-a-project-folder">Step 2: Create a Project Folder</h3>
<p>Choose a location on your computer and create a folder for your project:</p>
<pre><code class="lang-bash">mkdir my_drf_project
<span class="hljs-built_in">cd</span> my_drf_project
</code></pre>
<p>This keeps all your files organized in one place.</p>
<h3 id="heading-step-3-create-a-virtual-environment">Step 3: Create a Virtual Environment</h3>
<p>A virtual environment keeps your project dependencies separate from other projects.</p>
<p>Create a virtual environment:</p>
<pre><code class="lang-bash">python -m venv venv
</code></pre>
<p>Next, activate it. For Windows (PowerShell):</p>
<pre><code class="lang-powershell">.\venv\Scripts\Activate.ps1
</code></pre>
<p>For Mac/Linux:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">source</span> venv/bin/activate
</code></pre>
<p>You’ll know it’s active when your terminal prompt starts with <code>(venv)</code>.</p>
<h3 id="heading-step-4-install-django">Step 4: Install Django</h3>
<p>Now install Django inside the virtual environment:</p>
<pre><code class="lang-bash">pip install django
</code></pre>
<p>Check that Django is installed:</p>
<pre><code class="lang-bash">python -m django --version
</code></pre>
<h3 id="heading-step-5-create-a-django-project">Step 5: Create a Django Project</h3>
<p>Create a new Django project:</p>
<pre><code class="lang-bash">django-admin startproject core .
</code></pre>
<p>The <code>.</code> at the end means “create the project here.” Run the server to make sure it works:</p>
<pre><code class="lang-bash">python manage.py runserver
</code></pre>
<p>Visit <a target="_blank" href="http://127.0.0.1:8000/"><code>http://127.0.0.1:8000/</code></a> in your browser. You should see the Django welcome page.</p>
<h3 id="heading-step-6-install-django-rest-framework">Step 6: Install Django REST Framework</h3>
<p>Install DRF using pip:</p>
<pre><code class="lang-bash">pip install djangorestframework
</code></pre>
<h3 id="heading-step-7-add-drf-to-installed-apps">Step 7: Add DRF to Installed Apps</h3>
<p>Open <code>core/</code><a target="_blank" href="http://settings.py"><code>settings.py</code></a> and find the <code>INSTALLED_APPS</code> list. Add:</p>
<pre><code class="lang-bash"><span class="hljs-string">'rest_framework'</span>,
</code></pre>
<p>It should look like this:</p>
<pre><code class="lang-bash">INSTALLED_APPS = [
    <span class="hljs-string">'django.contrib.admin'</span>,
    <span class="hljs-string">'django.contrib.auth'</span>,
    <span class="hljs-string">'django.contrib.contenttypes'</span>,
    <span class="hljs-string">'django.contrib.sessions'</span>,
    <span class="hljs-string">'django.contrib.messages'</span>,
    <span class="hljs-string">'django.contrib.staticfiles'</span>,
    <span class="hljs-string">'rest_framework'</span>,
]
</code></pre>
<h3 id="heading-step-8-run-initial-migrations">Step 8: Run Initial Migrations</h3>
<p>Set up your database:</p>
<pre><code class="lang-bash">python manage.py migrate
</code></pre>
<p>Create a superuser for accessing the admin panel:</p>
<pre><code class="lang-bash">python manage.py createsuperuser
</code></pre>
<p>Follow the prompts for username, email, and password.</p>
<h3 id="heading-step-9-start-the-server">Step 9: Start the Server</h3>
<p>Run your development server again:</p>
<pre><code class="lang-bash">python manage.py runserver
</code></pre>
<p>Visit:</p>
<ul>
<li><p><a target="_blank" href="http://127.0.0.1:8000/"><code>http://127.0.0.1:8000/</code></a> → Django welcome page</p>
</li>
<li><p><a target="_blank" href="http://127.0.0.1:8000/admin/"><code>http://127.0.0.1:8000/admin/</code></a> → Admin panel (login with superuser)</p>
</li>
</ul>
<p>You now have Django + DRF installed and ready for API development.</p>
<h3 id="heading-step-10-verify-drf-installation">Step 10: Verify DRF Installation</h3>
<p>The easiest way to confirm that Django REST Framework is installed correctly is to build a very small test API. Each part of the setup helps you verify that DRF is working end-to-end.</p>
<p>Create a new app:</p>
<pre><code class="lang-bash">python manage.py startapp api
</code></pre>
<p>This creates an <code>api</code> folder where you’ll place your test model, serializer, and view. Adding it to <code>INSTALLED_APPS</code> tells Django to recognize the new app.</p>
<p>Add it to <code>INSTALLED_APPS</code>:</p>
<pre><code class="lang-bash"><span class="hljs-string">'api'</span>,
</code></pre>
<p>Create a simple <a target="_blank" href="http://models.py"><code>models.py</code></a> in the <code>api</code> app:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> django.db <span class="hljs-keyword">import</span> models

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Task</span>(<span class="hljs-params">models.Model</span>):</span>
    title = models.CharField(max_length=<span class="hljs-number">200</span>)
    completed = models.BooleanField(default=<span class="hljs-literal">False</span>)
</code></pre>
<p>This model represents a basic task with a title and a completion status. Creating even a simple model lets you test whether DRF can serialize and expose database objects as API responses.</p>
<p>Run migrations:</p>
<pre><code class="lang-bash">python manage.py makemigrations
python manage.py migrate
</code></pre>
<p>These commands generate and apply database tables for the <code>Task</code> model. Without migrations, DRF won’t have anything to fetch and serialize.</p>
<p>Create a serializer (<code>api/</code><a target="_blank" href="http://serializers.py"><code>serializers.py</code></a>):</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> rest_framework <span class="hljs-keyword">import</span> serializers
<span class="hljs-keyword">from</span> .models <span class="hljs-keyword">import</span> Task

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TaskSerializer</span>(<span class="hljs-params">serializers.ModelSerializer</span>):</span>
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Meta</span>:</span>
        model = Task
        fields = <span class="hljs-string">'__all__'</span>
</code></pre>
<p>A serializer converts your <code>Task</code> model into JSON so it can be returned as an API response. This step confirms that DRF’s serializer tools are working.</p>
<p>Create a view (<code>api/</code><a target="_blank" href="http://views.py"><code>views.py</code></a>):</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> rest_framework <span class="hljs-keyword">import</span> viewsets
<span class="hljs-keyword">from</span> .models <span class="hljs-keyword">import</span> Task
<span class="hljs-keyword">from</span> .serializers <span class="hljs-keyword">import</span> TaskSerializer

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TaskViewSet</span>(<span class="hljs-params">viewsets.ModelViewSet</span>):</span>
    queryset = Task.objects.all()
    serializer_class = TaskSerializer
</code></pre>
<p><code>ModelViewSet</code> automatically creates the CRUD API endpoints for your model. If this loads correctly, it means DRF’s generic views and viewsets are functioning.</p>
<p>Wire it to URLs (<code>core/</code><a target="_blank" href="http://urls.py"><code>urls.py</code></a>):</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> django.urls <span class="hljs-keyword">import</span> path, include
<span class="hljs-keyword">from</span> rest_framework.routers <span class="hljs-keyword">import</span> DefaultRouter
<span class="hljs-keyword">from</span> api.views <span class="hljs-keyword">import</span> TaskViewSet

router = DefaultRouter()
router.register(<span class="hljs-string">'tasks'</span>, TaskViewSet)

urlpatterns = [
    path(<span class="hljs-string">'admin/'</span>, admin.site.urls),
    path(<span class="hljs-string">'api/'</span>, include(router.urls)),
]
</code></pre>
<p>The router generates routes <code>/api/tasks/</code> for you. If routing works, DRF is properly integrated into your Django project.</p>
<p>Test the API by visiting:</p>
<pre><code class="lang-bash">http://127.0.0.1:8000/api/tasks/
</code></pre>
<p>If everything is set up correctly, you’ll see Django REST Framework’s browsable API. This confirms that DRF is installed, your project recognizes it, and it can serialize and return data successfully.</p>
<h2 id="heading-the-backend-developers-mindset">The Backend Developer’s Mindset</h2>
<p>When writing backend code, your goal isn’t just to make something <em>work</em>; it’s to make it predictable, scalable, and maintainable.</p>
<p>Professional backend developers focus on:</p>
<ul>
<li><p><strong>Predictability over cleverness</strong> — Code should be clear to others.</p>
</li>
<li><p><strong>Separation of concerns</strong> — Keep logic, data, and presentation layers distinct.</p>
</li>
<li><p><strong>Validation</strong> — Never trust user input; always validate.</p>
</li>
<li><p><strong>Consistency</strong> — Stick to naming conventions and reusable patterns.</p>
</li>
</ul>
<p>This mindset is what separates backend <em>coders</em> from backend <em>engineers</em>.</p>
<h2 id="heading-common-mistakes-beginners-make">Common Mistakes Beginners Make</h2>
<ul>
<li><p><strong>Writing too much logic in views:</strong> Keep views light. Move business logic into services or serializers.</p>
</li>
<li><p><strong>Ignoring validation</strong>: Always define validation rules in your serializers.</p>
</li>
<li><p><strong>Not planning for scalability:</strong> Even small projects grow. Build like you expect more users.</p>
</li>
</ul>
<h2 id="heading-further-reading">Further Reading</h2>
<p><a target="_blank" href="https://docs.djangoproject.com/en/5.2/">Django official documentation</a></p>
<p><a target="_blank" href="https://www.freecodecamp.org/news/how-to-build-a-rest-api-in-django/">How to Build a REST API in Django</a></p>
<p><a target="_blank" href="https://www.freecodecamp.org/news/what-is-serialization/">What is Serialization?</a></p>
<p><a target="_blank" href="https://www.freecodecamp.org/news/rest-api-best-practices-rest-endpoint-design-examples/">REST API Best Practices – REST Endpoint Design Examples</a></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Thinking like a backend developer isn’t about memorizing syntax; it’s about understanding how systems behave.</p>
<p>When you start reasoning through requests, logic, and responses, you begin to see the bigger picture, and that’s when you stop writing code and start building systems.</p>
<p>With Django REST Framework, that process becomes easier, cleaner, and more intuitive.</p>
<p>As you continue learning, build small APIs and gradually add features. The more you understand how data flows through a system, the more naturally backend thinking will come.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
