<?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[ Prabodh Tuladhar - 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[ Prabodh Tuladhar - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 19 May 2026 10:28:00 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/prabodhtuladhar/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Scoped Note-Taking API with Django Rest Framework and SimpleJWT ]]>
                </title>
                <description>
                    <![CDATA[ If you've built a Django API and you're wondering how to add authentication so that each user can only access their own data, you're in the right place. Most Django tutorials teach you session-based a ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-scoped-note-taking-api-with-django-rest-framework-and-simplejwt/</link>
                <guid isPermaLink="false">69fa4395a386d7f121cd3bfc</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Django ]]>
                    </category>
                
                    <category>
                        <![CDATA[ REST API ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Programming Blogs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ django rest framework ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Prabodh Tuladhar ]]>
                </dc:creator>
                <pubDate>Tue, 05 May 2026 19:23:01 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/36921ffa-4741-4e11-8f16-2c84322ebceb.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you've built a Django API and you're wondering how to add authentication so that each user can only access their own data, you're in the right place.</p>
<p>Most Django tutorials teach you session-based authentication. That works fine when your frontend and backend live on the same server. But the moment you separate them – say, a React app on Netlify talking to a Django API on PythonAnywhere – then sessions start to break down.</p>
<p>Cookies don't travel well across different domains, and suddenly your login system stops working.</p>
<p>That's where JSON Web Tokens (JWT) come in. JWTs give you a stateless, cookie-free way to authenticate users. They work seamlessly across domains, devices, and platforms. The server doesn't need to remember anything. It just verifies the token's signature and knows exactly who's making the request.</p>
<p>But authentication is only half the problem. Once you know who a user is, you still need to control what they can see. This is where <strong>scoping</strong> comes in.</p>
<p>Scoping means ensuring that each user can only access their own data. User A should never be able to read, edit, or delete User B's data (notes in our case), even if they somehow guess the right ID.</p>
<p>In this tutorial, you'll build a a personal note-taking API where users can register, log in with JWT tokens, and store notes that only they can access.</p>
<p>Along the way, you'll implement a custom user model, configure SimpleJWT for token-based authentication, and write scoped views that lock each user's data behind their own credentials.</p>
<h3 id="heading-what-well-cover">What We'll Cover:</h3>
<ul>
<li><p><a href="#heading-prerequisities">Prerequisities</a></p>
</li>
<li><p><a href="#heading-what-is-jwt-and-why-use-it-over-session-authentication">What is JWT and Why Use It Over Session Authentication</a>?</p>
<ul>
<li><p><a href="#heading-how-session-authentication-works">How Session Authentication Works</a></p>
</li>
<li><p><a href="#heading-how-jwt-authentication-works">How JWT Authentication Works</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-1-how-to-set-up-the-project-and-install-the-dependecies">Step 1: How to Set Up the Project and Install the Dependecies</a></p>
<ul>
<li><p><a href="#heading-11-how-to-create-the-project">1.1 How to Create the Project</a></p>
</li>
<li><p><a href="#heading-12-how-to-create-a-virtual-environment-and-install-the-required-dependencies">1.2 How to Create a Virtual Environment and Install the Required Dependencies</a></p>
</li>
<li><p><a href="#heading-13-how-to-create-the-project-and-the-app">1.3 How to Create the Project and the App</a></p>
</li>
<li><p><a href="#heading-14-how-to-register-the-app-and-django-rest-framework-drf">1.4 How to Register the App and Django Rest Framework (DRF)</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-2-how-to-create-a-custom-user-model">Step 2: How to Create a Custom User Model</a></p>
<ul>
<li><p><a href="#heading-21-how-to-define-the-custom-user-model">2.1 How to Define the Custom User Model</a></p>
</li>
<li><p><a href="#heading-22-how-to-tell-django-to-use-your-custom-user-model">2.2 How to Tell Django to Use Your Custom User Model</a></p>
</li>
<li><p><a href="#heading-23-how-to-run-migrations">2.3 How to Run Migrations</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-3-how-to-define-the-note-model">Step 3: How to Define the Note Model</a></p>
<ul>
<li><p><a href="#heading-32-how-to-apply-migration">3.2 How to Apply Migration</a></p>
</li>
<li><p><a href="#heading-33-how-to-register-models-in-the-admin">3.3 How to Register Models in the Admin</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-4-how-to-create-the-serializer">Step 4: How to Create the Serializer</a></p>
<ul>
<li><p><a href="#heading-41-how-to-create-userserializer">4.1 How to Create UserSerializer</a></p>
</li>
<li><p><a href="#heading-42-how-to-create-noteserializer">4.2 How to Create NoteSerializer</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-5-how-to-configure-simplejwt">Step 5: How to Configure SimpleJWT</a></p>
<ul>
<li><p><a href="#heading-51-how-to-update-rest-framework-settings">5.1 How to Update REST Framework Settings</a></p>
</li>
<li><p><a href="#heading-52-how-to-add-token-url-endpoints">5.2 How to Add Token URL Endpoints</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-6-how-to-build-the-authentication-logic">Step 6: How to Build the Authentication Logic</a></p>
</li>
<li><p><a href="#heading-step-7-how-to-implement-scoped-views">Step 7: How to Implement Scoped Views</a></p>
<ul>
<li><p><a href="#heading-71-how-to-create-a-noteviewset">7.1 How to Create a NoteViewSet</a></p>
</li>
<li><p><a href="#heading-72-why-this-matters-preventing-id-enumeration-attacks">7.2 Why This Matters: Preventing ID Enumeration Attacks</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-8-how-to-connect-a-url">Step 8: How to Connect a URL</a></p>
<ul>
<li><p><a href="#heading-81-how-to-create-app-level-urls">8.1 How to Create App-level URLs</a></p>
</li>
<li><p><a href="#heading-82-how-to-verify-the-project-level-urls">8.2 How to Verify the Project-Level URLs</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-9-how-to-test-the-apis-with-postman">Step 9: How to Test the APIs with Postman</a></p>
<ul>
<li><p><a href="#heading-91-how-to-register-a-user">9.1 How to Register a User</a></p>
</li>
<li><p><a href="#heading-92-how-to-obtain-access-and-refresh-tokens">9.2 How to Obtain Access and Refresh Tokens</a></p>
</li>
<li><p><a href="#heading-93-how-to-create-a-note">9.3 How to Create a Note</a></p>
</li>
<li><p><a href="#heading-94-how-to-list-your-notes">9.4 How to List Your Notes</a></p>
</li>
<li><p><a href="#heading-95-how-to-demostrate-scoping">9.5 How to Demostrate Scoping</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-10-how-to-handle-token-expiration-with-refresh-tokens">Step 10: How to Handle Token Expiration with Refresh Tokens</a></p>
</li>
<li><p><a href="#heading-how-you-can-improve-this-project">How You Can Improve This Project</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<p>Here's what this tutorial covers:</p>
<ol>
<li><p>How to set up a custom user model (and why you should always do this)</p>
</li>
<li><p>How to configure SimpleJWT for access and refresh token authentication</p>
</li>
<li><p>How to build serializers that protect sensitive fields</p>
</li>
<li><p>How to scope your API views so users only see their own data</p>
</li>
<li><p>How to test the entire flow using Postman</p>
</li>
</ol>
<p>Let's get started</p>
<h2 id="heading-prerequisities">Prerequisities</h2>
<p>Before you begin, make sure you're comfortable with the following:</p>
<ol>
<li><p><strong>Django fundamentals</strong>: You should understand how Django projects and apps work, including models, views, URLs, and migrations.</p>
</li>
<li><p><strong>Django REST Framework basics</strong>: You should be familiar with serializers, viewsets or API views, and how DRF handles requests and responses.</p>
</li>
<li><p><strong>Basic command line usage</strong>: You'll run commands in your terminal throughout this tutorial.</p>
</li>
</ol>
<p>Tools you'll need installed:</p>
<ul>
<li><p>Python 3.8 or higher</p>
</li>
<li><p>pip (Python's package manager)</p>
</li>
<li><p>A code editor like Visual Studio Code</p>
</li>
<li><p>Postman (or any API testing tool) for testing your endpoints. You'll use this to send requests to your API.</p>
</li>
</ul>
<h2 id="heading-what-is-jwt-and-why-use-it-over-session-authentication">What is JWT and Why Use It Over Session Authentication?</h2>
<p>Before you write any code, it's important to understand what problem JWTs solve and why Django's built-in session authentication isn't always enough.</p>
<h3 id="heading-how-session-authentication-work">How Session Authentication Work</h3>
<p>Django ships with a session-based authentication system. Here's how it works at a high level:</p>
<ol>
<li><p>A user sends their username and password to the server.</p>
</li>
<li><p>The server verifies the credentials and creates a <strong>session</strong> which is a small record stored in the server's database that says "this user is logged in."</p>
</li>
<li><p>The server sends back a <strong>session ID</strong> as a cookie. The browser stores this cookie automatically.</p>
</li>
<li><p>On every subsequent request, the browser sends the cookie back to the server. The server looks up the session ID in its database and says "ah, this is User A. Let them through."</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/2689a08a-f8a9-4b83-ad7b-cc4c2de90419.png" alt="The infographics shows the steps taken in Django session authentication" style="display:block;margin:0 auto" width="2708" height="1252" loading="lazy">

<p>This works perfectly when your frontend and backend live on the same domain. The browser handles cookies automatically, and Django manages sessions in the database without you thinking about it.</p>
<p>But this approach has some limitations.</p>
<ol>
<li><p><strong>The cross-domain problem:</strong> If your React frontend lives at app.example.com and your Django API lives at <a href="http://api.example.com">api.example.com</a>, cookies become tricky. Browsers enforce strict rules about which domains can send and receive cookies.</p>
<p>You can work around this with CORS (Cross-Origin Resource Sharing) headers and special cookie settings, but it adds complexity and can be fragile.</p>
</li>
<li><p><strong>The scalability problem:</strong> Every active session is stored in the server's database. If you have 10,000 users logged in at the same time, that's 10,000 session records the server has to look up on every single request. As your application grows, this lookup becomes a bottleneck.</p>
</li>
<li><p><strong>The mobile problem:</strong> Mobile apps don't handle cookies the same way browsers do. If you're building an API that will serve both a web app and a mobile app, session cookies create extra headaches.</p>
</li>
</ol>
<h3 id="heading-how-jwt-authentication-works">How JWT Authentication Works</h3>
<p>JWTs take a fundamentally different approach. Instead of storing session data on the server, they put the authentication information directly into the token itself.</p>
<p>Here's how the flow works:</p>
<ol>
<li><p>A user sends their username and password to the server.</p>
</li>
<li><p>The server verifies the credentials and creates a JWT – a long encoded string that contains information like the user's ID and when the token expires.</p>
</li>
<li><p>The server sends this token back to the client. The client stores it (usually in memory or local storage).</p>
</li>
<li><p>On every subsequent request, the client includes the token in the request header. The server reads the token, verifies its signature, and says "this is User A. Let them through."</p>
</li>
</ol>
<p>Notice the key difference: <strong>the server never stores anything</strong>.</p>
<p>It doesn't look up a session in a database. It simply reads the token, checks its cryptographic signature to make sure nobody tampered with it, and extracts the user information. That's why JWTs are called <strong>stateless</strong> – the server doesn't maintain any state about who is logged in.</p>
<p><strong>This solves the cross-domain problem</strong> because tokens are sent in the request header, not as cookies. Headers work the same way regardless of which domain the request comes from.</p>
<p><strong>This solves the scalability problem</strong> because the server doesn't store sessions. Verifying a token is a quick cryptographic check, not a database lookup.</p>
<p><strong>This solves the mobile problem</strong> because any client that can send HTTP headers can use JWT. Mobile apps, desktop apps, other servers – they all work the same way.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/41d60dbd-707c-4483-8374-8910024bda7f.png" alt="The infographics shows the steps taken in JWT authentication" style="display:block;margin:0 auto" width="2682" height="1272" loading="lazy">

<h2 id="heading-step-1-how-to-set-up-the-project-and-install-the-dependecies">Step 1: How to Set Up the Project and Install the Dependecies</h2>
<h3 id="heading-11-how-to-create-the-project">1.1 How to Create the Project</h3>
<p>Open your terminal, navigate to where you want your project to live, and run the following commands:</p>
<pre><code class="language-shell">mkdir notes-project

cd notes-project
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/594f6c90-ea92-4859-9d9b-442d2fd2f23d.png" alt="The image shows the creation of notes project folder" style="display:block;margin:0 auto" width="1642" height="429" loading="lazy">

<h3 id="heading-12-how-to-create-a-virtual-environment-and-install-the-required-dependencies">1.2 How to Create a Virtual Environment and Install the Required Dependencies</h3>
<p>You will create a virtual environment here. Type the following command:</p>
<pre><code class="language-shell">python3 -m venv venv
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/4c1c9c33-9ffb-433a-a8f0-60cf0f598e14.png" alt="The image shows the creation of the virtual environment folder after tying the command" style="display:block;margin:0 auto" width="1778" height="380" loading="lazy">

<p>The above command creates a virtual environment inside a folder called <code>venv</code>. The first <code>venv</code> is the command and the second <code>venv</code> represents the name of the folder. You can name the folder anything though <code>venv</code> is usually preferred.</p>
<p>To activate the virtual environment, we need to use the following command:</p>
<p>On macOS/Linux:</p>
<pre><code class="language-shell">source venv/bin/activate
</code></pre>
<p>On Windows:</p>
<pre><code class="language-shell">venv\Scripts\activate
</code></pre>
<p>You'll know it worked when you see <code>(venv)</code> at the beginning of your terminal prompt. From this point on, any Python packages you install will only exist inside this <strong>virtual environment</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/0a58f106-684c-4286-91fc-3c60f6e45483.png" alt="The image shows virtual environment being activated" style="display:block;margin:0 auto" width="2072" height="558" loading="lazy">

<p>With the virutal environment activated, install Django, Django Rest Framework, and Simple JWT Framework using the command:</p>
<pre><code class="language-shell">pip install django djangorestframework djangorestframework-simplejwt 
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/af83ad3f-3201-4e59-9367-e95630ae3cb3.png" alt="The image shows the installation of the packages after running the pip command" style="display:block;margin:0 auto" width="2314" height="1326" loading="lazy">

<p>You can verify everything installed correctly by running:</p>
<pre><code class="language-shell">pip list
</code></pre>
<p>You should see all three packages listed along with their dependencies.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/b725778c-57ca-42cb-a0a2-486ec3e6286e.png" alt="The image shows a list of all the dependencies along with the dependencies installed just now" style="display:block;margin:0 auto" width="2316" height="1038" loading="lazy">

<h3 id="heading-13-how-to-create-the-project-and-the-app">1.3 How to Create the Project and the App</h3>
<p>Run the following command to create the Django project:</p>
<pre><code class="language-plaintext">django-admin startproject notes_core .
</code></pre>
<p>The dot at the end is important. It tells Django to create the project files in your current directory instead of creating an extra nested folder.</p>
<p>Now let's type this command to create the app:</p>
<pre><code class="language-shell">python manage.py startapp notes
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/f0f95ce1-b8b0-4acb-ae5a-194fa3d74e08.png" alt="The image shows the folder structure of django project and app" style="display:block;margin:0 auto" width="2860" height="1638" loading="lazy">

<h3 id="heading-14-how-to-register-the-app-and-django-rest-framework-drf">1.4 How to Register the App and Django Rest Framework (DRF)</h3>
<p>Open <code>notes_core/settings.py</code> and add <code>rest_framework</code> and <code>notes</code> in the <code>INSTALLED_APPS</code> list:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/35988eff-506c-43af-ac6c-ef698e843ad3.png" alt="The image show the DRF and notes app being added to installed app list" style="display:block;margin:0 auto" width="2740" height="1312" loading="lazy">

<p>Django now knows about your new app and the REST framework. Let's move on to the most important architectural decision you'll make for this project.</p>
<h2 id="heading-step-2-how-to-create-a-custom-user-model">Step 2: How to Create a Custom User Model</h2>
<p>If you've built Django projects before, you might have used Django's default User model. For quick prototypes, that works fine. But for any project you plan to grow or maintain, starting with a custom user model is a best practice you should never skip.</p>
<p>Here's why: Django's default <code>User</code> model uses a <code>username</code> field as the primary identifier. If you later decide you want users to log in with their email address instead, or you need to add a profile picture field, or a phone number, then you're stuck.</p>
<p>Using a custom user model gives you full control over what a "user" means in your app. Instead of being tied to a username, you can design login around something more practical, like email or phone_number for a fitness or mobile-based app. You can also include fields like role (doctor, patient, receptionist in a clinic system) or date of birth directly in the user model, instead of managing a separate profile.</p>
<p>It also helps future-proof your project. If you start with the default model and later decide to switch login from username to email, or add required fields, it becomes difficult and risky to change. Using a custom user model from the beginning avoids this problem and makes it much easier to adapt your authentication system as your app grows.</p>
<p>By creating a custom user model from the start, even if it's identical to the default one, you give yourself the freedom to make changes later without any of that pain.</p>
<h3 id="heading-21-how-to-define-the-custom-user-model">2.1 How to Define the Custom User Model</h3>
<p>Open <code>notes/models/py</code> and add the following code:</p>
<pre><code class="language-python">from django.contrib.auth.models import AbstractUser
from django.db import models

class CustomUser(AbstractUser):
    pass
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/29310f13-cdc9-409a-9c36-23234882fd6e.png" alt="The image shows the code for the custom user model" style="display:block;margin:0 auto" width="2802" height="1194" loading="lazy">

<p>You are importing Django’s built-in <code>AbstractUser</code> class.</p>
<p>Think of <code>AbstractUser</code> as a ready-made blueprint for a user. It already includes fields like username, password, email, first name, last name , and authentication logic.</p>
<p>The <code>pass</code> statement means you're not adding any extra fields yet.</p>
<p>But the key point is that this model is yours. So this model behaves exactly like Django’s default user model, but with one <strong>big advantage</strong>: you now have the flexibility to customize it later.</p>
<p>If three months from now you need to add a <code>phone_number</code> field or switch to email-based login, you just add a field to this class and run a migration.</p>
<pre><code class="language-python">from django.contrib.auth.models import AbstractUser
from django.db import models

class CustomUser(AbstractUser):
    phone_number = models.CharField(max_length=15)
</code></pre>
<p>You can also see all the fields that the <code>CustomUser</code> class has inherited from the <code>AbstractUser</code> class.</p>
<p>To do this we can use the Python shell. Type the following command:</p>
<pre><code class="language-shell">python manage.py shell
</code></pre>
<p>When you type this command, make sure that the virtual environment is active:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/3c958f7f-6403-4eaf-b339-43532a02af6a.png" alt="The image shows the command to enter into the python shell with the virtual environment being activated" style="display:block;margin:0 auto" width="2588" height="604" loading="lazy">

<p>After this, import the <code>CustomUser</code> model in the shell:</p>
<pre><code class="language-shell">from notes.models import CustomUser
</code></pre>
<p>After that, type the following code:</p>
<pre><code class="language-shell">[fields.name for field in CustomUser._meta.get_fields()]
</code></pre>
<p>The above statement lists out all the fields in the <code>CustomUser</code> class.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/5a1b3314-7b48-41f6-98a9-dc3133cfce4c.png" alt="The image shows the output of all the fileds inherited by the CustomUser model" style="display:block;margin:0 auto" width="2638" height="904" loading="lazy">

<h3 id="heading-22-how-to-tell-django-to-use-your-custom-user-model">2.2 How to Tell Django to Use Your Custom User Model</h3>
<p>Now comes the important bit. Open <code>notes_core/settings.py</code> and add this line:</p>
<pre><code class="language-python">AUTH_USER_MODEL = 'notes.CustomUser'
</code></pre>
<p>This setting tells Django to use your <code>CustomUser</code> model instead of the built-in one for everything authentication-related such as login, permissions, foreign keys, and so on.</p>
<p>There's no strict rule to where you need to add it, but the best practice is to add it near the end of the file.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/87c9b03c-908d-4b2d-a6d5-9528ff98d1ab.png" alt="The image shows the above code being added to the settings.py file" style="display:block;margin:0 auto" width="2870" height="1280" loading="lazy">

<p>You can see which user model Django is using by using the method <code>get_user_model()</code>.</p>
<p>Open the Python shell again and import the <code>get_user_model()</code> method:</p>
<pre><code class="language-shell">from django.contrib.auth import get_user_model 
</code></pre>
<p>Then use <code>get_user_model()</code> and print the output:</p>
<pre><code class="language-shell">user = get_user_model()
print(user)
</code></pre>
<p>You should see the name of our model being used:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/895d5bcc-6880-4c4d-9007-96d44e9fa496.png" alt="895d5bcc-6880-4c4d-9007-96d44e9fa496" style="display:block;margin:0 auto" width="1580" height="276" loading="lazy">

<p>If you hadn't added the <code>AUTH_USER_MODEL</code> in the <code>settings.py</code> file, then Django would have used the default user model:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/a89f013a-e5d8-4e59-90b8-7da594188c13.png" alt="The image shows the default user model being used by Django" style="display:block;margin:0 auto" width="1886" height="1126" loading="lazy">

<p><strong>Note:</strong> You'll need to do this before you run your first migration. If you run migrate before setting AUTH_USER_MODEL, Django creates tables for the default User model, and switching afterward becomes a headache.</p>
<h3 id="heading-23-how-to-run-migrations">2.3 How to Run Migrations</h3>
<p>Now create and apply the initial migrations:</p>
<pre><code class="language-shell">python manage.py makemigrations
python manage.py migrate
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/cbca499a-286c-4f07-9a3e-375a69b4c374.png" alt="The image shows the output after running the above commands" style="display:block;margin:0 auto" width="2072" height="1222" loading="lazy">

<p>Django will create the necessary tables for your custom user model along with all the built-in Django tables.</p>
<p>We can again peek under the hood to see the SQL queries that Django used to create the tables especially the <code>CustomUser</code> table.</p>
<p>Type this command:</p>
<pre><code class="language-shell">python manage.py sqlmigrate notes 0001
</code></pre>
<p>Here <code>notes</code> is the name of the app and <code>0001</code> represents the migration number.</p>
<p>And you should get this output:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/850bba6c-8e54-4beb-8b85-f30f193341d5.png" alt="The image shows the output after the sqlmigrate command is executed" style="display:block;margin:0 auto" width="2714" height="1486" loading="lazy">

<p>Let's also create a superuser so you can access the admin panel later for debugging:</p>
<pre><code class="language-shell">python manage.py createsuperuser
</code></pre>
<p>Fill in the username, email (optional), and password when prompted.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/43820470-86b4-4274-834a-19a97adbc208.png" alt="The image shows the super user being created" style="display:block;margin:0 auto" width="2172" height="596" loading="lazy">

<h2 id="heading-step-3-how-to-define-the-note-model">Step 3: How to Define the Note Model</h2>
<p>Now let's create the data model for the core of your application. First add a new import to use the <code>settings</code> object.</p>
<pre><code class="language-python">from django.conf import settings
</code></pre>
<p>Then add the following code below the <code>CustomUser</code> class:</p>
<pre><code class="language-python">class Notes(models.Model):
    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='notes'
    )
    title = models.CharField(max_length=200)
    body = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    def __str__(self):
        return f"{self.title} (by {self.owner.username})"
