<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: kihuni</title>
    <description>The latest articles on Forem by kihuni (@kihuni).</description>
    <link>https://forem.com/kihuni</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F592901%2F2af60943-3c00-44fb-93e0-1c737544687d.jpg</url>
      <title>Forem: kihuni</title>
      <link>https://forem.com/kihuni</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/kihuni"/>
    <language>en</language>
    <item>
      <title>My Journey Contributing to Django: From Intimidation to a Merged Ticket 🚀</title>
      <dc:creator>kihuni</dc:creator>
      <pubDate>Mon, 22 Dec 2025 08:46:57 +0000</pubDate>
      <link>https://forem.com/kihuni/my-journey-contributing-to-django-from-intimidation-to-a-merged-ticket-bg7</link>
      <guid>https://forem.com/kihuni/my-journey-contributing-to-django-from-intimidation-to-a-merged-ticket-bg7</guid>
      <description>&lt;h3&gt;
  
  
  Contributing to Django felt intimidating at first.
&lt;/h3&gt;

&lt;p&gt;This isn’t just any open-source project. Django powers millions of applications worldwide. The codebase is massive, the standards are high, and every change goes through careful review. As someone still growing as a backend engineer, I often wondered:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Am I really ready to contribute here?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Spoiler: You don’t need to feel ready; you just need the right environment, guidance, and consistency. That environment, for me, was &lt;strong&gt;Djangonaut Space&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Djangonaut Space
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2elr47sp1li7c911eo4a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2elr47sp1li7c911eo4a.png" alt=" " width="800" height="581"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Djangonaut Space is more than a mentorship program; it’s a launchpad into real-world open source. It pairs contributors with experienced navigators and captains who help you move from "I want to contribute" to "my code is live in Django."&lt;/p&gt;

&lt;p&gt;Through the program, I learned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to read and understand Django’s internals&lt;/li&gt;
&lt;li&gt;How to navigate tickets, discussions, and code reviews&lt;/li&gt;
&lt;li&gt;How to communicate clearly with maintainers&lt;/li&gt;
&lt;li&gt;How to accept feedback without ego, and improve fast&lt;/li&gt;
&lt;li&gt;Most importantly, I learned that open source is collaborative, not competitive.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Ticket That Changed Everything
&lt;/h3&gt;

&lt;p&gt;My contribution focused on improving &lt;code&gt;argparse error handling in Django management commands&lt;/code&gt;, a small but meaningful enhancement that improves developer experience.&lt;/p&gt;

&lt;p&gt;At first, the problem looked simple. But as with most Django work, the real challenge wasn’t writing code, it was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understanding existing behavior&lt;/li&gt;
&lt;li&gt;Making changes without breaking backwards compatibility&lt;/li&gt;
&lt;li&gt;Writing code that aligns with Django’s philosophy&lt;/li&gt;
&lt;li&gt;Justifying decisions clearly during review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The review process taught me a lot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;My code was reviewed line by line&lt;/li&gt;
&lt;li&gt;I rebased multiple times&lt;/li&gt;
&lt;li&gt;I received suggestions that genuinely improved the implementation&lt;/li&gt;
&lt;li&gt;I learned when to push back and when to listen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And then it happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🎉 The ticket was merged into Django’s main branch.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgy9teetfrd41xwd6dk6l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgy9teetfrd41xwd6dk6l.png" alt=" " width="800" height="895"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Seeing that merge wasn’t just exciting, it was validating. It confirmed that with the right mentorship and persistence, contributing to major open-source projects is &lt;code&gt;absolutely achievable&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What This Journey Taught Me
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffdedmckdeeyovlopoxud.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffdedmckdeeyovlopoxud.png" alt=" " width="562" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This experience reshaped how I think about open source and software engineering in general.&lt;/p&gt;

&lt;p&gt;Here are a few lessons I’m carrying forward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You don’t need to know everything to contribute&lt;/li&gt;
&lt;li&gt;Good questions are as valuable as good code&lt;/li&gt;
&lt;li&gt;Feedback is a gift, even when it’s tough&lt;/li&gt;
&lt;li&gt;Consistency beats confidence&lt;/li&gt;
&lt;li&gt;Community accelerates growth&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open source isn’t about being perfect. It’s about showing up, learning in public, and improving with every iteration.&lt;/p&gt;

&lt;p&gt;If you’re a developer sitting on the fence, wondering whether you’re &lt;code&gt;good enough&lt;/code&gt; to contribute to a large project like Django, this is your sign.&lt;/p&gt;

&lt;p&gt;Start small. Ask questions. Join a mentorship program if you can. And don’t underestimate the impact of one well-reviewed contribution. That one ticket might just change how you see yourself as a developer.&lt;/p&gt;

&lt;h3&gt;
  
  
  🙏 Acknowledgements &amp;amp; Thanks
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F82x113fyrux3o9g9rith.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F82x113fyrux3o9g9rith.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This journey wouldn’t have been possible without the incredible people behind the Djangonaut Space team Mars.&lt;/p&gt;

&lt;p&gt;Navigator: &lt;strong&gt;&lt;a class="mentioned-user" href="https://dev.to/lilian"&gt;@lilian&lt;/a&gt;&lt;/strong&gt; — thank you for the guidance, structure, and encouragement throughout the journey.&lt;/p&gt;

&lt;p&gt;Captain: &lt;strong&gt;&lt;a class="mentioned-user" href="https://dev.to/sean"&gt;@sean&lt;/a&gt;&lt;/strong&gt; — thank you for the thoughtful reviews, patience, and for the encourangement.&lt;/p&gt;

&lt;p&gt;Djangonauts:&lt;br&gt;
&lt;strong&gt;&lt;a class="mentioned-user" href="https://dev.to/eddy"&gt;@eddy&lt;/a&gt;&lt;/strong&gt;,** &lt;a class="mentioned-user" href="https://dev.to/rim"&gt;@rim&lt;/a&gt; Choi**, thank you for the collaboration, discussions, and shared learning. Building alongside you made the experience even better.&lt;/p&gt;

&lt;p&gt;And to the wider &lt;strong&gt;Django&lt;/strong&gt; community, thank you for maintaining such a welcoming and high-quality open-source ecosystem.&lt;/p&gt;

</description>
      <category>django</category>
      <category>djangonauts</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>Learn URLs in Django: From Setup to Dynamic Routing</title>
      <dc:creator>kihuni</dc:creator>
      <pubDate>Sat, 27 Sep 2025 02:04:16 +0000</pubDate>
      <link>https://forem.com/kihuni/mastering-urls-in-django-from-setup-to-dynamic-routing-44i3</link>
      <guid>https://forem.com/kihuni/mastering-urls-in-django-from-setup-to-dynamic-routing-44i3</guid>
      <description>&lt;p&gt;In our previous blog, &lt;a href="https://dev.to/kihuni/how-the-web-talks-to-django-a-beginner-friendly-guide-5945"&gt;how the Web Talks to Django&lt;/a&gt;, we saw how Django handles requests and responses internally. Now that you have a clear picture of what happens when a user types a web address and hits enter, it’s time to take the next step: building that bridge between the browser and your Django code.&lt;/p&gt;

&lt;p&gt;In this blog, we’ll focus on Django’s URL system, the backbone of how requests find their way to the right &lt;strong&gt;view&lt;/strong&gt;(" a chunk of Python code"). We will explore more about **views **later.&lt;/p&gt;

&lt;p&gt;You’ll learn how to set up a Django project, declare URLs, organize them into logical groups, and even extract useful information directly from the URL to use in your views.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Django Set Up
&lt;/h2&gt;

&lt;p&gt;We’ll walk step by step through setting up your first Django project and running it locally.&lt;/p&gt;

&lt;p&gt;Django runs on Python, so the first step is to make sure you have it installed. Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python --version

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6830nldx6yregydh7344.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6830nldx6yregydh7344.png" alt="python" width="800" height="110"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If not installed, visit &lt;a href="https://www.python.org/" rel="noopener noreferrer"&gt;python&lt;/a&gt; and follow instructions to install Python.&lt;/p&gt;

&lt;p&gt;Then, we need a place to put our work. You can name your project the name you prefer. I am going to use "mastering-django".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir mastering-django
cd mastering-django

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feqh2pzqisddcsvuq5bif.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feqh2pzqisddcsvuq5bif.png" alt="Create folder" width="800" height="97"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next, we install Django into a virtual environment so we keep our project dependencies separate from the rest of the installed Python packages on our machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 -m venv venv

Activate it:
source venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4h9m3d1k9mcogbbocvgg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4h9m3d1k9mcogbbocvgg.png" alt="virtual env" width="800" height="97"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With your environment active, install Django:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(venv) $ pip install Django
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Django comes with some tools that we can use to get a project started quickly. let’s start a new Django project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(venv) $ django-admin startproject myproject .

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you run &lt;strong&gt;django-admin startproject myproject .&lt;/strong&gt;, Django creates a project structure for you. It might look like a lot at first, but each file has a clear purpose. Let’s walk through them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    manage.py
    myproject/
        __init__.py
        settings.py
        urls.py
        asgi.py
        wsgi.py

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;manage.py&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is your project’s remote control. It’s a command-line tool that lets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start the development server&lt;/li&gt;
&lt;li&gt;Apply database migrations&lt;/li&gt;
&lt;li&gt;Create apps&lt;/li&gt;
&lt;li&gt;Manage users (like creating a superuser)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whenever you interact with your project, you’ll usually do it through &lt;strong&gt;manage.py&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Inner myproject/ Folder&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This folder holds your project’s configuration files.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;strong&gt;init&lt;/strong&gt;.py&lt;/strong&gt;&lt;br&gt;
Marks the folder as a Python package. It’s usually empty, but it tells Python “this is code you can import.”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;settings.py&lt;/strong&gt;&lt;br&gt;
The brain of your project. It stores all configurations — installed apps, database setup, middleware, templates, static files, and more.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;urls.py&lt;/strong&gt;&lt;br&gt;
The map of your project. It connects browser requests (like /about/ or /blog/) to the right views in your code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;asgi.py&lt;/strong&gt;&lt;br&gt;
Entry point for ASGI servers. It allows Django to support modern features like WebSockets and async handling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;wsgi.py&lt;/strong&gt;&lt;br&gt;
Entry point for WSGI servers. This is what most traditional web servers (like Gunicorn or uWSGI) use to run Django apps in production.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By understanding these files, you know where your configurations live and how Django connects the dots.&lt;/p&gt;

&lt;p&gt;Next, let’s run the development server to make sure everything is working correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(venv) $ python manage.py runserver

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you open &lt;a href="http://127.0.0.1:8000/" rel="noopener noreferrer"&gt;http://127.0.0.1:8000/&lt;/a&gt; in your browser, you’ll see Django’s welcome page 🎉. That means your project is alive and ready&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh62t15szhbh5n97pq8uw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh62t15szhbh5n97pq8uw.png" alt="django welcoming page" width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now that our project is running and we understand its structure. Now it’s time to focus on one of the most important pieces: urls.py.&lt;/p&gt;

&lt;h2&gt;
  
  
  URL configuration in Django
&lt;/h2&gt;

&lt;p&gt;Every time you type a URL into your browser and hit Enter, Django needs to know: where should this request go?&lt;/p&gt;

&lt;p&gt;That’s where &lt;strong&gt;urls.py configuration&lt;/strong&gt; comes in. Think of Django’s URL configuration (urls.py) as a list of paths. Whenever a request comes in, Django scans that list from top to bottom. As soon as it finds a match, it stops searching and routes the request to the matching view (note: a view is just a chunk of Python code that returns a response; we’ll dive into views in the next blog).&lt;/p&gt;

&lt;p&gt;Here’s a simple example of a &lt;strong&gt;url.py&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# myproject/urls.py
from django.urls import path
from application import views

urlpatterns = [
    path("", views.home),
    path("about/", views.about),
    path("contact/", views.contact),
    path("terms/", views.terms),
]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you visit &lt;a href="https://www.example.com/about/" rel="noopener noreferrer"&gt;https://www.example.com/about/&lt;/a&gt;, Django trims off the domain and leading slash, leaving just &lt;strong&gt;about/&lt;/strong&gt;. That matches the second entry above, so the request is sent to &lt;code&gt;views.about&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And if you simply visit the root URL &lt;a href="https://www.example.com/" rel="noopener noreferrer"&gt;https://www.example.com/&lt;/a&gt;, Django matches the empty string &lt;strong&gt;" "&lt;/strong&gt; and sends the request to &lt;code&gt;views.home&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The order matters. If you accidentally define two paths that could match the same URL, Django will always go with the first one it finds and ignore the rest.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Notice the trailing slash (/). By default, Django expects URLs to end with one. If you type &lt;a href="https://www.example.com/about" rel="noopener noreferrer"&gt;https://www.example.com/about&lt;/a&gt; (without the slash), Django will redirect you to the correct version because of the &lt;a href="https://docs.djangoproject.com/en/4.1/ref/settings/#append-slash" rel="noopener noreferrer"&gt;APPEND_SLASH default setting&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Dynamic Paths with Converters
&lt;/h3&gt;

&lt;p&gt;Static paths like &lt;strong&gt;/about/&lt;/strong&gt; are nice, but most real-world apps need to be flexible. Imagine if every blog post had to live at /blog/ with no way of telling them apart, that wouldn’t work.&lt;/p&gt;

&lt;p&gt;Instead, we usually want URLs to carry information. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A blog post URL might include a year and a slug, like &lt;strong&gt;/blog/2024/django-rocks/&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A user profile URL should work for any username, like &lt;strong&gt;/users/stephen/&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s where path converters come in. They let you pull pieces of information straight out of the URL and hand them directly to your view.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# myproject/urls.py
from django.urls import path
from application import views

urlpatterns = [
    path("blog/&amp;lt;int:year&amp;gt;/", views.blog_by_year),
    path("users/&amp;lt;str:username&amp;gt;/", views.profile),
]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s what happens behind the scenes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If someone visits &lt;strong&gt;/blog/2024/&lt;/strong&gt;, Django sees &lt;strong&gt;&lt;a&gt;int:year&lt;/a&gt;&lt;/strong&gt; and automatically captures 2024 as an integer. That value is then passed into &lt;strong&gt;views.blog_by_year(year=2024)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If someone visits /users/stephen/, Django captures "stephen" as a string and hands it to your view: views.profile(username="stephen").&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best part? Django validates the values before they ever hit your code. If a user tries &lt;strong&gt;/blog/notayear/&lt;/strong&gt;, Django knows that “notayear” is not an integer, so it returns a clean &lt;strong&gt;404 Not Found&lt;/strong&gt; without bothering your view.&lt;/p&gt;

&lt;p&gt;In other words:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You get dynamic, flexible URLs.&lt;/li&gt;
&lt;li&gt;You don’t need to write messy validation logic in your views.&lt;/li&gt;
&lt;li&gt;Your app stays both user-friendly and safe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of converters as Django’s built-in &lt;strong&gt;parsers&lt;/strong&gt; for your URLs. They keep your code clean while giving you the power to make URLs that feel natural and meaningful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Regular Expression Paths
&lt;/h3&gt;

&lt;p&gt;Sometimes, path converters aren’t enough. Let’s say you only want to match a four-digit year, such as &lt;code&gt;2025&lt;/code&gt;. In that case, you can reach for re_path and use a regular expression (regex).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.urls import re_path
from application import views

urlpatterns = [
    re_path(r"^blog/(?P&amp;lt;year&amp;gt;[0-9]{4})/(?P&amp;lt;slug&amp;gt;[\w-]+)/$", views.blog_post),
]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, regex looks intimidating, but here’s what’s happening:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;(?P[0-9]{4}) - captures exactly four digits and passes them as a variable called year.&lt;/li&gt;
&lt;li&gt;(?P[\w-]+) - captures a word-like slug (letters, numbers, underscores, or dashes).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So if someone visits &lt;strong&gt;/blog/2025/django-rocks/&lt;/strong&gt;, Django will match it and call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;views.blog_post(year=2025, slug="django-rocks")

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Regex is powerful, like a chainsaw. You can cut through particular patterns, but you don’t always need it. Most of the time, Django’s simple path converters (int, str, slug, etc.) are more than enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Grouping Related URLs
&lt;/h3&gt;

&lt;p&gt;As your project grows, dumping all routes into one urls.py file can get messy. Instead, Django encourages you to let each app manage its own URLs and then connect them in the main project.&lt;/p&gt;

&lt;p&gt;For example, suppose you have two apps: schools and students.&lt;/p&gt;

&lt;p&gt;myproject/urls.py&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.urls import path, include

urlpatterns = [
    path("schools/", include("schools.urls")),
    path("students/", include("students.urls")),
]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;school.url&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.urls import path
from . import views

urlpatterns = [
    path("", views.index),
    path("&amp;lt;int:school_id&amp;gt;/", views.school_detail),
]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;students.urls&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.urls import path
from . import views