</code></pre>
<p>Here's the complete <code>model.py</code> code:</p>
<pre><code class="language-python">from django.contrib.auth.models import AbstractUser
from django.db import models
from django.conf import settings

class CustomUser(AbstractUser):
    pass

class Notes(models.Model):
    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='notes'
    )
    title = models.CharField(max_length=200)
    body = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    def __str__(self):
        return f"{self.title} (by {self.owner.username})"
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/24f1c5e3-6f9a-4dce-a7dc-d5d17aecc202.png" alt="The image shows the complete models.py file" style="display:block;margin:0 auto" width="2728" height="1314" loading="lazy">

<p>Let's walk through each field:</p>
<ol>
<li><p><code>owner = models.ForeignKey(settings.AUTH_USER_MODEL, ...)</code>: Creates a relationship between each note and a user. The <code>ForeignKey</code> field tells Django that each note belogs to exactly one user but a user can have many notes.</p>
<p>Notice that we use <code>settings.AUTH_USER_MODEL</code> instead of directly importing <code>CustomUser</code>. This is the recommended practice because it keeps your code flexible. If you ever change the user model reference in settings, this foreign key adapts automatically.</p>
<p>The <code>on_delete=models.CASCADE</code> means that if a user is deleted, all their notes are deleted too.</p>
<p>The <code>related_name='notes'</code> lets you access a user's notes with <code>user.notes.all()</code>.</p>
</li>
<li><p><code>title = models.CharField(max_length=200)</code>: Creates a text field for the task name, limited to 200 characters.</p>
</li>
<li><p><code>body = models.TextField()</code>: Holds the actual note content. <code>TextField</code> has no character limit, so users can write as much as they need.</p>
</li>
<li><p><code>created_at = models.DateTimeField(auto_now_add=True)</code>: Automatically records the date and time when a task is created. You never need to set this manually.</p>
<p>The <code>__str__()</code> method gives each note a readable representation. Instead of seeing "Note object (1)" in the admin panel or during debugging, you'll see something like "Meeting Notes (by Solina)."</p>
</li>
</ol>
<h3 id="heading-32-how-to-apply-migration">3.2 How to Apply Migration</h3>
<p>Run the migration commands to create the Note table:</p>
<pre><code class="language-shell">python manage.py makemigrations
python manage.py migrate
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/bf4b3469-89d8-4f8b-8b0d-1ea24eda5e2e.png" alt="The image shows the result of migrating the notes model" style="display:block;margin:0 auto" width="2594" height="1304" loading="lazy">

<p>As before, we can see the exact SQL query Django used to create the <code>notes</code> table:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/606d0d20-7c4f-40fb-a37b-de623ccee574.png" alt="The image shows the SQL query to create the notes table  and reference to the custom user table created earlier" style="display:block;margin:0 auto" width="2700" height="634" loading="lazy">

<h3 id="heading-33-how-to-register-models-in-the-admin">3.3 How to Register Models in the Admin</h3>
<p>Open <code>notes/admin.py</code> and register both models so you can inspect data through the admin panel:</p>
<pre><code class="language-python">from django.contrib import admin
from .models import CustomUser, Notes

admin.site.register(CustomUser)
admin.site.register(Notes)
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/482fc6de-33d2-4d09-8c6f-ce3df684eaa3.png" alt="The image shows the code for admin.py" style="display:block;margin:0 auto" width="2376" height="736" loading="lazy">

<p>This is helpful during development when you want to quickly check whether data is being saved correctly.</p>
<h2 id="heading-step-4-how-to-create-the-serializer">Step 4: How to Create the Serializer</h2>
<p>In DRF, a serializer is like a bridge between your database and the internet.</p>
<p>Django models store data as Python objects. But when you want to send that data to a frontend application (like React or a mobile app), you can't send Python objects. You need to send a format that everyone understands which is usually JSON.</p>
<p>Serializers perform three main jobs:</p>
<ol>
<li><p><strong>Serialization:</strong> Converting complex Python objects (Models) into Python dictionaries (which can be easily rendered into JSON).</p>
</li>
<li><p><strong>Deserialization:</strong> Converting JSON data coming from a user back into complex Python objects.</p>
</li>
<li><p><strong>Validation:</strong> Checking if the incoming data is correct before saving it to the database.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/a7339d6d-e338-4e12-a837-a780e85752a6.png" alt="The image shows the serialization deserialization process" style="display:block;margin:0 auto" width="800" height="287" loading="lazy">

<h3 id="heading-41-how-to-create-userserializer">4.1 How to Create UserSerializer</h3>
<p>Create a new file called <code>notes/serializers.py</code> and add the following code:</p>
<pre><code class="language-python">from rest_framework import serializers
from django.contrib.auth import get_user_model

User = get_user_model()
class UserSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True)

    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'password']

    def create(self, validated_data):
        user = User.objects.create_user(
            username=validated_data['username'],
            email=validated_data.get('email', ''),
            password=validated_data['password']
        )
        return user
</code></pre>
<p>Let's break down this serializer.</p>
<ol>
<li><p>The <code>UserSerializer</code> handles user registration.</p>
</li>
<li><p><code>User = get_user_model()</code> gets the user model that you're using and stores in the variable <code>User</code>. In our case, we're using the <code>CustomUser</code> model</p>
</li>
<li><p><code>class UserSerializer(serializers.ModelSerializer):</code>: Here you've created the UserSerializer class, which inherits <code>ModelSerializer</code>.</p>
<p>A <code>ModelSerializer</code> is a shortcut that automatically creates a serializers class with fields that are in the model class.</p>
<p>When we use a <code>ModelSerializer</code>, DRF inspects the model and automatically does these things:</p>
<p>1. Generates fields from the model so you don't have to<br>2. Automatically adds field validations that are present in the model<br>3. Implements <code>create()</code> and <code>update()</code> methods. A <code>ModelSerializer</code> knows which model to use and how to update and create it. You can override <code>create()</code> and <code>update()</code> methods if you need customized behaviors. <strong>You have overridden the</strong> <code>create()</code> <strong>method in the above code.</strong></p>
</li>
<li><p><code>password = serializers.CharField(write_only=True)</code>: This line is crucial. The <code>write_only=True</code> flag means the password will be accepted during registration but will <strong>never</strong> appear in any API response. Without this, your API would send back the password (even if hashed) every time user data is returned.</p>
<p>So users can create accounts, but their passwords are never exposed back.</p>
</li>
<li><p><code>class Meta</code>: Inside the <code>Meta</code> class, you tell the serializer which model to use. In this case, the model to use is <code>User</code> and the fields to be handled.</p>
</li>
<li><p>The <code>create()</code> method: This is the most important part. This method runs when we create a new user. Instead of using the default <code>.create()</code> method you have overridden it.</p>
<p>It's important to understand why we have overridden this method. The default <code>create()</code> method is not suitable for creating users securely.</p>
<p>By default this method stores the password in plain text format. This is a serious problem because passwords should never be stored in raw form. They need to be <strong>hashed</strong> so that even if the database is compromised, the passwords are never exposed.</p>
<p>Django provides a special method called <code>create_user()</code> that automatically handles this by <strong>hashing the password</strong> and setting up the user properly for authentication.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/c1246e5d-104d-486c-965c-8edb04c850dc.png" alt="The image shows the annoated explanation of the code above" style="display:block;margin:0 auto" width="2340" height="1280" loading="lazy">

<h3 id="heading-42-how-to-create-noteserializer">4.2 How to Create NoteSerializer</h3>
<p>After the <code>UserSerializer</code> class, let's create the <code>NoteSerializer</code> class. The <code>NoteSerializer</code> handles the notes data</p>
<p>First of all, you need to add an import to the <code>Notes</code> class. Add the line <code>from .models import Notes</code> at the end of the last import.</p>
<p>Put this code below the <code>UserSerializer</code> class:</p>
<pre><code class="language-python">class NoteSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')
    class Meta:
        model = Notes
        fields = ['id', 'owner', 'title', 'body', 'created_at']
</code></pre>
<p>Now let's break it down:</p>
<ol>
<li><p><code>owner = serializers.ReadOnlyField(source='owner.username')</code>: This is the most important line in the code. This makes the <code>owner</code> field <strong>read-only</strong>. That means the API will display who owns a note (showing their username), but no one can set or change the owner through the API.</p>
<p>Without this protection, a malicious user could send a POST request with <code>"owner": 5</code> and assign their note to someone else's account, or worse, modify someone else's notes by reassigning ownership.</p>
<p>The <code>source='owner.username'</code> part tells DRF to display the owner's username instead of their numeric ID, which makes the API responses more readable.</p>
</li>
<li><p><code>class Meta:</code> ...: As before the <code>Meta</code> class contains the model which the serializer use and the fields that the API will expose.</p>
<p>Here is the complete code in the <code>serializers.py</code> file</p>
</li>
</ol>
<pre><code class="language-python">from rest_framework import serializers
from django.contrib.auth import get_user_model
from .models import Notes

User = get_user_model()
class UserSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True)
    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'password']

    def create(self, validated_data):
        user = User.objects.create_user(
            username=validated_data['username'],
            email=validated_data.get('email', ''),
            password=validated_data['password']
        )
        return user

class NoteSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')
    class Meta:
        model = Notes
        fields = ['id', 'owner', 'title', 'body', 'created_at']
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/3073f560-79dc-4950-8acb-96d871e0511c.png" alt="The image shows the complete code for the serializers.py file" style="display:block;margin:0 auto" width="2006" height="1310" loading="lazy">