urlpatterns = [
    path("", views.index),
    path("&amp;lt;int:student_id&amp;gt;/", views.student_detail),
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, each app is responsible for its own routing, and the main project needs to know where to plug things in. This makes your project cleaner, modular, and much easier to maintain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Naming URLs
&lt;/h3&gt;

&lt;p&gt;Hardcoding URLs everywhere in your project is risky. Imagine you define a route at &lt;strong&gt;/blog/categories/&lt;/strong&gt;, but later decide to move it to &lt;strong&gt;/marketing/blog/categories/&lt;/strong&gt;. If you’ve hardcoded links, you’d need to hunt down and update every single one.&lt;/p&gt;

&lt;p&gt;Instead, Django lets you name your URLs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# project/urls.py
from django.urls import path
from blog import views

urlpatterns = [
    path("marketing/blog/categories/", views.categories, name="blog_categories"),
]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can reference this route by name instead of by path.&lt;/p&gt;

&lt;p&gt;Example in a view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.http import HttpResponseRedirect
from django.urls import reverse

def old_blog_categories(request):
    return HttpResponseRedirect(reverse("blog_categories"))

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if the path changes later, as long as the name (blog_categories) stays the same, your code keeps working.&lt;/p&gt;

&lt;p&gt;For larger projects, you can even use namespaces (like schools:index vs. students:index) to avoid naming conflicts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;And that’s a full tour of Django’s URL system! 🎉&lt;/p&gt;

&lt;p&gt;Here’s what we covered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How Django matches requests against urlpatterns.&lt;/li&gt;
&lt;li&gt;Why the order of your routes matters.&lt;/li&gt;
&lt;li&gt;How to use path converters to capture values from the URL.&lt;/li&gt;
&lt;li&gt;When to reach for re_path and regex for advanced matching.&lt;/li&gt;
&lt;li&gt;How to group URLs by app using include().&lt;/li&gt;
&lt;li&gt;How naming (and namespacing) URLs makes your project more flexible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;URLs are the map of your Django project. They decide how a user’s request finds its way to the right piece of code. Once you understand this mapping system, everything else in Django starts to click into place.&lt;/p&gt;

&lt;p&gt;In the next blog, we’ll zoom in on the views, those chunks of Python code that actually generate the response. That’s where we’ll really bring our URLs to life. 🚀&lt;/p&gt;

&lt;h2&gt;
  
  
  The Big Picture
&lt;/h2&gt;

&lt;p&gt;Here’s the request flow at a glance:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F86whtittq6b2tu9tjwwo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F86whtittq6b2tu9tjwwo.png" alt="Request-responce" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Browser – the user types in a URL and hits enter.&lt;/li&gt;
&lt;li&gt;Django URLconf (urls.py) – Django looks through your list of paths to find a match.&lt;/li&gt;
&lt;li&gt;View – the matching view (Python function or class) runs and decides what to return.&lt;/li&gt;
&lt;li&gt;Response – Django sends the result (HTML, JSON, redirect, etc.) back to the browser.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>python</category>
      <category>django</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>How the Web Talks to Django: A Beginner's Guide</title>
      <dc:creator>kihuni</dc:creator>
      <pubDate>Sun, 14 Sep 2025 11:03:54 +0000</pubDate>
      <link>https://forem.com/kihuni/how-the-web-talks-to-django-a-beginner-friendly-guide-5945</link>
      <guid>https://forem.com/kihuni/how-the-web-talks-to-django-a-beginner-friendly-guide-5945</guid>
      <description>&lt;h2&gt;
  
  
  Learn how your keystrokes in the browser turn into responses from Django.
&lt;/h2&gt;

&lt;p&gt;Imagine you open your browser, type &lt;a href="http://www.example.com" rel="noopener noreferrer"&gt;www.example.com&lt;/a&gt;, and within a blink, a fully loaded page appears. Feels like magic, right? But behind that magic is a chain of events quietly working together to make it possible.&lt;/p&gt;

&lt;p&gt;Django calls itself a web framework, but unless you know how the web itself works, that phrase can feel like jargon. So before we talk about Django, let’s peel back the curtain on how the web actually delivers that page to your screen.&lt;/p&gt;

&lt;p&gt;Think of it like sending a letter. You write the address on the envelope, drop it in the mailbox, and somehow, almost instantly, it arrives at the right house. The internet works in a very similar way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Few41tdt4og28pbe9rz1j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Few41tdt4og28pbe9rz1j.png" alt="request and response" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, I’m going to walk you through the journey of a request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How a browser knows where to go.&lt;/li&gt;
&lt;li&gt;How the internet finds the right server.&lt;/li&gt;
&lt;li&gt;How your request gets translated into something a web app (like Django) can understand.&lt;/li&gt;
&lt;li&gt;And finally, how Django receives the request, does the necessary work, and sends a response back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end, you’ll see exactly how the web and Django talk to each other, from your first keystroke in the browser bar to Django sending back the page you see.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Web Works
&lt;/h2&gt;

&lt;p&gt;When you hit &lt;strong&gt;Enter&lt;/strong&gt; in your browser to visit &lt;strong&gt;&lt;a href="http://www.example.com" rel="noopener noreferrer"&gt;www.example.com&lt;/a&gt;&lt;/strong&gt;, a whole chain of events kicks off. Tiny, invisible steps (happening in milliseconds) connect you to the right server, fetch the right information, and display it neatly on your screen.&lt;/p&gt;

&lt;p&gt;To understand how a request reaches a server, we’ll focus on two major steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DNS (Domain Name System)&lt;/li&gt;
&lt;li&gt;HTTP (Hypertext Transfer Protocol)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What is DNS (Domain Name System)?
&lt;/h3&gt;

&lt;p&gt;Every computer connected to the internet can be reached using a network number called an IP address. Maybe you’ve seen some of these before:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;127.0.0.1 — the address your computer uses for itself (on its internal network).&lt;/li&gt;
&lt;li&gt;192.0.2.172 — an example of a public IP address that makes a computer reachable on the internet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Computers love numbers. But for humans? Remembering long strings of numbers is not fun. That’s where the Domain Name System (DNS) comes in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvufdk3e3b73l1mghsixv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvufdk3e3b73l1mghsixv.png" alt="DNS" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;DNS is like the Internet’s phonebook. When you type a website address (like example.com) into your browser, DNS translates that human-friendly name into the actual IP address of the server hosting the site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Structure of Domain Names&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A domain name has a simple structure, made of several parts separated by dots and read from right to left.&lt;/p&gt;

&lt;p&gt;Take our example: &lt;a href="http://www.example.com" rel="noopener noreferrer"&gt;www.example.com&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;.com&lt;/strong&gt; → This is the Top-Level Domain (TLD). TLDs are carefully managed by an organization called ICANN. You’ve probably also seen others like .org, .net, .dev, or country codes like .ke (Kenya) or .uk (United Kingdom).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;example&lt;/strong&gt; → This is the domain name. It’s the unique identity of a service on the internet, the part users are most likely to recognize.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;www&lt;/strong&gt; → This considered the subdomain of a domain. A domain might have many of these, like www, m, mail, wiki, or whatever a domain owner might want to name them. Subdomains can also be more than one level deep, so a.b.example.com is valid, and a is a subdomain of b.example.com and b is a subdomain of example.com.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So far, we’ve seen how the internet uses DNS like a massive phonebook (or a global postal system) to turn human-friendly addresses like example.com into the exact IP address of the server you want to reach. Without DNS, you’d be stuck memorizing numbers instead of names, and the web would feel a lot less human.&lt;/p&gt;

&lt;p&gt;But finding the right server is just the beginning. Once your browser knows where to go, the next big question is: how do the browser and server actually talk to each other? That’s where HTTP comes in the language of the web.&lt;/p&gt;

&lt;p&gt;Next, we’ll follow the request a little further and see how your browser and Django exchange messages, step by step.&lt;/p&gt;

&lt;h2&gt;
  
  
  HTTP — The Language of the Web
&lt;/h2&gt;

&lt;p&gt;We saw how DNS acts like the internet’s phonebook, helping your browser find the exact server behind a domain name. But once your browser has the server’s address, another question pops up:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do the browser and server actually talk to each other?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s where HTTP (Hypertext Transfer Protocol) comes in. If DNS is the postal system that makes sure your letter reaches the right house, HTTP is the language you and the house’s owner use when you start a conversation at the door.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is HTTP?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;HTTP is the protocol of communication between browsers (clients) and servers. It defines the rules for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How a request should be written.&lt;/li&gt;
&lt;li&gt;How a server should respond.&lt;/li&gt;
&lt;li&gt;What happens if something goes wrong?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every time you visit a website, your browser is quietly saying something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Hey server, I’d like the homepage, please.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the server responds:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Here you go, HTML, CSS, images, and everything you need to display the page.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This back-and-forth happens in milliseconds, but it’s the foundation of how the web works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fput6bhk1k7u097tpa8e8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fput6bhk1k7u097tpa8e8.png" alt="request/response" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Anatomy of an HTTP Request&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When your browser asks for a page, it doesn’t just shout “give me stuff!” The request is structured, like a well-written letter. It usually has three main parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Request Line&lt;/strong&gt;
Example: GET /about/ HTTP/1.1&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This tells the server what you want (GET means “fetch something,” /about/ means a path to a particular resource on a site).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Headers&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of these as little notes with extra info.&lt;/p&gt;

&lt;p&gt;Example: User-Agent: Chrome/120 (so the server knows the type of browser making the request).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Body (sometimes)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For requests such as submitting a form, the actual data (e.g., your username/password) should be entered here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Anatomy of an HTTP Response&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once the server understands your request, it replies with a structured response. Again, three main parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Status Line&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example: 200 OK → Everything went fine.&lt;/p&gt;

&lt;p&gt;Or 404 Not Found → Page doesn’t exist.&lt;/p&gt;

&lt;p&gt;Or 500 Internal Server Error → Something broke on the server side.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Headers&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Extra info for the browser, like how long to cache the response or what type of file it is.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Body&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the actual content, HTML, JSON, an image, or whatever the browser asked for.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the Request Finally Reaches Django
&lt;/h2&gt;

&lt;p&gt;Alright, we’ve talked about browsers, DNS, and HTTP. We know how a request leaves the browser, finds the right server, and arrives at its destination.&lt;/p&gt;

&lt;p&gt;Now the spotlight is on Django. This is where the real action begins. Django’s job is simple at first glance: take an incoming HTTP request and return an HTTP response.&lt;/p&gt;

&lt;p&gt;That response could be a web page, JSON data for an API, or even a file download. But before Django can step in, there’s one last hurdle to clear: the Python web server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Python Web Server&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When people say “web server,” they might mean two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the machine running the software, or&lt;/li&gt;
&lt;li&gt;the software itself that listens for requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here, we’re talking about the software.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;web server&lt;/strong&gt; is the first piece of software to actually *&lt;em&gt;hear *&lt;/em&gt; the HTTP request when it arrives at the server. Its job is to pass that raw HTTP request into a format Django can understand.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fahvm0o9we7v6zq62onz9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fahvm0o9we7v6zq62onz9.png" alt="Request and Response cycle" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In Python, there is a format called &lt;strong&gt;WSGI&lt;/strong&gt; (Web Server Gateway Interface). &lt;strong&gt;WSGI&lt;/strong&gt; is used so that any web server(Gunicorn, uWSGI, or mod_wsgi) can speak to any Python web framework. Without &lt;strong&gt;Python's standard WSGI&lt;/strong&gt;, every web server would need a custom way of talking to every framework.&lt;/p&gt;

&lt;p&gt;Once the request is handed off by the server through WSGI, Django steps in. This is where your work as a Django developer really matters.&lt;/p&gt;

&lt;p&gt;Your job is to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tell Django which URLs it should listen for.&lt;/li&gt;
&lt;li&gt;Write the views that decide what happens when those URLs are requested.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From there, Django builds the response, whether that’s rendering an HTML template, returning JSON data, or redirecting the user somewhere else.&lt;/p&gt;

&lt;p&gt;We’ve followed the path of a request all the way from your browser to Django — through DNS, HTTP, the web server, and finally into Django itself. Along the way, we’ve seen how the internet acts like a massive postal system and how Django steps in to make sense of those requests and send back the right response.&lt;/p&gt;

&lt;p&gt;Now that you understand how the web and Django talk to each other, you’re ready to dive deeper into Django itself. In the next post, we’ll roll up our sleeves and explore how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;set up a Django project,&lt;/li&gt;
&lt;li&gt;declare URLs,&lt;/li&gt;
&lt;li&gt;group related URLs, and&lt;/li&gt;
&lt;li&gt;extract information from URLs to power your views.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of today’s post as the map, and the next one as the actual journey inside Django’s world.&lt;/p&gt;

</description>
      <category>python</category>
      <category>django</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Send Emails with Django: A Simple Guide</title>
      <dc:creator>kihuni</dc:creator>
      <pubDate>Sat, 02 Aug 2025 08:30:27 +0000</pubDate>
      <link>https://forem.com/kihuni/how-to-send-emails-with-django-a-simple-guide-2a2p</link>
      <guid>https://forem.com/kihuni/how-to-send-emails-with-django-a-simple-guide-2a2p</guid>
      <description>&lt;p&gt;Sending emails in Django is straightforward, thanks to its built-in email functionality. Whether you're notifying users, sending password resets, or automating communications, Django's &lt;strong&gt;send_mail&lt;/strong&gt; function makes it easy to do so. In this guide, we'll walk through the process of setting up and sending emails in Django, then demonstrate how to test it using the Django shell.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who Should Read This?
&lt;/h2&gt;

&lt;p&gt;Developers who are already familiar with Django. This is not for absolute beginners, as it assumes familiarity with Django project structure and basic configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure Email Settings in Django
&lt;/h3&gt;

&lt;p&gt;To send emails with Django, you need to have a local &lt;strong&gt;Simple Mail Transfer Protocol&lt;/strong&gt; (SMTP) server, or you need to access an external SMTP server, like your email service provider.&lt;/p&gt;

&lt;p&gt;The following settings allow you to define the SMTP configuration to send emails with Django:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EMAIL_HOST: The SMTP server host; the default is localhost
EMAIL_PORT: The SMTP port; the default is 25
EMAIL_HOST_USER: The username for the SMTP server
EMAIL_HOST_PASSWORD: The password for the SMTP server
EMAIL_USE_TLS: Whether to use a Transport Layer Security (TLS) secure connection
EMAIL_USE_SSL: Whether to use an implicit TLS secure connection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this blog, we will use &lt;strong&gt;Google's SMTP&lt;/strong&gt; server. If you have a Gmail account, edit the settings.py file of your project and add the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Email server configuration
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_HOST_USER = 'your_account@gmail.com'
EMAIL_HOST_PASSWORD = ''
EMAIL_PORT = 587
EMAIL_USE_TLS = True
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;a href="mailto:your_account@gmail.com"&gt;your_account@gmail.com&lt;/a&gt; with your actual Gmail account.&lt;/p&gt;

&lt;p&gt;N/b If you can’t use an SMTP server, you can tell Django to write emails to the console by adding the following settings to the settings.py file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Steps to Configure Gmail SMTP&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Gmail requires secure settings and an App Password for authentication. Google allows you to create app-specific passwords for your account. An app password is a 16-digit passcode that grants less secure apps or devices permission to access your Google account.&lt;/p&gt;

&lt;p&gt;Open &lt;a href="https://myaccount.google.com/" rel="noopener noreferrer"&gt;https://myaccount.google.com/&lt;/a&gt; in your browser. On the left menu, click on Security. You will See the following screen:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7yrzmy14jh48qh9om3c1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7yrzmy14jh48qh9om3c1.png" alt="Google account" width="800" height="682"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on the 2-Step Verification. When you click 2-step verification, you will see the following screen:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwdi6c1bikr8rpqc3frut.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwdi6c1bikr8rpqc3frut.png" alt="2-step password" width="800" height="670"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on 'app passwords,' enter a name for your app, and then click 'Create.'&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fztwo8voh1mgyy1gxf6qy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fztwo8voh1mgyy1gxf6qy.png" alt="goodle verifications" width="683" height="653"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A new password will be generated like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftzfjhexiflpfzwdt37f2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftzfjhexiflpfzwdt37f2.png" alt="google verification" width="776" height="808"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Copy the generated app password.&lt;/p&gt;

&lt;p&gt;Edit the settings.py file of your project and add the app password to the EMAIL_HOST_PASSWORD setting, as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Email server configuration
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_HOST_USER = 'your_account@gmail.com'
EMAIL_HOST_PASSWORD = 'xxxxxxxxxxxxxxxx'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the Python shell by running the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python manage.py shell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the following code in the python shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Python 3.12.3 (main, Jun 18 2025, 17:59:45) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
&amp;gt;&amp;gt;&amp;gt; from django.core.mail import send_mail
&amp;gt;&amp;gt;&amp;gt; send_mail('Django mail', 'Hey there, i hope this email finds you well', 'stephenkihuni55@gmail.com',
... ['stephenkihuni55@gmail.com'], fail_silently=False)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The send_mail() function takes the subject, message, sender, and list of recipients as required arguments. By setting the optional argument fail_silently=False, we are telling it to raise an exception if the email cannot be sent. If the output you see is 1, then your email was successfully sent.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa8e0tr1f50h3wapktmpz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa8e0tr1f50h3wapktmpz.png" alt="google verification" width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check your inbox. You should have received the email:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj0p75iun200sgxdtx7wn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj0p75iun200sgxdtx7wn.png" alt="google verification" width="683" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Congrats, you just sent your first email with Django!&lt;/p&gt;

&lt;p&gt;Thank you for reading all the way through, and if you enjoyed it, please leave a comment below. &lt;/p&gt;

</description>
      <category>python</category>
      <category>django</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>How to Build a Payment Gateway with Django and PayPal: A Step-by-Step Guide</title>
      <dc:creator>kihuni</dc:creator>
      <pubDate>Wed, 11 Jun 2025 03:12:03 +0000</pubDate>
      <link>https://forem.com/kihuni/how-to-build-a-payment-gateway-with-django-and-paypal-a-step-by-step-guide-2e20</link>
      <guid>https://forem.com/kihuni/how-to-build-a-payment-gateway-with-django-and-paypal-a-step-by-step-guide-2e20</guid>
      <description>&lt;p&gt;Hey there, today we’re diving into an exciting project: building a Payment Gateway API using Django and PayPal! A Payment Gateway API is a RESTful service that lets businesses accept online payments securely, and we’ll create one you can deploy and use. By the end, you’ll have a working system (like &lt;a href="https://payment-gateway-api-2c52.onrender.com/api/v1/payments/" rel="noopener noreferrer"&gt;Payment-gateway&lt;/a&gt;) to handle transactions with ease. Let’s get started!&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Build a Payment Gateway?
&lt;/h2&gt;

&lt;p&gt;A payment gateway is like a digital cashier for online stores, securely processing payments. Integrating PayPal with Django gives you a reliable, user-friendly solution without starting from scratch. This project uses minimal data (name, email, amount), skips authentication for simplicity, and includes automated tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You’ll Need
&lt;/h2&gt;

&lt;p&gt;Before we jump in, ensure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.12: Check with python --version.&lt;/li&gt;
&lt;li&gt;Django 5.0.6 and Django REST Framework 3.15.1: For the API backbone.&lt;/li&gt;
&lt;li&gt;A PayPal Developer Account: To get API credentials (see below).&lt;/li&gt;
&lt;li&gt;PostgreSQL 16 or above: For database management.&lt;/li&gt;
&lt;li&gt;Git: For version control.&lt;/li&gt;
&lt;li&gt;Basic knowledge of Python, Django, and REST APIs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Get PayPal Credentials
&lt;/h2&gt;

&lt;p&gt;To integrate PayPal, you need a Client ID and Secret. &lt;/p&gt;

&lt;p&gt;Here’s how to get them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sign Up or Log In:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to the &lt;a href="https://developer.paypal.com/home/" rel="noopener noreferrer"&gt;PayPal Developer Portal&lt;/a&gt; and log in with your PayPal account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0pn12ml6xeylzptnrqd7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0pn12ml6xeylzptnrqd7.png" alt="Paypal" width="800" height="204"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create an App:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click Log in to Dashboard &amp;gt; My Apps &amp;amp; Credentials.&lt;/li&gt;
&lt;li&gt;Under REST API apps, click Create App&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb6g33nzwcgytbhg7zhlh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb6g33nzwcgytbhg7zhlh.png" alt="dashboard" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give it a name (e.g., DjangoPaymentGateway) and select your sandbox account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Get Credentials:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;After creating the app, you’ll see a Client ID and a Secret under the app details.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Copy these values, they’ll go into your &lt;strong&gt;.env&lt;/strong&gt; file later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sandbox Mode:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For testing, we will use the sandbox environment. Switch to live mode in production after testing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Save Securely:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store them in a safe place (not hardcoded in your code!).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step-by-Step Guide
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1. Project Setup&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Start by setting up a new Django project.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a folder and cd into it
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir payment_gateway
cd payment_gateway
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Create a virtual environment and activate it
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python -m venv venv
source venv/bin/activate  # Linux/macOS
venv\Scripts\activate     # Windows
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Install dependencies:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install django==5.0.6 djangorestframework==3.15.1 paypalrestsdk==1.13.1 psycopg2-binary==2.9.9 gunicorn==22.0.0 whitenoise==6.7.0 python-dotenv==1.0.1

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Create a django project and an app
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;django-admin startproject payment_gateway .
python manage.py startapp payments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Configure Settings&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Configure the Django project’s environment, database, middleware, PostgreSQL, and PayPal&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from pathlib import Path
import os
from dotenv import load_dotenv
import paypalrestsdk


load_dotenv()


# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY')

# SECURITY WARNING: don't run with debug turned on in production!
#DEBUG = True # development
DEBUG = False

ALLOWED_HOSTS = [
    'payment-gateway-api-2c52.onrender.com',
    'localhost',  # For local testing
    '127.0.0.1',  # For local testing
]

PAYPAL_CLIENT_ID = os.getenv('PAYPAL_CLIENT_ID', '')
PAYPAL_CLIENT_SECRET = os.getenv('PAYPAL_CLIENT_SECRET', '')
PAYPAL_MODE = os.getenv('PAYPAL_MODE', 'sandbox')


paypalrestsdk.configure({
  "mode": PAYPAL_MODE,
  "client_id": PAYPAL_CLIENT_ID,
  "client_secret": PAYPAL_CLIENT_SECRET
})

# Application definition

INSTALLED_APPS = [

  "payments",
  "rest_framework",

]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ]
}

MIDDLEWARE = [

    'whitenoise.middleware.WhiteNoiseMiddleware',

]

ROOT_URLCONF = 'payment_gateway.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'payment_gateway.wsgi.application'


# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases

# DATABASES = {
#     'default': {
#         'ENGINE': 'django.db.backends.sqlite3',
#         'NAME': BASE_DIR / 'db.sqlite3',
#     }
# }

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.getenv('DB_NAME', 'payment_gateway'),
        'USER': os.getenv('DB_USER', 'payment_gateway'),
        'PASSWORD': os.getenv('DB_PASSWORD'),
        'HOST': os.getenv('DB_HOST', 'localhost'),
        'PORT': os.getenv('DB_PORT', '5432'),
    }
}

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.2/howto/static-files/

STATIC_URL = 'static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Create a .env file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DB_NAME=payment_gateway
DB_USER=payment_gateway
DB_PASSWORD=payment_gateway
DB_HOST=localhost
DB_PORT=5432
SECRET_KEY=your_secret_key
PAYPAL_CLIENT_ID=your_paypal_client_id
PAYPAL_CLIENT_SECRET=your_paypal_client_secret
PAYPAL_MODE=sandbox
DEBUG=True

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Set Up PostgreSQL&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Install PostgreSQL and create a Database:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;bash&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt install postgresql postgresql-contrib  # Ubuntu
psql -U postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;sql&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE DATABASE mydb;
CREATE USER myuser WITH PASSWORD 'mypassword';
GRANT ALL PRIVILEGES ON DATABASE mydb TO myuser;

# exit SQL shell
\q

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run migrations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python manage.py makemigrations
python manage.py migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2. Define the Data Model
&lt;/h2&gt;

&lt;p&gt;In payments/models.py, create a Payment model to store transaction details:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.db import models

class Payment(models.Model):
    PAYMENT_STATUS = [
        ('created', 'Created'),
        ('approved', 'Approved'),
        ('failed', 'Failed'),
        ('completed', 'Completed'),
    ]

    customer_name = models.CharField(max_length=100)
    customer_email = models.EmailField()
    amount = models.DecimalField(max_digits=10, decimal_places=2)
    paypal_payment_id = models.CharField(max_length=100, unique=True)
    status = models.CharField(max_length=20, choices=PAYMENT_STATUS, default='completed')
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"{self.customer_name} - {self.amount}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tracks payment details and statuses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3. Build the API
&lt;/h2&gt;

&lt;p&gt;Let’s create the endpoints to initiate, execute, cancel, and check payments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Serializers&lt;/strong&gt;&lt;br&gt;
In payments/serializers.py, define a serializer:&lt;/p&gt;
&lt;h1&gt;
  
  
  payments/serializers.py
&lt;/h1&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from rest_framework import serializers
from .models import Payment

class PaymentSerializer(serializers.ModelSerializer):
    class Meta:
        model = Payment
        fields = '__all__'
        read_only_fields = ['status', 'paypal_payment_id', 'created_at']

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Views&lt;/strong&gt;&lt;br&gt;
In payments/views.py, implement the API logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Payment
from .serializers import PaymentSerializer
import paypalrestsdk
import logging

logger = logging.getLogger(__name__)