<h2 id="heading-step-5-how-to-configure-simplejwt">Step 5: How to Configure SimpleJWT</h2>
<p>Now let's set up the authentication system. This is where you tell DRF to use JWT for authentication instead of sessions. This step is crucial because without it, DRF will default to session-based auth.</p>
<p>SimpleJWT provides a complete JWT implementation for DRF, so you don't have to build token generation, signing, or verification from scratch.</p>
<p>The access token is what your client sends with every API request. It's short-lived by design. Think of it like a visitor badge at an office building: it gets you through the door, but it expires at the end of the day. If someone steals it, the damage is limited because it stops working soon.</p>
<p>The refresh token is longer-lived and has a single purpose: getting a new access token when the current one expires. The client stores it securely and only sends it to one specific endpoint. Think of it like your employee ID card. You use it to get a new visitor badge each morning, but you don't flash it at every door.</p>
<p>This separation exists for security. If the short-lived access token is compromised (which is more likely since it's sent with every request), the attacker has a narrow window before it expires. The refresh token, which is sent less frequently, has a lower risk of interception.</p>
<p>Let's look at how the access and refresh token work together</p>
<ol>
<li><p>User logs in, server gives both access token and refresh token</p>
</li>
<li><p>User makes requests using the access token</p>
</li>
<li><p>Access token expires</p>
</li>
<li><p>App sends refresh token to server</p>
</li>
<li><p>Server checks it and gives a new access token</p>
</li>
<li><p>User continues without logging in again</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/9cf61a53-99c6-4665-bf5d-dccafa71ca8d.png" alt="The image shows the use of access and refresh tokens" style="display:block;margin:0 auto" width="1840" height="1006" loading="lazy">

<h3 id="heading-51-how-to-update-rest-framework-settings">5.1 How to Update REST Framework Settings</h3>
<p>Open <code>notes_core/settings.py</code> and add the following code:</p>
<pre><code class="language-python">from datetime import timedelta
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),

    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
}

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
}
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/54e89c41-b456-4bbb-a129-ac3e92a09a6d.png" alt="The image shows the code being added to settings.py file" style="display:block;margin:0 auto" width="2492" height="1528" loading="lazy">

<p>Let's unpack what each section does.</p>
<p>The <code>DEFAULT_AUTHENTICATION_CLASSES</code> setting tells DRF to use JWT as the authentication method for all API endpoints. Every incoming request will be checked for a valid JWT token in the Authorization header.</p>
<p>The <code>DEFAULT_PERMISSION_CLASSES</code> setting sets <code>IsAuthenticated</code> as the global permission policy. This means every endpoint in your API is locked down by default. Only users with a valid token can access any endpoint.</p>
<p>This is a secure-by-default approach: instead of remembering to protect each view individually, everything is protected, and you explicitly open up the endpoints that need to be public <em>(like the registration endpoint, which you'll handle in the next step).</em></p>
<p>The <code>SIMPLE_JWT</code> dictionary controls token behavior. The access token lasts 30 minutes. This is the token clients include in every request. If someone intercepts it, the damage is limited to a 30-minute window. The refresh token lasts one day.</p>
<p>When the access token expires, the client can use the refresh token to get a new access token without forcing the user to log in again. The duration of the refresh token is 1 day. This means after 1 day, the user must log in again with their username and password. You'll see exactly how this works later when you test with Postman.</p>
<h3 id="heading-52-how-to-add-token-url-endpoints">5.2 How to Add Token URL Endpoints</h3>
<p>SimpleJWT provides ready-made views for obtaining and refreshing tokens. You just need to wire them up to URLs.</p>
<p>Open <code>notes_core/urls.py</code> and update it with the following code:</p>
<pre><code class="language-python">from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('notes.urls')),
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
</code></pre>
<p>The <code>token/</code> endpoint accepts a username and password, and returns an access token and a refresh token.</p>
<p>The <code>token/refresh/</code> endpoint accepts a refresh token and returns a new access token. You'll see these in action during testing.</p>
<h2 id="heading-step-6-how-to-build-the-authentication-logic">Step 6: How to Build the Authentication Logic</h2>
<p>Open <code>notes/views.py</code> and add the following:</p>
<pre><code class="language-python">from rest_framework import generics, permissions
from django.contrib.auth import get_user_model
from .serializers import UserSerializer

User = get_user_model()

class RegisterView(generics.CreateAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [permissions.AllowAny]
</code></pre>
<p>Now let's walk through this code.</p>
<p>The first section are the imports and after that we have used the the <code>get_user_model()</code> method to get the <code>CustomUser</code> model.</p>
<p>Now the main part is <code>RegisterView</code> class. The class inherits from <code>generics.CreateAPIView</code> which is a built in DRF view designed specifically for handling POST requests that create new objects.</p>
<p>Because of this, you don’t have to manually write the logic for handling POST requests, validating data, or saving to the database. DRF does all of that for you behind the scenes.</p>
<p>Inside the class, <code>queryset = Users.objects.all()</code> defines the set of user objects this view can work with.</p>
<p>The <code>serializer_class = UserSerializer</code> tells the view which serializer to use for validating incoming data and creating the user.</p>
<p>Finally <code>permission_classes = [permissions.AllowAny]</code> overrides the global <code>IsAuthenticated</code> permission you set earlier in the value of <code>DEFAULT_PERMISSION_CLASSES</code> .</p>
<p>This means that anyone can access the registration endpoint, even if they aren't logged in. This makes sense for a registration endpoint because new users won’t have accounts yet.</p>
<p>Every other view in your API will inherit the global IsAuthenticated permission, so only this registration endpoint is open.</p>
<h2 id="heading-step-7-how-to-implement-scoped-views">Step 7: How to Implement Scoped Views</h2>
<p>This is the heart of the tutorial. You've set up authentication so the API knows <strong>who</strong> is making a request. Now you need to make sure each user can only interact with <strong>their</strong> <strong>own</strong> notes.</p>
<p>Think of it this way: authentication is the lock on the front door of an apartment building. It keeps strangers out. But scoping is the lock on each individual apartment. Just because you live in the building doesn't mean you can walk into your neighbor's apartment.</p>
<p>Without scoping, an authenticated user could potentially see every note in the database, or worse, modify notes that belong to someone else. Two method overrides on your viewset prevent this entirely.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/03747660-068c-4757-9fc2-d2a122f22f5f.png" alt="The image represents the differences of access resources with and without scoping" style="display:block;margin:0 auto" width="1024" height="559" loading="lazy">

<h3 id="heading-71-how-to-create-a-noteviewset">7.1 How to Create a NoteViewSet</h3>
<p>Now let's create the <code>NoteViewSet</code>. First add these imports to the top of the file. We're importing the viewsets, serializers, and model.</p>
<pre><code class="language-python">from .models import Note
from .serializers import UserSerializer, NoteSerializer
from rest_framework import generics, viewsets, permissions
</code></pre>
<p>Add the following to <code>notes/views.py</code>, below the RegisterView:</p>
<pre><code class="language-python">class NoteViewSet(viewsets.ModelViewSet):
    serializer_class = NoteSerializer

    def get_queryset(self):
        return Notes.objects.filter(owner=self.request.user).order_by('-created_at')

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)
</code></pre>
<p>Now let's talk about this code in detail.</p>
<p>You've created a new class called <code>NoteViewSet</code> which inherits from the DRF class <code>ModelViewSet</code>. This gives you full CRUD operations, meaning you can list notes and retrieve a single note, as well as create, update, and delete a note.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/25999ad8-3534-424a-ab35-cad9cecec8ef.png" alt="The image shows the Model Viewset being imported" style="display:block;margin:0 auto" width="1338" height="248" loading="lazy">

<p>The next part <code>serializer_class = NoteSerializer</code> tells Django to use the <code>NoteSerializer</code> class to convert between Python objects and JSON.</p>
<p>But the magic is the two methods that you are overriding: <code>get_queryset()</code> and <code>perform_create()</code>.</p>
<p>The <code>get_queryset()</code> method controls which notes the API returns. If you didn't override this method, it would return <code>Note.objects.all()</code> (which would give every user access to every note in the database).</p>
<p>But here, you've overridden this method so that it filters notes by the current user.</p>
<p>Next is the <code>perform_create()</code> method, which is called when the note is saved. You've overridden this method so that it saves the notes of the user who's currently logged in. If you hadn't overridden the this method, it would return all the notes regardless of the logged in user.</p>
<p>Notice that you have passed <code>self.request.user</code> parameters in to the <code>filter()</code> function. This is the code that attaches the logged-in user as the owner of the note.</p>
<p>Remember how you made the owner field read-only in the serializer? This is the other half of that security measure.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/6db94c2f-673f-480a-bf20-730ed4af4bdb.png" alt="6db94c2f-673f-480a-bf20-730ed4af4bdb" style="display:block;margin:0 auto" width="1718" height="710" loading="lazy">

<p>The user can't set the owner through the API request, and the server automatically sets it to whoever is authenticated. These two pieces work together to make ownership tamper-proof.</p>
<h3 id="heading-72-why-this-matters-preventing-id-enumeration-attacks">7.2 Why This Matters: Preventing ID Enumeration Attacks</h3>
<p>Without get_queryset filtering, your API might allow something like this: a user sends a GET request to <code>/api/notes/42/</code> and sees a note that belongs to someone else, simply because they guessed the ID.</p>
<p>This is called an <strong>ID enumeration attack</strong> — an attacker cycles through IDs (1, 2, 3, 4...) to discover and access other people's data.</p>
<p>With your scoped <code>get_queryset</code>, even if User B sends a request to <code>/api/notes/42/</code> and note 42 belongs to User A, the viewset won't find it in User B's filtered queryset. DRF will return a 404 — as far as User B is concerned, that note doesn't exist.</p>
<h2 id="heading-step-8-how-to-connect-a-url">Step 8: How to Connect a URL</h2>
<p>Now you need to wire up the views to URL paths so the API knows which view to call for each endpoint.</p>
<h3 id="heading-81-how-to-create-app-level-urls">8.1 How to Create App-level URLs</h3>
<p>Create a new file called <code>notes/urls.py</code> and add the following:</p>
<pre><code class="language-python">from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import RegisterView, NoteViewSet

router = DefaultRouter()
router.register(r'notes', NoteViewSet, basename='note')

urlpatterns = [
    path('register/', RegisterView.as_view(), name='register'),
    path('', include(router.urls)),
]
</code></pre>
<p>The <code>DefaultRouter</code> automatically generates URL patterns for the NoteViewSet. Since you're using a <code>ModelViewSet</code>, the router creates endpoints for listing all notes, creating a note, retrieving a single note, updating a note, and deleting a note — <strong>all from that single router.register call.</strong></p>
<p>The <code>basename='note'</code> parameter is required here because your viewset doesn't have a queryset attribute defined directly on the class <em>(you're using get_queryset instead)</em>. DRF uses the <code>basename</code> to generate the URL pattern names like <code>note-list</code> and <code>note-detail</code>.</p>
<h3 id="heading-82-how-to-verify-the-project-level-urls">8.2 How to Verify the Project-Level URLs</h3>
<p>Make sure your <code>notes_core/urls.py</code> looks like this (you set this up in Step 5, but let's confirm):</p>
<pre><code class="language-python">from django.contrib import admin
from django.urls import path, include
from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('notes.urls')),
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
</code></pre>
<p>Here's the full picture of your API's URL structure:</p>
<table>
<thead>
<tr>
<th><strong>Endpoint</strong></th>
<th><strong>Method</strong></th>
<th><strong>Description</strong></th>
</tr>
</thead>
<tbody><tr>
<td><code>api/register/</code></td>
<td><strong>POST</strong></td>
<td>Create a new user account</td>
</tr>
<tr>
<td><code>api/token/</code></td>
<td><strong>POST</strong></td>
<td>Get access and refresh tokens</td>
</tr>
<tr>
<td><code>api/token/refresh/</code></td>
<td><strong>POST</strong></td>
<td>Get a new access token using a refresh token</td>
</tr>
<tr>
<td><code>api/notes/</code></td>
<td><strong>GET</strong></td>
<td>List all notes for the authenticated user</td>
</tr>
<tr>
<td><code>api/notes/</code></td>
<td><strong>POST</strong></td>
<td>Create a new note</td>
</tr>
<tr>
<td><code>api/notes/&lt;id&gt;/</code></td>
<td><strong>GET</strong></td>
<td>Retrieve a specific note</td>
</tr>
<tr>
<td><code>api/notes/&lt;id&gt;/</code></td>
<td><strong>PUT/PATCH</strong></td>
<td>Update a specific note</td>
</tr>
<tr>
<td><code>api/notes/&lt;id&gt;/</code></td>
<td><strong>DELETE</strong></td>
<td>Delete a specific note</td>
</tr>
</tbody></table>
<p>Start the development server to make sure everything runs without errors:</p>
<pre><code class="language-shell">python manage.py runserver
</code></pre>
<p>If the server starts without complaints, your code is wired up correctly.</p>
<h2 id="heading-step-9-how-to-test-the-apis-with-postman">Step 9: How to Test the APIs with Postman</h2>
<p>Building the API is one thing. Proving it works is another. Let's walk through the entire flow using Postman, from registering a user to demonstrating that scoping actually works.</p>
<p>If you haven't used Postman before, it's a tool that lets you send HTTP requests to your API and inspect the responses. You can download it from <a href="https://www.postman.com/downloads/">postman.com/downloads</a>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/cc154f8b-d3db-4c48-b884-1dbd3d517209.png" alt="Postman software download page" style="display:block;margin:0 auto" width="2518" height="1552" loading="lazy">

<p>Alternatively, you can use curl from the command line or any other API testing tool you're comfortable with.</p>
<p>Make sure your development server is running before proceeding.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/90524445-9d7c-43d0-bcf7-105f54626d85.png" alt="python server running" style="display:block;margin:0 auto" width="2078" height="678" loading="lazy">

<h3 id="heading-91-how-to-register-a-user">9.1 How to Register a User</h3>
<p>Open Postman:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/8951a205-ee01-4646-8e0b-d1d16d21c749.png" alt="opening Postman" style="display:block;margin:0 auto" width="2764" height="1792" loading="lazy">

<p>Create a new request:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>POST</th>
</tr>
</thead>
<tbody><tr>
<td><strong>URL</strong></td>
<td><code>http://127.0.0.1:8000/api/register/</code></td>
</tr>
<tr>
<td><strong>Body tab</strong></td>
<td>Select "raw" and choose "JSON" from the dropdown</td>
</tr>
<tr>
<td><strong>Body Content</strong></td>
<td>{ "username": "priya", "email": "<a href="mailto:priya@example.com">priya@example.com</a>", "password": "securepassword123" }</td>
</tr>
</tbody></table>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/f043164f-184c-4f29-8a9f-55f604d521fb.png" alt="postman UI for registering a new user" style="display:block;margin:0 auto" width="1918" height="770" loading="lazy">

<p>Click <strong>Send</strong>. You should get a <code>201 Created</code> response with the user data <strong>(without the password</strong>, thanks to your <code>write_only=True</code> field) which you wrote in the <code>UserSerializer</code> class.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/021a79db-418f-4057-97f8-f3c5c4b9761c.png" alt="response of registering a user" style="display:block;margin:0 auto" width="2024" height="1240" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/c8fd6b4e-f539-4f16-85c8-72515abadf8f.png" alt="The image describes the codes of the User serializer classs" style="display:block;margin:0 auto" width="2340" height="1280" loading="lazy">

<h3 id="heading-92-how-to-obtain-access-and-refresh-tokens">9.2 How to Obtain Access and Refresh Tokens</h3>
<p>Now log in to get your JWTs:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>POST</th>
</tr>
</thead>
<tbody><tr>
<td><strong>URL</strong></td>
<td><code>http://127.0.0.1:8000/api/token/</code></td>
</tr>
<tr>
<td><strong>Body</strong></td>
<td>{"username" : "priya", "password" : "securepassword123"}</td>
</tr>
</tbody></table>
<p>You'll get a response with access and refresh tokens.</p>
<p><strong>Copy the access token.</strong> You'll need it for every subsequent request. Also save the refresh token, as you'll use it later.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/e5667e26-b11a-463e-85d4-ba99082bee21.png" alt="The image shows the api returning access and refresh token" style="display:block;margin:0 auto" width="2016" height="1224" loading="lazy">