class PaymentCreateView(APIView):
    def post(self, request):
        try:
            serializer = PaymentSerializer(data=request.data)
            if not serializer.is_valid():
                return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

            data = serializer.validated_data

            payment = paypalrestsdk.Payment({
                "intent": "sale",
                "payer": {"payment_method": "paypal"},
                "redirect_urls": {
                    "return_url": "https://payment-gateway-api-2c52.onrender.com/api/v1/payment/execute",
                    "cancel_url": "https://payment-gateway-api-2c52.onrender.com/api/v1/payment/cancel"
                },
                "transactions": [{
                    "item_list": {
                        "items": [{
                            "name": "Business Payment",
                            "sku": "001",
                            "price": str(data['amount']),
                            "currency": "USD",
                            "quantity": 1
                        }]
                    },
                    "amount": {
                        "total": str(data['amount']),
                        "currency": "USD"
                    },
                    "description": f"Payment by {data['customer_name']}"
                }]
            })

            if payment.create():
                new_payment = Payment.objects.create(
                    customer_name=data['customer_name'],
                    customer_email=data['customer_email'],
                    amount=data['amount'],
                    paypal_payment_id=payment.id,
                    status='created'
                )
                return Response({
                    "status": "success",
                    "payment_id": new_payment.id,
                    "paypal_payment_id": payment.id,
                    "approval_url": next(link.href for link in payment.links if link.rel == "approval_url")
                }, status=status.HTTP_201_CREATED)

            logger.error(f"PayPal payment creation failed: {payment.error}")
            return Response({"status": "error", "message": str(payment.error)}, status=status.HTTP_400_BAD_REQUEST)
        except Exception as e:
            logger.error(f"Error in PaymentCreateView: {str(e)}", exc_info=True)
            return Response({
                "status": "error",
                "message": f"An unexpected error occurred: {str(e)}"
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

class PaymentStatusView(APIView):
    def get(self, request, pk):
        try:
            payment = Payment.objects.get(id=pk)
            serializer = PaymentSerializer(payment)
            return Response({
                "payment": serializer.data,
                "status": "success",
                "message": "Payment details retrieved successfully."
            }, status=status.HTTP_200_OK)
        except Payment.DoesNotExist:
            return Response({
                "status": "error",
                "message": "Payment not found."
            }, status=status.HTTP_404_NOT_FOUND)
        except Exception as e:
            logger.error(f"Error in PaymentStatusView for pk={pk}: {str(e)}", exc_info=True)
            return Response({
                "status": "error",
                "message": "An unexpected error occurred."
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

class PaymentExecuteView(APIView):
    def get(self, request):
        try:
            payment_id = request.GET.get('paymentId')
            payer_id = request.GET.get('PayerID')
            if not payment_id or not payer_id:
                return Response({
                    "status": "error",
                    "message": "Missing paymentId or PayerID. Please ensure the payment is approved via PayPal."
                }, status=status.HTTP_400_BAD_REQUEST)

            payment = paypalrestsdk.Payment.find(payment_id)
            if payment.execute({"payer_id": payer_id}):
                db_payment = Payment.objects.get(paypal_payment_id=payment_id)
                db_payment.status = 'completed'
                db_payment.save()
                return Response({
                    "status": "success",
                    "message": "Payment executed successfully",
                    "payment_id": db_payment.id
                }, status=status.HTTP_200_OK)
            else:
                error = payment.error
                logger.error(f"Payment execution failed: {error}")
                if error.get('name') == 'PAYMENT_NOT_APPROVED_FOR_EXECUTION':
                    return Response({
                        "status": "error",
                        "message": "Payment not approved by payer. Please complete approval on PayPal."
                    }, status=status.HTTP_400_BAD_REQUEST)
                return Response({
                    "status": "error",
                    "message": str(error)
                }, status=status.HTTP_400_BAD_REQUEST)
        except paypalrestsdk.ResourceNotFound:
            return Response({
                "status": "error",
                "message": "Payment not found"
            }, status=status.HTTP_404_NOT_FOUND)
        except Payment.DoesNotExist:
            return Response({
                "status": "error",
                "message": "Payment not found"
            }, status=status.HTTP_404_NOT_FOUND)
        except Exception as e:
            logger.error(f"Error in PaymentExecuteView: {str(e)}", exc_info=True)
            return Response({
                "status": "error",
                "message": f"An unexpected error occurred: {str(e)}"
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

class PaymentCancelView(APIView):
    def get(self, request):
        try:
            payment_id = request.GET.get('paymentId')
            if not payment_id:
                return Response({
                    "status": "error",
                    "message": "Missing paymentId"
                }, status=status.HTTP_400_BAD_REQUEST)

            db_payment = Payment.objects.get(paypal_payment_id=payment_id)
            db_payment.status = 'cancelled'
            db_payment.save()
            return Response({
                "status": "success",
                "message": "Payment cancelled successfully",
                "payment_id": db_payment.id
            }, status=status.HTTP_200_OK)
        except Payment.DoesNotExist:
            return Response({
                "status": "error",
                "message": "Payment not found"
            }, status=status.HTTP_404_NOT_FOUND)
        except Exception as e:
            logger.error(f"Error in PaymentCancelView: {str(e)}", exc_info=True)
            return Response({
                "status": "error",
                "message": f"An unexpected error occurred: {str(e)}"
            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;URLs&lt;/strong&gt;&lt;br&gt;
In payments/urls.py:&lt;/p&gt;
&lt;h1&gt;
  
  
  payments/urls.py
&lt;/h1&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.urls import path
from .views import PaymentCreateView, PaymentStatusView, PaymentExecuteView, PaymentCancelView

urlpatterns = [
    path('payments/', PaymentCreateView.as_view(), name='initiate-payment'),
    path('payments/&amp;lt;int:pk&amp;gt;/', PaymentStatusView.as_view(), name='payment-status'),
    path('payment/execute/', PaymentExecuteView.as_view(), name='payment-execute'),
    path('payment/cancel/', PaymentCancelView.as_view(), name='payment-cancel'),
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;In payment_gateway/urls.py:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include('payments.urls')),
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc5253v62s5m1wj4fwcd8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc5253v62s5m1wj4fwcd8.png" alt="flowchart" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 4. Test Your API&lt;/p&gt;

&lt;p&gt;Create tests in payments/tests.py to ensure everything works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from unittest.mock import patch, MagicMock
import paypalrestsdk

class PaymentTests(APITestCase):
    def setUp(self):
        # Mock PayPal payment
        self.mock_payment = MagicMock()
        self.mock_payment.id = "PAY-123456"
        self.mock_payment.create = lambda: True
        # Mock links as a list of MagicMock objects with rel and href attributes
        link = MagicMock()
        link.rel = "approval_url"
        link.href = "https://paypal.com/approve"
        self.mock_payment.links = [link]
        self.mock_payment.execute = lambda data: True

    @patch('paypalrestsdk.Payment')
    def test_get_payment_status_success(self, MockPayment):
        MockPayment.return_value = self.mock_payment
        create_url = reverse('initiate-payment')
        data = {
            "customer_name": "John Smith",
            "customer_email": "john@example.com",
            "amount": 30.00
        }
        create_response = self.client.post(create_url, data, format='json')
        payment_id = create_response.data.get("payment_id")
        self.assertEqual(create_response.status_code, status.HTTP_201_CREATED)

        status_url = reverse('payment-status', args=[payment_id])
        response = self.client.get(status_url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.data["payment"]["customer_name"], "John Smith")

    def test_create_payment_missing_fields(self):
        url = reverse('initiate-payment')
        data = {
            "customer_email": "incomplete@example.com"
        }
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

    def test_create_payment_invalid_amount(self):
        url = reverse('initiate-payment')
        data = {
            "customer_name": "Test User",
            "customer_email": "test@example.com",
            "amount": "invalid"
        }
        response = self.client.post(url, data, format='json')
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

    @patch('paypalrestsdk.Payment')
    def test_payment_execute_success(self, MockPayment):
        MockPayment.return_value = self.mock_payment
        MockPayment.find.return_value = self.mock_payment
        create_url = reverse('initiate-payment')
        data = {
            "customer_name": "John Smith",
            "customer_email": "john@example.com",
            "amount": 30.00
        }
        create_response = self.client.post(create_url, data, format='json')
        payment_id = create_response.data.get("payment_id")
        self.assertEqual(create_response.status_code, status.HTTP_201_CREATED)

        execute_url = reverse('payment-execute') + f'?paymentId=PAY-123456&amp;amp;PayerID=123'
        response = self.client.get(execute_url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.data["status"], "success")

    @patch('paypalrestsdk.Payment')
    def test_payment_execute_not_found(self, MockPayment):
        # Mock ResourceNotFound with a mock response object
        mock_response = MagicMock()
        mock_response.status_code = 404
        MockPayment.find.side_effect = paypalrestsdk.ResourceNotFound(response=mock_response)
        execute_url = reverse('payment-execute') + f'?paymentId=INVALID&amp;amp;PayerID=123'
        response = self.client.get(execute_url)
        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
        self.assertEqual(response.data["status"], "error")

    @patch('paypalrestsdk.Payment')
    def test_payment_cancel_success(self, MockPayment):
        MockPayment.return_value = self.mock_payment
        create_url = reverse('initiate-payment')
        data = {
            "customer_name": "John Smith",
            "customer_email": "john@example.com",
            "amount": 30.00
        }
        create_response = self.client.post(create_url, data, format='json')
        payment_id = create_response.data.get("payment_id")
        self.assertEqual(create_response.status_code, status.HTTP_201_CREATED)

        cancel_url = reverse('payment-cancel') + f'?paymentId=PAY-123456'
        response = self.client.get(cancel_url)
        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.data["status"], "success")

    def test_payment_cancel_not_found(self):
        cancel_url = reverse('payment-cancel') + f'?paymentId=INVALID'
        response = self.client.get(cancel_url)
        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
        self.assertEqual(response.data["status"], "error") 
    @patch('paypalrestsdk.Payment')
    def test_payment_execute_missing_payer_id(self, MockPayment):
        mock_payment = MagicMock()
        mock_payment.id = "PAY-123456"
        MockPayment.find.return_value = mock_payment
        execute_url = reverse('payment-execute') + '?paymentId=PAY-123456'
        response = self.client.get(execute_url)
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
        self.assertEqual(response.data["message"], "Missing paymentId or PayerID. Please ensure the payment is approved via PayPal.")


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python manage.py test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6drhrx9l5b2257dp00gh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6drhrx9l5b2257dp00gh.png" alt="testing api" width="691" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5. Deploy with CI/CD
&lt;/h2&gt;

&lt;p&gt;Set up GitHub Actions in .github/workflows/django.yml:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Django CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:17.2
        env:
          POSTGRES_DB: ${{ secrets. DB_NAME}}
          POSTGRES_USER: ${{ secrets.DB_USER }}
          POSTGRES_PASSWORD:  ${{ secrets.DB_PASSWORD }}
        ports:
          - 5432:5432
        options: &amp;gt;-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    env:
      SECRET_KEY: ${{ secrets.SECRET_KEY }}
      PAYPAL_CLIENT_ID: ${{ secrets.PAYPAL_CLIENT_ID }}
      PAYPAL_CLIENT_SECRET: ${{ secrets.PAYPAL_CLIENT_SECRET }}
      DB_NAME: ${{ secrets. DB_NAME}}
      DB_USER: ${{ secrets.DB_USER }}
      DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
      DB_HOST: ${{ secrets.DB_HOST }}
      DB_PORT: 5432

    steps:
    - uses: actions/checkout@v2

    - name: Set up Python '3.10'
      uses: actions/setup-python@v2
      with:
        python-version: '3.10'

    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt

    - name: Wait for PostgreSQL
      run: |
        until pg_isready -h localhost -p 5432; do sleep 1; done

    - name: Run migrations
      run: |
        python manage.py migrate

    - name: Collect static files
      run: |
        python manage.py collectstatic --noinput

    - name: Run tests
      run: |
        python manage.py test

    - name: Deploy to Render
      if: github.ref == 'refs/heads/main' &amp;amp;&amp;amp; success()
      run: |
        curl -X POST ${{ secrets.RENDER_DEPLOY_HOOK }}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Deploy to Render:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Push to GitHub.&lt;/li&gt;
&lt;li&gt;Connect to Render, set environment variables (matching .env), and use:&lt;/li&gt;
&lt;li&gt;Build Command: pip install -r requirements.txt &amp;amp;&amp;amp; python manage.py migrate&lt;/li&gt;
&lt;li&gt;Start Command: gunicorn payment_gateway.wsgi:application&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Common Pitfalls &amp;amp; Fixes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PayPal Error (PAYMENT_NOT_APPROVED_FOR_EXECUTION): I hit this when testing manually without approving the payment. Ensure the user follows the &lt;code&gt;approval_URL&lt;/code&gt; and includes the &lt;code&gt;Payer_ID&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Test Failures: Fixed by mocking PayPal responses correctly. Check tests.py for details.&lt;/li&gt;
&lt;li&gt;Database Issues: Use PostgreSQL to avoid local vs. production mismatches.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Test It Out!
&lt;/h2&gt;

&lt;p&gt;Try it live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "customer_name": "John Doe",
  "customer_email": "john@example.com",
  "amount": 50.00
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow the approval_url, approve or cancel, and check the status with:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Facxduov3vnhekvdrjzm6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Facxduov3vnhekvdrjzm6.png" alt=" " width="800" height="678"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://payment-gateway-api-2c52.onrender.com/api/v1/payments/1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwnce1fs3tm1srt2dalb6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwnce1fs3tm1srt2dalb6.png" alt=" " width="800" height="795"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You’ve now built a Payment Gateway API with Django and PayPal!. Experiment by adding a frontend or more features,&lt;br&gt;
like refunds. Happy coding!&lt;/p&gt;

&lt;p&gt;GitHub repo:&lt;br&gt;
&lt;a href="https://github.com/kihuni/Payment-Gateway-API" rel="noopener noreferrer"&gt;Payment-Gateway&lt;/a&gt;&lt;br&gt;
Questions? Drop a comment below!&lt;/p&gt;

</description>
      <category>python</category>
      <category>django</category>
      <category>paypal</category>
      <category>webdev</category>
    </item>
    <item>
      <title>`Lets-Collab`: Building a Secure Collaboration Platform with Django, React, and Permit.io</title>
      <dc:creator>kihuni</dc:creator>
      <pubDate>Sun, 04 May 2025 07:38:41 +0000</pubDate>
      <link>https://forem.com/kihuni/hackathon-diaries-building-lets-collab-with-django-react-and-permitio-2m21</link>
      <guid>https://forem.com/kihuni/hackathon-diaries-building-lets-collab-with-django-react-and-permitio-2m21</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/permit_io"&gt;Permit.io Authorization Challenge&lt;/a&gt;: API-First Authorization Reimagined&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Lets-Collab&lt;/code&gt; is a full-stack web application that enables users to collaborate on projects and tasks while enforcing strict access control policies. The app supports two main user roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Admins (e.g., admin users) can access all resources, including creating projects, managing tasks, and viewing audit logs to track user actions.&lt;/li&gt;
&lt;li&gt;Members (e.g., newuser): Can list projects, create tasks within those projects, but are restricted from accessing audit logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyffzgxdsxwxp3ayuhgyg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyffzgxdsxwxp3ayuhgyg.png" alt="members" width="712" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s a breakdown of the key features:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project and Task Management:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users can create and list projects and tasks via a clean React frontend.&lt;/li&gt;
&lt;li&gt;The backend, built with Django and Django REST Framework, exposes APIs (/api/projects/, /api/tasks/, /api/audit_logs/) for managing these resources.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Authorization with Permit.io:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Integrated Permit.io for externalized authorization.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Configured policies in Permit.io to enforce declarative rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Admins have full access to all resources (*) for all actions (read, create, update, delete).&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Members can list projects (read on projects), create tasks (create on tasks), but are explicitly denied access to audit logs (audit_logs).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Used Permit.check() to dynamically evaluate permissions for each API request, logging allowed actions to an AuditLog model.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2o6uh89vuoc9v65z49mz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2o6uh89vuoc9v65z49mz.png" alt="user permission" width="586" height="1095"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit Logging:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Implemented audit logs to track user actions (e.g., project_read, task_create) for transparency and accountability.&lt;/li&gt;
&lt;li&gt;Only admins can view audit logs, ensuring sensitive data is protected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Frontend with React:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Built a responsive UI with React, featuring a Dashboard for managing projects and tasks, and an Access Control Dashboard for admins to view audit logs.&lt;/li&gt;
&lt;li&gt;Added error handling to gracefully manage permission denials (e.g., “Audit logs are restricted to admins only” for non-admins).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Here’s a quick demo of CollabSphere in action:&lt;br&gt;
&lt;em&gt;&lt;strong&gt;Member Experience (newuser):&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Log in as newuser (username: newuser, password: 2025DEVChallenge).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6awpne97i3vyganucklu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6awpne97i3vyganucklu.png" alt="login" width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Navigate to the Dashboard (/):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;List projects: Successfully displays all projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2kzxl24o8ruz0bhgm7oc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2kzxl24o8ruz0bhgm7oc.png" alt="list projects" width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a task: Successfully adds a task to a selected project.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fedyyawg4hrmdj09e0qbo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fedyyawg4hrmdj09e0qbo.png" alt="create" width="800" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Attempt to create a project: Denied (403 Forbidden), as members cannot create projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi79yijmgn0bfhqb6axs7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi79yijmgn0bfhqb6axs7.png" alt="attempt failed" width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Navigate to the Access Control Dashboard (/access-control):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Denied access to audit logs with a message: “Audit logs are restricted to admins only.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvdsu12y5gotebq4clx4t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvdsu12y5gotebq4clx4t.png" alt="audit failed" width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Admin Experience (admin):&lt;/strong&gt;&lt;/em&gt;&lt;br&gt;
Log in as admin (username: admin, password: 2025DEVChallenge).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmgmug0bpq8nlhx1kyed8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmgmug0bpq8nlhx1kyed8.png" alt="admin" width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Navigate to the Dashboard (/):&lt;/strong&gt;&lt;br&gt;
List projects and tasks: Successfully displays all data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnj2ceyfk94dy25tsmxci.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnj2ceyfk94dy25tsmxci.png" alt="list" width="800" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Create a project and task: Successfully adds new resources.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiwxmntw80fa3mokze13p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiwxmntw80fa3mokze13p.png" alt="added new resource" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Navigate to the Access Control Dashboard (/access-control):&lt;br&gt;
View audit logs: Successfully displays logs of all user actions (e.g., “User admin performed project_read on projects at [timestamp]”).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu9i8hrmbm8hdxhj7gttd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu9i8hrmbm8hdxhj7gttd.png" alt="audit_logs" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can try the live demo here:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Deployed URL: &lt;a href="https://letscollab-eight.vercel.app/" rel="noopener noreferrer"&gt;https://letscollab-eight.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Test Credentials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Admin: admin / 2025DEVChallenge&lt;/li&gt;
&lt;li&gt;Member: newuser / 2025DEVChallenge&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Project Repo
&lt;/h2&gt;

&lt;p&gt;The source code for Let's-Collab is available on GitHub, split into two repositories:&lt;/p&gt;

&lt;p&gt;Backend: &lt;a href="https://github.com/kihuni/Lets-Collab-API" rel="noopener noreferrer"&gt;https://github.com/kihuni/Lets-Collab-API&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Contains the Django backend, including the PermitPermission class, API endpoints, and audit logging logic.&lt;/p&gt;

&lt;p&gt;Frontend: &lt;a href="https://github.com/kihuni/Lets-Collab-frontend" rel="noopener noreferrer"&gt;https://github.com/kihuni/Lets-Collab-frontend&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Contains the React frontend with the Dashboard and Access Control Dashboard components.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Journey
&lt;/h2&gt;

&lt;p&gt;Building &lt;code&gt;Lets-Collab&lt;/code&gt; for the Permit.io Authorization Challenge was a rollercoaster of learning and problem-solving. Here’s a glimpse into my journey:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Starting Point:&lt;/strong&gt; I began with a basic Django-React app for managing projects and tasks, using a mocked authorization system. The challenge required integrating Permit.io for API-first authorization, which was new to me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning Permit.io:&lt;/strong&gt; I dove into Permit.io’s documentation to understand its Policy Decision Point (PDP) and how to define roles, resources, and policies. Setting up the initial policies (e.g., admin with full access, member with restricted access) was straightforward, but integrating them into Django posed challenges.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Challenge 1:&lt;/strong&gt; Async Issues: The Permit.io SDK’s Permit.check() method is asynchronous, but Django is synchronous by default. I encountered a RuntimeWarning: coroutine 'Permit.check' was never awaited error. I fixed this by using asyncio.run() for development, learning that async_to_sync would be better for production with an async server like Uvicorn.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Challenge 2&lt;/strong&gt;: Policy Misconfiguration: Initially, Permit.check() returned False for all requests, denying access to both admin and newuser. This was due to a mismatch between resource names (projects in the code vs. project in Permit.io) and an incorrect API key. I debugged this by adding detailed logging, aligning resource names, and verifying the API key in Permit.io’s dashboard.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Challenge 3&lt;/strong&gt;: Frontend Linting: My React frontend had ESLint no-unused-vars warnings for unused err variables in catch blocks. I fixed this by using &lt;code&gt;err.message&lt;/code&gt; to provide specific error messages, improving user experience and code quality.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Final Touches:&lt;/strong&gt; I added input validation in the frontend, visual indicators for expired tasks, and ensured the app was deployable on Render and Vercel. Testing with both admin and newuser confirmed that access control worked as expected.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This journey taught me the importance of externalized authorization, the nuances of integrating async libraries with synchronous frameworks, and the value of clean code practices. Despite the challenges, seeing CollabSphere come together with secure, scalable authorization was incredibly rewarding.&lt;/p&gt;

&lt;h2&gt;
  
  
  API-First Authorization
&lt;/h2&gt;

&lt;p&gt;The Permit.io Authorization Challenge emphasized API-first authorization, a paradigm where authorization logic is decoupled from the application and managed externally via APIs. &lt;code&gt;Lets-Collab&lt;/code&gt; embraces this approach by leveraging &lt;code&gt;Permit.io&lt;/code&gt; to enforce access control, aligning with the challenge’s goal of reimagining authorization in an API-first world.&lt;/p&gt;

&lt;p&gt;Here’s how &lt;code&gt;Lets-Collab&lt;/code&gt; implements API-first authorization:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Externalized Authorization with &lt;code&gt;Permit.io&lt;/code&gt;:&lt;/strong&gt; Instead of hardcoding access control logic in the Django backend, I offloaded it to &lt;code&gt;Permit.io&lt;/code&gt;. The &lt;code&gt;PermitPermission&lt;/code&gt; class calls &lt;code&gt;Permit.check()&lt;/code&gt; for each API request, querying Permit.io’s PDP (&lt;a href="https://cloudpdp.api.permit.io" rel="noopener noreferrer"&gt;https://cloudpdp.api.permit.io&lt;/a&gt;) to determine if the action is allowed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Declarative Policies:&lt;/strong&gt; I defined policies in &lt;code&gt;Permit.io’s&lt;/code&gt; dashboard using a declarative model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Roles: admin, member.&lt;/li&gt;
&lt;li&gt;Resources: projects, tasks, audit_logs.&lt;/li&gt;
&lt;li&gt;Actions: read, create, update, delete.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Policies:&lt;/strong&gt; admin gets full access (*), while member is restricted to read on projects and create on tasks, with an explicit deny on audit_logs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scalability and Flexibility:&lt;/strong&gt; By using Permit.io, I can update access control policies without changing the application code. For example, adding a new role (e.g., guest) or modifying permissions can be done via Permit.io’s dashboard, making the system scalable and adaptable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditability:&lt;/strong&gt; The app logs allowed actions to the AuditLog model, providing a clear audit trail of user activities, which is crucial for security and compliance in an API-first architecture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;API-first authorization with Permit.io allowed me to focus on building &lt;code&gt;Lets-Collab&lt;/code&gt; core features while ensuring robust, maintainable access control. It also highlighted the power of decoupling authorization logic, enabling faster iteration and better security practices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Lets-Collab demonstrates how API-first authorization can enhance a collaborative platform, ensuring secure and role-based access control with Permit.io. I’m proud of the result and grateful for the learning experience this challenge provided. I hope you enjoy exploring Let's-Collab as much as I enjoyed building it!&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>permitchallenge</category>
      <category>webdev</category>
      <category>security</category>
    </item>
    <item>
      <title>Build Your First Blog with Django: Part 1 - Setting Up Your Development Environment</title>
      <dc:creator>kihuni</dc:creator>
      <pubDate>Thu, 01 May 2025 06:19:12 +0000</pubDate>
      <link>https://forem.com/kihuni/build-your-first-blog-with-django-part-1-setting-up-your-development-environment-4igc</link>
      <guid>https://forem.com/kihuni/build-your-first-blog-with-django-part-1-setting-up-your-development-environment-4igc</guid>
      <description>&lt;p&gt;Ever dreamed of creating your website but felt overwhelmed by the techy details? Say hello to Django, the Python-powered web framework that makes building web apps, like a fully functional blog, surprisingly simple!&lt;/p&gt;

&lt;p&gt;In this &lt;strong&gt;mini-series&lt;/strong&gt;, we’ll guide you step by step to craft a blog app from scratch, even if you’re new to web development. You’ll learn how to set up Django, create blog posts, and display them on a sleek webpage, all while mastering concepts like models, views, and templates.&lt;/p&gt;

&lt;p&gt;By the end, you’ll have a working blog and the skills to build your web projects. Ready to dive into the world of Django and create something awesome? Let’s code!&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Part 1&lt;/strong&gt;, we’ll focus on setting up your development environment and testing it to ensure everything works. This foundation is crucial for building our blog app in the next posts. Let’s make sure you’re ready to roll!&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;What is Django?&lt;/li&gt;
&lt;li&gt;Why Use Django for a Blog App?&lt;/li&gt;
&lt;li&gt;
Setting Up the Development Environment

&lt;ul&gt;
&lt;li&gt;Step 1: Install Python&lt;/li&gt;
&lt;li&gt;Step 2: Create a Virtual Environment&lt;/li&gt;
&lt;li&gt;Step 3: Install Django&lt;/li&gt;
&lt;li&gt;Step 4: Create a Django Project&lt;/li&gt;
&lt;li&gt;Step 5: Run the Development Server&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;What’s Next?&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;This series is designed for beginners, so we’ll keep things simple. To follow along, you’ll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A computer&lt;/strong&gt; with Windows, macOS, or Linux.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python 3.8 or higher&lt;/strong&gt; installed (we’ll guide you through checking and installing it).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A text editor&lt;/strong&gt; like VS Code, PyCharm, or even Notepad (VS Code is recommended for its Python support).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Basic command-line knowledge&lt;/strong&gt; (e.g., how to open a terminal and run commands). Don’t worry if you’re new—we’ll explain each step!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An internet connection&lt;/strong&gt; to download Python and Django.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No prior Django or web development experience is required. If you know some Python (like variables or functions), that’s a bonus, but we’ll explain everything as we go.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Django?
&lt;/h2&gt;

&lt;p&gt;Django is a high-level web framework written in Python that makes building web applications fast, secure, and scalable. Think of it as a toolbox packed with pre-built components, user authentication, database management, and templates that save you time and effort. Whether you’re creating a blog, an e-commerce site, or a social platform, Django streamlines development with a clear structure and robust tools.&lt;/p&gt;

&lt;p&gt;Django follows the &lt;strong&gt;Model-View-Template (MVT)&lt;/strong&gt; pattern, organizing your code into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Models&lt;/strong&gt;: Define your data (e.g., blog posts).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Views&lt;/strong&gt;: Handle the logic (e.g., fetching posts).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Templates&lt;/strong&gt;: Create the visuals (e.g., HTML pages).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It includes an &lt;strong&gt;Object-Relational Mapper (ORM)&lt;/strong&gt; to interact with databases using Python code, a built-in &lt;strong&gt;admin panel&lt;/strong&gt; for managing content, and strong security features to protect your app. For our blog app, Django will help us quickly set up a system to create, display, and manage posts. It’s beginner-friendly, powers major sites like Instagram and Pinterest, and has a vibrant community with excellent &lt;a href="https://docs.djangoproject.com" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Use Django for a Blog App?
&lt;/h2&gt;

&lt;p&gt;A blog app needs common features: posts, comments, user accounts, and an admin interface. Django is perfect because it provides these components out of the box, letting you focus on customizing your app rather than building from scratch. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Django’s &lt;strong&gt;ORM&lt;/strong&gt; simplifies database tasks, like storing blog posts.&lt;/li&gt;
&lt;li&gt;Its &lt;strong&gt;admin panel&lt;/strong&gt; lets you manage posts without extra coding.&lt;/li&gt;
&lt;li&gt;Built-in &lt;strong&gt;security&lt;/strong&gt; protects against threats like SQL injection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Django’s &lt;strong&gt;rapid development&lt;/strong&gt; approach means you can prototype your blog quickly. Its &lt;strong&gt;scalability&lt;/strong&gt; ensures it can grow with your audience, and the &lt;strong&gt;Python syntax&lt;/strong&gt; is approachable for beginners. In this mini-series, we’ll use Django to build a blog app while learning key web development concepts. Part 1 sets the stage by getting your environment ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up the Development Environment
&lt;/h2&gt;

&lt;p&gt;Let’s prepare your computer to run Django. This involves installing Python, creating a virtual environment, installing Django, setting up a project, and running a test server. We’ll break it down into clear steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Install Python
&lt;/h3&gt;

&lt;p&gt;Django requires &lt;strong&gt;Python 3.8 or higher&lt;/strong&gt;. To check if Python is installed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open a terminal:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Windows: Press &lt;code&gt;Win + R&lt;/code&gt;, type &lt;code&gt;cmd&lt;/code&gt;, and press Enter.&lt;/li&gt;
&lt;li&gt;macOS: Search for “Terminal” in Spotlight.&lt;/li&gt;
&lt;li&gt;Linux: Open your terminal app.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Run:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python --version

or

python3 --version

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb6dhc1vsfw6u4qufb4s0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb6dhc1vsfw6u4qufb4s0.png" alt="python version" width="800" height="381"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you see a version like &lt;code&gt;Python 3.8.0&lt;/code&gt; or higher, you’re good. If not, download Python from &lt;a href="https://www.python.org/" rel="noopener noreferrer"&gt;python.org&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choose the latest version (e.g., 3.11).&lt;/li&gt;
&lt;li&gt;Follow the installer, ensuring you check “Add Python to PATH” on Windows.&lt;/li&gt;
&lt;li&gt;Verify installation by running &lt;code&gt;python --version&lt;/code&gt; again.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Create a Virtual Environment
&lt;/h3&gt;

&lt;p&gt;A virtual environment is like a sandbox for your project’s dependencies, keeping them separate from other projects. This prevents conflicts (e.g., different projects needing different Django versions).&lt;/p&gt;

&lt;p&gt;Create a folder for your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir my_blog
cd my_blog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwfkkz9hq4vymoeq9s9wg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwfkkz9hq4vymoeq9s9wg.png" alt="create folder" width="606" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Create a virtual environment named venv:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python -m venv venv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Activate it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Windows:

venv\Scripts\activate

macOS/Linux:


source venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your terminal prompt should change (e.g., (venv) appears), indicating the virtual environment is active.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdl9nyfjir57hecbip5rq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdl9nyfjir57hecbip5rq.png" alt="virtual env" width="606" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Install Django
&lt;/h3&gt;

&lt;p&gt;With the virtual environment active, install Django using pip (Python’s package manager):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install django
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;django-admin --version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see a version like 4.2.7 (or newer). If not, ensure the virtual environment is active and try again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvmkpw9vgjqe9lncs9oqg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvmkpw9vgjqe9lncs9oqg.png" alt="django installations" width="800" height="568"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Create a Django Project
&lt;/h3&gt;

&lt;p&gt;A Django project is the container for your web app, holding settings and apps. Create one named blog_project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;django-admin startproject blog_project
cd blog_project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh31zusmtc5o49qprcw0p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh31zusmtc5o49qprcw0p.png" alt="blog_project" width="800" height="568"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;blog_project/
├── manage.py
└── blog_project/
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    ├── asgi.py
    └── wsgi.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;manage.py: A script for running Django commands.&lt;/li&gt;
&lt;li&gt;settings.py: Configures your project (e.g., apps, database).&lt;/li&gt;
&lt;li&gt;urls.py: Maps URLs to your app’s pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 5: Run the Development Server
&lt;/h3&gt;

&lt;p&gt;Django includes a lightweight server for testing. Start it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python manage.py runserver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6npchbrfe63zjcuc8f5c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6npchbrfe63zjcuc8f5c.png" alt="server running" width="800" height="568"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open your browser and visit &lt;a href="http://127.0.0.1:8000" rel="noopener noreferrer"&gt;http://127.0.0.1:8000&lt;/a&gt;. You should see Django’s welcome page, a green rocket with “The install worked successfully! Congratulations!” This confirms your setup is working.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx3p8wc3maotspo1eyc58.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx3p8wc3maotspo1eyc58.png" alt="Congratulations" width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you see an error (e.g., “port in use”), stop the server with Ctrl + C and try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python manage.py runserver 8001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then visit &lt;a href="http://127.0.0.1:8001" rel="noopener noreferrer"&gt;http://127.0.0.1:8001&lt;/a&gt;. Check Django’s documentation for troubleshooting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What’s Next?
&lt;/h2&gt;

&lt;p&gt;You’ve set up Django and confirmed it works—great job! In Part 2, we’ll create the blog app, define a Post model to store blog posts, and apply database migrations. Future posts will cover displaying posts, setting up the admin panel, and more.&lt;/p&gt;

&lt;p&gt;For now, explore your project folder or try re-running the server. Share your progress or questions in the comments below, and check Django’s documentation for extra help. Stay tuned for the next part of our mini-series!&lt;/p&gt;

</description>
      <category>django</category>
      <category>python</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building a Secure JWT Authentication System with Django</title>
      <dc:creator>kihuni</dc:creator>
      <pubDate>Wed, 30 Apr 2025 06:42:33 +0000</pubDate>
      <link>https://forem.com/kihuni/building-a-secure-jwt-authentication-system-with-django-558h</link>
      <guid>https://forem.com/kihuni/building-a-secure-jwt-authentication-system-with-django-558h</guid>
      <description>&lt;p&gt;I recently implemented a custom authentication system for my Django project, Shopease-API, using JSON Web Tokens (JWT).&lt;/p&gt;

&lt;p&gt;The system includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Email verification at signup to ensure that email addresses are unique and valid.&lt;/li&gt;
&lt;li&gt;Secure login with access and refresh tokens for seamless user sessions.&lt;/li&gt;
&lt;li&gt;Token-based authentication using &lt;code&gt;rest_framework_simplejwt&lt;/code&gt; for robust security.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this post, I’ll walk you through the step-by-step process of building this system, from setting up the custom user model to creating the authentication endpoints. Whether you’re a developer diving into the Django REST Framework, this journey offers valuable insights and practical tips!&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Choose JWT for Shopease-API?
&lt;/h2&gt;

&lt;p&gt;Shopease-API is an e-commerce platform I’m building using Django, designed to handle user authentication, product listings, carts, and orders. I opted for JWT for its stateless nature, scalability, and compatibility with modern APIs. My specific needs included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Email-based authentication: Users sign in with their email and password, eliminating the need for usernames.&lt;/li&gt;
&lt;li&gt;Email verification: This enforces unique email addresses and validates user input.&lt;/li&gt;
&lt;li&gt;Secure token management: Access tokens are used for short-term authentication, while refresh tokens allow for session renewal.&lt;/li&gt;
&lt;li&gt;Logout with token blacklisting: This feature prevents the reuse of tokens.&lt;/li&gt;
&lt;li&gt;Auto-login after signup: To ensure a frictionless user experience.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I chose &lt;code&gt;rest_framework_simplejwt&lt;/code&gt; for its reliable JWT implementation, which includes token blacklisting. The authentication system is integrated within a modular users' Django app, keeping the codebase clean and maintainable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Authentication Flow
&lt;/h2&gt;

&lt;p&gt;Here’s how the Shopease-API authentication system works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Signup (/api/auth/register/): Users register with an email and password. The system validates the email for uniqueness, hashes the password, and returns access and refresh tokens for auto-login.&lt;/li&gt;
&lt;li&gt;Login (/api/auth/login/): Users authenticate with email and password, receiving new access and refresh tokens.&lt;/li&gt;
&lt;li&gt;Logout (/api/auth/logout/): Users send their refresh token to blacklist it, requiring an access token in the header for authentication.&lt;/li&gt;
&lt;li&gt;Token Refresh (/api/auth/token/refresh/): Users refresh their access token using the refresh token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv9fmse4rt3mmpeuv9uhc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv9fmse4rt3mmpeuv9uhc.png" alt="authentication flow" width="800" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step-by-Step Implementation
&lt;/h2&gt;

&lt;p&gt;Let’s break down the code, organized in the user's app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Custom User Model (users/models.py)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I created a CustomUser model using Django’s &lt;code&gt;AbstractBaseUser&lt;/code&gt; to support email-based authentication.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
from django.db import models
from django.utils import timezone

class CustomUserManager(BaseUserManager):
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError("Email must be provided")
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_staff', True)
        extra_fields.setdefault('is_superuser', True)
        return self.create_user(email, password, **extra_fields)

class CustomUser(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(unique=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    date_joined = models.DateTimeField(default=timezone.now)

    objects = CustomUserManager()
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    def __str__(self):
        return self.email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;email is the unique identifier (USERNAME_FIELD).&lt;/li&gt;
&lt;li&gt;unique=True ensures email uniqueness at the database level.&lt;/li&gt;
&lt;li&gt;set_password hashes passwords securely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Serializers (users/serializers.py)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Serializers handle data validation, including email verification.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from rest_framework import serializers
from .models import CustomUser
 “

from rest_framework import serializers
from .models import CustomUser
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from django.core.validators import EmailValidator
from django.core.exceptions import ValidationError

class RegisterSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True, min_length=8)

    class Meta:
        model = CustomUser
        fields = ['email', 'password']

    def validate_email(self, value):
        # Validate email format
        validator = EmailValidator()
        try:
            validator(value)
        except ValidationError:
            raise serializers.ValidationError("Invalid email format")
        # Check email uniqueness
        if CustomUser.objects.filter(email=value).exists():
            raise serializers.ValidationError("Email already exists")
        return value

    def create(self, validated_data):
        return CustomUser.objects.create_user(**validated_data)

class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
    @classmethod
    def get_token(cls, user):
        token = super().get_token(user)
        return token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RegisterSerializer validates email format and uniqueness using EmailValidator and a custom check.&lt;/li&gt;
&lt;li&gt;CustomTokenObtainPairSerializer supports email-based login (leveraging USERNAME_FIELD).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Views (users/views.py)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Views define the API endpoints.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status, generics
from rest_framework.permissions import IsAuthenticated
from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.exceptions import TokenError
from .models import CustomUser
from .serializers import RegisterSerializer, CustomTokenObtainPairSerializer

class RegisterView(generics.CreateAPIView):
    queryset = CustomUser.objects.all()
    serializer_class = RegisterSerializer

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = serializer.save()
        refresh = RefreshToken.for_user(user)
        return Response({
            "user": {"email": user.email},
            "refresh": str(refresh),
            "access": str(refresh.access_token),
        }, status=status.HTTP_201_CREATED)

class CustomTokenObtainPairView(TokenObtainPairView):
    serializer_class = CustomTokenObtainPairSerializer

class LogoutView(APIView):
    permission_classes = [IsAuthenticated]

    def post(self, request):
        try:
            refresh_token = request.data.get("refresh")
            if not refresh_token:
                return Response({"error": "Refresh token is required"}, status=status.HTTP_400_BAD_REQUEST)
            token = RefreshToken(refresh_token)
            token.blacklist()
            return Response({"message": "Successfully logged out"}, status=status.HTTP_205_RESET_CONTENT)
        except TokenError as e:
            return Response({"error": f"Invalid refresh token: {str(e)}"}, status=status.HTTP_400_BAD_REQUEST)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RegisterView: Creates users and returns tokens for auto-login.&lt;/li&gt;
&lt;li&gt;CustomTokenObtainPairView: Handles secure login with tokens.&lt;/li&gt;
&lt;li&gt;LogoutView: Blacklists refresh tokens, requiring an access token for authentication.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 4: URLs (shopease/urls.py)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Include app URLs in the project's urls.py.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/auth/', include('users.urls')),
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5: URLs (users/urls.py)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Define the API endpoints.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.urls import path
from .views import RegisterView, CustomTokenObtainPairView, LogoutView
from rest_framework_simplejwt.views import TokenRefreshView

urlpatterns = [
    path('register/', RegisterView.as_view(), name='register'),
    path('login/', CustomTokenObtainPairView.as_view(), name='login'),
    path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path('logout/', LogoutView.as_view(), name='logout'),
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 6: Settings (shopease/settings.py)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Configure JWT and the custom user model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from datetime import timedelta

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'rest_framework_simplejwt',
    'rest_framework_simplejwt.token_blacklist',
    'users',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
}

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
    'AUTH_HEADER_TYPES': ('Bearer',),
    'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
}

AUTH_USER_MODEL = 'users.CustomUser'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Features in Action
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Email Verification: The RegisterSerializer uses EmailValidator and checks for existing emails, ensuring only valid, unique emails are accepted.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Auto-Login: The RegisterView returns tokens immediately after signup, streamlining the user experience.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvxb2hpc6b3rk0h2p9a17.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvxb2hpc6b3rk0h2p9a17.png" alt="registeruser" width="800" height="741"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Secure Login: The CustomTokenObtainPairView authenticates users and issues access and refresh tokens, with access tokens expiring in 60 minutes for security.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Token-Based Auth: SimpleJWT’s JWTAuthentication validates access tokens for protected endpoints like /logout/.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8xbmai4r7o1syzj2wwo6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8xbmai4r7o1syzj2wwo6.png" alt="loginUser" width="800" height="741"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logout with Blacklisting: The token_blacklist app ensures refresh tokens are invalidated on logout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxc9pavsf11ibxx0aujp0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxc9pavsf11ibxx0aujp0.png" alt="logout" width="800" height="741"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Token Distinction: Access tokens authenticate requests, while refresh tokens handle session renewal or blacklisting. Mixing them up causes errors like 401.&lt;/li&gt;
&lt;li&gt;Debugging: Logging request headers and payloads is essential for troubleshooting auth issues.&lt;/li&gt;
&lt;li&gt;Validation: Combining model-level (unique=True) and serializer-level email checks ensures robust verification.&lt;/li&gt;
&lt;li&gt;Testing: Early testing with Postman catches bugs before they impact the system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's a wrap! I’m a Django developer with a passion for creating scalable APIs. Let’s connect! &lt;a href="https://github.com/kihuni" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>django</category>
      <category>api</category>
      <category>programming</category>
    </item>
    <item>
      <title>Understanding Authentication with JWT and Registration with Email Verification: A Case Study with LiveStatusAPI</title>
      <dc:creator>kihuni</dc:creator>
      <pubDate>Wed, 09 Apr 2025 07:35:18 +0000</pubDate>
      <link>https://forem.com/kihuni/understanding-authentication-with-jwt-and-registration-with-email-verification-a-case-study-with-2909</link>
      <guid>https://forem.com/kihuni/understanding-authentication-with-jwt-and-registration-with-email-verification-a-case-study-with-2909</guid>
      <description>&lt;p&gt;In API development, securing your endpoints and ensuring only legitimate users can access them is paramount. Two key mechanisms for achieving this are &lt;code&gt;JWT (JSON Web Tokens)&lt;/code&gt; authentication and email verification during registration. In this article, I’ll break down these concepts, explain how to implement them in a Django project using &lt;code&gt;Django REST Framework (DRF)&lt;/code&gt;, and demonstrate their application in a real-world project: LiveStatusAPI, a real-time presence tracking API I'm building.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/kihuni/LiveStatusAPI" rel="noopener noreferrer"&gt;LiveStatusAPI&lt;/a&gt; enables applications to monitor user activity, predict response times, and analyze engagement trends. It includes real-time presence tracking, engagement analytics, webhook integration, and role-based access control features.&lt;/p&gt;

&lt;p&gt;In this post, I’ll focus on how I rebuilt the user registration and login system for LiveStatusAPI, replacing its basic session-based authentication with JWT and adding email verification to ensure that only verified users can access the API. Let’s dive in!&lt;/p&gt;

&lt;h2&gt;
  
  
  What is JWT Authentication?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Understanding JWT
&lt;/h3&gt;

&lt;p&gt;JWT (JSON Web Token) is a compact, self-contained standard for securely transmitting information between parties as a JSON object. It’s widely used for authentication in APIs because it’s stateless and scalable. A JWT consists of three parts, separated by dots (.):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Header: Contains metadata about the token, such as the type (JWT) and the signing algorithm (e.g., HMAC SHA256).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Payload: Contains the claims, which are statements about the user (e.g., user ID, email) and additional data (e.g., expiration time).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Signature: Verifies the token’s integrity by combining the encoded header, payload, and a secret key.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, a JWT might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzNDU2Nzg5MCIsImV4cCI6MTY3ODkwMTIzNH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When decoded, the payload might reveal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "user_id": "1234567890",
  "exp": 1678901234
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How JWT Authentication Works
&lt;/h3&gt;

&lt;p&gt;JWT authentication is stateless, meaning the server doesn’t store session information. Here’s the typical flow:&lt;br&gt;
User Logs In: The user sends their credentials (e.g., email and password) to a login endpoint.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Token Generation: The server verifies the credentials and generates a JWT (access token and optionally a refresh token). The access token is short-lived (e.g., 60 minutes), while the refresh token is longer-lived (e.g., 1 day).&lt;/li&gt;
&lt;li&gt;Token Usage: The client includes the access token in the Authorization header of subsequent requests (e.g., Authorization: Bearer ).&lt;/li&gt;
&lt;li&gt;Token Verification: The server verifies the token’s signature and checks its expiration. If valid, the request is processed; otherwise, it’s rejected.&lt;/li&gt;
&lt;li&gt;Token Refresh: When the access token expires, the client can use the refresh token to request a new access token without re-authenticating.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Why Use JWT?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Stateless: No need to store session data on the server, making it ideal for scalable APIs.&lt;/li&gt;
&lt;li&gt;Secure: Tokens are signed, ensuring data integrity and authenticity.&lt;/li&gt;
&lt;li&gt;Cross-Domain: JWTs can be used across different domains, making them suitable for microservices or single sign-on (SSO).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  What is Registration with Email Verification?
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The Importance of Email Verification
&lt;/h3&gt;

&lt;p&gt;Email verification is a security measure that ensures a user’s email address is valid and belongs to them before granting access to an application. Without verification, malicious users could register with fake emails, potentially leading to spam accounts or unauthorized access. Email verification typically involves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User Registration: The user provides their email, username, and password.&lt;/li&gt;
&lt;li&gt;Email Sending: The server sends a verification email with a unique link or token to the user’s email address.&lt;/li&gt;
&lt;li&gt;Email Verification: The user clicks the link, confirming their email and account is activated.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Why Use Email Verification?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Security: Ensures only legitimate users can access the system.&lt;/li&gt;
&lt;li&gt;User Trust: Verifying emails reduces spam and builds trust with users.&lt;/li&gt;
&lt;li&gt;Data Quality: Helps maintain a database of valid email addresses for communication.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Implementing JWT Authentication and Email Verification in Django
&lt;/h2&gt;

&lt;p&gt;Let’s walk through the steps to implement JWT authentication and email verification in a Django project using DRF. I’ll use the &lt;code&gt;djangorestframework-simplejwt&lt;/code&gt; package for &lt;code&gt;JWT authentication&lt;/code&gt;, which provides a robust implementation of JWT with &lt;code&gt;access and refresh tokens&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Set Up the Project&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Install Dependencies&lt;/p&gt;

&lt;p&gt;First, install the necessary packages:&lt;br&gt;
bash&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install django djangorestframework djangorestframework-simplejwt python-decouple
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add these to your requirements.txt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;django==4.2.7
djangorestframework==3.14.0
djangorestframework-simplejwt==5.3.1
python-decouple==3.8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Configure Django Settings&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Update your settings.py to include &lt;code&gt;DRF&lt;/code&gt;, &lt;code&gt;rest_framework_simplejwt&lt;/code&gt;, and &lt;code&gt;email settings&lt;/code&gt; for sending verification emails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# your_project/settings.py

import os
from datetime import timedelta
from decouple import config

# Define the custom user model (we’ll create this next)
AUTH_USER_MODEL = 'users.CustomUser'

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'rest_framework_simplejwt',
    'users',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
}

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
    'AUTH_HEADER_TYPES': ('Bearer',),
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'user_id',
}

# Email settings for verification
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = config('EMAIL_HOST', default='smtp.gmail.com')
EMAIL_PORT = config('EMAIL_PORT', default=587, cast=int)
EMAIL_USE_TLS = True
EMAIL_HOST_USER = config('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD')
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; file to store sensitive information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .env
SECRET_KEY=your-django-secret-key
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_HOST_USER=your-email@gmail.com
EMAIL_HOST_PASSWORD=your-app-password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Understanding the Email Settings in Django
&lt;/h2&gt;

&lt;p&gt;Django provides a built-in email-sending framework that can be configured to send emails via various backends (e.g., &lt;code&gt;SMTP&lt;/code&gt;, &lt;code&gt;console&lt;/code&gt;, &lt;code&gt;file-based&lt;/code&gt;). We’re using the &lt;code&gt;SMTP backend&lt;/code&gt; (django.core.mail.backends.smtp.EmailBackend) to send emails through an external SMTP server (Gmail, by default). These settings define how Django connects to the SMTP server and sends emails.&lt;/p&gt;

&lt;p&gt;Here’s a breakdown of each setting in the configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend': Specifies the backend Django will use to send emails.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EMAIL_HOST = config('EMAIL_HOST', default='smtp.gmail.com'): Defines the hostname of the SMTP server Django will connect to. The default is &lt;code&gt;smtp.gmail.com&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EMAIL_PORT = config('EMAIL_PORT', default=587, cast=int): Specifies the port number to use when connecting to the SMTP server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EMAIL_USE_TLS = True: Enables TLS encryption for the connection to the SMTP server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EMAIL_HOST_USER = config('EMAIL_HOST_USER') - Specifies the email address (username) used to authenticate with the SMTP server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD'): Specifies the password to authenticate with the SMTP server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DEFAULT_FROM_EMAIL = EMAIL_HOST_USER: Sets the default &lt;code&gt;from&lt;/code&gt; email address for emails sent by Django.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Setting Up Gmail for Sending Emails
&lt;/h3&gt;

&lt;p&gt;Since the default &lt;code&gt;EMAIL_HOST&lt;/code&gt; is &lt;code&gt;smtp.gmail.com&lt;/code&gt;, let’s set up Gmail to work with Django. Gmail has strict security measures, so you’ll need to use an &lt;code&gt;App Password&lt;/code&gt; instead of your regular &lt;code&gt;Gmail password&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Step 1. Enable 2-Step Verification on Your Gmail Account&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to your &lt;a href="https://myaccount.google.com/" rel="noopener noreferrer"&gt;Google Account settings:&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Navigate to Security &amp;gt; 2-Step Verification.&lt;/li&gt;
&lt;li&gt;Follow the steps to enable 2-step verification (e.g., using your phone number for verification).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffxfv81n7ax4aivqoizdl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffxfv81n7ax4aivqoizdl.png" alt="google verification" width="800" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 2. Generate an App Password&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;After enabling 2-step verification, go to Security &amp;gt; App Passwords (or search for “App Passwords” in your Google Account settings).&lt;/li&gt;
&lt;li&gt;Click Generate.&lt;/li&gt;
&lt;li&gt;Select App as “Mail” and Device as “Other (Custom name)”, then name it (e.g., “Django”).&lt;/li&gt;
&lt;li&gt;Click Generate. Google will provide a 16-character App Password (e.g., abcd efgh ijkl mnop).&lt;/li&gt;
&lt;li&gt;Copy this password (ignore the spaces).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbj2xjqhf2slnndkam43m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbj2xjqhf2slnndkam43m.png" alt="google key" width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 3. Update Your .env File&lt;br&gt;
Add the Gmail credentials to your .env file using the App Password:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .env
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_HOST_USER=your-email@gmail.com
EMAIL_HOST_PASSWORD=your-app-password  # e.g., abcdefghijklmnop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 4. Test Sending an Email&lt;br&gt;
To confirm the setup works, run a quick test in the Django shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python manage.py shell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.core.mail import send_mail

send_mail(
    'kamau@gmail.com',
    'This is a test email from Django.',
    'stephenkihuni55@gmail.com',
    ['test-recipient@example.com'],
    fail_silently=False,
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqur8v689a2mkc68z11mw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqur8v689a2mkc68z11mw.png" alt="email verification" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the email is sent successfully, your configuration is correct.&lt;/p&gt;

&lt;p&gt;If it fails, check the error message:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SMTPAuthenticationError: Likely an issue with &lt;code&gt;EMAIL_HOST_USER&lt;/code&gt; or &lt;code&gt;EMAIL_HOST_PASSWORD&lt;/code&gt;. Double-check your &lt;code&gt;App Password&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;ConnectionRefusedError: Ensure &lt;code&gt;EMAIL_PORT&lt;/code&gt; and &lt;code&gt;EMAIL_USE_TLS&lt;/code&gt; are correct (587 and True for Gmail).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Set Up URLs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add the JWT token endpoints to your project’s urls.py:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# your_project/urls.py

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('users.urls')),
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Create a Custom User Model&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;CustomUser&lt;/code&gt; model to support email verification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# users/models.py

from django.contrib.auth.models import AbstractUser
from django.db import models
import uuid

class CustomUser(AbstractUser):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    email = models.EmailField(unique=True)
    verification_token = models.CharField(max_length=36, blank=True, null=True)
    is_verified = models.BooleanField(default=False)

    REQUIRED_FIELDS = ['email']

    def __str__(self):
        return self.username
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run migrations to apply the changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python manage.py makemigrations
python manage.py migrate

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Implement Registration with Email Verification&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create a Registration Serializer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a serializer to handle user registration and send a verification email:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# users/serializers.py

from rest_framework import serializers
from django.contrib.auth import get_user_model
from django.core.mail import send_mail
from django.urls import reverse
import uuid

User = get_user_model()

class RegisterSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True, min_length=8)
    email = serializers.EmailField()

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

    def validate_email(self, value):
        if User.objects.filter(email=value).exists():
            raise serializers.ValidationError("A user with this email already exists.")
        return value

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

        verification_token = str(uuid.uuid4())
        user.verification_token = verification_token
        user.save()

        verification_url = self.context['request'].build_absolute_uri(
            reverse('verify-email', kwargs={'token': verification_token})
        )
        subject = 'Verify Your Email Address'
        message = f'Click the link to verify your email: {verification_url}'
        send_mail(
            subject,
            message,
            'from@example.com',
            [user.email],
            fail_silently=False,
        )

        return user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Create Registration and Verification Views&lt;/strong&gt;&lt;br&gt;
Create views for registration and email verification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# users/views.py

from rest_framework import generics, status
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from .serializers import RegisterSerializer
from django.contrib.auth import get_user_model

User = get_user_model()

class RegisterView(generics.CreateAPIView):
    queryset = User.objects.all()
    serializer_class = RegisterSerializer
    permission_classes = [AllowAny]

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = serializer.save()
        return Response({
            "message": "User registered successfully. Please verify your email to activate your account.",
            "user": {
                "id": str(user.id),
                "username": user.username,
                "email": user.email
            }
        }, status=status.HTTP_201_CREATED)

class VerifyEmailView(generics.GenericAPIView):
    permission_classes = [AllowAny]

    def get(self, request, token, *args, **kwargs):
        try:
            user = User.objects.get(verification_token=token)
            if user.is_verified:
                return Response({"message": "Email already verified."}, status=status.HTTP_400_BAD_REQUEST)
            user.is_verified = True
            user.is_active = True
            user.verification_token = None
            user.save()
            return Response({"message": "Email verified successfully. You can now log in."}, status=status.HTTP_200_OK)
        except User.DoesNotExist:
            return Response({"error": "Invalid verification token."}, status=status.HTTP_400_BAD_REQUEST)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Set Up URLs&lt;/strong&gt;&lt;br&gt;
Add the endpoints to users/urls.py:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# users/urls.py

from django.urls import path
from .views import RegisterView, VerifyEmailView

urlpatterns = [
    path('register/', RegisterView.as_view(), name='register'),
    path('verify-email/&amp;lt;str:token&amp;gt;/', VerifyEmailView.as_view(), name='verify-email'),
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Implement JWT Authentication for Login&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Customize the Login Process&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since the CustomUser model uses email as the unique identifier, customize the login process to allow users to log in with their email:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# users/views.py (continued)

from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework import status
from rest_framework.response import Response

class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
    def validate(self, attrs):
        email = attrs.get("email")
        password = attrs.get("password")

        if email and password:
            user = User.objects.filter(email=email).first()
            if user and user.check_password(password):
                if not user.is_verified:
                    raise serializers.ValidationError("Please verify your email before logging in.")
                if not user.is_active:
                    raise serializers.ValidationError("User account is inactive.")
                attrs['username'] = user.username
                return super().validate(attrs)
            else:
                raise serializers.ValidationError("Invalid email or password.")
        else:
            raise serializers.ValidationError("Must include 'email' and 'password'.")

class CustomTokenObtainPairView(TokenObtainPairView):
    serializer_class = CustomTokenObtainPairSerializer

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        return Response({
            "access": serializer.validated_data['access'],
            "refresh": serializer.validated_data['refresh'],
            "user": {
                "id": str(serializer.user.id),
                "username": serializer.user.username,
                "email": serializer.user.email
            }
        }, status=status.HTTP_200_OK)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Update URLs&lt;/strong&gt;&lt;br&gt;
Add the custom login endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# users/urls.py

from django.urls import path
from .views import RegisterView, VerifyEmailView, CustomTokenObtainPairView

urlpatterns = [
    path('register/', RegisterView.as_view(), name='register'),
    path('verify-email/&amp;lt;str:token&amp;gt;/', VerifyEmailView.as_view(), name='verify-email'),
    path('login/', CustomTokenObtainPairView.as_view(), name='login'),
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Case Study: Applying JWT Authentication and Email Verification to LiveStatusAPI
&lt;/h2&gt;

&lt;p&gt;Now that we’ve covered the theory and implementation, let’s see how I applied these concepts to LiveStatusAPI, a real-time presence-tracking API I’ve been building. LiveStatusAPI already had basic user registration and login functionality, but it used session-based authentication and lacked email verification. On Day 1 of my project, I rebuilt these components to use JWT authentication and added email verification to ensure only verified users can access the API.&lt;/p&gt;

&lt;p&gt;Step 1: Reviewing the Existing Setup&lt;/p&gt;

&lt;p&gt;LiveStatusAPI uses Django and DRF, with a &lt;code&gt;CustomUser model&lt;/code&gt;that extends &lt;code&gt;AbstractUser&lt;/code&gt; and uses a &lt;code&gt;UUID&lt;/code&gt; as the primary key. The project had basic registration (&lt;code&gt;POST /api/register/&lt;/code&gt;) and &lt;code&gt;login endpoints&lt;/code&gt;, but they didn’t enforce email verification, and authentication was session-based.&lt;/p&gt;

&lt;p&gt;I started by ensuring the project was running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd LiveStatusAPI
source livevenv/bin/activate
git pull # since its open-sourced
pip install -r requirements.txt
python manage.py runserver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 2: Configuring JWT and Email Settings&lt;/p&gt;

&lt;p&gt;I updated settings.py to replace session-based authentication with JWT and add email settings for verification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# LiveStatusApi/settings.py

import os
from datetime import timedelta
from decouple import config

AUTH_USER_MODEL = 'users.CustomUser'

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'rest_framework_simplejwt',
    'users',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    ),
}

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
    'AUTH_HEADER_TYPES': ('Bearer',),
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'user_id',
}

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = config('EMAIL_HOST', default='smtp.gmail.com')
EMAIL_PORT = config('EMAIL_PORT', default=587, cast=int)
EMAIL_USE_TLS = True
EMAIL_HOST_USER = config('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD')
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I updated the .env file with my Gmail credentials, using an App Password for security:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .env
SECRET_KEY=your-django-secret-key
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_HOST_USER=your-email@gmail.com
EMAIL_HOST_PASSWORD=your-app-password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 3: Updating the CustomUser Model&lt;/p&gt;

&lt;p&gt;The existing CustomUser model didn’t have fields for email verification. I added verification_token and is_verified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# users/models.py

from django.contrib.auth.models import AbstractUser
from django.db import models
import uuid

class CustomUser(AbstractUser):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    email = models.EmailField(unique=True)
    verification_token = models.CharField(max_length=36, blank=True, null=True)
    is_verified = models.BooleanField(default=False)

    REQUIRED_FIELDS = ['email']

    def __str__(self):
        return self.username
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran migrations to apply the changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python manage.py makemigrations
python manage.py migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 4: Rebuilding Registration with Email Verification&lt;/p&gt;

&lt;p&gt;I created a RegisterSerializer to handle the updated registration process, ensuring users must verify their email before their account is activated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# users/serializers.py

from rest_framework import serializers
from django.contrib.auth import get_user_model
from django.core.mail import send_mail
from django.urls import reverse
import uuid

User = get_user_model()

class RegisterSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True, min_length=8)
    email = serializers.EmailField()

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

    def validate_email(self, value):
        if User.objects.filter(email=value).exists():
            raise serializers.ValidationError("A user with this email already exists.")
        return value

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

        verification_token = str(uuid.uuid4())
        user.verification_token = verification_token
        user.save()

        verification_url = self.context['request'].build_absolute_uri(
            reverse('verify-email', kwargs={'token': verification_token})
        )
        subject = 'Verify Your Email Address'
        message = f'Click the link to verify your email: {verification_url}'
        send_mail(
            subject,
            message,
            'from@example.com',
            [user.email],
            fail_silently=False,
        )

        return user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I updated the existing RegisterView and added a VerifyEmailView:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# users/views.py

from rest_framework import generics, status
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
from .serializers import RegisterSerializer
from django.contrib.auth import get_user_model

User = get_user_model()

class RegisterView(generics.CreateAPIView):
    queryset = User.objects.all()
    serializer_class = RegisterSerializer
    permission_classes = [AllowAny]

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = serializer.save()
        return Response({
            "message": "User registered successfully. Please verify your email to activate your account.",
            "user": {
                "id": str(user.id),
                "username": user.username,
                "email": user.email
            }
        }, status=status.HTTP_201_CREATED)

class VerifyEmailView(generics.GenericAPIView):
    permission_classes = [AllowAny]

    def get(self, request, token, *args, **kwargs):
        try:
            user = User.objects.get(verification_token=token)
            if user.is_verified:
                return Response({"message": "Email already verified."}, status=status.HTTP_400_BAD_REQUEST)
            user.is_verified = True
            user.is_active = True
            user.verification_token = None
            user.save()
            return Response({"message": "Email verified successfully. You can now log in."}, status=status.HTTP_200_OK)
        except User.DoesNotExist:
            return Response({"error": "Invalid verification token."}, status=status.HTTP_400_BAD_REQUEST)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I updated users/urls.py to include the email verification endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# users/urls.py

from django.urls import path
from .views import RegisterView, VerifyEmailView

urlpatterns = [
    path('register/', RegisterView.as_view(), name='register'),
    path('verify-email/&amp;lt;str:token&amp;gt;/', VerifyEmailView.as_view(), name='verify-email'),
]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 5: Rebuilding Login with JWT&lt;/p&gt;

&lt;p&gt;I replaced the existing session-based login with JWT, customizing the login process to use email:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# users/views.py

from rest_framework_simplejwt.views import TokenObtainPairView
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework import status
from rest_framework.response import Response

class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
    def validate(self, attrs):
        email = attrs.get("email")
        password = attrs.get("password")

        if email and password:
            user = User.objects.filter(email=email).first()
            if user and user.check_password(password):
                if not user.is_verified:
                    raise serializers.ValidationError("Please verify your email before logging in.")
                if not user.is_active:
                    raise serializers.ValidationError("User account is inactive.")
                attrs['username'] = user.username
                return super().validate(attrs)
            else:
                raise serializers.ValidationError("Invalid email or password.")
        else:
            raise serializers.ValidationError("Must include 'email' and 'password'.")

class CustomTokenObtainPairView(TokenObtainPairView):
    serializer_class = CustomTokenObtainPairSerializer

    def post(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        return Response({
            "access": serializer.validated_data['access'],
            "refresh": serializer.validated_data['refresh'],
            "user": {
                "id": str(serializer.user.id),
                "username": serializer.user.username,
                "email": serializer.user.email
            }
        }, status=status.HTTP_200_OK)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I updated &lt;code&gt;users/urls.py&lt;/code&gt; to include the custom login endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# users/urls.py

from django.urls import path
from .views import RegisterView, VerifyEmailView, CustomTokenObtainPairView

urlpatterns = [
    path('register/', RegisterView.as_view(), name='register'),
    path('verify-email/&amp;lt;str:token&amp;gt;/', VerifyEmailView.as_view(), name='verify-email'),
    path('login/', CustomTokenObtainPairView.as_view(), name='login'),
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 6: Testing the Implementation&lt;/p&gt;

&lt;p&gt;I tested the updated registration and login flow with Postman:&lt;/p&gt;

&lt;p&gt;Register a User (POST /api/register/):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "username": "testuser",
    "email": "testuser@example.com",
    "password": "testpassword123"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "message": "User registered successfully. Please verify your email to activate your account.",
    "user": {
        "id": "some-uuid",
        "username": "testuser",
        "email": "testuser@example.com"
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxiy41ws8fjryhueqxgom.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxiy41ws8fjryhueqxgom.png" alt="screenshot" width="800" height="607"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I received a verification email with a link like &lt;a href="http://localhost:8000/api/verify-email/" rel="noopener noreferrer"&gt;http://localhost:8000/api/verify-email/&lt;/a&gt;/.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fazfiwwsnhuyxsc6in1zd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fazfiwwsnhuyxsc6in1zd.png" alt="verification link" width="800" height="264"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Verify Email (GET /api/verify-email//):&lt;br&gt;
Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
{
    "message": "Email verified successfully. You can now log in."
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgqef32xrtmyf9x14i812.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgqef32xrtmyf9x14i812.png" alt="verification login" width="800" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Login (POST /api/login/):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "email": "testuser@example.com",
    "password": "testpassword123"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "access": "your-access-token",
    "refresh": "your-refresh-token",
    "user": {
        "id": "some-uuid",
        "username": "testuser",
        "email": "testuser@example.com"
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fss1pv47caq1sdruvmku0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fss1pv47caq1sdruvmku0.png" alt="verification" width="800" height="581"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This implementation sets a secure foundation for LiveStatusAPI. In future articles, I’ll build on this by implementing features like the GET &lt;code&gt;/users/{userId}/presence&lt;/code&gt; endpoint to retrieve user presence data, ensuring it’s protected with &lt;code&gt;JWT authentication&lt;/code&gt;. I’ll also add &lt;code&gt;caching&lt;/code&gt;, &lt;code&gt;webhooks&lt;/code&gt;, and &lt;code&gt;analytics&lt;/code&gt;, continuing to use LiveStatusAPI as a case study to demonstrate how these concepts apply in a real-world project.&lt;/p&gt;

&lt;p&gt;If you have feedback or suggestions for improving LiveStatusAPI, feel free to leave a comment or reach out on GitHub &lt;a href="https://github.com/kihuni/LiveStatusAPI" rel="noopener noreferrer"&gt;https://github.com/kihuni/LiveStatusAPI&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>django</category>
      <category>programming</category>
    </item>
    <item>
      <title>Secure Image Processing with Pulumi ESC, Clarifai, and pCloud</title>
      <dc:creator>kihuni</dc:creator>
      <pubDate>Sat, 05 Apr 2025 17:59:55 +0000</pubDate>
      <link>https://forem.com/kihuni/secure-image-processing-with-pulumi-esc-clarifai-and-pcloud-40ni</link>
      <guid>https://forem.com/kihuni/secure-image-processing-with-pulumi-esc-clarifai-and-pcloud-40ni</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/pulumi"&gt;Pulumi Deploy and Document Challenge&lt;/a&gt;: Shhh, It's a Secret!&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built a Secure Image Processing Pipeline that processes images to detect labels and securely stores the results in the cloud. The pipeline uses Clarifai for image processing, pCloud for storage, and Pulumi ESC to manage secrets and configurations securely. To make the project more interactive, I added a simple web UI using Flask, allowing users to upload an image, view the detected labels, and access the results file.&lt;/p&gt;

&lt;p&gt;Here’s how the pipeline works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A user uploads an image through the web UI.&lt;/li&gt;
&lt;li&gt;The script fetches the Clarifai API key and pCloud credentials from Pulumi ESC.&lt;/li&gt;
&lt;li&gt;Clarifai processes the image to detect labels (e.g., dog, pet, animal).&lt;/li&gt;
&lt;li&gt;The results are saved locally as a JSON file (e.g., analysis_results_1743817982.json) with a unique timestamp to prevent overwrites.&lt;/li&gt;
&lt;li&gt;The UI displays the uploaded image, detected labels, and a result file link.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This project demonstrates secure configuration management using &lt;code&gt;Pulumi ESC&lt;/code&gt; to handle sensitive data (API keys and credentials) without hardcoding them in the source code. It also showcases a practical use case for image processing and cloud storage.&lt;/p&gt;

&lt;p&gt;Web UI Home Page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhlvc8p54vse0opt2brbs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhlvc8p54vse0opt2brbs.png" alt="Web UI" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Results Page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg833jt93jto9asulmwgk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fg833jt93jto9asulmwgk.png" alt="Results" width="800" height="893"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Results File Content&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6uyc3sltzpnox191sx04.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6uyc3sltzpnox191sx04.png" alt="Cloud" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;pCloud Storage:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsyjwlcndr7vr7jfr000l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsyjwlcndr7vr7jfr000l.png" alt="pCloud" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Live Demo Link
&lt;/h2&gt;

&lt;p&gt;Due to connectivity restrictions on PythonAnywhere’s free tier, which blocked outbound HTTPS connections to api.pulumi.com for fetching secrets at runtime, I was unable to deploy the app live within the hackathon timeline. Instead, I’ve tested the app locally and provided detailed screenshots and setup instructions to demonstrate its functionality. You can replicate the app on your local machine by following the setup instructions below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup Instructions&lt;/strong&gt;&lt;br&gt;
To run the Secure Image Processing Pipeline locally, follow these steps:&lt;br&gt;
&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.8+&lt;/li&gt;
&lt;li&gt;Pulumi CLI (for managing ESC environments)&lt;/li&gt;
&lt;li&gt;A Clarifai account with an API key&lt;/li&gt;
&lt;li&gt;A pCloud account with a username and password&lt;/li&gt;
&lt;li&gt;A folder named /hackathon-results in your pCloud account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Steps&lt;/strong&gt;&lt;br&gt;
Clone the Repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/kihuni/Secure-Image-Processing-Pipeline.git
cd Secure-Image-Processing-Pipeline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set Up a Virtual Environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Install Dependencies:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The requirements.txt includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
flask
clarifai-grpc
pcloud
pulumi-esc-sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Set Up Pulumi ESC:&lt;/strong&gt;&lt;br&gt;
Install the Pulumi CLI: &lt;a href="https://www.pulumi.com/docs/iac/download-install/" rel="noopener noreferrer"&gt;Pulumi Installation Guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Set your Pulumi access token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export PULUMI_ACCESS_TOKEN="your-access-token"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a new ESC environment (or use the existing one):&lt;br&gt;
bash&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;esc env init kihuni/Secure-Multi-Cloud-Backup-Orchestrator/dev-environment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set the secrets and configurations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;esc env set --secret kihuni/Secure-Multi-Cloud-Backup-Orchestrator/dev-environment clarifai:apiKey "your-clarifai-api-key"
esc env set --secret kihuni/Secure-Multi-Cloud-Backup-Orchestrator/dev-environment pcloud:username "your-pcloud-email"
esc env set --secret kihuni/Secure-Multi-Cloud-Backup-Orchestrator/dev-environment pcloud:password "your-pcloud-password"
esc env set kihuni/Secure-Multi-Cloud-Backup-Orchestrator/dev-environment pcloud:folderPath "/hackathon-results"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fazdewtuncm7krtli5xvz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fazdewtuncm7krtli5xvz.png" alt=" " width="800" height="780"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run the App Locally:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2e436inbmy8xx24wwimv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2e436inbmy8xx24wwimv.png" alt="run server" width="800" height="617"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open &lt;a href="http://localhost:5000" rel="noopener noreferrer"&gt;http://localhost:5000&lt;/a&gt; in your browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe1bj1wxyo1q4z0havyn3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe1bj1wxyo1q4z0havyn3.png" alt="processing an image" width="800" height="826"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Repo
&lt;/h2&gt;

&lt;p&gt;The source code for the Secure Image Processing Pipeline is available on GitHub:&lt;br&gt;
&lt;a href="https://github.com/kihuni/Secure-Image-Processing-Pipeline" rel="noopener noreferrer"&gt;Secure-Image-Processing-Pipeline&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My Journey
&lt;/h2&gt;

&lt;p&gt;Building the Secure Image Processing Pipeline was a rewarding experience that taught me a lot about secure configuration management, image processing, and web development. Here’s a summary of my journey:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Initial Setup: I started by setting up a Flask app with a simple UI for uploading images. I chose Clarifai for image processing and pCloud for storage due to their free tiers and ease of use.&lt;/li&gt;
&lt;li&gt;Integrating Pulumi ESC: I used Pulumi ESC to manage my Clarifai API key and pCloud credentials securely. I initially encountered a 401 Unauthorized error due to an incorrect import of the pulumi_esc_sdk and a malformed environment path. I resolved this by using the correct Client class and fixing the path to kihuni/Secure-Multi-Cloud-Backup-Orchestrator/dev-environment.&lt;/li&gt;
&lt;li&gt;Challenges with Deployment: I planned to deploy the app on PythonAnywhere, but connectivity restrictions blocked outbound HTTPS connections to api.pulumi.com. I pivoted to a local setup, providing screenshots and detailed setup instructions to demonstrate the app’s functionality.&lt;/li&gt;
&lt;li&gt;Adding Unique Filenames: I noticed that multiple requests would overwrite the analysis_results.json file, so I added a timestamp to the filename (e.g., analysis_results_1743817982.json), ensuring each request generates a unique file.&lt;/li&gt;
&lt;li&gt;Lessons Learned: This project taught me the importance of secure configuration management and the challenges of deploying on restricted platforms. I also gained experience with Flask, Clarifai, pCloud, and Pulumi ESC, and I’m excited to apply these skills to future projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Using Pulumi ESC
&lt;/h2&gt;

&lt;p&gt;Pulumi ESC played a crucial role in ensuring the security of my Secure Image Processing Pipeline. Here’s how I used it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Environment Setup: I created an ESC environment named kihuni/Secure-Multi-Cloud-Backup-Orchestrator/dev-environment to store my secrets and configurations.&lt;/li&gt;
&lt;li&gt;Storing Secrets: I stored the Clarifai API key and pCloud credentials as secrets in the ESC environment, ensuring they were encrypted and not exposed in the source code.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;esc env set --secret kihuni/Secure-Multi-Cloud-Backup-Orchestrator/dev-environment clarifai:apiKey "your-clarifai-api-key"
esc env set --secret kihuni/Secure-Multi-Cloud-Backup-Orchestrator/dev-environment pcloud:username "your-pcloud-email"
esc env set --secret kihuni/Secure-Multi-Cloud-Backup-Orchestrator/dev-environment pcloud:password "your-pcloud-password"
esc env set kihuni/Secure-Multi-Cloud-Backup-Orchestrator/dev-environment pcloud:folderPath "/hackathon-results"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Fetching Secrets: In the app, I used the &lt;code&gt;pulumi-esc-sdk&lt;/code&gt; to fetch the secrets at runtime:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from pulumi_esc_sdk import Client
client = Client()
environment = client.open_and_read_environment("kihuni/Secure-Multi-Cloud-Backup-Orchestrator/dev-environment")
clarifai_api_key = environment["clarifai"]["apiKey"]
pcloud_username = environment["pcloud"]["username"]
pcloud_password = environment["pcloud"]["password"]
pcloud_folder_path = environment["pcloud"]["folderPath"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Security Benefits: Using Pulumi ESC allowed me to keep sensitive data out of my source code and environment variables, reducing the risk of accidental leaks. It also provided a centralized way to manage configurations, making it easy to update secrets without modifying the code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj2c30kg3wjrdrfy8j52g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj2c30kg3wjrdrfy8j52g.png" alt="pulumi environment" width="800" height="826"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;The Secure Image Processing Pipeline demonstrates how to build a secure, user-friendly application with Pulumi ESC, Clarifai, and pCloud. Although I couldn’t deploy the app live due to platform restrictions, the local setup and detailed documentation showcase its functionality and the benefits of using Pulumi ESC for secure configuration management.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>pulumichallenge</category>
      <category>webdev</category>
      <category>cloud</category>
    </item>
    <item>
      <title>How to Add Google OAuth to Django REST API</title>
      <dc:creator>kihuni</dc:creator>
      <pubDate>Tue, 25 Mar 2025 00:42:39 +0000</pubDate>
      <link>https://forem.com/kihuni/how-to-add-google-oauth-to-django-rest-api-29h9</link>
      <guid>https://forem.com/kihuni/how-to-add-google-oauth-to-django-rest-api-29h9</guid>
      <description>&lt;p&gt;User authentication is a crucial part of any modern web application, especially APIs that require secure access. Instead of traditional username-password logins, integrating Google OAuth allows users to authenticate seamlessly with their Google accounts. This enhances security, improves user experience, and eliminates the hassle of managing passwords.&lt;/p&gt;

&lt;p&gt;In this guide, we’ll walk through how to add Google OAuth authentication to a Django REST API. By the end, users will be able to log in with Google, receive a JSON Web Token (JWT), and access protected API endpoints securely.&lt;/p&gt;

&lt;p&gt;We'll cover:&lt;/p&gt;

&lt;p&gt;✅ Setting up Google OAuth credentials in the Google Developer Console&lt;br&gt;
✅ Configuring Django REST Framework with JWT authentication&lt;br&gt;
✅ Implementing backend routes to handle Google login&lt;/p&gt;

&lt;p&gt;Let’s dive in!&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Google OAuth for a Django REST API?
&lt;/h2&gt;

&lt;p&gt;For an API focused on project collaboration, Google OAuth with DRF offers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Seamless Sign-In: Users log in with Google, skipping manual signup.&lt;/li&gt;
&lt;li&gt;API Security: JWTs protect endpoints like /projects/ or /contributions/.&lt;/li&gt;
&lt;li&gt;Developer Appeal: A trusted, scalable authentication method.
This setup enhances user experience, making it quick and reliable for users to access your API.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Setting Up the Project
&lt;/h2&gt;

&lt;p&gt;Let’s create a Django REST API from scratch, using Pipenv for environment management to keep dependencies clean.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Initial Setup&lt;/em&gt;&lt;br&gt;
Create a Project Directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir ProjectAPI
cd ProjectAPI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Install Dependencies:&lt;/em&gt;&lt;br&gt;
Add Django, DRF, and OAuth tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipenv install django djangorestframework django-allauth djangorestframework-simplejwt python-decouple
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Activate the Virtual Environment&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipenv shell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Start the Django Project:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipenv run django-admin startproject ProjectAPI .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note: The &lt;code&gt;.&lt;/code&gt; keeps the project files in the current directory.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Add an App:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipenv run python manage.py startapp core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Test the Setup:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipenv run python manage.py runserver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit &lt;code&gt;http://localhost:8000/&lt;/code&gt; to confirm it’s working.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa19pu5kbo8tscmgnkh6o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa19pu5kbo8tscmgnkh6o.png" alt="testing" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Task 1: Set Up Google OAuth Credentials
&lt;/h2&gt;

&lt;p&gt;Google OAuth starts with registering your app in the Developer Console.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steps:&lt;/strong&gt;&lt;br&gt;
Step 1. Create a Project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Visit &lt;a href="https://console.cloud.google.com/welcome?inv=1&amp;amp;invt=Abs3_g&amp;amp;project=projectcollabapi" rel="noopener noreferrer"&gt;Google Cloud Console&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft3m9lwk3rwfjz6591s7y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft3m9lwk3rwfjz6591s7y.png" alt="google cloud console" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Create a project (e.g., "ProjectCollabAPI").&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Choose the Organization and Location (if applicable).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click Create.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgo8o1uwvhc33lomq0hbn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgo8o1uwvhc33lomq0hbn.png" alt="Creating project" width="672" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 2. Enable APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the Google Cloud Console, select the project you just created.&lt;/li&gt;
&lt;li&gt;Navigate to APIs &amp;amp; Services &amp;gt; Library.&lt;/li&gt;
&lt;li&gt;Search for "Google+ API" or "People API"
.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd28mt41oa0vktf35v0ey.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd28mt41oa0vktf35v0ey.png" alt="Enable OAuth" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;and click "Enable." This allows your app to access user info.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr9uft7i1f4hyp7nkh2qz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr9uft7i1f4hyp7nkh2qz.png" alt="enabled" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 3. Configure OAuth Consent Screen:&lt;/p&gt;

&lt;p&gt;Still on the "APIs &amp;amp; Services", go to the "OAuth consent screen."&lt;/p&gt;

&lt;p&gt;You will see a "Google Auth Platform not configured yet" message with a "Get started" button—this is normal for a new project. Click "Get started."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose User Type:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Select "External" to allow any Google account to sign in (good for testing). Click "Create."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4dg04gh03z4xpnackq95.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4dg04gh03z4xpnackq95.png" alt=" " width="800" height="319"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add app Information:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;App Name: Enter "Project Collaboration API."&lt;/li&gt;
&lt;li&gt;User Support Email: Use your Gmail address.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;App Logo (optional): Upload a 512x512px image (e.g., a handshake icon).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;App Domain (optional): Skip for now unless you have a production domain.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Developer Contact Information: Add your email.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click "Save and Continue."&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgkv5l2ptq5ejy3vd965r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgkv5l2ptq5ejy3vd965r.png" alt="app info" width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scopes:&lt;/strong&gt;&lt;br&gt;
Click "Add or Remove Scopes."&lt;/p&gt;

&lt;p&gt;Add profile and email (e.g., &lt;a href="https://www.googleapis.com/auth/userinfo.profile" rel="noopener noreferrer"&gt;https://www.googleapis.com/auth/userinfo.profile&lt;/a&gt;, &lt;a href="https://www.googleapis.com/auth/userinfo.email" rel="noopener noreferrer"&gt;https://www.googleapis.com/auth/userinfo.email&lt;/a&gt;) to access basic user info.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxdxtanmq61nj1isubpib.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxdxtanmq61nj1isubpib.png" alt="scopes" width="756" height="1093"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click "Update," then "Save and Continue."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F33eecya8tsykw7pu3dr3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F33eecya8tsykw7pu3dr3.png" alt="scopes" width="800" height="908"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test Users (optional):&lt;/strong&gt;&lt;br&gt;
If prompted, add your Gmail as a test user for development.&lt;/p&gt;

&lt;p&gt;Click "Save and Continue."&lt;/p&gt;

&lt;p&gt;Step 4. Create Credentials:&lt;/p&gt;

&lt;p&gt;In the left sidebar, go to "APIs &amp;amp; Services" &amp;gt; "Credentials."&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click "Create Credentials" &amp;gt; "OAuth 2.0 Client IDs."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjdkalfz2b5tmj3j80wdn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjdkalfz2b5tmj3j80wdn.png" alt="create credentials" width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Select "Web application."&lt;/li&gt;
&lt;li&gt;Set "Authorized redirect URIs" to:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;- http://localhost:8000/auth/google/login/callback/ (for the default Django development server port using localhost).&lt;/code&gt;&lt;br&gt;
&lt;code&gt;- http://127.0.0.1:8000/auth/google/login/callback/ (if you access via 127.0.0.1).&lt;/code&gt;&lt;br&gt;
&lt;code&gt;- Add additional URIs if you use a different port (e.g., http://localhost:8080/auth/google/login/callback/, http://127.0.0.1:8080/auth/google/login/callback/).&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Note:&lt;/em&gt;&lt;/strong&gt; The callback path &lt;code&gt;/auth/google/login/callback/&lt;/code&gt; is the default path used by &lt;code&gt;django-allauth&lt;/code&gt; for the Google provider when the base path is &lt;code&gt;/auth/&lt;/code&gt;. This path is determined by django-allauth’s URL patterns for the Google provider.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click "Create" and note your Client ID and Client Secret.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frdh02ndbx1id7x76sm8z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frdh02ndbx1id7x76sm8z.png" alt="create" width="800" height="784"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Secure Storage&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In .env (create this file in ProjectAPI/):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GOOGLE_CLIENT_ID=your-client-id-here
GOOGLE_CLIENT_SECRET=your-client-secret-here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In ProjectAPI/settings.py:&lt;br&gt;
python&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from decouple import config
GOOGLE_CLIENT_ID = config('GOOGLE_CLIENT_ID')
GOOGLE_CLIENT_SECRET = config('GOOGLE_CLIENT_SECRET')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Task 2: Configure JWT for Authenticated Users
&lt;/h2&gt;

&lt;p&gt;For an API app, JWTs provide stateless authentication, ideal for securing endpoints after Google sign-in. Let’s configure the necessary settings in ProjectAPI/settings.py.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update settings.py&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add the following to enable Django’s authentication, DRF, &lt;code&gt;allauth&lt;/code&gt; for &lt;code&gt;Google OAuth&lt;/code&gt;, and &lt;code&gt;SimpleJWT for token-based API access&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.sites',
    'rest_framework',
    'rest_framework_simplejwt',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.google',
    'core',
]

SITE_ID = 1
AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    'allauth.account.auth_backends.AuthenticationBackend',
]
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
}

SOCIALACCOUNT_PROVIDERS = {
    'google': {
        'SCOPE': ['profile', 'email'],
        'AUTH_PARAMS': {'access_type': 'online'},
    }
}

MIDDLEWARE = [
 ...
    'allauth.account.middleware.AccountMiddleware',  # Required for allauth
]
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Understanding the Settings&lt;br&gt;
Let’s break down what each part does:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;INSTALLED_APPS:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;'django.contrib.auth'&lt;/code&gt;: Enables Django’s user authentication system (e.g., User model, login/logout).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'django.contrib.contenttypes'&lt;/code&gt;: Supports generic relationships, used by other apps.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'django.contrib.sessions'&lt;/code&gt;: Manages user sessions during the OAuth flow (even though we’ll use JWTs for API access).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'django.contrib.messages'&lt;/code&gt;: Handles one-time messages (e.g., success notifications).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'django.contrib.staticfiles'&lt;/code&gt;: Manages static files for the minimal frontend.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'django.contrib.sites'&lt;/code&gt;: Required by allauth to associate users with a site.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'rest_framework':&lt;/code&gt; Adds DRF for API functionality.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'rest_framework_simplejwt'&lt;/code&gt;: Provides JWT authentication for DRF.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'allauth', 'allauth.account'&lt;/code&gt;, 'allauth.socialaccount': Core allauth packages for authentication and social logins.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'allauth.socialaccount.providers.google'&lt;/code&gt;: Enables Google OAuth support.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'core'&lt;/code&gt;: Your custom app for API logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SITE_ID = 1:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Identifies the site your app runs on, required by &lt;code&gt;allauth&lt;/code&gt;—the default SITE_ID of 1 matches the first site in the django_site table (created during migration).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AUTHENTICATION_BACKENDS:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;'django.contrib.auth.backends.ModelBackend'&lt;/code&gt;: Default Django backend for username/password authentication.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;'allauth.account.auth_backends.AuthenticationBackend':&lt;/code&gt; Handles allauth’s authentication, including Google OAuth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;REST_FRAMEWORK:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;'DEFAULT_AUTHENTICATION_CLASSES':&lt;/code&gt; Sets JWT as the default authentication method for DRF. After Google sign-in, users get a JWT to access API endpoints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SOCIALACCOUNT_PROVIDERS:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Configures Google OAuth settings for &lt;code&gt;allauth&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;'SCOPE': ['profile', 'email']: Requests the user’s name and email.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;'AUTH_PARAMS'&lt;/code&gt;: {'access_type': 'online'}: Ensures the OAuth flow is for online access (no refresh tokens needed).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MIDDLEWARE:&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;'allauth.account.middleware.AccountMiddleware'&lt;/code&gt;: Required by &lt;code&gt;django-allauth&lt;/code&gt; (version 0.53.0+) to manage user authentication flows, such as Google OAuth. It must come after &lt;code&gt;AuthenticationMiddleware&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Migrate&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run migrations to apply these changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipenv run python manage.py migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr8kwd2zx4er1ldn1z08b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr8kwd2zx4er1ldn1z08b.png" alt="migrations" width="800" height="677"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Troubleshooting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Error: &lt;code&gt;allauth.account.middleware.AccountMiddleware missing&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you see ImproperlyConfigured: allauth.account.middleware.AccountMiddleware must be added to settings.MIDDLEWARE, ensure you’re using django-allauth version 0.53.0 or higher (check with &lt;code&gt;pipenv run pip show django-allauth&lt;/code&gt;). Add the middleware as shown above, after AuthenticationMiddleware.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Error: ModuleNotFoundError: No module named 'requests':&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The requests library is required by &lt;code&gt;allauth.socialaccount.providers.google&lt;/code&gt; to make HTTP requests to Google’s OAuth endpoints. Install it with:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipenv install requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Error: ModuleNotFoundError: No module named 'cryptography':&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The cryptography library is required by &lt;code&gt;allauth&lt;/code&gt; for secure &lt;code&gt;JWT&lt;/code&gt; handling in Google OAuth. Install it with:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipenv install cryptography
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;em&gt;Configure the Google SocialApp in the Database&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before you can use Google OAuth, you need to configure a SocialApp in the database to store your Google OAuth credentials. &lt;code&gt;django-allauth&lt;/code&gt; uses this to authenticate with Google.&lt;/p&gt;

&lt;p&gt;Step 1. Create a Superuser:&lt;/p&gt;

&lt;p&gt;To access the Django admin interface, create a superuser account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipenv run python manage.py createsuperuser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow the prompts to set a username, email, and password.&lt;/p&gt;

&lt;p&gt;Step 2. Access the Admin Interface:&lt;br&gt;
Start the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipenv run python manage.py runserver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit &lt;code&gt;http://localhost:8000/admin/&lt;/code&gt; and log in with your superuser credentials.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F23wsdgeslww9ywr287id.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F23wsdgeslww9ywr287id.png" alt="Admin dashboard" width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Step 3. Add a Google SocialApp:&lt;/p&gt;

&lt;p&gt;In the admin interface, under Social Accounts, click Social applications &amp;gt; Add social application.&lt;/p&gt;

&lt;p&gt;Fill in the form:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provider: Select "Google".&lt;/li&gt;
&lt;li&gt;Name: Enter "Google OAuth".&lt;/li&gt;
&lt;li&gt;Client ID: Enter the GOOGLE_CLIENT_ID from your .env file.&lt;/li&gt;
&lt;li&gt;Secret key: Enter the GOOGLE_CLIENT_SECRET from your .env file.&lt;/li&gt;
&lt;li&gt;Key: Leave blank.&lt;/li&gt;
&lt;li&gt;Sites: Move the site with SITE_ID=1 (e.g., example.com) to the "Chosen sites" box.&lt;/li&gt;
&lt;li&gt;Click Save.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This step ensures &lt;code&gt;django-allauth&lt;/code&gt; can find the Google credentials when initiating the OAuth flow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpqbg1s4xp9luskujj8q1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpqbg1s4xp9luskujj8q1.png" alt="admin" width="800" height="816"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Task 3: Implement Backend Routes
&lt;/h2&gt;

&lt;p&gt;We’ll set up routes for Google login, token issuance, and a sample /projects/ endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;URLs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In ProjectAPI/urls.py:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('auth/', include('allauth.urls')),
    path('api/', include('core.urls')),
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In core/urls.py:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.urls import path
from . import views

urlpatterns = [
    path('', views.home, name='home'),
    path('token/', views.get_jwt_token, name='get_token'),
    path('projects/', views.projects, name='projects'),
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Views&lt;/strong&gt;&lt;br&gt;
In core/views.py:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.shortcuts import render
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken

def home(request):
    return render(request, 'core/home.html')

@api_view(['GET'])
def get_jwt_token(request):
    if not request.user.is_authenticated:
        return Response({"error": "Please sign in first"}, status=401)
    refresh = RefreshToken.for_user(request.user)
    return Response({
        'refresh': str(refresh),
        'access': str(refresh.access_token),
    })

@api_view(['GET'])
@permission_classes([IsAuthenticated])
def projects(request):
    data = {
        "message": "Welcome to the project collaboration API!",
        "user": request.user.email,
        "projects": ["Sample Project 1", "Sample Project 2"]
    }
    return Response(data)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Custom Adapter&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In core/adapters.py. Add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from allauth.socialaccount.adapter import DefaultSocialAccountAdapter

class CustomSocialAccountAdapter(DefaultSocialAccountAdapter):
    def save_user(self, request, sociallogin, form=None):
        user = super().save_user(request, sociallogin, form)
        sociallogin.state['next'] = '/api/token/'
        return user

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In settings.py. Add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SOCIALACCOUNT_ADAPTER = 'core.adapters.CustomSocialAccountAdapter'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The custom adapter controls what happens after a user logs in with Google. Here’s what it does:&lt;/p&gt;

&lt;p&gt;Why It’s Needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;By default, after a Google login, django-allauth redirects the user to a default URL (e.g., /accounts/profile/) or a URL specified in the &lt;code&gt;next&lt;/code&gt; parameter of the login request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In an API app, you want the user to be redirected to &lt;code&gt;/api/token/&lt;/code&gt; to immediately obtain a JWT for API access, rather than a generic page.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;How It Works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CustomSocialAccountAdapter: This class inherits from DefaultSocialAccountAdapter, the default adapter for social logins in allauth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;save_user Method:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Called by &lt;code&gt;allauth&lt;/code&gt; when a user logs in with Google to create or link a Django User.&lt;/li&gt;
&lt;li&gt;super().save_user(request, sociallogin, form): Calls the default save_user method to handle user creation/linking.&lt;/li&gt;
&lt;li&gt;sociallogin.state['next'] = '/api/token/': Sets the redirect URL to /api/token/ after login. The sociallogin.state dictionary is used by allauth to manage redirect state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SOCIALACCOUNT_ADAPTER Setting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tells &lt;code&gt;allauth&lt;/code&gt; to use your custom adapter (core.adapters.CustomSocialAccountAdapter) instead of the default one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Role in the Flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;After the user logs in with Google and is redirected back to &lt;code&gt;/auth/google/callback/&lt;/code&gt;, allauth uses the custom adapter to set the redirect to /api/token/.&lt;/li&gt;
&lt;li&gt;At &lt;code&gt;/api/token/&lt;/code&gt;, the get_jwt_token view issues a JWT, which the user can use to access protected endpoints like &lt;code&gt;/api/projects/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Troubleshooting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Error: SocialApp.DoesNotExist:&lt;/p&gt;

&lt;p&gt;If you see this error when trying to log in with Google, it means the Google SocialApp is not configured in the database. Ensure you’ve completed adding a Google SocialApp via the Django admin interface, including your &lt;code&gt;GOOGLE_CLIENT_ID&lt;/code&gt; and &lt;code&gt;GOOGLE_CLIENT_SECRET&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frontend&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create templates/core/:&lt;/p&gt;

&lt;p&gt;home.html:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
&amp;lt;head&amp;gt;&amp;lt;title&amp;gt;Project Collaboration API&amp;lt;/title&amp;gt;&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  &amp;lt;h1&amp;gt;Sign In with Google&amp;lt;/h1&amp;gt;
  {% if user.is_authenticated %}
    &amp;lt;p&amp;gt;Welcome, {{ user.first_name }}! &amp;lt;a href="/api/token/"&amp;gt;Get JWT Token&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;&amp;lt;a href="/auth/logout/"&amp;gt;Logout&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;
  {% else %}
    &amp;lt;p&amp;gt;&amp;lt;a href="/auth/google/login/?process=login"&amp;gt;Sign In with Google&amp;lt;/a&amp;gt;&amp;lt;/p&amp;gt;
  {% endif %}
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In settings.py:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],
        'APP_DIRS': True,
        ...
    },
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Flow&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sign-In: &lt;code&gt;/auth/google/login/&lt;/code&gt; → Google → &lt;code&gt;/auth/google/login/callback/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Token: Redirects to&lt;code&gt;/api/token/&lt;/code&gt; for a &lt;code&gt;JWT&lt;/code&gt; (thanks to the custom adapter).&lt;/li&gt;
&lt;li&gt;API: Use the access token in &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; for &lt;code&gt;/api/projects/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing the Authentication
&lt;/h2&gt;

&lt;p&gt;Activate Pipenv:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipenv shell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python manage.py runserver
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit &lt;a href="http://localhost:8000/api/" rel="noopener noreferrer"&gt;http://localhost:8000/api/&lt;/a&gt;, and sign in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F65hsk8gg5dpr89suek1f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F65hsk8gg5dpr89suek1f.png" alt="browsable api" width="800" height="552"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8hn85tdz5ez1s4fq7w2q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8hn85tdz5ez1s4fq7w2q.png" alt="sign in" width="800" height="568"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu5ziaio623rkdzpj433l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu5ziaio623rkdzpj433l.png" alt="sign in" width="800" height="563"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Get your &lt;code&gt;JWT&lt;/code&gt;, and test &lt;code&gt;/api/projects/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -H "Authorization: Bearer &amp;lt;access_token&amp;gt;" http://localhost:8000/api/projects/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fib881ycd4cyn40xb5qqi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fib881ycd4cyn40xb5qqi.png" alt="token" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;You’ve now added Google OAuth to a Django REST API, creating a seamless sign-in option that boosts user experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Summary&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We Set up Google OAuth credentials.&lt;/li&gt;
&lt;li&gt;Configured JWT for authenticated users.&lt;/li&gt;
&lt;li&gt;Configured the Google SocialApp in the database.&lt;/li&gt;
&lt;li&gt;Implemented backend routes for login and API access.&lt;/li&gt;
&lt;li&gt;This foundation is ready for project collaboration features—expand it with your endpoints! Check out &lt;a href="https://django-allauth.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;Django-Allauth&lt;/a&gt; Docs or &lt;a href="https://www.django-rest-framework.org/" rel="noopener noreferrer"&gt;DRF&lt;/a&gt; Docs for more.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What I Offer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Need a custom API solution? Here’s what I bring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; Custom Django REST APIs: Secure, scalable backends tailored to your needs.&lt;/li&gt;
&lt;li&gt; Fast API Development: Using DRF and GraphQL for rapid builds.&lt;/li&gt;
&lt;li&gt; Authentication &amp;amp; Security: JWT, OAuth2, rate limiting, CORS, API keys.&lt;/li&gt;
&lt;li&gt; Database Optimization: Tuning PostgreSQL, MySQL, Redis, or MongoDB.&lt;/li&gt;
&lt;li&gt; Third-Party API Integrations: Stripe, Twilio, OpenAI, Zapier, Google Maps, and more.&lt;/li&gt;
&lt;li&gt; Real-time Features: WebSockets and Celery for dynamic apps.&lt;/li&gt;
&lt;li&gt; Comprehensive API Documentation: Swagger, OpenAPI, or Postman for clarity.
&lt;/li&gt;
&lt;li&gt;Let’s build something amazing together!&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>python</category>
      <category>django</category>
      <category>backenddevelopment</category>
    </item>
    <item>
      <title>How to Build a Task Manager API with Django REST Framework: Part 7 - API Documentation with OpenAPI and Swagger</title>
      <dc:creator>kihuni</dc:creator>
      <pubDate>Wed, 19 Mar 2025 06:47:15 +0000</pubDate>
      <link>https://forem.com/kihuni/how-to-build-a-task-manager-api-with-django-rest-framework-part-7-api-documentation-with-openapi-2fp9</link>
      <guid>https://forem.com/kihuni/how-to-build-a-task-manager-api-with-django-rest-framework-part-7-api-documentation-with-openapi-2fp9</guid>
      <description>&lt;p&gt;Welcome back, to our Django REST Framework (DRF) series!  Over the past six parts, we’ve built a Task Manager API using Django REST Framework (DRF)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/kihuni/how-to-build-a-task-manager-api-with-django-rest-framework-step-by-step-guide-22l8"&gt;Part 1: &lt;/a&gt;we set up Django and DRF.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/kihuni/how-to-build-a-task-manager-api-with-django-rest-framework-part-2-1mn4"&gt;Part 2: &lt;/a&gt; added CRUD operations for tasks.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/kihuni/how-to-build-a-task-manager-api-with-django-rest-framework-part-3-authentication-and-permission-2jb1"&gt;Part 3: &lt;/a&gt; secured it with token authentication.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/kihuni/how-to-build-a-task-manager-api-with-django-rest-framework-part-4-personalize-with-user-owned-a7f"&gt;Part 4: &lt;/a&gt; personalized it with user-owned tasks.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/kihuni/how-to-build-a-task-manager-api-with-django-rest-framework-part-5-optimizing-api-performance-24p6"&gt;Part 5: &lt;/a&gt; optimized it with filtering, pagination, and search.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/kihuni/how-to-build-a-task-manager-api-with-django-rest-framework-part-6-best-practices-for-securing-3n9f"&gt;Part 6: &lt;/a&gt; locked it down with security best practices like rate limiting, CORS, and HTTPS readiness.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, in Part 7—the final part of this series—we’ll make our API developer-friendly by adding interactive API documentation using OpenAPI and Swagger ui. By the end, you’ll have an API with documentation that developers can use to understand and test your endpoints interactively.&lt;/p&gt;

&lt;h1&gt;
  
  
  Table of Contents
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Why API Documentation Matters&lt;/li&gt;
&lt;li&gt;Step 1: Install drf-spectacular&lt;/li&gt;
&lt;li&gt;Step 2: Configure drf-spectacular in Settings&lt;/li&gt;
&lt;li&gt;Step 3: Set Up Swagger UI&lt;/li&gt;
&lt;li&gt;Step 4: Enhance Documentation with Descriptions&lt;/li&gt;
&lt;li&gt;Step 5: Test the Interactive Documentation&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;li&gt;What’s Next?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why API Documentation Matters
&lt;/h2&gt;

&lt;p&gt;API documentation is a bridge between your API and its users (developers). Good documentation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Explains what each endpoint does, its parameters, and expected responses.&lt;/li&gt;
&lt;li&gt;Reduces the learning curve for developers integrating with your API.&lt;/li&gt;
&lt;li&gt;Provides an interactive interface (via Swagger UI) for testing endpoints without external tools like Postman.&lt;/li&gt;
&lt;li&gt;Builds trust by making your API professional and transparent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We’ll use &lt;a href="https://www.openapis.org/" rel="noopener noreferrer"&gt;OpenAPI&lt;/a&gt; (a standard for API specifications) and &lt;a href="https://swagger.io/tools/swagger-ui/" rel="noopener noreferrer"&gt;Swagger UI&lt;/a&gt; (a tool to visualize and interact with the API) to generate our documentation. The &lt;a href="https://drf-spectacular.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;drf-spectacular&lt;/a&gt; library will help us create an &lt;code&gt;OpenAPI schema&lt;/code&gt; for our DRF API and integrate Swagger UI seamlessly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Install drf-spectacular
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://drf-spectacular.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;drf-spectacular&lt;/a&gt; is a library for generating OpenAPI schemas with DRF. It’s well-maintained and supports advanced features like schema customization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install the Package&lt;/strong&gt;&lt;br&gt;
In your project directory, activate your virtual environment (if not already activated) and install &lt;code&gt;drf-spectacular&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install drf-spectacular
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it to your &lt;code&gt;INSTALLED_APPS&lt;/code&gt; in &lt;code&gt;taskmanager/settings.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INSTALLED_APPS = [
    ...

    'drf_spectacular',  # Add drf-spectacular for OpenAPI schema generation
    ...
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Configure drf-spectacular in Settings
&lt;/h2&gt;

&lt;p&gt;Let’s configure &lt;code&gt;drf-spectacular&lt;/code&gt; to generate an OpenAPI schema for our API. We’ll also set it as the default schema generator for DRF.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update taskmanager/settings.py&lt;/strong&gt;&lt;br&gt;
Add the following to your REST_FRAMEWORK settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REST_FRAMEWORK = {
...

# Use drf-spectacular for schema generation
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',

...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;drf-spectacular&lt;/code&gt; settings to customize the API documentation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SPECTACULAR_SETTINGS = {
    'TITLE': 'Task Manager API',
    'DESCRIPTION': 'A RESTful API for managing tasks, built with Django REST Framework. Features include CRUD operations, user authentication, filtering, pagination, search, and security best practices.',
    'VERSION': '1.0.0',
    'SERVE_INCLUDE_SCHEMA': True,  # Include the schema in the Swagger UI
    'SWAGGER_UI_SETTINGS': {
        'deepLinking': True,
        'displayOperationId': True,
        'defaultModelsExpandDepth': 1,
        'defaultModelExpandDepth': 1,
    },
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;'DEFAULT_SCHEMA_CLASS': Tells DRF to use &lt;code&gt;drf-spectacular’s&lt;/code&gt; schema generator instead of the default.&lt;/li&gt;
&lt;li&gt;SPECTACULAR_SETTINGS:

&lt;ul&gt;
&lt;li&gt;TITLE, DESCRIPTION, and VERSION define metadata for your API.&lt;/li&gt;
&lt;li&gt;SERVE_INCLUDE_SCHEMA: Ensures the OpenAPI schema is accessible via Swagger UI.&lt;/li&gt;
&lt;li&gt;SWAGGER_UI_SETTINGS: Customizes the Swagger UI interface for better usability.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Set Up Swagger UI
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;drf-spectacular&lt;/code&gt; provides built-in views for &lt;code&gt;Swagger UI&lt;/code&gt;, &lt;code&gt;Redoc&lt;/code&gt; (another documentation UI), and the raw OpenAPI schema. We’ll set up Swagger UI to provide an interactive interface for testing our API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update taskmanager/urls.py&lt;/strong&gt;&lt;br&gt;
Modify your project’s URL configuration to include &lt;code&gt;drf-spectacular’s views&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from django.contrib import admin
from django.urls import path, include
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('tasks.urls')),  # Your existing API routes
    path('api/schema/', SpectacularAPIView.as_view(), name='schema'),  # Endpoint for the OpenAPI schema
    path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),  # Swagger UI
]

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;SpectacularAPIView: Provides the raw OpenAPI schema at /api/schema/.&lt;/li&gt;
&lt;li&gt;SpectacularSwaggerView: Renders the Swagger UI at /api/docs/, using the schema from /api/schema/.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Enhance Documentation with Descriptions
&lt;/h2&gt;

&lt;p&gt;To make the documentation more informative, let’s add descriptions to our serializers and views using &lt;code&gt;drf-spectacular’s&lt;/code&gt; &lt;code&gt;@extend_schema decorator&lt;/code&gt;. This step is optional but recommended for clarity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update tasks/serializers.py&lt;/strong&gt;&lt;br&gt;
Add a docstring to the TaskSerializer to provide a description that will appear in the OpenAPI schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from rest_framework import serializers
from django.contrib.auth import get_user_model
from .models import Task

User = get_user_model()

class LoginSerializer(serializers.Serializer):
    """Serializer for login credentials."""
    username = serializers.CharField(max_length=150)
    password = serializers.CharField(max_length=128, write_only=True)

class TokenSerializer(serializers.Serializer):
    """Serializer for the token response."""
    token = serializers.CharField()

class TaskSerializer(serializers.ModelSerializer):
    """Serializer for Task objects, including validation for the title field."""
    class Meta:
        model = Task
        fields = ['id', 'title', 'description', 'completed', 'created_at', 'created_by']
        read_only_fields = ['id', 'created_at', 'created_by']

    def validate_title(self, value):
        if len(value) &amp;lt; 3:
            raise serializers.ValidationError("Title must be at least 3 characters long.")
        if not value.replace(' ', '').isalnum():
            raise serializers.ValidationError("Title must be alphanumeric with optional spaces.")
        return value

class UserSerializer(serializers.ModelSerializer):
    """Serializer for User objects during registration."""
    class Meta:
        model = User
        fields = ['id', 'username', 'password']
        extra_kwargs = {'password': {'write_only': True}}

    def create(self, validated_data):
        user = User.objects.create_user(**validated_data)
        return user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The docstring (Serializer for Task objects, including validation for the title field.) will be used by &lt;code&gt;drf-spectacular&lt;/code&gt; as the serializer’s description in the OpenAPI schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update tasks/views.py&lt;/strong&gt;&lt;br&gt;
Add descriptions to the views using @extend_schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from rest_framework import generics, views
from .models import Task
from .serializers import TaskSerializer, UserSerializer, LoginSerializer, TokenSerializer
from django.contrib.auth import authenticate
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.authentication import TokenAuthentication
from rest_framework.authtoken.models import Token
from rest_framework.response import Response
from rest_framework import status
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.pagination import PageNumberPagination
from rest_framework.filters import SearchFilter
from .pagination import CustomPageNumberPagination
from drf_spectacular.utils import extend_schema, OpenApiResponse

class RegisterView(views.APIView):
    permission_classes = [AllowAny]
    serializer_class = UserSerializer

    @extend_schema(
        summary="Register a new user",
        description="Create a new user account. Requires a username and password in the request body.",
        request=UserSerializer,
        responses={
            201: OpenApiResponse(response=TokenSerializer, description="User created successfully"),
            400: OpenApiResponse(description="Invalid data"),
        },
    )
    def post(self, request):
        serializer = UserSerializer(data=request.data)
        if serializer.is_valid():
            user = serializer.save()
            token, _ = Token.objects.get_or_create(user=user)
            return Response({'token': token.key}, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

class LoginView(views.APIView):
    permission_classes = [AllowAny]
    serializer_class = LoginSerializer

    @extend_schema(
        summary="Login to get a token",
        description="Authenticate a user and return a token. Requires username and password.",
        request=LoginSerializer,
        responses={
            200: OpenApiResponse(response=TokenSerializer, description="Token retrieved successfully"),
            400: OpenApiResponse(description="Invalid credentials"),
        },
    )
    def post(self, request):
        serializer = LoginSerializer(data=request.data)
        if serializer.is_valid():
            username = serializer.validated_data['username']
            password = serializer.validated_data['password']
            user = authenticate(username=username, password=password)
            if user:
                token, _ = Token.objects.get_or_create(user=user)
                return Response({'token': token.key}, status=status.HTTP_200_OK)
        return Response({'error': 'Invalid Credentials'}, status=status.HTTP_400_BAD_REQUEST)

@extend_schema(
    summary="List or create tasks",
    description="Retrieve a paginated list of tasks for the authenticated user or create a new task. Supports filtering by completed status (e.g., ?completed=true) and search by title (e.g., ?search=urgent). The title must be at least 3 characters long and alphanumeric (spaces allowed) when creating a task.",
)
class TaskListCreateView(generics.ListCreateAPIView):
    serializer_class = TaskSerializer
    permission_classes = [IsAuthenticated]
    authentication_classes = [TokenAuthentication]
    filter_backends = [DjangoFilterBackend, SearchFilter]
    filterset_fields = ['completed']
    search_fields = ['title']
    pagination_class = CustomPageNumberPagination

    def get_queryset(self):
        if getattr(self, "swagger_fake_view", False):  # Handle schema generation
            return Task.objects.none()  # Return empty queryset for schema generation
        return Task.objects.filter(created_by=self.request.user).order_by('-created_at')

    def perform_create(self, serializer):
        serializer.save(created_by=self.request.user)

@extend_schema(
    summary="Retrieve, update, or delete a task",
    description="Retrieve, update, or delete a specific task by ID, if it belongs to the authenticated user.",
)
class TaskDetailView(generics.RetrieveUpdateDestroyAPIView):
    serializer_class = TaskSerializer
    permission_classes = [IsAuthenticated]
    authentication_classes = [TokenAuthentication]

    def get_queryset(self):
        return Task.objects.filter(created_by=self.request.user)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The @extend_schema decorators provide summaries and descriptions for the combined actions of each generic view, aligning with your implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Test the Interactive Documentation
&lt;/h2&gt;

&lt;p&gt;Let’s access the Swagger UI and test our API endpoints interactively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start the Server&lt;/strong&gt;&lt;br&gt;
Ensure your server is running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python manage.py runserver

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Access Swagger UI&lt;/strong&gt;&lt;br&gt;
Open your browser and navigate to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://127.0.0.1:8000/api/docs/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the Swagger UI interface with the title Task Manager API and a description matching what you set in SPECTACULAR_SETTINGS.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmr83o2pqttg3mnj9six9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmr83o2pqttg3mnj9six9.png" alt="swagger Ui" width="800" height="824"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Test an Endpoint&lt;/strong&gt;&lt;br&gt;
Authenticate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the &lt;a href="http://127.0.0.1:8000/api/login/" rel="noopener noreferrer"&gt;http://127.0.0.1:8000/api/login/&lt;/a&gt; endpoint to get a token:&lt;/li&gt;
&lt;li&gt;Click on POST /api/login/.&lt;/li&gt;
&lt;li&gt;Click Try it out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the request body, enter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "username": "user1",
    "password": "pass123"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click Execute.&lt;/p&gt;

&lt;p&gt;Copy the token from the response (e.g., token1).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftqwy86mu1zgddw7vcykv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftqwy86mu1zgddw7vcykv.png" alt="swagger Ui" width="800" height="755"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Authorize Swagger UI:&lt;br&gt;
Click the Authorize button (top right).&lt;/p&gt;

&lt;p&gt;Enter Token  (e.g., Token token1) in the Value field and click Authorize.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzb5acr7o51uvs92ab5dc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzb5acr7o51uvs92ab5dc.png" alt="authorize" width="800" height="677"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. List Tasks:&lt;/strong&gt;&lt;br&gt;
Go to GET /api/tasks/.&lt;/p&gt;

&lt;p&gt;Click Try it out and Execute.&lt;/p&gt;

&lt;p&gt;Expect a 200 OK response with a paginated list of tasks for the authenticated user.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6vku1w3m7q5govifixe7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6vku1w3m7q5govifixe7.png" alt="Swagger UI" width="800" height="616"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Create a Task:&lt;/strong&gt;&lt;br&gt;
Go to POST /api/tasks/.&lt;/p&gt;

&lt;p&gt;Click Try it out.&lt;/p&gt;

&lt;p&gt;Enter a task in the request body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    "title": "New Task",
    "description": "This is a test task.",
    "completed": false
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click Execute.&lt;/p&gt;

&lt;p&gt;Expect a 201 Created response with the new task’s details.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw9d8iwjry7dq8zomrmr8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw9d8iwjry7dq8zomrmr8.png" alt="swagger UI" width="800" height="731"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Get a Task:&lt;/strong&gt;&lt;br&gt;
Go to GET /api/tasks/{id}/.&lt;/p&gt;

&lt;p&gt;Add query ID (e.g., 6).&lt;/p&gt;

&lt;p&gt;Click Execute.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6f0kskt08nl6oxzw7e48.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6f0kskt08nl6oxzw7e48.png" alt="Get id" width="800" height="617"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Congratulations! You’ve built a fully functional, secure, well-documented Task Manager API with Django REST Framework! Over this series, you’ve learned how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set up a Django project with DRF (Part 1).&lt;/li&gt;
&lt;li&gt;Implement CRUD operations for tasks (Part 2).&lt;/li&gt;
&lt;li&gt;Secure the API with token authentication (Part 3).&lt;/li&gt;
&lt;li&gt;Personalize tasks for users using generic views (Part 4).&lt;/li&gt;
&lt;li&gt;Optimize with filtering, pagination, and search (Part 5).&lt;/li&gt;
&lt;li&gt;Add security best practices like rate limiting and HTTPS (Part 6).&lt;/li&gt;
&lt;li&gt;Generate interactive API documentation with OpenAPI and Swagger (Part 7).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Summary&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Installed &lt;code&gt;drf-spectacular&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; Configured OpenAPI schema generation&lt;/li&gt;
&lt;li&gt; Set up Swagger UI&lt;/li&gt;
&lt;li&gt; Enhanced documentation with descriptions for generic views&lt;/li&gt;
&lt;li&gt; Tested the interactive documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What’s Next?
&lt;/h2&gt;

&lt;p&gt;This series has ended, but your journey as an API developer is just beginning! Here are some ideas to take your Task Manager API to the next level:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy to Production: Deploy your API on a platform like Render, Heroku, or AWS. Use a production-grade server like Gunicorn with Nginx, and enable HTTPS with a free SSL certificate from Let’s Encrypt.&lt;/li&gt;
&lt;li&gt;Add More Features: Implement task categories, due dates, or reminders. Add email notifications for task updates using Django’s email backend.&lt;/li&gt;
&lt;li&gt;Build a Frontend: Create a frontend using React, Vue.js, or Angular to consume your API and provide a user-friendly interface for managing tasks.&lt;/li&gt;
&lt;li&gt;Explore Advanced DRF Features: Dive into DRF’s advanced features like custom permissions, nested serializers, or WebSocket support for real-time updates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thank you for following along with this series! I hope you’ve enjoyed building this API as much as I’ve enjoyed guiding you through it. &lt;/p&gt;

&lt;p&gt;Loved this series? Share your feedback, questions, or what you’d like to see next in the comments below&lt;/p&gt;

</description>
      <category>python</category>
      <category>django</category>
      <category>api</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