<p>A JWT is only encoded and not encrypted. The encoding is merely a way to transform the data into a safe, standard string format that can be easily transmitted over the internet.</p>
<p>Any one can peel through the encoding to see the data. This is done using base64url encoding.</p>
<p>We can use the Python library <code>pyjwt</code> to decode JWTs or use any of the online sites to decode. It's important to note that you should use online sites with caution since JWTs may contain sensitive information.</p>
<p>For this demo, we'll use site called <a href="https://www.jwt.io">jwt.io</a>.</p>
<p>Open the site and paste in the access token that you have just created:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/f8b9f3e0-db1e-49fb-82be-2b4d27adc8bd.png" alt="The image describes the various sections after decoding the JWT token" style="display:block;margin:0 auto" width="2448" height="1392" loading="lazy">

<p>The JWT has three parts: the header, the payload, and the signature.</p>
<p>The header sections tells you how the header is signed. In this case it is signed using the <strong>HS256</strong> algorithm.</p>
<p>The payload is where the actual data or claim lives. It contains standard claims such as token types, expiration time ( <code>exp</code> ), issued at time ( <code>iat</code> ), and custom claims.</p>
<p>The signature section is used to verify integrity. You <strong>can't decode it to meaningful data.</strong> This section ensures that the token wasn't tampered with.</p>
<h3 id="heading-93-how-to-create-a-note">9.3 How to Create a Note</h3>
<p>Now use the access token to create a note:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>POST</th>
</tr>
</thead>
<tbody><tr>
<td><strong>URL</strong></td>
<td><code>http://127.0.0.1:8000/api/notes/</code></td>
</tr>
<tr>
<td><strong>Header tab:</strong></td>
<td>Add a new header:</td>
</tr>
<tr>
<td>Key: Authorization, Value: Bearer</td>
<td></td>
</tr>
<tr>
<td><strong>Body</strong></td>
<td>{'title': 'My note', 'body': 'This contains secret information'}</td>
</tr>
</tbody></table>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/d04e2bfb-edc6-4c12-ac93-65611ed0d805.png" alt="The image shows adding new header into postman" style="display:block;margin:0 auto" width="1762" height="1084" loading="lazy">

<p>Notice that you don't include an owner field. That's handled automatically by perform_create. You should get a <code>201 Created response</code>:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/19a8b564-4d49-4dc1-823c-d7d96fcb5319.png" alt="The image shows the output (response) after creating a note" style="display:block;margin:0 auto" width="1802" height="1494" loading="lazy">

<p>You can create a few more notes, so that we have some data to work with.</p>
<h3 id="heading-94-how-to-list-your-notes">9.4 How to List Your Notes</h3>
<p>Now to fetch all of Priya's notes:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>GET</th>
</tr>
</thead>
<tbody><tr>
<td><strong>URL</strong></td>
<td><code>http://127.0.0.1:8000/api/notes/</code></td>
</tr>
<tr>
<td><strong>Header tab:</strong></td>
<td>Same Authorization: Bearer header</td>
</tr>
</tbody></table>
<p>You should see all the notes created, sorted by most recent first.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/8b7107e6-44ef-465c-b1aa-076ec9de1979.png" alt="The image shows the response of getting list of notes" style="display:block;margin:0 auto" width="2122" height="1634" loading="lazy">

<h3 id="heading-95-how-to-demonstrate-scoping">9.5 How to Demonstrate Scoping</h3>
<p>Let's prove that a second user can't view the first user's notes.</p>
<p>First, register the second user.</p>
<p>Send a POST request to <code>http://127.0.0.1/api/register</code> with the following data:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>POST</th>
</tr>
</thead>
<tbody><tr>
<td><strong>URL</strong></td>
<td><code>http://127.0.0.1:8000/api/register/</code></td>
</tr>
<tr>
<td><strong>Body tab</strong></td>
<td>Select "raw" and choose "JSON" from the dropdown</td>
</tr>
<tr>
<td><strong>Body Content</strong></td>
<td>{ "username": "sujan", "email": "<a href="mailto:sujan@example.com">sujan@example.com</a>", "password": "anotherpassword123" }</td>
</tr>
</tbody></table>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/094af273-4220-416a-9c8f-f33079badf83.png" alt="The image shows a new user being created" style="display:block;margin:0 auto" width="2028" height="994" loading="lazy">

<p>Then get tokens for Sujan by sending a POST request to <code>http://127.0.0.1:8000/api/token/</code> with Sujan's credentials (username and password) and then copy Sujan's access token.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/8fe22f1b-f36e-4b35-a478-1b48ea0218c3.png" alt="8fe22f1b-f36e-4b35-a478-1b48ea0218c3" style="display:block;margin:0 auto" width="2118" height="1274" loading="lazy">

<p>Now send a GET request to <code>http://127.0.0.1:8000/api/notes/</code> using Sujan's token in the Authorization header.</p>
<p>The response should be an empty list since this user hasn't created any notes:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/ef8e17d0-4f8f-4a51-8bfd-01c078247075.png" alt="The image shows the response of the get query new user's access token" style="display:block;margin:0 auto" width="2094" height="1352" loading="lazy">

<p>More importantly, Priya's notes are completely invisible to him. Even if Sujan tries to access a specific note by ID – say, <code>http://127.0.0.1:8000/api/notes/1/</code> – he'll get a <code>404 Not Found</code> response, not a <code>403 Forbidden</code>.</p>
<p>This is intentional. A <code>404 Not Found</code> doesn't reveal that the note exists, while a <code>403 Forbidden</code> would confirm its existence to a potential attacker.</p>
<p>A <code>403 Forbidden</code> response is like a door with a sign: <em>“Authorized personnel only”.</em> You now know something important is inside. A <code>404 Not Found</code> response is like a blank wall. You don’t even know a room exists.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/a4619fbb-42ce-4f80-9207-31eb81336c7c.png" alt="The image shows the difference between a 403 and 404 response code" style="display:block;margin:0 auto" width="1860" height="910" loading="lazy">

<p>Now that you know why we've used the <code>404</code> response instead of <code>403</code>, let's demonstrate this.</p>
<p>First, I'll access Priya's individual note using her credentials and her access token:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/e68e33cc-93a4-4eb5-99fe-934b283defeb.png" alt="The image shows the result of accessing individual note using the first user (Priya)" style="display:block;margin:0 auto" width="2116" height="1170" loading="lazy">

<p>Now, I'll change the access token and put Sujan's (new user) access token:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/e5a6d07c-a7f0-4a50-ba05-94cc5159489c.png" alt="The image shows the response of accessing the second note using the new user's (sujan) credentials" style="display:block;margin:0 auto" width="2110" height="1166" loading="lazy">

<p>You can see that using the new user's token to access the previous user's note leads to <code>404 Not Found</code> response.</p>
<h2 id="heading-step-10-how-to-handle-token-expiration-with-refresh-tokens">Step 10: How to Handle Token Expiration with Refresh Tokens</h2>
<p>Access tokens are deliberately short-lived (30 minutes in your configuration). This limits the window of damage if a token is stolen.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/b86f4f24-b8b0-45d0-bcee-5e39e2268e21.png" alt="b86f4f24-b8b0-45d0-bcee-5e39e2268e21" style="display:block;margin:0 auto" width="1330" height="870" loading="lazy">

<p>But you don't want users to re-enter their credentials every 30 minutes. That's what refresh tokens are for.</p>
<p>When Priya's access token expires, her API requests will start returning <code>401 Unauthorized</code> responses. Instead of logging in again, the client sends the refresh token to get a fresh access token.</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>POST</th>
</tr>
</thead>
<tbody><tr>
<td><strong>URL</strong></td>
<td><code>http://127.0.0.1:8000/api/token/refresh/</code></td>
</tr>
<tr>
<td><strong>Body tab</strong></td>
<td>Select "raw" and choose "JSON" from the dropdown</td>
</tr>
<tr>
<td><strong>Body Content</strong></td>
<td>{ refresh: &lt; Priya's refresh token &gt;}</td>
</tr>
</tbody></table>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/67d255ac-8696-45e6-be17-c80b2a6e8af0.png" alt="The image shows the response of getting a new access token using a refresh token" style="display:block;margin:0 auto" width="2110" height="1360" loading="lazy">

<p>Replace your old access token with this new one, and you're good for another 30 minutes. The refresh token itself lasts for one day, so the user only needs to fully log in again once every 24 hours.</p>
<p>In a real application, the frontend client handles this automatically. When an API call returns a <code>401</code>, the client catches it, sends the refresh token to get a new access token, and retries the original request — all without the user noticing.</p>
<p>Here's what that flow looks like in pseudocode:</p>
<ol>
<li><p>Client sends request with access token</p>
</li>
<li><p>Server responds with 401 (token expired)</p>
</li>
<li><p>Client sends refresh token to /api/token/refresh/</p>
</li>
<li><p>Server responds with a new access token</p>
</li>
<li><p>Client retries the original request with the new access token</p>
</li>
<li><p>Server responds with the data</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/f0fd3fd8-842b-4de3-8f5b-2f1d4ad2181f.png" alt="The image show the steps to get a new token after a previous one has expired" style="display:block;margin:0 auto" width="1868" height="906" loading="lazy"></li>
</ol>
<p>If the refresh token itself has expired (after 24 hours in your configuration), step 4 will also return a <code>401</code>. At that point, the user truly needs to log in again with their username and password. This is the intended behavior: it means even a stolen refresh token has a limited useful life.</p>
<h2 id="heading-how-you-can-improve-this-project">How You Can Improve This Project</h2>
<p>This API is functional and secure, but there's plenty of room to build on it. Here are some directions you could take.</p>
<ol>
<li><p><strong>Add search and filtering.</strong> Let users search their notes by title or body text. You can use DRF's SearchFilter and django-filter to add query parameters like <code>?search=meeting</code> to the notes list endpoint.</p>
</li>
<li><p><strong>Add categories or tags.</strong> Create a <code>Category</code> model and add a <strong>foreign key</strong> to <code>Note</code>, or use a many-to-many relationship for tags. This would let users organize their notes and filter by category.</p>
</li>
<li><p><strong>Add pagination.</strong> Once a user has hundreds of notes, returning them all in a single response becomes slow. DRF has built-in pagination classes that let you return notes in pages of 10, 20, or whatever size you choose.</p>
</li>
<li><p><strong>Deploy to a production server.</strong> The API currently runs on your local machine. You could deploy it to platforms like PythonAnywhere, Railway, or Render to make it accessible from anywhere. You'd need to configure a production database (like PostgreSQL), set a secure SECRET_KEY, and serve the application behind HTTPS.</p>
</li>
<li><p><strong>Build a frontend.</strong> Connect a React, Next.js, or Vue.js frontend to this API. Store the JWTs in the client and implement the token refresh flow so users stay logged in seamlessly.</p>
</li>
<li><p><strong>Add token blacklisting.</strong> SimpleJWT supports token blacklisting, which lets you invalidate refresh tokens when a user logs out. Without this, a refresh token remains valid until it expires, even after the user "logs out."</p>
</li>
</ol>
<p>Each of these improvements builds on the patterns you've already learned and will deepen your understanding of Django, DRF, and API design.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You've built a fully functional, secure note-taking API with Django, Django REST Framework, and SimpleJWT. Along the way, you learned some fundamental concepts that apply to any API you'll build in the future.</p>
<p>You started with a custom user model — a small decision at the beginning that saves you from a painful migration later. You configured JWT authentication so your API can serve mobile clients and decoupled frontends that can't rely on session cookies.</p>
<p>You built serializers that protect sensitive data by keeping passwords write-only and ownership read-only. Most importantly, you implemented scoped views that ensure each user's data is completely isolated from everyone else's.</p>
<p>The patterns you practiced here — overriding <code>get_queryset</code> to filter by the current user, overriding <code>perform_create</code> to assign ownership automatically, and using <code>read-only</code> fields to prevent data tampering — are the same patterns you'll use in production APIs handling real user data.</p>
<p>The best way to solidify what you've learned is to keep building. Try adding search and filtering, build a React frontend that consumes this API, or start a completely new project may be a task manager, a journal app, or a bookmarks API using the same JWT and scoping patterns. The core workflow stays the same. Only the models and business logic change.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build and Deploy a Fitness Tracker Using Python Django and PythonAnywhere - A Beginner Friendly Guide ]]>
                </title>
                <description>
                    <![CDATA[ If you've learned some Python basics but still feel stuck when it comes to building something real, you're not alone. Many beginners go through tutorials, learn about variables, functions, and loops,  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-and-deploy-a-fitness-tracker-using-python-django-and-pythonanywhere/</link>
                <guid isPermaLink="false">69cfff6ce466e2b762506a84</guid>
                
                    <category>
                        <![CDATA[ Programming Blogs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Django ]]>
                    </category>
                
                    <category>
                        <![CDATA[ deployment ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Beginner Developers ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Prabodh Tuladhar ]]>
                </dc:creator>
                <pubDate>Fri, 03 Apr 2026 17:57:00 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/a1ae273b-9f92-4fc2-89aa-1452fc0df895.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you've learned some Python basics but still feel stuck when it comes to building something real, you're not alone. Many beginners go through tutorials, learn about variables, functions, and loops, and then hit a wall when they try to create an actual project.</p>
<p>The gap between "I know Python syntax" and "I can build a working web app" can feel enormous. But it does not have to be.</p>
<p>In this tutorial, you'll build a fitness tracker web application from scratch using Django, one of the most popular Python web frameworks. By the end, you'll have a fully functional app running live on the internet – something you can show to friends, add to your portfolio, or keep building on.</p>
<p>Here's what you'll learn:</p>
<ul>
<li><p>How Django projects and apps are structured</p>
</li>
<li><p>How to define database models to store workout data</p>
</li>
<li><p>How to create views that handle user requests</p>
</li>
<li><p>How to build HTML templates that display your data</p>
</li>
<li><p>How to connect URLs to views so users can navigate your app</p>
</li>
<li><p>How to deploy your finished app to PythonAnywhere so anyone can access it</p>
</li>
</ul>
<p>The app itself is straightforward: you can log a workout by entering an activity name, duration, and date. You can then view all your logged workouts on a separate page. It's simple, but it covers the core Django concepts you need to build much bigger things later.</p>
<p>Let's get started.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-you-are-going-build">What You Are Going Build</a></p>
</li>
<li><p><a href="#heading-step-1-how-to-set-up-your-django-project">Step 1: How to Set Up Your Django Project</a></p>
<ul>
<li><p><a href="#heading-1-1-how-to-create-a-virtual-environment">1. 1 How to create a virtual environment</a></p>
</li>
<li><p><a href="#heading-12-how-to-install-django">1.2 How to install Django</a></p>
</li>
<li><p><a href="#heading-13-how-to-create-the-project">1.3 How to Create the Project</a></p>
</li>
<li><p><a href="#heading-14-how-to-run-the-development-server">1.4 How to run the development server</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-2-how-to-create-a-django-app">Step 2: How to Create a Django App</a></p>
<ul>
<li><p><a href="#heading-21-how-to-generate-the-app">2.1 How to Generate the App</a></p>
</li>
<li><p><a href="#heading-22-how-to-register-the-app">2.2 How to Register the App</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-3-how-to-create-a-workout-model">Step 3: How to create a Workout Model</a></p>
<ul>
<li><a href="#heading-31-how-to-define-the-model">3.1 How to Define the Model</a></li>
</ul>
</li>
<li><p><a href="#heading-step-4-how-to-apply-migrations">Step 4: How to Apply Migrations</a></p>
<ul>
<li><p><a href="#heading-41-how-to-generate-the-migration">4.1 How to Generate the Migration</a></p>
</li>
<li><p><a href="#heading-42-how-to-apply-the-migration">4.2 How to Apply the Migration</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-5-how-to-register-the-model-in-the-admin-panel">Step 5: How to Register the Model in the Admin Panel</a></p>
<ul>
<li><p><a href="#heading-52-how-to-create-a-superuser">5.2 How to Create a Superuser</a></p>
</li>
<li><p><a href="#heading-53-how-to-access-the-admin-panel">5.3 How to Access the Admin Panel</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-6-how-to-create-views-for-the-app">Step 6: How to Create Views for the App</a></p>
<ul>
<li><p><a href="#heading-61-how-to-create-a-form-class">6.1 How to Create a Form Class</a></p>
</li>
<li><p><a href="#heading-62-how-to-write-views">6.2 How to Write Views</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-7-how-to-create-templates">Step 7: How to Create Templates</a></p>
<ul>
<li><p><a href="#heading-71-how-to-set-up-the-template-directory">7.1 How to Set Up the Template Directory</a></p>
</li>
<li><p><a href="#heading-72-how-to-create-the-workout-list-template">7.2 How to Create the Workout List Template</a></p>
</li>
<li><p><a href="#heading-73-how-to-create-add-workout-template">7.3 How to Create Add Workout Template</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-8-how-to-connect-urls">Step 8: How to Connect URLs</a></p>
<ul>
<li><p><a href="#heading-81-how-to-create-app-level-urls">8.1 How to Create App Level URLs</a></p>
</li>
<li><p><a href="#heading-82-how-to-link-app-urls-to-project">8.2 How to Link App URLs to project</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-step-9-how-to-test-the-application-locally">Step 9: How to Test the Application Locally</a></p>
</li>
<li><p><a href="#heading-step-10-how-to-prepare-for-deployment">Step 10: How to Prepare for Deployment</a></p>
<ul>
<li><a href="#heading-101-how-to-update-settings-for-production">10.1 How to Update Settings for Production</a></li>
</ul>
</li>
<li><p><a href="#heading-step-11-how-to-deploy-your-django-app-on-pythonanywhere">Step 11: How to Deploy Your Django App on PythonAnywhere</a></p>
<ul>
<li><p><a href="#heading-111-how-to-create-a-pythonanywhere-account">11.1 How to Create a PythonAnywhere Account</a></p>
</li>
<li><p><a href="#heading-112-how-to-upload-your-project-files">11.2 How to Upload Your Project Files</a></p>
</li>
<li><p><a href="#heading-113-how-to-set-up-a-virtual-environment-in-pythonanywhere">11.3 How to Set Up a Virtual Environment in PythonAnywhere</a></p>
</li>
<li><p><a href="#heading-114-how-to-run-migrations-and-create-a-superuser-on-pythonanywhere">11.4 How to Run Migrations and Create a SuperUser on PythonAnywhere</a></p>
</li>
<li><p><a href="#heading-114-how-to-configure-the-web-app-in-pythonanywhere">11.4 How to Configure the Web App in Pythonanywhere</a></p>
</li>
<li><p><a href="#heading-115-how-to-set-the-virtual-environment-path">11.5 How to Set the Virtual Environment Path</a></p>
</li>
<li><p><a href="#heading-116-how-to-configure-the-wsgi-file">11.6 How to Configure the WSGI file</a></p>
</li>
<li><p><a href="#heading-117-how-to-set-up-static-files">11.7 How to Set Up Static Files</a></p>
</li>
<li><p><a href="#heading-118-how-to-view-your-live-application">11.8 How to View Your Live Application</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-common-mistakes-and-how-to-fix-them">Common Mistakes and How to Fix Them</a></p>
</li>
<li><p><a href="#heading-how-you-can-improve-this-project">How You Can Improve This Project</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you begin, make sure you are comfortable with the following:</p>
<p><strong>Python fundamentals:</strong> You should understand variables, functions, lists, dictionaries, and basic control flow (if/else statements and loops).</p>
<p><strong>Basic command line usage:</strong> You'll be running commands in your terminal throughout this tutorial. You should know how to open a terminal, navigate between folders, and run commands. If you're on Windows, you can use Command Prompt or PowerShell. On macOS or Linux, the default Terminal app works well.</p>
<p><strong>Tools you'll need installed:</strong></p>
<ul>
<li><p><strong>Python 3.8 or higher.</strong> You can check your version by running <code>python --version</code> or <code>python3 --version</code> in your terminal.&nbsp; If you don't have Python installed, download it from <a href="https://www.python.org">python.org</a></p>
</li>
<li><p><strong>pip.</strong> This is Python's package manager. It usually comes bundled with Python. You can verify by running <code>pip --version</code> or pip3 --version. Note the commands <code>python3</code> and <code>pip3</code> tell the terminal that you are explicitly using <strong>Python Version 3</strong></p>
</li>
<li><p><strong>A code editor.</strong> Visual Studio Code is a great free option, but you can use any editor you're comfortable with.</p>
</li>
</ul>
<p>That's everything. You don't need prior Django experience or web development knowledge. This tutorial will walk you through each step.</p>
<h2 id="heading-what-you-are-going-build">What You Are Going Build</h2>
<p>The fitness tracker you will build has two main features:</p>
<ol>
<li><strong>A form to log workouts.</strong> You will enter the name of an activity (like "Running" or "Push-ups"), how long you did it (in minutes), and the date. When you submit the form, Django saves that workout to a database.</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/fe6b2a89-fc29-4710-a640-ce2757267e38.png" alt="The image shows a form to log workouts" style="display:block;margin:0 auto" width="1864" height="1544" loading="lazy">

<ol>
<li><strong>A page to view all your workouts.</strong> This page displays every workout you have logged, showing the activity, duration, and date in a clean list.</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/8f6bd09e-497a-4480-83e5-af162028a0a3.png" alt="The image shows a list of logged workouts" style="display:block;margin:0 auto" width="2144" height="1720" loading="lazy">

<p>Here's how data flows through the app at a high level:</p>
<ol>
<li><p>You fill out the workout form in your browser and click submit.</p>
</li>
<li><p>Your browser sends that data to Django.</p>
</li>
<li><p>Django's view function receives the data, validates it, and saves it to the database.</p>
</li>
<li><p>When you visit the workouts page, Django's view function pulls all saved workouts from the database.</p>
</li>
<li><p>Django passes that data to an HTML template, which renders it as a page your browser can display.</p>
</li>
</ol>
<img alt="The image shows the data flow of the fitness tracker app with 5 steps" style="display:block;margin-left:auto" width="600" height="400" loading="lazy">

<p>This request-response cycle is the foundation of how Django works. Once you understand it, you can build almost anything.</p>
<h2 id="heading-step-1-how-to-set-up-your-django-project">Step 1: How to Set Up Your Django Project</h2>
<p>Every Django project starts with a few setup steps. You'll create an isolated Python environment, install Django, and generate the initial project structure.</p>
<h3 id="heading-1-1-how-to-create-a-virtual-environment">1. 1 How to Create a Virtual Environment</h3>
<p>A virtual environment is a self-contained folder that contains its own Python interpreter and installed packages for a specific project. This keeps your project's dependencies separate from other Python projects on your computer. This separation prevents version conflicts and keeps setups consistent.</p>
<p>For example, one project might require an older version of Django, while another needs the latest version, and a virtual environment allows both to work smoothly on the same system.</p>
<p>Without it, global installations can clash, break projects, and make setups hard to reproduce. Over time, the system environment becomes cluttered with unused or incompatible packages making debugging and maintenance more difficult.</p>
<p>Now let's set it up.</p>
<p>Open your terminal, and navigate to where you want your project to live and run the following command</p>
<pre><code class="language-shell">mkdir fitness-tracker
cd fitness-tracker
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/b4715e51-c2e3-4e97-ad7b-b41066aeefd9.png" alt="An image of the terminal showing the commands mkdir (make directory) and cd (change directory) being typed " style="display:block;margin:0 auto" width="2442" height="542" loading="lazy">

<p>The first command creates a new folder called <code>fitness-tracker</code>. The second command moves you into that folder.</p>
<p>You'll create the Python virutal environment here.</p>
<pre><code class="language-shell">python3 -m venv venv
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/3d1723ae-d069-48fd-9954-440a191f585f.png" alt="The image shows the command to create the python virtual enviroment." style="display:block;margin:0 auto" width="2374" height="512" loading="lazy">

<p>The above command creates a virtual environment inside a folder called <code>venv</code>. The first <code>venv</code> is the command and the second <code>venv</code> represents the name of the folder. You can name the folder anything though <code>venv</code> is usually preferred.</p>
<p>By using the <code>ls</code> command, you can see that we've created the virtual environment folder.</p>
<p>To activate the virtual environment, we need to use the following command:</p>
<p>On macOS/Linux:</p>
<pre><code class="language-shell">source venv/bin/activate
</code></pre>
<p>On Windows:</p>
<pre><code class="language-shell">venv\Scripts\activate
</code></pre>
<p>You'll know it worked when you see <code>(venv)</code> at the beginning of your terminal prompt. From this point on, any Python packages you install will only exist inside this <strong>virtual environment</strong>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/dabe362d-2f50-4745-a0bc-e57ad3536723.png" alt="The image shows the virtual environment being activated" style="display:block;margin:0 auto" width="2434" height="1066" loading="lazy">

<h3 id="heading-12-how-to-install-django">1.2 How to Install Django</h3>
<p>With your virtual environment activated, install Django using pip:</p>
<pre><code class="language-shell">pip install django
</code></pre>
<p>This downloads and installs the latest stable version of Django. You can verify the installation by running:</p>
<pre><code class="language-shell">python3 -m django --version
</code></pre>
<p>After running both these commands, you should see Django being installed and the version number:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/bda2ed0d-44bf-439b-a9fd-1cf3fcaf35ca.png" alt="The image shows django being installed and the version of django that has been installed" style="display:block;margin:0 auto" width="2818" height="968" loading="lazy">

<h3 id="heading-13-how-to-create-the-project">1.3 How to Create the Project</h3>
<p>We have finished installing Django. Now let's create a Django project. Django provides a command line utility that generates the boilerplate files that you need. Type the following command:</p>
<pre><code class="language-shell">django-admin startproject fitness_project .
</code></pre>
<p>The command creates a folder named <code>fitness-project</code>. Notice the dot at the end of the command. The dot at the end is important. It tells Django to create the project files in your current directory instead of creating an extra nested folder.</p>
<p>Now that we've created our Django project, let's open the project in your favourite text editor and look at folder structure.</p>
<p>You'll notice that the folder already comes with a bunch of files.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/eaffe95e-7078-4c2e-91f4-88a6c3696e88.png" alt="The image show the list of files created by the django-admin startproject command" width="2362" height="1488" loading="lazy">

<h3 id="heading-14-how-to-run-the-development-server">1.4 How to Run the Development Server</h3>
<p>Now let's make sure everything is working. You'll need to run a server for this. Type the following command:</p>
<pre><code class="language-shell">python manage.py runserver
</code></pre>
<p>You can type this command in the terminal with the virtual environment activated or you can use the integrated terminal if you're using VS Code. I'll be using the integrated terminal from this point on.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/53dbba38-9863-4972-a899-1e6ff66fb3f5.png" alt="This is an image of the server running after typing the runserver command" style="display:block;margin:0 auto" width="2870" height="1662" loading="lazy">

<p>Open your browser and go to <a href="http://127.0.0.1:8000/">http://127.0.0.1:8000/</a>. You should see Django's default welcome page with a rocket ship graphic confirming that your project is set up correctly.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/25d2e3ce-ce72-44f6-9f5f-78aeaeb88b3e.png" alt="This is an image of Django's default homepage" style="display:block;margin:0 auto" width="2872" height="1788" loading="lazy">

<p>Press <code>Ctrl + C</code> in your terminal to stop the server when you're ready to move on.</p>
<h2 id="heading-step-2-how-to-create-a-django-app">Step 2: How to Create a Django App</h2>
<p>In Django, a project is the overall container for your entire web application, while an app is a smaller, self-contained module inside that project that focuses on a specific piece of functionality.</p>
<p>A useful way to picture this is to think of a house. The project is the whole house. Each app is like a room inside that house. One room might be a kitchen, another a bedroom, each designed with a clear purpose. In the same way, a Django app is built to handle one responsibility, such as authentication, payments, or in this case, workout tracking.</p>
<p>Now, here's the important part: why not just put everything into one big project instead of using apps? You technically could, especially for very small projects. But as your application grows, that approach quickly becomes difficult to manage.</p>
<p>By using apps, you naturally separate concerns. It also makes collaboration smoother, since different people can work on different apps without constantly stepping on each other’s code.</p>
<p>Another major benefit is reusability. Since apps are modular, you can take an app from one project and reuse it in another.</p>
<p>For example, if you build a workout tracking app once, you could plug it into a completely different Django project later without rebuilding it from scratch. Later, you might create a completely different project, say a fitness coaching platform or a health dashboard. Instead of rebuilding the tracking feature from scratch, you can reuse the same app.</p>
<p>For this project, you'll create a single app called <code>tracker</code> that handles everything related to logging and displaying workouts.</p>
<h3 id="heading-21-how-to-generate-the-app">2.1 How to Generate the App</h3>
<p>Make sure you're in the same directory as the <code>manage.py</code> file, then run the following code:</p>
<pre><code class="language-shell">python manage.py startapp tracker
</code></pre>
<p>This create a new folder called tracker with the following following structure:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/cb07105a-6e65-49f9-9c7a-d5a64db42b49.png" alt="The image shows the folder strucutre created by after running the startapp command" style="display:block;margin:0 auto" width="2860" height="1250" loading="lazy">

<p>Each file has its own purpose. You'll work with <code>models.py</code>, <code>views.py</code> and <code>admin.py</code> throughout this project.</p>
<h3 id="heading-22-how-to-register-the-app">2.2 How to Register the App</h3>
<p>Django doesn't automatically know about your new app. You need to tell it by adding the app to the <code>INSTALLED_APPS</code> list in <code>settings.py</code> file.</p>
<p>Open <code>fitness_project/settings.py</code> and find the <code>INSTALLED_APPS</code> list. Add the name of the app, that is <code>tracker</code>, to the end of the list:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/eec90a01-5219-449e-97f8-97465e4ac23f.png" alt="eec90a01-5219-449e-97f8-97465e4ac23f" style="display:block;margin:0 auto" width="2868" height="1306" loading="lazy">

<p>You'll notice that a number of apps have already been installed automatically by Django. This is part of Django’s “batteries-included” philosophy, where many common features are ready to use out of the box.</p>
<p>Here is a short summary of what each of the apps does.</p>
<table>
<thead>
<tr>
<th><strong>App Name</strong></th>
<th><strong>Purpose</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>django.contrib.admin</strong></td>
<td>Powers the built-in admin dashboard, letting you manage your data through a web interface.</td>
</tr>
<tr>
<td><strong>django.contrib.auth</strong></td>
<td>Handles users, login systems, permissions, and password management.</td>
</tr>
<tr>
<td><strong>django.contrib.contenttypes</strong></td>
<td>Helps Django track and manage relationships between different models.</td>
</tr>
<tr>
<td><strong>django.contrib.sessions</strong></td>
<td>Stores user session data, so users stay logged in across requests.</td>
</tr>
<tr>
<td><strong>django.contrib.messages</strong></td>
<td>Lets you show temporary notifications like success or error messages.</td>
</tr>
<tr>
<td><strong>django.contrib.staticfiles</strong></td>
<td>Manages static assets such as CSS, JavaScript, and images</td>
</tr>
</tbody></table>
<p>Now Django knows your <code>tracker</code> app exists and will include it when running the project.</p>
<h2 id="heading-step-3-how-to-create-a-workout-model">Step 3: How to Create a Workout Model</h2>
<p>A model in Django is a Python class that defines the structure of your data. Each model maps directly to a table in your database. Each attribute on the model becomes a column in that table.</p>
<p>Think of a model as a blueprint for a spreadsheet. The class name is the name of the spreadsheet, and each field is a column header. Every time you save a new workout, Django creates a new row in that spreadsheet.</p>
<h3 id="heading-31-how-to-define-the-model">3.1 How to Define the Model</h3>
<p>Open <code>tracker/models.py</code> and replace its contents with this code:</p>
<pre><code class="language-python">from django.db import models

class Workout(models.Model):
    activity = models.CharField(max_length=200)
    duration = models.IntegerField(help_text="Duration in minutes")
    date = models.DateField()

    def __str__(self):
        return f"{self.activity} - {self.duration} min on {self.date}"
</code></pre>
<p>Let's discuss what each part does:</p>
<ul>
<li><p><code>activity = models.CharField(max_length=200)</code> creates a text fields that can hold up to 200 characters. This is where you'll store the name of the exercise like "Running" or "Cycling".</p>
</li>
<li><p><code>duration = models.IntegerField(help_text="Duration in minutes")</code> creates a whole number field for storing how many minutes the workout lasted. The <code>help_text</code> parameter adds a hint that will appear in forms and the admin panel.</p>
</li>
<li><p><code>date = models.DateField()</code> creates a date field for recording when the workout happened.</p>
</li>
</ul>
<p>The <code>__str__()</code> method defines how a Workout object appears when printed or displayed in the admin panel. Instead of seeing something unhelpful like "<strong>Workout object (1)</strong>," you will see "<strong>Running - 30 min on 2025-03-15.</strong>"</p>
<h2 id="heading-step-4-how-to-apply-migrations">Step 4: How to Apply Migrations</h2>
<p>You've defined your model, but Django hasn't created the actual database table yet. To do that, you need to run migrations.</p>
<p>Migrations are Django's way of translating your Python model definitions into database instructions. Migrations are done in two steps.</p>
<p>When you change a model – maybe by adding a field, removing a field, or renaming one – you create a new migration that describes that change. You can do this using the <code>makemigrations</code> command.</p>
<p>Then you apply the migration using the <code>migrate</code> command and Django updates the database to match.</p>
<p>This two-step process of first detecting the change and then applying the change gives you a reliable record of every change to your database structure over time.</p>
<h3 id="heading-41-how-to-generate-the-migration">4.1 How to Generate the Migration</h3>
<p>Run the following command in the integrated terminal:</p>
<pre><code class="language-shell">python manage.py makemigrations
</code></pre>
<p>You should see output like this:</p>
<pre><code class="language-shell">Migrations for 'tracker': tracker/migrations/0001_initial.py 
    + Create model Workout
</code></pre>
<p>Django inspected your Workout model and created a migration file that describes how to build the corresponding database table. You can find this file at <code>tracker/migrations/0001_initial.py</code> if you want to look at it, but you don't need to edit it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/fa46eed5-6ef3-408a-8c23-f39518b117f4.png" alt="The image shows the file creating after makemigrations command runs" style="display:block;margin:0 auto" width="2880" height="1424" loading="lazy">

<h3 id="heading-42-how-to-apply-the-migration">4.2 How to Apply the Migration</h3>
<p>Now tell Django to execute that migration and actually create the table in the database:</p>
<pre><code class="language-shell">python manage.py migrate
</code></pre>
<p>You'll see several lines of output as Django applies not just your migration, but also the default migrations for Django's built-in apps (authentication, sessions, and so on).</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/fcaae5fe-0cc7-4c1f-b4c3-a3b173fd2551.png" alt="The image shows the output after applying migrations" style="display:block;margin:0 auto" width="2864" height="1650" loading="lazy">

<p>When it finishes, your database has a table ready to store workouts.</p>
<p>When the migrate command runs, we can see the exact SQL commands that Django used to build and change the database. Though this isn't required for creating the application, it's always good to know what's happening under hood.</p>
<p>Run this command:</p>
<pre><code class="language-shell">python manage.py sqlmigrate tracker 001
</code></pre>
<p>And you should get this output:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/016b0a33-06d0-47e8-97de-580e79a7d0e3.png" alt="The image shows the command to view sql queries created by django" style="display:block;margin:0 auto" width="2824" height="1628" loading="lazy">

<p>The <code>001</code> you added at the end is the migration number and represents first version of the database schema.</p>
<p>In practice, your workflow usually looks like this: you change your models, run <code>makemigrations</code> to generate the migration files, and then run the <code>migrate</code> command to apply those changes to the database.</p>
<h2 id="heading-step-5-how-to-register-the-model-in-the-admin-panel">Step 5: How to Register the Model in the Admin Panel</h2>
<p>Django comes with a powerful admin interface built in. It gives you a graphical way to view, add, edit, and delete records in your database without writing any extra code. This is incredibly useful during development because you can quickly test your models and see your data.</p>
<p>But by default, it doesn’t know:</p>
<ul>
<li><p>Which models you want to manage</p>
</li>
<li><p>How you want them displayed</p>
</li>
</ul>
<p>So you <em>register</em> models in <code>admin.py</code> to tell Django to include the specific model in the admin interface.</p>
<h3 id="heading-51-how-to-add-model-to-admin">5.1 How to Add Model to Admin</h3>
<p>Open <code>tracker/admin.py</code> and add the following code:</p>
<pre><code class="language-python">from django.contrib import admin
from .models import Workout

admin.site.register(Workout)
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/ad017508-993a-4c28-ade1-8e73fa0c6a4a.png" alt="ad017508-993a-4c28-ade1-8e73fa0c6a4a" style="display:block;margin:0 auto" width="2854" height="816" loading="lazy">

<p>This single line tells Django to include the <code>Workout</code> model in the admin interface.</p>
<h3 id="heading-52-how-to-create-a-superuser">5.2 How to Create a Superuser</h3>
<p>To access the admin panel, you need an admin account. Create one by running:</p>
<pre><code class="language-python">python manage.py createsuperuser
</code></pre>
<p>Django will prompt you for a username, email address, and password. Choose something you will remember. The email is optional – you can press Enter to skip it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/4bbc7a15-682e-497d-a4a4-3e2dc4b848ac.png" alt="The image shows the superuser being created by adding username, email and password" style="display:block;margin:0 auto" width="2878" height="1362" loading="lazy">

<h3 id="heading-53-how-to-access-the-admin-panel">5.3 How to Access the Admin Panel</h3>
<p>Start the development server:</p>
<pre><code class="language-python">python manage.py runserver
</code></pre>
<p>Then navigate to <a href="http://127.0.0.1:8000/admin/">http://127.0.0.1:8000/admin/</a> in your browser. Log in with the credentials you just created.</p>
<p>You should see the Django administration dashboard with a "<strong>Tracker</strong>" section containing your "<strong>Workouts</strong>" model.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/a1e576bf-45f6-40dc-b6b9-69899b2df9d5.png" alt="The image shows the Django admin panel and the Worker model of the Tracker app being added to the admin panel" style="display:block;margin:0 auto" width="2860" height="1406" loading="lazy">

<p>Try clicking "Add" to create a couple of test workouts. This will confirm that your model is working correctly before you build the rest of the app.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/944ea6a4-bc6f-4321-87c0-5c7bcb267e26.png" alt="The image show some workouts (running and cycling) being added to the admin panel" style="display:block;margin:0 auto" width="2876" height="1146" loading="lazy">

<h2 id="heading-step-6-how-to-create-views-for-the-app">Step 6: How to Create Views for the App</h2>
<p>A view in Django is a Python function (or class) that receives a web request and returns a web response. That response could be an HTML page, a redirect, a 404 error, or anything else a browser can handle.</p>
<p>Views are where your application logic lives. They decide what data to fetch, what processing to do, and what to show the user.</p>
<p>For this app, you need two views: one to display the form where users add a workout, and one to display the list of all saved workouts.</p>
<h3 id="heading-61-how-to-create-a-form-class">6.1 How to Create a Form Class</h3>
<p>Before writing the views, you need a Django form that handles the workout input.</p>
<p>Django forms are a built-in way to handle user input like login forms, contact forms, or anything that collects data from a user. Instead of manually writing HTML, validating inputs, and handling errors, Django gives you a structured way to do all of that in one place.</p>
<p>Most user inputs are based on the models you’ve created, and Django can automatically generate forms from those models using <code>ModelForms</code>, which speeds things up significantly.</p>
<p>Let's create a new file called <code>forms.py</code> in the <code>tracker</code> folder and add the following code:</p>
<pre><code class="language-python">from django import forms
from .models import Workout

class WorkoutForm(forms.ModelForm):

    class Meta:
        model = Workout
        fields = ['activity', 'duration', 'date']
        widgets = {
            'date': forms.DateInput(attrs={'type': 'date'}),
        }
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/e8bce19c-7184-45b3-9afe-3a5f73cff43b.png" alt="The image shows the file location of forms.py as well the code for forms.py file" style="display:block;margin:0 auto" width="2866" height="1232" loading="lazy">

<p>In the above code, the <code>ModelForm</code> automatically generates form fields based on the <code>Workout</code> model. The <code>widgets</code> dictionary tells Django to render the date field as an HTML date picker instead of a plain text input.</p>
<p>We can actually see the forms being automatically created by Django. For this we need to enter the shell. In the terminal, type the following command:</p>
<pre><code class="language-shell">python manage.py shell
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/951829fb-98e8-48f5-8187-80cc98346e06.png" alt="The image shows the python shell being activated" style="display:block;margin:0 auto" width="2244" height="932" loading="lazy">

<p>Now lets import the <code>WorkoutForm</code> class that we just created.</p>
<p>Type the following code:</p>
<pre><code class="language-shell">from tracker.forms import WorkoutForm
</code></pre>
<p>Notice that we've given the <strong>name of the app</strong> as well when we imported the form.</p>
<p>Then create an object of the <code>WorkoutForm</code> class and print it.</p>
<pre><code class="language-shell">from tracker.forms import WorkoutForm
workoutform = WorkoutForm()
print(workoutform) 
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/b81265a2-7c21-453b-ae57-3cfec97fbaf9.png" alt="The image shows the command to open the python shell where you can execute python statement throught the terminal" style="display:block;margin:0 auto" width="2236" height="652" loading="lazy">

<p>You should get the following output:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/094b0d89-b003-49b6-939d-07eacfb0c745.png" alt="This image shows the html generated from ModelForm" style="display:block;margin:0 auto" width="936" height="632" loading="lazy">

<p>You can see that all the model fields have been renderd as HTML forms and the date field has been created as a date type that is <code>type="date"</code> instead of plain text.</p>
<h3 id="heading-62-how-to-write-views">6.2 How to Write Views</h3>
<p>As we've discussed above, our project has two views: one to add a workout and the other to display all the saved workouts.</p>
<p>First, let's create a view to add a workout. In the <code>tracker/views.py</code> file, type the following code:</p>
<pre><code class="language-python">from django.shortcuts import render, redirect
from .models import Workout

# view to list all workouts
def workout_list(request):
    workouts = Workout.objects.all().order_by('-date')
    return render(request, 'tracker/workout_list.html', {'workouts': workouts})
</code></pre>
<p>Let's walk through this view:</p>
<ul>
<li><p>The <code>workout_list</code> view handles the page that displays all workouts.</p>
</li>
<li><p>It queries the database for every <code>Workout</code> object, orders them by date (most recent first, thanks to the <code>-</code> prefix), and passes that list to a template called <code>workout_list.html</code>.</p>
</li>
<li><p>The <code>render</code> function combines the template with the data and returns the finished HTML page.</p>
</li>
</ul>
<p>To create the logic to add a workout, first add the <code>Workout</code> form import at the end of the import section. Then add the following code after the <code>workout_list</code> view:</p>
<pre><code class="language-python">from django.shortcuts import render, redirect
from .models import Workout
from .forms import WorkoutForm

# view to list all the workouts
def workout_list(request):
    workouts = Workout.objects.all().order_by('-date')
    return render(request, 'tracker/workout_list.html', {'workouts': workouts})

# view to add a workout
def add_workout(request):
    if request.method == 'POST':
        form = WorkoutForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect('workout_list')
    else:
        form = WorkoutForm()
    return render(request, 'tracker/add_workout.html', {'form': form})
</code></pre>
<ul>
<li><p>The <code>add_workout</code> view handles both displaying the empty form and processing submitted form data.</p>
</li>
<li><p>When a user first visits the page, the request method is GET, so Django creates a blank form and renders it.</p>
</li>
<li><p>When the user fills out the form and clicks submit, the request method is POST. Django then validates the submitted data, saves it to the database if everything is correct, and redirects the user to the workout list page.</p>
</li>
<li><p>If the data isn't valid, Django re-renders the form with error messages.</p>
</li>
</ul>
<p>Here is the complete views code:</p>
<pre><code class="language-python">from django.shortcuts import render, redirect
from .models import Workout
from .forms import WorkoutForm

# view to list all workouts
def workout_list(request):
    workouts = Workout.objects.all().order_by('-date')
    return render(request, 'tracker/workout_list.html', {'workouts': workouts})

# view to add a workout
def add_workout(request):
    if request.method == 'POST':
        form = WorkoutForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect('workout_list')
    else:
        form = WorkoutForm()
    return render(request, 'tracker/add_workout.html', {'form': form})

</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/8a0878c1-f029-49a4-8a7f-308cdb843b62.png" alt="The image shows the complete code for views.py with explanation about the add workout view" style="display:block;margin:0 auto" width="2316" height="1076" loading="lazy">

<h2 id="heading-step-7-how-to-create-templates">Step 7: How to Create Templates</h2>
<p>Templates are HTML files that Django fills in with dynamic data. They're the front end of your application: the part users actually see in their browser.</p>
<h3 id="heading-71-how-to-set-up-the-template-directory">7.1 How to Set Up the Template Directory</h3>
<p>Django looks for templates inside a <code>templates</code> folder within each app. Create the following folder structure inside your <code>tracker</code> app.</p>
<p><code>tracker/templates/tracker</code></p>
<p>The double <code>tracker</code> folder name might look redundant, but it's a Django convention called <strong>template namespacing</strong>. It prevents naming conflicts if you have multiple apps with templates that share the same filename.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/cfbe85c3-36dc-413e-918a-aa64d706d2fc.png" alt="The image shows folder structure of the templates folder" style="display:block;margin:0 auto" width="832" height="474" loading="lazy">

<h3 id="heading-72-how-to-create-the-workout-list-template">7.2 How to Create the Workout List Template</h3>
<p>Create a file called <code>tracker/templates/tracker/workout_list.html</code> and add the following code:</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;My Workouts&lt;/title&gt;
    &lt;style&gt;
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background-color: #f5f7fa;
            color: #333;
            line-height: 1.6;
            padding: 2rem;
        }

        .container {
            max-width: 700px;
            margin: 0 auto;
        }

        h1 {
            font-size: 1.8rem;
            margin-bottom: 1rem;
            color: #1a1a2e;
        }

        .add-link {
            display: inline-block;
            background-color: #4361ee;
            color: white;
            padding: 0.6rem 1.2rem;
            border-radius: 6px;
            text-decoration: none;
            margin-bottom: 1.5rem;
            font-size: 0.95rem;
        }

        .add-link:hover {
            background-color: #3a56d4;

        }

        .workout-card {
            background: white;
            border-radius: 8px;
            padding: 1rem 1.2rem;
            margin-bottom: 0.8rem;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
            display: flex;
            justify-content: space-between;
            align-items: center;

        }

        .workout-activity {
            font-weight: 600;
            font-size: 1.05rem;

        }

        .workout-details {
            color: #666;
            font-size: 0.9rem;

        }

        .empty-state {
            text-align: center;
            padding: 3rem 1rem;
            color: #888;

        }

    &lt;/style&gt;
&lt;/head&gt;

&lt;body&gt;
    &lt;div class="container"&gt;
        &lt;h1&gt;My Workouts&lt;/h1&gt;
        &lt;a href="{% url 'add_workout' %}" class="add-link"&gt;+ Log a Workout&lt;/a&gt;
        {% if workouts %}
            {% for workout in workouts %}
                &lt;div class="workout-card"&gt;
                    &lt;div&gt;
                        &lt;div class="workout-activity"&gt;{{ workout.activity }}&lt;/div&gt;
                        &lt;div class="workout-details"&gt;{{ workout.duration }} minutes&lt;/div&gt;
                    &lt;/div&gt;
                    &lt;div class="workout-details"&gt;{{ workout.date }}&lt;/div&gt;
                &lt;/div&gt;
            {% endfor %}

        {% else %}
            &lt;div class="empty-state"&gt;
                &lt;p&gt;No workouts logged yet. Start by adding one!&lt;/p&gt;
            &lt;/div&gt;
        {% endif %}
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>There are a few things worth noting here:</p>
<p>If you look closely at the HTML, you'll spot some weird-looking tags wrapped in curly braces ( <code>{% %}</code> and <code>{{ }}</code> ). Think of them as special instructions for Django.</p>
<p>You use the double curly braces (<code>{{ }}</code>) when you want to output or display a piece of data directly on the page.</p>
<p>On the other hand, you use the brace-and-percent-sign combo ( <code>{% %}</code> ) when you need Django to actually perform an action or apply logic, like running a loop or checking a condition.</p>
<p>They allow us to inject dynamic data straight from our Python backend right into our otherwise static HTML.</p>
<p>Lets look at this code snippet for the <code>workout_list.html</code></p>
<pre><code class="language-html">&lt;body&gt;
    &lt;div class="container"&gt;
        &lt;h1&gt;My Workouts&lt;/h1&gt;
        &lt;a href="{% url 'add_workout' %}" class="add-link"&gt;+ Log a Workout&lt;/a&gt;
        {% if workouts %}
            {% for workout in workouts %}
                &lt;div class="workout-card"&gt;
                    &lt;div&gt;
                        &lt;div class="workout-activity"&gt;{{ workout.activity }}&lt;/div&gt;
                        &lt;div class="workout-details"&gt;{{ workout.duration }} minutes&lt;/div&gt;
                    &lt;/div&gt;
                    &lt;div class="workout-details"&gt;{{ workout.date }}&lt;/div&gt;
                &lt;/div&gt;
            {% endfor %}

        {% else %}
            &lt;div class="empty-state"&gt;
                &lt;p&gt;No workouts logged yet. Start by adding one!&lt;/p&gt;
            &lt;/div&gt;
        {% endif %}
    &lt;/div&gt;
&lt;/body&gt;
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/864ed3dc-ceda-44f8-ba3d-b061222714c7.png" alt="The image shows the the body section of the workout_list.html with the focus on django template tags" style="display:block;margin:0 auto" width="1988" height="1160" loading="lazy">

<p>There are a few things worth noting here.</p>
<p>Right under the main heading, you'll see this line:<br><code>&lt;a href="{% url 'add_workout' %}"&gt;</code></p>
<p>Instead of hardcoding a web link like <code>href="/add-workout/"</code>, Django uses the <code>{% url %}</code> tag to generate the link dynamically. You pass it the name of the route (in this case, <code>add_workout</code>), and Django automatically figures out the correct URL path.</p>
<p>If you ever change the URL structure in your Python code later, Django updates this link automatically. You never have to hunt through HTML files to fix broken links!</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/2dc56430-d146-4fc9-8ae5-a3fb5a0d7dd1.png" alt="The image highlights the code that generates dynamic url" style="display:block;margin:0 auto" width="1748" height="632" loading="lazy">

<p>The <code>{% if workouts %}</code> block checks whether there are any workouts to display. If the list is empty, it shows a friendly message instead of a blank page.</p>
<p>The <code>{% for workout in workouts %}</code> loop iterates over every workout in the list and renders a card for each one. The double curly braces <code>{{ workout.activity }}</code> insert the value of each field into the HTML</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/f75b5c7e-5cd0-457c-901f-e489c26a8175.png" alt="f75b5c7e-5cd0-457c-901f-e489c26a8175" style="display:block;margin:0 auto" width="2012" height="1002" loading="lazy">

<p>Inside the loop, you'll notice tags that look like this:</p>
<ul>
<li><p><code>{{ workout.activity }}</code></p>
</li>
<li><p><code>{{ workout.duration }}</code></p>
</li>
<li><p><code>{{ workout.date }}</code></p>
</li>
</ul>
<p>As Django loops through each workout object, it uses dot notation to peek inside that specific object and grab its details. It grabs the activity type (like "Running"), the duration ("30"), and the date ("March 30"), and prints that exact text directly onto the webpage for the user to see.</p>
<h3 id="heading-73-how-to-create-add-workout-template">7.3 How to Create Add Workout Template</h3>
<p>Create a file called <code>tracker/templates/tracker/add_workout.html</code> and add the following code:</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;Log a Workout&lt;/title&gt;
    &lt;style&gt;
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;

        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background-color: #f5f7fa;
            color: #333;
            line-height: 1.6;
            padding: 2rem;
        }

        .container {
            max-width: 500px;
            margin: 0 auto;

        }

        h1 {
            font-size: 1.8rem;
            margin-bottom: 1.5rem;
            color: #1a1a2e;
        }

        .form-group {
            margin-bottom: 1.2rem;
        }

        label {
            display: block;
            margin-bottom: 0.3rem;
            font-weight: 600;
            font-size: 0.95rem;

        }

        input[type="text"],
        input[type="number"],
        input[type="date"] {
            width: 100%;
            padding: 0.6rem 0.8rem;
            border: 1px solid #ddd;
            border-radius: 6px;
            font-size: 1rem;
            transition: border-color 0.2s;
        }

        input:focus {
            outline: none;
            border-color: #4361ee;

        }

        .btn {
            background-color: #4361ee;
            color: white;
            padding: 0.7rem 1.5rem;
            border: none;
            border-radius: 6px;
            font-size: 1rem;
            cursor: pointer;
            margin-right: 0.5rem;
        }

        .btn:hover {
            background-color: #3a56d4;
        }

        .back-link {
            color: #4361ee;
            text-decoration: none;
            font-size: 0.95rem;
        }

        .back-link:hover {
            text-decoration: underline;
        }

        .actions {
            display: flex;
            align-items: center;
            gap: 1rem;
            margin-top: 0.5rem;
        }

        .error-list {
            color: #e74c3c;
            font-size: 0.85rem;
            margin-top: 0.3rem;

        }

    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div class="container"&gt;
       &lt;h1&gt;Log a Workout&lt;/h1&gt;
        &lt;form method="post"&gt;
            {% csrf_token %}
            &lt;div class="form-group"&gt;
                &lt;label for="id_activity"&gt;Activity&lt;/label&gt;
                {{ form.activity }}
                {% if form.activity.errors %}
                    &lt;div class="error-list"&gt;{{ form.activity.errors }}&lt;/div&gt;
                {% endif %}
            &lt;/div&gt;
            &lt;div class="form-group"&gt;
                &lt;label for="id_duration"&gt;Duration (minutes)&lt;/label&gt;
                {{ form.duration }}
                {% if form.duration.errors %}
                    &lt;div class="error-list"&gt;{{ form.duration.errors }}&lt;/div&gt;
                {% endif %}
            &lt;/div&gt;

            &lt;div class="form-group"&gt;
                &lt;label for="id_date"&gt;Date&lt;/label&gt;
                {{ form.date }}
                {% if form.date.errors %}
                    &lt;div class="error-list"&gt;{{ form.date.errors }}&lt;/div&gt;
                {% endif %}
            &lt;/div&gt;

            &lt;div class="actions"&gt;
                &lt;button type="submit" class="btn"&gt;Save Workout&lt;/button&gt;
                &lt;a href="{% url 'workout_list' %}" class="back-link"&gt;Cancel&lt;/a&gt;
            &lt;/div&gt;
        &lt;/form&gt;
    &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>In the previous template, we learned how to display data. Now, we're looking at a form that actually collects data. Handling forms manually in web development can get messy, but Django provides some powerful template tags to do the heavy lifting for us.</p>
<p>Let's look at the Django-specific logic powering this form:</p>
<p>First, right after opening the <code>&lt;form&gt;</code> tag, you'll spot a very important line: <code>{% csrf_token %}</code>. Whenever you submit data to a server using a "POST" method, malicious sites can potentially intercept or forge that request.</p>
<p>By including this <code>{% csrf_token %}</code>, you tell Django to generate a unique, hidden security key for the form. When the user clicks "Save Workout," Django checks this token to guarantee the request is legitimate. <strong>If you forget this tag, Django will simply reject your form!</strong></p>
<pre><code class="language-html">&lt;form method="post"&gt;
            {% csrf_token %}
            &lt;div class="form-group"&gt;
                &lt;label for="id_activity"&gt;Activity&lt;/label&gt;
                {{ form.activity }}
                {% if form.activity.errors %}
                    &lt;div class="error-list"&gt;{{ form.activity.errors }}&lt;/div&gt;
                {% endif %}
            &lt;/div&gt;
            &lt;div class="form-group"&gt;
                &lt;label for="id_duration"&gt;Duration (minutes)&lt;/label&gt;
                {{ form.duration }}
                {% if form.duration.errors %}
                    &lt;div class="error-list"&gt;{{ form.duration.errors }}&lt;/div&gt;
                {% endif %}
            &lt;/div&gt;

            &lt;div class="form-group"&gt;
                &lt;label for="id_date"&gt;Date&lt;/label&gt;
                {{ form.date }}
                {% if form.date.errors %}
                    &lt;div class="error-list"&gt;{{ form.date.errors }}&lt;/div&gt;
                {% endif %}
            &lt;/div&gt;

            &lt;div class="actions"&gt;
                &lt;button type="submit" class="btn"&gt;Save Workout&lt;/button&gt;
                &lt;a href="{% url 'workout_list' %}" class="back-link"&gt;Cancel&lt;/a&gt;
            &lt;/div&gt;
        &lt;/form&gt;
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/4ca2a034-119c-4dc7-a14a-cd0a81815c58.png" alt="The image shows a screenshot of the code and highlight the csrf token tag" style="display:block;margin:0 auto" width="1580" height="824" loading="lazy">

<p>Now let's talk about automatically generating the form fields. Instead of manually typing out all the HTML <code>&lt;input&gt;</code> tags for the activity, duration, and date, we let Django do it for us using display tags (<code>{{ }}</code>).</p>
<p>Each <code>{{ form.activity }}</code>, <code>{{ form.duration }}</code>, and <code>{{ form.date }}</code> tag renders the corresponding form input. Django handles the HTML attributes, input types, and validation for you based on the model and form definitions.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/c2403685-b78d-4aa7-876d-63ca53481e37.png" alt="This image shows the code that automatically generates HTML forms" style="display:block;margin:0 auto" width="936" height="572" loading="lazy">

<p>The error blocks below each field display validation messages if a user submits invalid data, like entering text in the duration field instead of a number. Users make mistakes. They might leave a required field blank or type text into a number field. Fortunately, Django validates the data for you and sends back errors if something goes wrong.</p>
<p>Underneath each input field, we use a logic block that looks like this:<br><code>{% if form.activity.errors %}</code></p>
<p>This code checks a simple condition: Did the user mess up this specific field? If Django found an error with the "activity" input, the code drops into the if block and uses<code>{{ form.activity.errors }}</code> block to print the exact error message (like "<strong>This field is required</strong>") right below the input box.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/97a61f9d-faf6-4ddd-ab17-d53da88e0d07.png" alt="This image displays the error blocks" style="display:block;margin:0 auto" width="1908" height="1116" loading="lazy">

<p>You may notice that both templates include inline CSS rather than a separate stylesheet. For a small project like this, inline styles keep things simple and self-contained. In a larger project, you would use Django's static files system to manage CSS separately.</p>
<h2 id="heading-step-8-how-to-connect-urls">Step 8: How to Connect URLs</h2>
<p>You have views and templates, but Django doesn't know when to use them yet. You need to map URLs to views so that visiting a specific address in the browser triggers the right view function.</p>
<h3 id="heading-81-how-to-create-app-level-urls">8.1 How to Create App Level URLs</h3>
<p>Create a new file called <code>tracker/urls.py</code> and add the following code:</p>
<pre><code class="language-python">from django.urls import path
from . import views

urlpatterns = [ 
    path('', views.workout_list, name='workout_list'), 
    path('add/', views.add_workout, name='add_workout'), 
]
</code></pre>
<p>Each path function takes three arguments.</p>
<p>The first is the route string that represents a URL pattern (an empty string means the root of the app).</p>
<p>The second is the view function to call when that URL is visited.</p>
<p>The third is a name you can use to reference this URL elsewhere in your code, like in the <code>{% url %}</code> template tags you used earlier.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/d9128270-eec3-43af-93de-08780b4a53f5.png" alt="The image contains the description of three arguments of the path function" style="display:block;margin:0 auto" width="2864" height="1056" loading="lazy">

<h3 id="heading-82-how-to-link-app-urls-to-project">8.2 How to Link App URLs to project</h3>
<p>Now that your app-level URLs are set up, the next step is to connect them to the main project so Django knows where to start routing requests. Think of it like linking a smaller map (your app) to a bigger map (your project), so everything works together smoothly.</p>
<p>Open <code>fitness_project.urls.py</code> and update it to include your app's URLs:</p>
<pre><code class="language-python">from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('tracker.urls')),
]
</code></pre>
<p>The <code>include()</code> function tells Django to look at the URL patterns defined in the <code>tracker/urls.py</code> file whenever someone visits your site. The empty string prefix means your tracker app handles requests at the root of the site.</p>
<p>Here's the full picture of how a request flows through the URL system.</p>
<p>When someone visits <a href="http://127.0.0.1:8000/add/">http://127.0.0.1:8000/add/</a>, Django first checks <code>fitness_project/urls.py</code>. It matches the empty prefix and delegates to <code>tracker/urls.py</code>. There, it matches <code>add/</code> and calls the <code>add_workout view</code>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/4590c4e3-0d32-4865-90db-a6504ee508c1.png" alt="The image shows the how the URL flows through the system" style="display:block;margin:0 auto" width="2764" height="1146" loading="lazy">

<h2 id="heading-step-9-how-to-test-the-application-locally">Step 9: How to Test the Application Locally</h2>
<p>At this point, your app has everything it needs to work. Let's test it.</p>
<p>Start the development server by running the command:</p>
<pre><code class="language-shell">python manage.py runserver
</code></pre>
<p>Open your browser and visit <a href="http://127.0.0.1:8000/">http://127.0.0.1:8000/</a>. You should see the workout list page with the heading "<strong>My Workouts</strong>" and a button that says "<strong>+ Log a Workout</strong>."</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/e15dc4cf-09b0-4ba9-95b1-baba32ce929f.png" alt="The image shows the My Workouts image with the button to log a workout" style="display:block;margin:0 auto" width="2186" height="1006" loading="lazy">

<p>Click that button. You should see the workout form with fields for activity, duration, and date.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/d9790d3e-0b6a-4e1b-b3d9-f305bc254cfc.png" alt="The image shows an empty form to log a workout" style="display:block;margin:0 auto" width="1146" height="838" loading="lazy">

<p>Fill in some test data:</p>
<ul>
<li><p>Activity: Skipping</p>
</li>
<li><p>Duration: 25</p>
</li>
<li><p>Date: Pick today's date from the date picker</p>
</li>
</ul>
<p>Click "<strong>Save Workout</strong>" You should be redirected back to the workout list page, and your new workout should appear as a card.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/3f1dbf8f-3547-45ec-961d-cff75092ec02.png" alt="The image shows the workout list after adding a new workout" style="display:block;margin:0 auto" width="1656" height="1042" loading="lazy">

<p>Try adding a few more workouts with different activities and dates. Make sure they all show up on the list page in the correct order (most recent first).</p>
<p>This is also a good time to experiment. Try submitting the form with missing fields and see how Django handles validation.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/542e32cf-abf5-4f11-9d4e-0733697c591b.png" alt="The image shows an incomplete form being submitted and a correspoding error message" style="display:block;margin:0 auto" width="1284" height="1012" loading="lazy">

<p>Try accessing the admin panel at <a href="http://127.0.0.1:8000/admin/">http://127.0.0.1:8000/admin/</a> to see your workouts there as well.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/cdfbcbf3-f7b6-4b97-92ee-dcc27e2ce60c.png" alt="This image shows the added workouts in Django admin" style="display:block;margin:0 auto" width="2876" height="848" loading="lazy">

<p>If everything works as expected, you're ready to put your app on the internet.</p>
<h2 id="heading-step-10-how-to-prepare-for-deployment">Step 10: How to Prepare for Deployment</h2>
<p>Running your app on localhost is great for development, but nobody else can see it. Deployment means putting your app on a server that's accessible from anywhere on the internet.</p>
<p>Before you deploy, you'll need to make a few changes to your project's settings.</p>
<h3 id="heading-101-how-to-update-settings-for-production">10.1 How to Update Settings for Production</h3>
<p>Open <code>fitness_project/settings.py</code> and make the following changes.</p>
<p>First, set <code>DEBUG</code> to <code>False</code>.</p>
<p>During development, <code>DEBUG = True</code> shows detailed error pages that help you fix problems. In production, these error pages would expose sensitive information about your code and server to anyone who triggers an error.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/6b8b491e-d0ae-4a93-80bf-92134e46ff22.png" alt="The image shows the DEBUG being set to False in the settings.py file" style="display:block;margin:0 auto" width="2866" height="1348" loading="lazy">

<p>Next, update <code>ALLOWED_HOSTS</code> to include <strong>PythonAnywhere's</strong> <strong>domain</strong>.</p>
<p>This setting tells Django which domain names are allowed to serve your app. Replace yourusername with the actual PythonAnywhere username you will create in the next step.</p>
<pre><code class="language-python">ALLOWED_HOSTS = ['yourusername.pythonanywhere.com']
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/050746f6-d076-41de-8810-08bf539bfda5.png" alt="The image shows the allowed host list being updated to add the pythonanywhere domain" style="display:block;margin:0 auto" width="1622" height="1068" loading="lazy">

<p>Finally, add a <code>STATIC_ROOT</code> setting so Django knows where to collect your static files (CSS, JavaScript, images) for production:</p>
<pre><code class="language-python">import os
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/712e0514-42f6-4292-ae52-99f49b6f162b.png" alt="The image shows the code to collect static files" style="display:block;margin:0 auto" width="2862" height="1054" loading="lazy">

<p>These are the minimum changes needed for a basic deployment.</p>
<p>💡 For a production app handling real user data, you would also want to set a secure SECRET_KEY, configure a proper database like PostgreSQL, and set up HTTPS. But for a learning project, these changes are enough.</p>
<h2 id="heading-step-11-how-to-deploy-your-django-app-on-pythonanywhere">Step 11: How to Deploy Your Django App on PythonAnywhere</h2>
<p>PythonAnywhere is a hosting platform designed specifically for Python web applications. It offers a free tier that's perfect for beginner projects, and it handles much of the server configuration that would otherwise be complex to set up on your own.</p>
<h3 id="heading-111-how-to-create-a-pythonanywhere-account">11.1 How to Create a PythonAnywhere Account</h3>
<p>Go to <a href="http://pythonanywhere.com">pythonanywhere.com</a> and sign up for a free "Beginner" account. Remember the username you choose, because your app will be available at <a href="http://yourusername.pythonanywhere.com"><strong>yourusername.pythonanywhere.com</strong></a><strong>.</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/e64e7ca4-7d7d-40a7-a56b-259bb706461f.png" alt="The image shows the homepage of pythonanywhere" style="display:block;margin:0 auto" width="2854" height="1782" loading="lazy">

<p>Now signup to the website. Fill in the username, email and password and click on the free tier or now.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/fde55ec6-16f4-4a4f-8927-7b01d3e36a96.png" alt="The image shows the various tiers of python anywhere websites" style="display:block;margin:0 auto" width="2758" height="1418" loading="lazy">

<h3 id="heading-112-how-to-upload-your-project-files">11.2 How to Upload Your Project Files</h3>
<p>After logging in, you have two options for getting your project files onto PythonAnywhere.</p>
<h4 id="heading-option-a-upload-using-git">Option A: Upload using Git</h4>
<p>If your project is in a Git repository, open a Bash console from the PythonAnywhere dashboard by clicking "Consoles" and then "Bash." Then clone your repository:</p>
<p>git clone <a href="https://github.com/yourusername/fitness-tracker.git">https://github.com/yourusername/fitness-tracker.git</a></p>
<p>In this tutorial, we won't be using Git. Instead we'll follow the second option.</p>
<h4 id="heading-option-b-upload-files-manually">Option B: Upload files manually</h4>
<p>First go your project folder in your computer and created a compressed version of the project.</p>
<p>IMPORTANT NOTE: When you create the compressed file, make sure to first create a copy of the project somewhere and remove the venv and pycache folder before you compress it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/169dcd52-50f0-44c0-82d4-1dea1196ac88.png" alt="The image shows the project folder being compressed" style="display:block;margin:0 auto" width="1564" height="422" loading="lazy">

<p>Navigate to your home directory and click on upload file tab and upload the compressed file.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/633c7b1f-0088-4d2d-8911-b45e86aa39c2.png" alt="The image shows the compressed file being uploaded to pythonanywhere" style="display:block;margin:0 auto" width="2668" height="1096" loading="lazy">

<p>Now we need to unzip the compressed file. To do this, go to the Consoles tab and click on Bash console.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/7774a88e-362e-4680-a19d-32e0ef09fe04.png" alt="The image shows the Consoles tab and bash option" style="display:block;margin:0 auto" width="2692" height="1198" loading="lazy">

<p>The bash console should open. Then type the following command in the console to unzip the folder:</p>
<pre><code class="language-shell">unzip fitness-tracker.zip
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/1ae044ad-8996-4191-868a-33566c2483d9.png" alt="The image shows the result of the unzip command" style="display:block;margin:0 auto" width="2330" height="1078" loading="lazy">

<h3 id="heading-113-how-to-set-up-a-virtual-environment-in-pythonanywhere">11.3 How to Set Up a Virtual Environment in PythonAnywhere</h3>
<p>Open a Bash console from the PythonAnywhere dashboard. Navigate to your project directory and create a fresh virtual environment:</p>
<pre><code class="language-shell">cd fitness-tracker
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/c8113d5d-68a6-400f-ab9f-7c8903485eda.png" alt="The image shows changing the directory to fitness tracker" style="display:block;margin:0 auto" width="2852" height="468" loading="lazy">

<p>Type the following command to install a virtual environment as we've done before and then activate the virtual environment:</p>
<pre><code class="language-shell">python3 -m venv venv

source venv/bin/activate
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/724767df-181c-4fcf-91e0-77c6dac566b0.png" alt="The image shows the virtual environment being created and activated" style="display:block;margin:0 auto" width="2108" height="624" loading="lazy">

<p>Now install Django as before using <code>pip install django</code> command:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/d6c37d87-7497-4ca1-a90f-6cd66422c2e5.png" alt="The image shows django being installed" style="display:block;margin:0 auto" width="1980" height="952" loading="lazy">

<h3 id="heading-114-how-to-run-migrations-and-create-a-superuser-on-pythonanywhere">11.4 How to Run Migrations and Create a SuperUser on PythonAnywhere</h3>
<p>While you're still in the Bash console with your virtual environment activated, run the migrations to create the database tables on the server:</p>
<pre><code class="language-shell">python manage.py makemigrations

python manage.py migrate

python manage.py createsuperuser
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/0b763118-5a1c-4c3f-87f4-693cc7de0da2.png" alt="The image shows the make migrations and migrate commands running" style="display:block;margin:0 auto" width="2026" height="768" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/77183d3c-b4bc-40b7-b00f-5f03d2aea2ab.png" alt="The image shows the super user being created" style="display:block;margin:0 auto" width="1814" height="506" loading="lazy">

<h3 id="heading-114-how-to-configure-the-web-app-in-pythonanywhere">11.4 How to Configure the Web App in Pythonanywhere</h3>
<p>Go to the "Web" tab on the PythonAnywhere dashboard and click "Add a new web app." Follow the setup wizard:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/e1296e93-3e52-4327-b12a-4d4555e80845.png" alt="The image shows the web tab and add a new web app button" style="display:block;margin:0 auto" width="2880" height="846" loading="lazy">

<p>Click "Next" on the domain name step (<em>remember the free tier uses</em> <a href="http://yourusername.pythonanywhere.com"><em>yourusername.pythonanywhere.com</em></a>).</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/24495b99-e1dc-437d-8ce8-47c0c962349f.png" alt="The image shows the web console where you specify the domain name" style="display:block;margin:0 auto" width="2870" height="1302" loading="lazy">

<p>Select "Manual configuration" (not "Django" – the manual option gives you more control).</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/db470bd1-bbd1-4c61-b846-b7147d8f820f.png" alt="The image highlight the manual configuration option which should be selected" style="display:block;margin:0 auto" width="2872" height="1326" loading="lazy">

<p>Then choose the Python version that matches what you installed. In my case it's 3.13, so I'll choose 3.13</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/dc51d44e-531c-4871-a225-e68b2db5dc65.png" alt="The image shows the Python version what is being selected" style="display:block;margin:0 auto" width="2780" height="1384" loading="lazy">

<p>Click on Next button and a WSGI (Web Server Gateway Interface) will be created.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/3565512b-9c84-4b0d-82d9-3964cb0ff46b.png" alt="The image shows the final page before the web app is created" style="display:block;margin:0 auto" width="2878" height="1422" loading="lazy">

<p>With this we've created the web app:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/eda9fe63-01f4-48bc-98f1-07696bb798bf.png" alt="The image shows the final creation of the web app" style="display:block;margin:0 auto" width="2874" height="1568" loading="lazy">

<p>After you've set up the web app, you have to do two more things:</p>
<ul>
<li><p>Set the virtual environment path</p>
</li>
<li><p>Configure the WSGI file</p>
</li>
</ul>
<h3 id="heading-115-how-to-set-the-virtual-environment-path">11.5 How to Set the Virtual Environment Path</h3>
<p>On the <strong>Web</strong> tab, scroll down to the "<strong>Virtualenv</strong>" section and enter the path to your virtual enviroment. The path to the file should be like this:</p>
<pre><code class="language-shell">/home/yourusername/fitness-tracker/venv
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/413ed047-ecc1-440f-a931-4a57aa91348b.png" alt="The image shows the added path of virtual environment" style="display:block;margin:0 auto" width="2878" height="994" loading="lazy">

<h3 id="heading-116-how-to-configure-the-wsgi-file">11.6 How to Configure the WSGI file</h3>
<p>Still on the Web tab, scroll to the code section and click on the WSGI configuration file link:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/934d1542-79d6-4140-8bb2-a4389fc17770.png" alt="The image shows the Code section and the WSGI configuration file path" style="display:block;margin:0 auto" width="2264" height="548" loading="lazy">

<p>Delete all the contents and replace them with the content below and save the file:</p>
<pre><code class="language-python">import os
import sys
path = '/home/prabodhtuladhardev/fitness-tracker' #replace with your username
if path not in sys.path:
    sys.path.append(path)

os.environ['DJANGO_SETTINGS_MODULE'] = 'fitness_project.settings'

from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/416adac6-61d7-464d-b91f-b3b35272ef08.png" alt="The image shows the edited wsgi.py file and the highlights the save button" style="display:block;margin:0 auto" width="1812" height="986" loading="lazy">

<h3 id="heading-117-how-to-set-up-static-files">11.7 How to Set Up Static Files</h3>
<p>Still on the "Web" tab, scroll down to the "Static files" section. Add an entry:</p>
<ul>
<li><p>URL: <code>/static/</code></p>
</li>
<li><p>Directory: <code>/home/yourusername/fitness-tracker/staticfiles</code></p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/f128dbd2-4acc-4387-89f8-d79de8c1e66e.png" alt="The image shows the static files section of the Web tab" style="display:block;margin:0 auto" width="2488" height="728" loading="lazy">

<p>Then go back to your Bash console and run the following command:</p>
<pre><code class="language-shell">python manage.py collectstatic
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/207f8e67-2679-4ccc-8ebf-7d8aae5d7494.png" alt="The image shows the results of the collect static command" style="display:block;margin:0 auto" width="2448" height="600" loading="lazy">

<p>This copies all static files to the staticfiles directory so PythonAnywhere can serve them directly.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/a14b75c4-cc1e-48ad-baf2-cf4f00906b85.png" alt="The image shows the folder named static files that was created" style="display:block;margin:0 auto" width="2720" height="1026" loading="lazy">

<p>Go back to the "Web" tab and click the green "Reload" button at the top. This restarts your app with all the new configuration.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/08fab36e-8c8d-4d1b-8342-6e340fcf45d4.png" alt="The image shows the web tab with the reload button" style="display:block;margin:0 auto" width="2874" height="1046" loading="lazy">

<h3 id="heading-118-how-to-view-your-live-application">11.8 How to View Your Live Application</h3>
<p>Open a new browser tab and visit <a href="https://yourusername.pythonanywhere.com">https://yourusername.pythonanywhere.com</a>. You should see your fitness tracker, live on the internet.</p>
<p>Try adding a workout.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/db67dbb1-3cb9-4b86-bfaa-67427ef5eac0.png" alt="The image shows the workout list view being opened in python anywhere" style="display:block;margin:0 auto" width="2290" height="1278" loading="lazy">

<p>Visit the admin panel at <a href="https://yourusername.pythonanywhere.com/admin/">https://yourusername.pythonanywhere.com/admin/</a>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/bd80d1e8-cf23-4ac2-b6a7-35bfdf61d023.png" alt="The image shows the workout django admin being opened in pythonanywhere" style="display:block;margin:0 auto" width="2128" height="1310" loading="lazy">

<p>Everything should work just as it did on your local machine, but now anyone with the link can access it.</p>
<p>This is a meaningful milestone. You've gone from zero to a deployed Django application. Share the link with a friend or post it in a coding community. Seeing your work live on the internet is one of the most motivating experiences in learning to code.</p>
<h2 id="heading-common-mistakes-and-how-to-fix-them">Common Mistakes and How to Fix Them</h2>
<p>Even when you follow each step carefully, things can go wrong. Here are the most common issues beginners run into and how to solve them.</p>
<p><strong>"ModuleNotFoundError: No module named 'django'"</strong> – This usually means your virtual environment isn't activated. Run <code>source venv/bin/activate</code> (macOS/Linux) or <code>venv\Scripts\activate</code> (Windows) and try again. On PythonAnywhere, make sure the <strong>virtualenv</strong> path in the "<strong>Web</strong>" tab points to the correct location.</p>
<p><strong>"DisallowedHost" error</strong> – You forgot to add your domain to <code>ALLOWED_HOSTS</code> in <code>settings.py</code>, or there's a typo. Double-check that it matches your PythonAnywhere URL exactly.</p>
<p><strong>Static files not loading in production</strong> – Make sure you ran <code>python manage.py collectstatic</code> and that the static file mapping on PythonAnywhere points to the <strong>correct staticfiles</strong> directory. Also verify that <code>STATIC_ROOT</code> is set in <code>settings.py</code>.</p>
<p><strong>"No such table" or migration errors</strong> – You probably forgot to run <code>python manage.py migrate</code> after cloning or uploading your project to PythonAnywhere. Run the <code>migrate</code> command in the Bash console.</p>
<p><strong>Changes not showing up on PythonAnywhere</strong> – After making any code changes, you must click the "<strong>Reload</strong>" button on the "<strong>Web</strong>" tab. PythonAnywhere does not automatically detect file changes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69bdd408475ca17974459537/08fab36e-8c8d-4d1b-8342-6e340fcf45d4.png" alt="The image shows the web tab and the reload buttton" style="display:block;margin:0 auto" width="2874" height="1046" loading="lazy">

<h2 id="heading-how-you-can-improve-this-project">How You Can Improve This Project</h2>
<p>The fitness tracker you built is intentionally simple. That's a feature, not a limitation. A working simple project is the perfect foundation for learning more.</p>
<p>Here are some ideas for expanding it.</p>
<ol>
<li><p><strong>Add user authentication:</strong> Right now, anyone who visits the site sees the same workout data. Django has a built-in authentication system that lets you add registration, login, and logout. Each user could then have their own private list of workouts.</p>
</li>
<li><p><strong>Add the ability to edit and delete workouts.</strong> Currently, once a workout is saved, there's no way to change or remove it from the interface (you can do it through the admin panel, but not the main app). Try creating new views and templates for editing and deleting.</p>
</li>
<li><p><strong>Add workout categories or tags.</strong> Let users categorize their workouts as "Cardio," "Strength," "Flexibility," and so on. This would involve adding a new field to the model or creating a separate Category model with a foreign key relationship.</p>
</li>
<li><p><strong>Add charts and progress tracking.</strong> Use a JavaScript charting library like Chart.js to display workout trends over time. For example, you could show a bar chart of total minutes exercised per week.</p>
</li>
<li><p><strong>Build an API with Django REST Framework.</strong> If you want to learn about building APIs, try installing Django REST Framework (DRF) and creating API endpoints for your workouts. This would let you build a mobile app or a separate front end that communicates with your Django back end.</p>
</li>
</ol>
<p>Each of these improvements will teach you something new about Django while building on the foundation you already have.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You've built a fully functional fitness tracker web app with Django and deployed it to the internet. That's no small achievement.</p>
<p>Along the way, you learned how Django projects and apps are structured, how models define the shape of your data, how migrations translate those models into database tables, how views handle the logic of your application, how templates render dynamic HTML, and how URLs tie everything together. You also went through the entire deployment process on PythonAnywhere.</p>
<p>These are the core building blocks of Django development. The patterns you practiced here – defining a model, creating a form, writing a view, building a template, and connecting a URL – are the same patterns you will use in every Django project, no matter how complex.</p>
<p>The best way to solidify what you have learned is to keep building. Try one of the improvements mentioned above, or start a completely new project. A calorie tracker, a habit tracker, an expense tracker, or a personal journal would all use the same Django concepts with slightly different models and views.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
