<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged django</title>
<link>https://coderlegion.com/tag/django</link>
<description>Powered by Question2Answer</description>
<item>
<title>Verifying Razorpay Payments: The Order ID You Shouldn't Trust</title>
<link>https://coderlegion.com/33032/verifying-razorpay-payments-the-order-id-you-shouldnt-trust</link>
<description>&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; A valid Razorpay signature proves the payment is real. It does &lt;em&gt;not&lt;/em&gt; prove the payment belongs to the order you're about to mark as paid. Verify against the Razorpay order ID you stored on the server, never the one the client sends, and back it up with a webhook.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Razorpay's checkout flow ends with your server checking a signature. Most tutorials show that check in a few lines, and the check itself is correct. But I recently reviewed a store backend before launch where the check passed and still let one payment mark a &lt;em&gt;different&lt;/em&gt; order as paid. The bug wasn't in the cryptography. It was in where one input came from.&lt;/p&gt;
&lt;h3 id=&quot;how-the-flow-works&quot;&gt;How the flow works&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Your server creates a Razorpay order for the amount and stores its ID (&lt;code&gt;order_xyz&lt;/code&gt;) on your own order.&lt;/li&gt;
&lt;li&gt;The browser opens Razorpay Checkout with that order ID.&lt;/li&gt;
&lt;li&gt;On success, Checkout hands the browser three values: &lt;code&gt;razorpay_order_id&lt;/code&gt;, &lt;code&gt;razorpay_payment_id&lt;/code&gt; and &lt;code&gt;razorpay_signature&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The browser sends them to your server, which checks that the signature is &lt;code&gt;HMAC-SHA256(order_id + &quot;|&quot; + payment_id, key_secret)&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Only Razorpay and your server know &lt;code&gt;key_secret&lt;/code&gt;, so a valid signature proves that Razorpay processed that payment for that Razorpay order.&lt;/p&gt;
&lt;h3 id=&quot;the-bug&quot;&gt;The bug&lt;/h3&gt;
&lt;p&gt;The endpoint looked roughly like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def verify_payment(request):
    order = Order.objects.get(id=request.data[&quot;order_id&quot;], user=request.user)
    rzp_order_id = request.data[&quot;razorpay_order_id&quot;]      # from the client
    rzp_payment_id = request.data[&quot;razorpay_payment_id&quot;]
    signature = request.data[&quot;razorpay_signature&quot;]

    expected = hmac.new(secret, f&quot;{rzp_order_id}|{rzp_payment_id}&quot;.encode(),
                        hashlib.sha256).hexdigest()
    if expected != signature:
        return error(&quot;Payment verification failed&quot;)

    order.payment_status = &quot;paid&quot;
    order.save()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every Razorpay value is checked, but nothing ties them to &lt;em&gt;this&lt;/em&gt; order. The internal &lt;code&gt;order_id&lt;/code&gt; and the Razorpay order ID both come from the client, and nothing checks that they belong together.&lt;/p&gt;
&lt;p&gt;So a buyer can:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a cheap order A and pay ₹99 for it.&lt;/li&gt;
&lt;li&gt;Create an expensive order B and close the checkout without paying.&lt;/li&gt;
&lt;li&gt;Send A's genuine &lt;code&gt;razorpay_order_id&lt;/code&gt;, &lt;code&gt;razorpay_payment_id&lt;/code&gt; and &lt;code&gt;razorpay_signature&lt;/code&gt; with B's internal &lt;code&gt;order_id&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The signature is genuine, so it verifies, and order B is marked as paid.&lt;/p&gt;
&lt;h3 id=&quot;the-fix&quot;&gt;The fix&lt;/h3&gt;
&lt;p&gt;Verify against the Razorpay order ID you stored when you created the order, not one the client sends. Compare in constant time, and make the update idempotent:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import hashlib
import hmac

from django.conf import settings
from django.db import transaction


@transaction.atomic  # required: select_for_update() only locks inside a transaction
def verify_payment(request):
    order = Order.objects.select_for_update().get(
        id=request.data[&quot;order_id&quot;], user=request.user
    )
    if order.payment_status == &quot;paid&quot;:
        return ok(order)  # idempotent: retries and double-clicks are fine

    payment_id = request.data.get(&quot;razorpay_payment_id&quot;, &quot;&quot;)
    signature = request.data.get(&quot;razorpay_signature&quot;, &quot;&quot;)
    expected = hmac.new(
        settings.RAZORPAY_KEY_SECRET.encode(),
        f&quot;{order.razorpay_order_id}|{payment_id}&quot;.encode(),  # stored, not sent
        hashlib.sha256,
    ).hexdigest()

    if not order.razorpay_order_id or not hmac.compare_digest(expected, signature):
        return error(&quot;Payment verification failed&quot;)

    order.payment_status = &quot;paid&quot;
    order.razorpay_payment_id = payment_id
    order.save(update_fields=[&quot;payment_status&quot;, &quot;razorpay_payment_id&quot;])
    return ok(order)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you use the official SDK, &lt;code&gt;client.utility.verify_payment_signature(...)&lt;/code&gt; does the HMAC for you. Just pass it &lt;strong&gt;your stored&lt;/strong&gt; &lt;code&gt;razorpay_order_id&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;don-t-rely-on-the-browser-at-all-add-the-webhook&quot;&gt;Don't rely on the browser at all: add the webhook&lt;/h3&gt;
&lt;p&gt;Even a correct verify endpoint depends on the buyer's browser getting back to your site. People close tabs, networks drop, and phones lock mid-redirect. The money arrives, but the order stays &quot;pending&quot;.&lt;/p&gt;
&lt;p&gt;Razorpay webhooks fix that. Subscribe to &lt;code&gt;payment.captured&lt;/code&gt; (or &lt;code&gt;order.paid&lt;/code&gt;) and treat the webhook as the source of truth:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import hashlib
import hmac
import json

from django.conf import settings
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST


@csrf_exempt
@require_POST
def razorpay_webhook(request):
    body = request.body  # raw bytes, before any JSON parsing
    received = request.headers.get(&quot;X-Razorpay-Signature&quot;, &quot;&quot;)
    expected = hmac.new(settings.RAZORPAY_WEBHOOK_SECRET.encode(), body,
                        hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, received):
        return HttpResponse(status=400)

    event = json.loads(body)
    if event[&quot;event&quot;] == &quot;payment.captured&quot;:
        payment = event[&quot;payload&quot;][&quot;payment&quot;][&quot;entity&quot;]
        Order.objects.filter(
            razorpay_order_id=payment[&quot;order_id&quot;], payment_status=&quot;pending&quot;
        ).update(payment_status=&quot;paid&quot;, razorpay_payment_id=payment[&quot;id&quot;])
    return HttpResponse(status=200)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note three things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The signature is computed over the &lt;strong&gt;raw body&lt;/strong&gt; with the &lt;strong&gt;webhook secret&lt;/strong&gt;, which is a different secret from your API key secret.&lt;/li&gt;
&lt;li&gt;The update is keyed by the Razorpay order ID that Razorpay itself sent, and it only moves &lt;code&gt;pending&lt;/code&gt; to &lt;code&gt;paid&lt;/code&gt;, so duplicate deliveries do nothing. (If the handler does more than a status update, such as sending emails or reducing stock, also de-duplicate on the &lt;code&gt;X-Razorpay-Event-Id&lt;/code&gt; header.)&lt;/li&gt;
&lt;li&gt;The browser flow and the webhook can now race safely. Whichever arrives first marks the order paid, and the other is a no-op.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;checklist&quot;&gt;Checklist&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[ ] The amount is set on the server when the Razorpay order is created, never taken from the client.&lt;/li&gt;
&lt;li&gt;[ ] The stored &lt;code&gt;razorpay_order_id&lt;/code&gt; is used for verification, not the one in the request.&lt;/li&gt;
&lt;li&gt;[ ] Signatures are compared with &lt;code&gt;hmac.compare_digest&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Marking an order as paid is idempotent.&lt;/li&gt;
&lt;li&gt;[ ] A &lt;code&gt;payment.captured&lt;/code&gt; webhook with its own secret is the source of truth.&lt;/li&gt;
&lt;li&gt;[ ] There's a reconciliation job for orders stuck in &quot;pending&quot;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Payment bugs rarely come from broken crypto. They come from trusting an input that looked like it was already verified.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;I'm Paritosh, a lead full-stack engineer working with React, Next.js, Django and FastAPI. I write about the bugs and design decisions I run into while shipping products. This post first appeared on &lt;a rel=&quot;nofollow&quot; href=&quot;https://paritosh-dev.vercel.app/blog/verifying-razorpay-payments-safely&quot;&gt;my blog&lt;/a&gt;. More of my work: &lt;a rel=&quot;nofollow&quot; href=&quot;https://paritosh-dev.vercel.app&quot;&gt;portfolio&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/pk2502&quot;&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/33032/verifying-razorpay-payments-the-order-id-you-shouldnt-trust</guid>
<pubDate>Fri, 30 Oct 2026 03:57:57 +0000</pubDate>
</item>
<item>
<title>How to Protect a Flask or Django App When the WAF Is a Reverse Proxy</title>
<link>https://coderlegion.com/32770/how-to-protect-a-flask-or-django-app-when-the-waf-is-a-reverse-proxy</link>
<description>&lt;p&gt;Python teams usually start web security inside the framework. Django has auth, CSRF middleware, and an ORM. Flask has signed sessions, request hooks, and whatever validators you added. Those controls still matter. They are not the same thing as a web application firewall sitting on the HTTP path. If the question is how to protect a Python web app such as Flask or Django with a WAF, the SafeLine documents support a placement decision, not a language plugin. SafeLine is a self-hosted WAF that filters and monitors HTTP traffic between a web application and the Internet, and it works as a reverse proxy so clients reach the WAF before they reach the server (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). The application guide says the same thing in operational terms: SafeLine receives traffic for the original site, cleans malicious traffic, and forwards safe traffic to the upstream application (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). Those pages do not describe a pip package, a Flask extension, or a Django middleware setting. Do not invent that integration. Decide whether a reverse-proxy shield fits the app you already run.&lt;/p&gt;
&lt;p&gt;Start with the entry path, not the view function. Write down the public hostname, the port clients use, and the real upstream that should stay private. In the SafeLine console, an application record takes a domain, hostname, or IP, a listen port such as 80 or 443, and an upstream address. HTTPS apps use the SSL option (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). After that, the domain must resolve to the SafeLine server. A Gunicorn process, a uWSGI socket, or an existing origin Nginx can remain the upstream. The change is that the public door moves. If a load balancer already terminates TLS, map that hop explicitly. Two public doors means the proxy is optional, and an optional WAF does not protect the app.&lt;/p&gt;
&lt;p&gt;Then check the host before you promise a cutover. The deploy guide requires Linux, x86_64 or arm64, Docker 20.10.14 or above, Docker Compose 2.0.0 or above, a CPU with ssse3, and at least 1 core, 1 GB of memory, and 5 GB of disk (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;). The same page states that a SafeLine install on ARM requires a Pro license and that the Personal Edition is not supported on ARM. A small Flask demo on a laptop does not waive those constraints. If the host fails the check, fix the platform first. Filtering rules will not help if the proxy cannot run.&lt;/p&gt;
&lt;p&gt;Match the documented attack list to routes you actually expose. The introduction names SQL injection, XSS, code injection, OS command injection, CRLF injection, LDAP injection, XPath injection, RCE, XXE, SSRF, path traversal, backdoor, bruteforce, HTTP flood, and bot abuse (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). A Django admin search box, a Flask query parameter, a file download, and a webhook that fetches a URL are different code paths, but they are still HTTP. The proxy is useful because it inspects that traffic before the view runs. It is not useful as a substitute for parameterized queries, template escaping, or a view that shells out to a user-controlled string. Keep those fixes in the app.&lt;/p&gt;
&lt;p&gt;The same introduction lists core capabilities beyond signature-style blocking: defenses for web attacks, proactive bot abuse defense, HTML and JS code encryption, IP-based rate limiting, and a web access control list (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). The feature list then describes rate limiting for traffic that exceeds defined limits, including DoS, bruteforce attempts, traffic surges, and other abuse. It also describes a CAPTCHA challenge intended to allow human users while blocking crawlers and bots, an authentication challenge that requires a password, and dynamic protection that encrypts HTML and JS on each visit when it is turned on. Treat those as perimeter switches in the WAF, not as Django settings or Flask blueprints. The supplied pages do not document numeric thresholds, so do not copy invented limits into a runbook. Turn a control on, watch real traffic, and keep the strictest control off any path you have not tested.&lt;/p&gt;
&lt;p&gt;A published evaluation on 33,669 samples is the only benchmark figure these sources provide. In that table, the SafeLine Balance row shows 71.65% detection, a 0.07% false positive rate, and 99.45% accuracy. The SafeLine Strict row shows 76.17% detection, a 0.22% false positive rate, and 99.38% accuracy (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). Use the table as a published comparison, not as a guarantee for your login form or JSON API. The Strict row detects more and also shows a higher false positive figure. That is a reason to replay ordinary browser flows, API clients, and file uploads after any policy change.&lt;/p&gt;
&lt;p&gt;Prove the proxy with your own hostname. The application guide publishes sample probes for SQL injection, XSS, path traversal, code injection, and XXE, and it says a block page means the attack was stopped. Details then appear in the console attack list (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). Run those patterns only against an application you are authorized to test, with the sample host replaced by your domain. A block proves the request passed through SafeLine. It does not prove that every Django permission check or Flask webhook is correct. Follow with a clean login, a normal form post, and a known-good API call. If a legitimate request is blocked, inspect the detail view before you weaken the whole site.&lt;/p&gt;
&lt;p&gt;The decision is complete when four statements are true. The app is reached over HTTP or HTTPS. One public name points at SafeLine, and the upstream is the real Flask or Django origin. The host meets the documented minimums, including the ARM license note if that architecture applies. Someone owns the attack list after cutover. If one statement is false, harden the Python app and wait. A reverse proxy is the supported path. A framework plugin is not.&lt;/p&gt;
&lt;h4 id=&quot;does-safeline-ship-a-flask-or-django-integration-&quot;&gt;Does SafeLine ship a Flask or Django integration?&lt;/h4&gt;
&lt;p&gt;No. The install and application guides describe a reverse-proxy WAF with a domain, a listen port, and an upstream. They do not document a Python package or a framework setting (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;).&lt;/p&gt;
&lt;h4 id=&quot;which-request-classes-should-a-python-team-map-first-&quot;&gt;Which request classes should a Python team map first?&lt;/h4&gt;
&lt;p&gt;Start with the attacks named in the product introduction: SQL injection, XSS, code injection, command injection, SSRF, path traversal, bruteforce, HTTP flood, and bot abuse are the ones most teams can map to a route (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;).&lt;/p&gt;
&lt;h4 id=&quot;are-rate-limiting-and-captcha-part-of-the-app-code-&quot;&gt;Are rate limiting and CAPTCHA part of the app code?&lt;/h4&gt;
&lt;p&gt;No. Rate limiting, CAPTCHA challenge, authentication challenge, and dynamic HTML and JS protection are WAF features described on the product page (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). Keep CSRF, sessions, and permissions in Flask or Django.&lt;/p&gt;
&lt;h4 id=&quot;what-proves-the-cutover-worked-&quot;&gt;What proves the cutover worked?&lt;/h4&gt;
&lt;p&gt;Point the domain at the SafeLine server, replay the documented probe patterns against your own site, expect a block page, and open the attack detail in the console (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). Then confirm normal user and API traffic still passes.&lt;/p&gt;
&lt;h4 id=&quot;what-host-checks-are-mandatory-&quot;&gt;What host checks are mandatory?&lt;/h4&gt;
&lt;p&gt;Linux, x86_64 or arm64, Docker 20.10.14 or newer, Docker Compose 2.0.0 or newer, ssse3, and at least 1 CPU core, 1 GB memory, and 5 GB disk. ARM also requires a Pro license, and the Personal Edition is not supported there (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;).&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;SafeLine has surpassed 700,000 installations worldwide as of September 30, 2026. SafeLine Pro annual promotional pricing is $650-$880, compared with the regular $1,000 per year.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SafeLine website: &lt;a rel=&quot;nofollow&quot; href=&quot;https://ly.cyberserval.tech/MqhYdmI&quot;&gt;https://ly.cyberserval.tech/MqhYdmI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Demo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://demo.waf.chaitin.com:9443/statistics&quot;&gt;https://demo.waf.chaitin.com:9443/statistics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Official docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chaitin/SafeLine&quot;&gt;https://github.com/chaitin/SafeLine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Pro offer: &lt;a rel=&quot;nofollow&quot; href=&quot;https://cyberserval.tech/landing/lto&quot;&gt;https://cyberserval.tech/landing/lto&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32770/how-to-protect-a-flask-or-django-app-when-the-waf-is-a-reverse-proxy</guid>
<pubDate>Wed, 28 Oct 2026 02:30:19 +0000</pubDate>
</item>
<item>
<title>How to Decide If a Flask or Django App Needs a WAF</title>
<link>https://coderlegion.com/32769/how-to-decide-if-a-flask-or-django-app-needs-a-waf</link>
<description>&lt;p&gt;Protecting a Flask or Django application with a WAF is a placement decision, not a framework-plugin decision. The SafeLine introduction describes a self-hosted web application firewall that filters and monitors HTTP traffic between a web application and the Internet (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). Those pages do not name Flask, Django, WSGI, or ASGI, and they do not document a Python package, middleware class, or SDK. If the requirement is an in-process integration, these sources do not support that promise. What they do support is a reverse-proxy checklist: confirm the host, put inspection in front of the existing process, forward only accepted traffic upstream, and prove a block before calling the app protected.&lt;/p&gt;
&lt;p&gt;SafeLine is documented as an HTTP and HTTPS reverse proxy developed on nginx. It receives traffic for the original site, cleans malicious attack traffic, and forwards safe traffic to the original application (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). The same introduction distinguishes this from a forward proxy. A forward proxy hides a client. This WAF sits in front of the server so clients pass through it before they reach the application (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). For Python, the useful reading is narrow. The Flask or Django process remains the upstream. The language of that process is not a documented configuration field.&lt;/p&gt;
&lt;h3 id=&quot;placement-checks&quot;&gt;Placement checks&lt;/h3&gt;
&lt;p&gt;Start with exposure, not with application code. In the web console, adding an application asks for Domain, Port, and Upstream. Domain may be a domain name, a hostname, or an IP address. Port is the port SafeLine listens on, such as 80 or 443, and HTTPS applications need the SSL option checked. Upstream is the real address of the original app, which is where traffic goes after inspection. After those fields are saved, the domain you entered must resolve to the IP of the server running SafeLine (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). That is the documented protection path. It does not include editing a Django settings module, registering a Flask before-request hook, or installing a Python dependency.&lt;/p&gt;
&lt;p&gt;Treat any assumption about process managers as out of scope. The sources do not require gunicorn, uWSGI, or a particular application server. They require a reachable upstream address. Keep the Python process on the address you enter, and do not invent header, health-check, or load-balancing behavior that the application guide never states.&lt;/p&gt;
&lt;h3 id=&quot;host-checks&quot;&gt;Host checks&lt;/h3&gt;
&lt;p&gt;Run the host checklist before telling anyone the app is covered. The deploy guide requires Linux, x86_64 or arm64, Docker 20.10.14 or above, and Docker Compose 2.0.0 or above. The minimum environment is 1 CPU core, 1 GB of memory, and 5 GB of disk, and the CPU must support ssse3 (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;). An ARM server has a further constraint in that same guide: SafeLine installed on ARM requires a Pro license to work properly, and the Personal Edition is not supported on ARM. If any item fails, stop. A Python app behind an unfinished install is not a protected app.&lt;/p&gt;
&lt;h3 id=&quot;what-you-can-claim&quot;&gt;What you can claim&lt;/h3&gt;
&lt;p&gt;Map documented detections to the routes you actually expose, and leave unsupported claims off the list. The introduction lists defenses for SQL injection, XSS, code injection, OS command injection, CRLF injection, LDAP injection, XPath injection, RCE, XXE, SSRF, path traversal, backdoor, brute force, HTTP flood, and bot abuse. Its core capabilities also include proactive bot-abuse defense, HTML and JS code encryption, IP-based rate limiting, and a web access control list (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). The feature list adds rate limiting for DoS attacks, brute-force attempts, traffic surges, and other abuse by throttling traffic that exceeds defined limits. It also describes a CAPTCHA challenge that allows human users while blocking crawlers and bots, an authentication challenge that blocks visitors who do not enter the password, and dynamic protection that encrypts HTML and JS on each visit (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Those controls can sit beside a Python app, but they are not framework fixes. SQL injection filtering does not replace parameterized queries. XSS filtering does not replace template escaping. Path-traversal and command-injection filtering do not replace safe file and process handling. Rate limiting can sit in front of a login route, but the supplied documents do not publish a Flask-specific or Django-specific threshold, so this checklist must not invent one. The authentication challenge is easy to misread. It is a visitor password gate, not a Django authentication backend and not a Flask session login. Dynamic protection is documented for HTML and JS, so do not assume it rewrites a JSON response body.&lt;/p&gt;
&lt;h3 id=&quot;what-stays-in-the-app&quot;&gt;What stays in the app&lt;/h3&gt;
&lt;p&gt;Keep application controls on the same checklist as proxy controls. SafeLine is described as filtering, monitoring, and blocking malicious HTTP traffic, and as preventing unauthorized data from leaving the app by applying policies that separate malicious traffic from safe traffic (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). That is a traffic decision. Authorization bugs, weak session secrets, leaked configuration, and vulnerable dependencies can still arrive as requests that look ordinary. CSRF checks, permission checks, input validation, and secret handling stay in Flask or Django after the proxy is live.&lt;/p&gt;
&lt;h3 id=&quot;proof-before-extra-controls&quot;&gt;Proof before extra controls&lt;/h3&gt;
&lt;p&gt;Verify with the cases the application guide already publishes, and only against a site you control. The examples cover SQL injection, XSS, path traversal, code injection, and XXE as query-string tests. Replace the sample domain with your own. If the blocking page appears, the guide treats that as evidence that the attack was defended. Then open the console attack list and click detail for the event (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). A checklist is incomplete if the only proof is that the homepage still loads. Legitimate traffic must pass, and a known-bad request from that documented set must be recorded.&lt;/p&gt;
&lt;p&gt;Turn on extra visitor challenges only after that path is proven. Rate limiting, CAPTCHA, the authentication challenge, and dynamic HTML and JS encryption all change what a normal user experiences. The documents describe what each control does. They do not give a Python preset or claim that one mode fits every Django admin and every Flask API. Choose the control that matches an abuse you can name: floods and brute force for rate limiting, bot traffic for CAPTCHA, a shared gate password for the authentication challenge, and HTML or JS exposure for dynamic protection. If you cannot name the abuse, leave the extra control off.&lt;/p&gt;
&lt;p&gt;The documentation states that SafeLine is production-ready (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). That statement does not add a Flask or Django connector, and it does not replace the checks above. Decide yes only when the host meets the deploy requirements, the application entry points at a real upstream, DNS lands on the SafeLine server, a documented attack is blocked and visible in the console, and the app still enforces its own authorization and input rules.&lt;/p&gt;
&lt;h4 id=&quot;does-safeline-document-a-flask-or-django-plugin-&quot;&gt;Does SafeLine document a Flask or Django plugin?&lt;/h4&gt;
&lt;p&gt;No. The install guide and the application guide describe deployment and an HTTP reverse proxy with Domain, Port, and Upstream. They do not document a Python package or framework middleware (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/Deploy&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/Deploy&lt;/a&gt;). Do not plan on an in-app integration that these sources never describe.&lt;/p&gt;
&lt;h4 id=&quot;where-does-the-python-process-belong-&quot;&gt;Where does the Python process belong?&lt;/h4&gt;
&lt;p&gt;It belongs at the upstream address. Clients should reach the domain that resolves to the SafeLine server. SafeLine listens on the configured port, inspects the request, and forwards safe traffic to the real application address (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;). The framework can keep serving on the address you configured as upstream.&lt;/p&gt;
&lt;h4 id=&quot;can-i-drop-csrf-query-safety-or-login-checks-&quot;&gt;Can I drop CSRF, query safety, or login checks?&lt;/h4&gt;
&lt;p&gt;No. Nothing in the cited documents says a WAF replaces application authorization, template escaping, or safe query construction. Use the proxy to filter documented web attacks and abuse patterns, and keep those controls in the app (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;).&lt;/p&gt;
&lt;h4 id=&quot;how-should-i-test-without-inventing-payloads-&quot;&gt;How should I test without inventing payloads?&lt;/h4&gt;
&lt;p&gt;Use the five cases published in the application guide: SQL injection, XSS, path traversal, code injection, and XXE. Point them at your own protected domain. Expect the blocking page, then confirm the event in the console attack list and open its detail (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&quot;&gt;https://docs.waf.chaitin.com/en/GetStarted/AddApplication&lt;/a&gt;).&lt;/p&gt;
&lt;h4 id=&quot;which-extra-control-is-not-an-application-login-&quot;&gt;Which extra control is not an application login?&lt;/h4&gt;
&lt;p&gt;The authentication challenge. When it is turned on, visitors must enter the password or they are blocked (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;). That is a site gate. Flask and Django user accounts remain a separate system, and this checklist should not merge the two.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;SafeLine has surpassed 700,000 installations worldwide as of September 30, 2026. SafeLine Pro annual promotional pricing is $650-$880, compared with the regular $1,000 per year.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SafeLine website: &lt;a rel=&quot;nofollow&quot; href=&quot;https://ly.cyberserval.tech/MqhYdmI&quot;&gt;https://ly.cyberserval.tech/MqhYdmI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Demo: &lt;a rel=&quot;nofollow&quot; href=&quot;https://demo.waf.chaitin.com:9443/statistics&quot;&gt;https://demo.waf.chaitin.com:9443/statistics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Official docs: &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.waf.chaitin.com/en/home&quot;&gt;https://docs.waf.chaitin.com/en/home&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/chaitin/SafeLine&quot;&gt;https://github.com/chaitin/SafeLine&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Pro offer: &lt;a rel=&quot;nofollow&quot; href=&quot;https://cyberserval.tech/landing/lto&quot;&gt;https://cyberserval.tech/landing/lto&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32769/how-to-decide-if-a-flask-or-django-app-needs-a-waf</guid>
<pubDate>Wed, 28 Oct 2026 02:29:47 +0000</pubDate>
</item>
<item>
<title>Is it possible to code while learning something new? Of course, it’s more fun.</title>
<link>https://coderlegion.com/28493/is-it-possible-to-code-while-learning-something-new-of-course-its-more-fun</link>
<description>&lt;p&gt;Hey guys;)&lt;br&gt;
Long time no see.&lt;/p&gt;
&lt;h3 id=&quot;general&quot;&gt;General&lt;/h3&gt;
&lt;p&gt;Actually I wanna change my blog format. Now I’m planning to do a &lt;strong&gt;few posts on X, and then 1-2 times per week do a big post here&lt;/strong&gt;. And I wanna try not only to show what I’ve done, but also how to do it. Some kind of teaching I don’t know yet.&lt;/p&gt;
&lt;h3 id=&quot;new-project-what-is-it-about-&quot;&gt;New project. What is it about?&lt;/h3&gt;
&lt;p&gt;Okay, so since my last post I’ve finished Yakaboo Sales Analysis. You could check it here - &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/kolyapisarenko/Yakaboo-Books-Sales-Analyser&quot;&gt;Yakaboo Project&lt;/a&gt;.&lt;br&gt;
The new project is basic - it’s Secret Santa. But if I wrote just an algorithm it wouldn't be so interesting. So I decided to add Django here and make some websites. But here was a problem, I’ve never worked with Django before. But thanks to &lt;strong&gt;Corey Schafer&lt;/strong&gt; I learnt some basics and even built something. &lt;em&gt;I’ll show the video in the end&lt;/em&gt;.&lt;/p&gt;
&lt;h3 id=&quot;a-bit-about-django&quot;&gt;A bit about Django&lt;/h3&gt;
&lt;p&gt;So it’s a great framework which allows us to build websites using Python. In the beginning I thought that I’ll explain almost everything that I’ve learnt, but I don’t think it’s necessary. So in short, to begin writing you should do&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a project directory and venv (of course you could do it without venv).&lt;/li&gt;
&lt;li&gt;Install Django.&lt;/li&gt;
&lt;li&gt;Create the project using django-admin startproject name_of_project.&lt;/li&gt;
&lt;li&gt;Create an app using django-admin startapp name_of_app&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After that we could do what we wanted.&lt;/p&gt;
&lt;h3 id=&quot;another-amazing-library&quot;&gt;Another amazing library&lt;/h3&gt;
&lt;p&gt;One of my favorite libraries is Faker, which allows generating data. I used it for creating participants (their full name, email and wishlist) for a party. That was the easiest part, after that I started exploring Django. So, I’ll skip all the theories and say only the most important.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;To write html and css inside the app directory you need to create templates (for html) and static (for css and js, about js I’m not sure), and inside these directories create a folder with the same name as app name.&lt;/li&gt;
&lt;li&gt;Inside the views.py file you create a function which returns html response and gives arguments which we could use inside html.&lt;/li&gt;
&lt;li&gt;After creating views functions you could use it in the url of the app, add a pattern to the project and don’t forget to add the app into project settings.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Okay now it’s time to watch some results for now.&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://x.com/NicholaDevua/status/2090096246950187072?s=20&quot;&gt;Link to video&lt;/a&gt;&lt;br&gt;
See you in the next post. I hope it was interesting to read.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28493/is-it-possible-to-code-while-learning-something-new-of-course-its-more-fun</guid>
<pubDate>Thu, 24 Sep 2026 15:05:25 +0000</pubDate>
</item>
<item>
<title>My Django Secret Key Was Public on GitHub - How I Fixed It</title>
<link>https://coderlegion.com/27597/my-django-secret-key-was-public-on-github-how-i-fixed-it</link>
<description>&lt;p&gt;We all make mistakes, especially when starting out. Recently, the ultimate developer mishap happened to me: I accidentally pushed my Django SECRET_KEY to a public GitHub repository.&lt;/p&gt;
&lt;p&gt;In Django, the SECRET_KEY is used for cryptographic signing, like session cookies or password reset tokens. It is essential for protecting sensitive data and preventing tampering or unauthorized access. Leaving it publicly exposed is a massive NO-GO.&lt;/p&gt;
&lt;p&gt;But that is exactly what learning projects are for! Luckily this happened to a project with no real user data. We learn the most from our mistakes, and I now know exactly how to handle this situation. Here is how to fix it:&lt;/p&gt;
&lt;h3 id=&quot;step-1-invalidate-the-old-key-immediately&quot;&gt;Step 1: Invalidate the old key immediately&lt;/h3&gt;
&lt;p&gt;Simply changing the key in your code and pushing it again is not enough. The old key remains visible forever in your GitHub commit history. The first step is damage control, so we need to generate a new key. Start the Python shell in your project terminal:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python manage.py shell
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then enter the following lines:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from django.core.management.utils import get_random_secret_key
print(get_random_secret_key())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first line imports the required utility function and the second line generates the new key and prints it to the terminal so you can copy it.&lt;/p&gt;
&lt;p&gt;*&lt;strong&gt;UPDATE&lt;/strong&gt; (September 21st 2026): Based on reader feedback and GitHub's official security guidance, I’ve updated this section. Rotating the secret key is the single most critical step. Purging your Git history with tools like git-filter-repo is a helpful next step to tidy up, but it does not guarantee absolute security, as old commits can still persist in forks or cached views on GitHub's servers.*&lt;/p&gt;
&lt;h3 id=&quot;step-2-move-the-secret-key-to-environment-variables&quot;&gt;Step 2: Move the Secret Key to environment variables&lt;/h3&gt;
&lt;p&gt;To make sure this never happens again, I moved the key into a .env file. Create this file in your root directory and make sure to add it to your .gitignore file so it never gets tracked by GitHub.&lt;/p&gt;
&lt;p&gt;To load the environment variables from the .env file into the Django project, we use the python-dotenv library. Install it first:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install python-dotenv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next update your settings.py file like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
from dotenv import load_dotenv

load_dotenv()

SECRET_KEY = os.getenv(&quot;SECRET_KEY&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally store your actual key inside the .env file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SECRET_KEY=&quot;your_new_secret_key_here&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;conclusion-mistakes-happen-it-s-how-you-handle-them-that-matters&quot;&gt;Conclusion: Mistakes happen, it's how you handle them that matters&lt;/h3&gt;
&lt;p&gt;Thanks to this faux pas my Django project is now secure and follows configuration management best practices. My brain has permanently saved this lesson so it won't happen again! 😊&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/27597/my-django-secret-key-was-public-on-github-how-i-fixed-it</guid>
<pubDate>Fri, 18 Sep 2026 10:07:50 +0000</pubDate>
</item>
<item>
<title>From Learning Python to Building Real Projects: My Developer Journey</title>
<link>https://coderlegion.com/25602/from-learning-python-to-building-real-projects-my-developer-journey</link>
<description>&lt;p&gt;Hello CoderLegion 👋🏽 — Ready to Begin My Developer Journey Here&lt;/p&gt;
&lt;p&gt;I’m thrilled to join CoderLegion! &lt;/p&gt;
&lt;p&gt;I’m currently on a journey of learning software development, and one core lesson I've already learned is that becoming a developer isn’t about knowing everything upfront. It’s about being willing to learn, build, make mistakes, debug, and iterate.&lt;/p&gt;
&lt;p&gt;My journey so far has introduced me to Python, Django, APIs, databases, and more recently, JavaScript.&lt;/p&gt;
&lt;p&gt;One of the things I enjoy most is moving beyond raw concepts and actually building functional projects. For example, I’ve been developing an &lt;strong&gt;AI-Powered Learning Companion&lt;/strong&gt; in Python. The goal is to build a tool that generates study questions via an AI language model, lets users practice and take tests, evaluates answers, and tracks overall progress.&lt;/p&gt;
&lt;p&gt;Building it has taught me that writing code is only a fraction of development understanding the problem, organizing project structure, and troubleshooting persistent errors are just as vital.&lt;/p&gt;
&lt;p&gt;And yes… there have been plenty of bugs along the way. &lt;/p&gt;
&lt;p&gt;I’m also diving deeper into JavaScript, which marks a strong next phase in my learning path.&lt;/p&gt;
&lt;p&gt;I joined CoderLegion to learn from experienced developers, document the projects I’m building, share what I discover, and connect with fellow tech enthusiasts.&lt;/p&gt;
&lt;p&gt;I’m still learning and figuring things out, but I’m proud of the progress made so far and eager to see where this path leads.&lt;/p&gt;
&lt;p&gt;So, hello CoderLegion! &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25602/from-learning-python-to-building-real-projects-my-developer-journey</guid>
<pubDate>Fri, 28 Aug 2026 08:13:14 +0000</pubDate>
</item>
<item>
<title>How I Moved Real-Time Out of Django (and Made Everything Simpler)</title>
<link>https://coderlegion.com/22825/how-i-moved-real-time-out-of-django-and-made-everything-simpler</link>
<description>&lt;p&gt;Your users want live updates: an order flips to &lt;em&gt;paid&lt;/em&gt;, a notification pops, a dashboard number ticks up without a refresh. The obvious answer in Django is &lt;a rel=&quot;nofollow&quot; href=&quot;https://channels.readthedocs.io/&quot;&gt;Channels&lt;/a&gt;. I reached for it first too. But as our scale grew, the costs of keeping connections in-process pushed me to rethink the architecture. I let Django stop handling WebSockets entirely, and almost everything got simpler. Here's the pattern that replaced it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;Django is a request/response framework. It processes a request, returns a response, and moves on. Real-time notifications — a dashboard ticking up, an order status flipping live — need a server that &lt;em&gt;holds a connection open&lt;/em&gt; for every connected client. That is a fundamentally different job.&lt;/p&gt;
&lt;p&gt;The standard answer in the Django ecosystem is &lt;a rel=&quot;nofollow&quot; href=&quot;https://channels.readthedocs.io/&quot;&gt;Channels&lt;/a&gt;. It moves you to ASGI, gives you consumers, and works well, especially when your real-time needs are deep and bidirectional.&lt;/p&gt;
&lt;p&gt;Ours weren't. We needed server→client push: notify a user when an order completed, or when a payment landed. The connections were mostly idle. Channels gave us that, but it came with costs that started hurting at scale.&lt;/p&gt;
&lt;p&gt;The concrete problem was this: every open WebSocket connection consumed memory and a small slice of CPU time on our app servers. A thousand connected dashboards was fine. Ten thousand, across dozens of tenants, each holding a persistent socket. Suddenly, our workers were doing two very different jobs at once. Serving fast API responses &lt;em&gt;and&lt;/em&gt; babysitting long-lived, mostly-quiet connections. Both suffered.&lt;/p&gt;
&lt;p&gt;What made it brittle was the reconnect storm. Deploy a new build, restart a server, or hit a brief network partition, and every connected client disconnects and &lt;em&gt;immediately reconnects&lt;/em&gt;. All at once. That thundering herd of TLS handshakes, authentication, and subscription requests would spike CPU to 100% on every app server as it tried to spin up thousands of ASGI consumer instances simultaneously, occasionally knocking them over entirely before the retry backoff kicked in.&lt;/p&gt;
&lt;p&gt;There's also the smaller but telling friction of auth. Authenticating the WebSocket handshake means reaching your existing auth (DRF/JWT) from inside an ASGI scope. I ended up writing middleware that reconstructed a DRF &lt;code&gt;Request&lt;/code&gt; out of the raw scope just to reuse the auth I already had. It worked, but it was a sign: I was bending the framework to do something it wasn't built for.&lt;/p&gt;
&lt;p&gt;The turning point was asking: &lt;em&gt;why is my web framework in the business of holding connections at all?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;For a fire-and-forget server→client push, it shouldn't be. There are servers built specifically to hold millions of open connections and do nothing else. The only two things your backend needs to do are &lt;strong&gt;say who's allowed to listen&lt;/strong&gt;, and &lt;strong&gt;publish events&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-solution&quot;&gt;The Solution&lt;/h3&gt;
&lt;p&gt;Put a dedicated pub/sub gateway in front of your app. I used &lt;a rel=&quot;nofollow&quot; href=&quot;https://centrifugal.dev/&quot;&gt;Centrifugo&lt;/a&gt;, a standalone real-time server. Clients connect to &lt;em&gt;it&lt;/em&gt;, not to Django. Django goes back to being a plain, boring WSGI/sync app and plays two small roles:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Issue a short-lived connection token&lt;/strong&gt; that embeds &lt;em&gt;which channels this user may subscribe to&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Publish events&lt;/strong&gt; to the gateway over its HTTP API.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The gateway holds the connections. Django holds nothing.&lt;/p&gt;
&lt;h4 id=&quot;step-1-issue-a-token-that-carries-its-own-permissions&quot;&gt;Step 1: Issue a token that carries its own permissions&lt;/h4&gt;
&lt;p&gt;When a client wants to connect, it asks your backend (over a normal authenticated request) for a connection token. The token is a JWT whose claims list the exact channels the user may subscribe to. The gateway verifies the signature and authorizes subscriptions &lt;strong&gt;without a single database lookup&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# realtime/auth.py
import time
import jwt
from django.conf import settings


class RealtimeTokenService:
    &quot;&quot;&quot;Issues short-lived JWTs for connecting to the realtime gateway.&quot;&quot;&quot;

    def generate_connection_token(self, user) -&amp;gt; str:
        tenant_id = user.tenant_id

        # Channel grants are baked into the token, so the gateway authorizes
        # subscriptions with zero database lookups on connect.
        channels = [
            f&quot;user_{user.id}&quot;,
            f&quot;critical:user_{user.id}&quot;,
            f&quot;tenant_{tenant_id}&quot;,
            f&quot;critical:tenant_{tenant_id}&quot;,
        ]

        claims = {
            &quot;sub&quot;: str(user.id),
            &quot;exp&quot;: int(time.time()) + settings.REALTIME_TOKEN_TTL_SECONDS,
            &quot;info&quot;: {&quot;name&quot;: user.get_full_name(), &quot;email&quot;: user.email},
            &quot;channels&quot;: channels,
        }
        return jwt.encode(claims, settings.REALTIME_HMAC_SECRET, algorithm=&quot;HS256&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the key idea: &lt;strong&gt;token-as-permission&lt;/strong&gt;. The channel names encode your authorization model. A user gets their own &lt;code&gt;user_{id}&lt;/code&gt; channel and their tenant's &lt;code&gt;tenant_{id}&lt;/code&gt; channel, and nothing else. Multi-tenant isolation falls out of the channel naming: a user simply has no grant for another tenant's channel, so the gateway will refuse the subscription.&lt;/p&gt;
&lt;h4 id=&quot;step-2-hand-the-token-out-behind-your-normal-auth&quot;&gt;Step 2: Hand the token out behind your normal auth&lt;/h4&gt;
&lt;p&gt;The endpoint that mints the token is an ordinary authenticated view. Your existing auth stack protects it; no ASGI gymnastics:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# realtime/views.py
class RealtimeTokenView(APIView):
    permission_classes = [IsAuthenticated]

    def post(self, request):
        token = RealtimeTokenService().generate_connection_token(request.user)
        return Response(
            {&quot;token&quot;: token, &quot;socket_url&quot;: settings.REALTIME_SOCKET_URL}
        )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The client takes &lt;code&gt;{token, socket_url}&lt;/code&gt;, opens a WebSocket to the gateway, and subscribes to its channels. Django is now out of the connection entirely.&lt;/p&gt;
&lt;h4 id=&quot;step-3-publish-events-over-http&quot;&gt;Step 3: Publish events over HTTP&lt;/h4&gt;
&lt;p&gt;To push an update, your backend POSTs to the gateway's publish API. No connection state, no sockets: just an HTTP call:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# realtime/publisher.py
import requests
from django.conf import settings


def push_event(channel, event_type, payload, *, recoverable=False, timeout=10.0):
    # The &quot;critical:&quot; namespace is configured on the gateway with message
    # history + recovery, so clients can catch up after a reconnect.
    final_channel = f&quot;critical:{channel}&quot; if recoverable else channel

    resp = requests.post(
        f&quot;{settings.REALTIME_API_URL}/publish&quot;,
        json={
            &quot;channel&quot;: final_channel,
            &quot;data&quot;: {&quot;type&quot;: event_type, &quot;payload&quot;: payload},
        },
        headers={&quot;X-API-Key&quot;: settings.REALTIME_API_KEY},
        timeout=timeout,
    )
    resp.raise_for_status()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice the &lt;code&gt;critical:&lt;/code&gt; channel prefix. On the gateway, that namespace is configured to keep a short history so a client that briefly drops can recover missed messages on reconnect:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;namespaces&quot;: [
    { &quot;name&quot;: &quot;critical&quot;, &quot;history_size&quot;: 50, &quot;history_ttl&quot;: &quot;120s&quot;, &quot;force_recovery&quot;: true }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ephemeral events (a &quot;user is typing&quot; blip) publish to the bare channel; events you can't afford to lose (a completed order) publish to &lt;code&gt;critical:&lt;/code&gt; and survive a reconnect.&lt;/p&gt;
&lt;h4 id=&quot;step-4-make-publishing-reliable-and-don-t-block-the-request&quot;&gt;Step 4: Make publishing reliable, and don't block the request&lt;/h4&gt;
&lt;p&gt;Publishing is a network call to a separate service. You do &lt;strong&gt;not&lt;/strong&gt; want it on the request's critical path, and you want it to survive a transient blip. Wrap it in a Celery task with retries and backoff:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# realtime/tasks.py
import logging
from requests.exceptions import RequestException, HTTPError
from celery import shared_task
from .publisher import push_event

logger = logging.getLogger(__name__)


@shared_task(bind=True, max_retries=3, default_retry_delay=5, retry_backoff=True)
def publish_realtime_event(self, channel, event_type, payload, recoverable=False):
    try:
        push_event(channel, event_type, payload, recoverable=recoverable)
    except HTTPError as exc:
        # A 4xx error (bad API key, malformed payload) is a deterministic
        # failure that retries can't fix. Log it and drop it.
        # A 5xx error might be transient, so retry it.
        if 400 &amp;lt;= exc.response.status_code &amp;lt; 500:
            logger.error(&quot;Deterministic 4xx error on %s, dropping.&quot;, channel)
            return
        raise self.retry(exc=exc)
    except RequestException as exc:
        # Pure network timeouts or connection errors — retry with backoff.
        raise self.retry(exc=exc)
    except Exception:
        # A serialization bug won't fix itself on a retry. Log and drop it,
        # rather than burning three attempts on a guaranteed failure.
        # By not re-raising, Celery marks the task as successful and removes
        # it from the queue — no poison pill looping forever.
        logger.exception(&quot;Dropping unrecoverable realtime event on %s&quot;, channel)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;except&lt;/code&gt; split matters: &lt;strong&gt;retry the transient, drop the deterministic.&lt;/strong&gt; A flaky network deserves another try; a payload that can't be JSON-encoded will fail identically three times in a row, so retrying it is just noise.&lt;/p&gt;
&lt;p&gt;Firing an event from anywhere in your app is now a one-liner:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;publish_realtime_event.delay(
    channel=f&quot;tenant_{order.tenant_id}&quot;,
    event_type=&quot;order.completed&quot;,
    payload={&quot;order_id&quot;: order.id, &quot;total&quot;: str(order.total)},
    recoverable=True,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-this-works&quot;&gt;Why This Works&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Your web tier goes back to being stateless.&lt;/strong&gt; Django stays on WSGI. No ASGI migration, no async server, no sockets held in your workers. You can scale, restart, and deploy the web tier like any other stateless service. The open connections live on the gateway and aren't disturbed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Connections scale independently.&lt;/strong&gt; Holding a lot of idle connections is a specialized job. A purpose-built gateway does it on its own box, tuned for that, while your app servers stay sized for request throughput.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Auth has zero connect-time cost.&lt;/strong&gt; Because the token carries its channel grants, the gateway never calls back into your database to authorize a subscription. Ten thousand clients reconnecting after a blip is ten thousand signature checks, not ten thousand DB queries.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Multi-tenancy is just naming.&lt;/strong&gt; Tenant isolation isn't a separate access-control layer. It's the channel names in the token. There's no code path by which a user can subscribe to a channel they weren't granted.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Delivery is fire-and-forget but durable.&lt;/strong&gt; Publishing happens off the request path via Celery, retries on transient failure, and the &lt;code&gt;critical:&lt;/code&gt; namespace lets clients recover what they missed across a reconnect.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;design-decision-the-token-is-a-cache-so-it-can-go-stale&quot;&gt;Design Decision: The Token Is a Cache, So It Can Go Stale&lt;/h3&gt;
&lt;p&gt;Baking permissions into the token is what buys you DB-free connects. But a token is a snapshot. If a user's access changes (you remove them from a tenant, say) and their token still has 24 hours to live, the gateway will keep honoring the &lt;em&gt;old&lt;/em&gt; grants until it expires. You've traded &lt;strong&gt;revocation freshness&lt;/strong&gt; for &lt;strong&gt;connect-time performance&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That's usually the right trade for a notification stream, but be deliberate about it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Keep the TTL short&lt;/strong&gt; and have the client transparently re-fetch a token. Shorter tokens = smaller stale window.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;For true revocation&lt;/strong&gt;, the gateway can be told to force-disconnect a user or refuse channels server-side. Reach for that only when a stream is sensitive enough to need it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One more operational gotcha: a single gateway is tempting to share across environments, but &lt;code&gt;tenant_42&lt;/code&gt; in staging and &lt;code&gt;tenant_42&lt;/code&gt; in production are the same channel name: cross-talk waiting to happen. If you must share one broker, &lt;strong&gt;prefix channels with the environment&lt;/strong&gt; (&lt;code&gt;prod_tenant_42&lt;/code&gt;, &lt;code&gt;staging_tenant_42&lt;/code&gt;) so the namespaces can't collide.&lt;/p&gt;
&lt;p&gt;And to be honest about the path here: I shipped the Channels version first. It worked. But the friction of running an async server and bridging my existing auth into the ASGI scope is exactly what made the gateway approach worth it. If your real-time needs are deep and bidirectional (collaborative editing, presence), in-process Channels may genuinely fit better. For &quot;push updates to subscribed clients,&quot; letting something else hold the sockets is the simpler system.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-result&quot;&gt;The Result&lt;/h3&gt;
&lt;p&gt;Adding a live update anywhere in the app is now one &lt;code&gt;.delay()&lt;/code&gt; call. The web tier never learned what a WebSocket is. Connections, fan-out, history, and recovery are the gateway's problem; authorization is a signed list of channel names; delivery is a retrying background task. Django went back to doing the thing it's good at. And the real-time problem moved to a server built for it.&lt;/p&gt;
&lt;p&gt;How are you doing real-time in your Django apps — in-process with Channels, or have you pushed connections out to a gateway? I'd love to hear what's worked for you in the comments.&lt;/p&gt;
&lt;p&gt;¡Hasta luego!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22825/how-i-moved-real-time-out-of-django-and-made-everything-simpler</guid>
<pubDate>Thu, 16 Jul 2026 14:21:03 +0000</pubDate>
</item>
<item>
<title>How I Built a &quot;Set It and Forget It&quot; Sync System with Django Signals</title>
<link>https://coderlegion.com/22824/how-i-built-a-set-it-and-forget-it-sync-system-with-django-signals</link>
<description>&lt;p&gt;Change a product's price anywhere in your app, and it instantly syncs to a third-party marketplace. No manual triggers, no polling, no fragile &lt;code&gt;save()&lt;/code&gt; overrides. Here's the signal pattern that powers it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem-1&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;In our app, a product's name or price can change from eight different places. The edit page. Bulk import. The variant editor. A pricing rule engine. An API endpoint that processes webhooks from suppliers. Every few weeks, someone adds a new feature and creates yet another code path that mutates a product.&lt;/p&gt;
&lt;p&gt;We needed every one of those changes, no matter where they came from, to sync to an external marketplace. The naive approach would be to add a sync call to each code path. That's eight places to maintain (and counting), eight chances to forget, and eight places that break if the sync API changes.&lt;/p&gt;
&lt;p&gt;Django's &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.djangoproject.com/en/stable/ref/signals/#post-save&quot;&gt;&lt;code&gt;post_save&lt;/code&gt;&lt;/a&gt; signals solve the discovery problem: hook into a model's save event and you catch every change, from every code path, in one place. Signals get a bad rap — fairly, they make control flow hard to trace. But when you need to react to changes from &lt;em&gt;everywhere&lt;/em&gt; without touching &lt;em&gt;anywhere&lt;/em&gt;, this specific discovery problem is exactly what they were designed for.&lt;/p&gt;
&lt;p&gt;There's a catch, though. If ten prices change in a single request, say, a bulk import, a naive signal handler fires ten times. That's ten API calls in rapid succession. At best, you are wasting resources. At worst, the external API rate-limits you.&lt;/p&gt;
&lt;p&gt;We needed signals to &lt;em&gt;detect&lt;/em&gt; changes, but we needed to batch them.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-solution-1&quot;&gt;The Solution&lt;/h3&gt;
&lt;p&gt;Here's the three-part pattern:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A signal handler&lt;/strong&gt; that collects change references instead of acting on them immediately.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A per-thread set&lt;/strong&gt; that deduplicates for free — adding the same product twice does nothing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A flush callback&lt;/strong&gt; deferred to &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.djangoproject.com/en/stable/topics/db/transactions/#django.db.transaction.on_commit&quot;&gt;&lt;code&gt;transaction.on_commit&lt;/code&gt;&lt;/a&gt; that processes everything once the database transaction lands.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;step-1-register-the-signal&quot;&gt;Step 1: Register the signal&lt;/h4&gt;
&lt;p&gt;In your app's &lt;code&gt;apps.py&lt;/code&gt;, connect &lt;code&gt;post_save&lt;/code&gt; to the model that holds pricing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# shopping/apps.py
from django.apps import AppConfig

class ShoppingAppConfig(AppConfig):
    name = &quot;shopping&quot;

    def ready(self):
        from django.db.models.signals import post_save
        from shopping.models import PriceRecord
        from shopping.signals import on_price_change

        post_save.connect(
            on_price_change,
            sender=PriceRecord,
            dispatch_uid=&quot;shopping_price_change&quot;,
        )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.djangoproject.com/en/stable/topics/signals/#preventing-duplicate-signals&quot;&gt;&lt;code&gt;dispatch_uid&lt;/code&gt;&lt;/a&gt; prevents duplicate connections if &lt;code&gt;ready()&lt;/code&gt; runs twice, a common gotcha during development with auto-reload.&lt;/p&gt;
&lt;h4 id=&quot;step-2-collect-don-t-act&quot;&gt;Step 2: Collect, don't act&lt;/h4&gt;
&lt;p&gt;The signal handler doesn't call an API. It just adds a reference to a set and registers a flush callback:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# shopping/signals.py
import threading
from django.db import transaction

_local = threading.local()


def _get_pending():
    if not hasattr(_local, &quot;pending&quot;):
        _local.pending = set()
    return _local.pending


def on_price_change(sender, instance, **kwargs):
    # Record just enough to look up the product later
    _get_pending().add(instance.object_id)
    transaction.on_commit(flush_changes)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's it. Four lines of logic. The handler doesn't care how many prices changed or where the change came from. It just records what changed and defers action to the flush.&lt;/p&gt;
&lt;p&gt;Notice that we use &lt;code&gt;threading.local()&lt;/code&gt; instead of a standard module-level variable. This ensures that each thread gets its own isolated storage.&lt;/p&gt;
&lt;p&gt;Why &lt;code&gt;transaction.on_commit&lt;/code&gt;? If the transaction rolls back, the price change never happened, so the flush callback is discarded. You never sync data that wasn't committed.&lt;/p&gt;
&lt;h4 id=&quot;step-3-flush-once-per-transaction&quot;&gt;Step 3: Flush once per transaction&lt;/h4&gt;
&lt;p&gt;When the transaction lands, the flush handler fires. It snapshots the set, clears it immediately, and processes everything in one batch:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def flush_changes():
    pending = _get_pending()
    refs = pending.copy()
    pending.clear()

    if not refs:
        return

    from shopping.services import SyncService
    SyncService.handle_price_changes(refs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The key detail&lt;/strong&gt;: we copy the set &lt;em&gt;before&lt;/em&gt; clearing it. If clearing happened after processing, and processing raised an exception, stale refs would linger into future transactions. Snapshot-first is defensive.&lt;/p&gt;
&lt;p&gt;This &lt;code&gt;clear()&lt;/code&gt; is also what keeps requests perfectly isolated. Once a transaction commits and the flush runs, the set is empty and ready for the next request.&lt;/p&gt;
&lt;h4 id=&quot;step-4-resolve-and-dispatch&quot;&gt;Step 4: Resolve and dispatch&lt;/h4&gt;
&lt;p&gt;The service layer resolves the raw references into business entities, groups them by destination, and dispatches one task per group. The grouping is the important part. One API call per store, regardless of how many products changed:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# shopping/services.py
from collections import defaultdict

class SyncService:

    @classmethod
    def _resolve_to_products(cls, refs: set[int]):
        &quot;&quot;&quot;
        Resolve product ID references to actual Product instances
        with their Store relationship.  Fetches everything in ONE
        query using filter(id__in=...), silently ignoring stale
        IDs that no longer exist.
        &quot;&quot;&quot;
        products = Product.objects.filter(
            id__in=refs
        ).select_related(&quot;store&quot;)

        resolved = []
        for product in products:
            resolved.append((product, product.store_id))
        return resolved

    @classmethod
    def handle_price_changes(cls, refs: set[int]):
        resolved = cls._resolve_to_products(refs)

        # Group changes by the store they belong to
        by_store = defaultdict(list)
        for product, store_id in resolved:
            by_store[store_id].append(product)

        # One API call per store, regardless of how many products changed
        for store_id, products in by_store.items():
            sync_to_marketplace.delay(store_id=store_id, products=products)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;sync_to_marketplace&lt;/code&gt; task is a &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.celeryq.dev/en/stable/userguide/tasks.html&quot;&gt;Celery task&lt;/a&gt; that calls the external API. It's configured with retries and backoff for transient failures:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def sync_to_marketplace(self, store_id, products):
    try:
        provider.bulk_update(store_id, products)
    except Exception as exc:
        raise self.retry(exc=exc)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-this-works-1&quot;&gt;Why This Works&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Deduplication is free.&lt;/strong&gt; A &lt;code&gt;set&lt;/code&gt; naturally deduplicates, adding the same &lt;code&gt;object_id&lt;/code&gt; twice is a no-op. If a price changes twice in the same request (say, a pricing rule recalculates it), the flush handler sees it once. And when it fires, it reads the latest price from the database. The most recent value always wins.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Transaction safety is built-in.&lt;/strong&gt; &lt;code&gt;transaction.on_commit&lt;/code&gt; guarantees the flush only runs after the database confirms the change. If the transaction rolls back (a validation error, a constraint violation, a &lt;code&gt;raise&lt;/code&gt; somewhere), the callback is discarded. You never sync phantom data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Batch resolution avoids N+1.&lt;/strong&gt; The service resolves all references in bulk queries, not one at a time. For 50 changed products, it takes exactly 1 query regardless of count. No per-product lazy loading.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;design-decision-avoiding-the-global-state-pitfall&quot;&gt;Design Decision: Avoiding the Global State Pitfall&lt;/h3&gt;
&lt;p&gt;You might be wondering why we used &lt;code&gt;threading.local()&lt;/code&gt; in Step 2 instead of a simple module-level variable like &lt;code&gt;_pending_changes = set()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A basic module-level set shares state across every request handled by the same worker process. If User A's request rolls back, stale refs from their failed transaction sit in the global set. When User B's request commits ten minutes later on the same worker, the flush accidentally picks up both User A's and User B's products.&lt;/p&gt;
&lt;p&gt;By using &lt;code&gt;threading.local()&lt;/code&gt;, we protect against this cross-request leakage. Even if a transaction rolls back, any leftover references in that thread's set will simply be cleared on its next successful commit. And in the rare event a stale reference makes it to the resolver, it evaluates to nothing and is skipped silently.&lt;/p&gt;
&lt;p&gt;One thing to watch for: use &lt;code&gt;.filter()&lt;/code&gt; when looking up products in your resolver, not &lt;code&gt;.get()&lt;/code&gt;. &lt;code&gt;.filter()&lt;/code&gt; returns an empty queryset gracefully. &lt;code&gt;.get()&lt;/code&gt; throws &lt;code&gt;DoesNotExist&lt;/code&gt; and tanks your flush.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-result-1&quot;&gt;The Result&lt;/h3&gt;
&lt;p&gt;The user experience is deceptively simple. A user toggles &quot;Sync product prices&quot; on a store configuration page. From that moment on, any price change from any code path in the application syncs to the external marketplace within seconds.&lt;/p&gt;
&lt;p&gt;No one has to remember to add a sync call to new features. No one has to track down every place a price can change. No one monitors a queue for failures (Celery retries handle that). The system just works.&lt;/p&gt;
&lt;p&gt;How are you handling external API syncs in your Django apps? Are you using signals, or do you prefer a different pattern? Drop your approach in the comments.&lt;/p&gt;
&lt;p&gt;¡Hasta luego!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22824/how-i-built-a-set-it-and-forget-it-sync-system-with-django-signals</guid>
<pubDate>Thu, 16 Jul 2026 14:20:14 +0000</pubDate>
</item>
<item>
<title>I Built a Full-Stack E-Learning Platform with AI Quiz Generation — Here's Everything I Learned</title>
<link>https://coderlegion.com/15748/i-built-a-full-stack-e-learning-platform-with-ai-quiz-generation-heres-everything-learned</link>
<description>&lt;p&gt;I'm Karim, a full-stack developer and AI/ML researcher from Cairo. Over the past year, I built Qodrateman — a production e-learning platform now live at qodrateman.com, serving Arabic-speaking students across 12+ courses.&lt;br&gt;
This isn't a &quot;look what I made&quot; post. It's the full breakdown — stack decisions, what broke, how I fixed it, and what I'd do differently. Let's get into it.&lt;/p&gt;
&lt;p&gt;What Is Qodrateman?&lt;br&gt;
An e-learning platform built for the Arabic-speaking market with:&lt;/p&gt;
&lt;p&gt;Structured courses with instructor/learner role separation&lt;br&gt;
JWT authentication + Role-Based Access Control (RBAC)&lt;br&gt;
AI quiz generation — instructors get a full quiz in under 30 seconds&lt;br&gt;
12+ live courses, 500+ authenticated users&lt;br&gt;
Deployed on Vercel (frontend) + Render (backend) with zero downtime&lt;/p&gt;
&lt;p&gt;The Stack — and the Reasoning Behind Every Choice&lt;br&gt;
React + TypeScript + Tailwind CSS&lt;br&gt;
E-learning UI state is genuinely complex — active course, current lesson, user progress, enrollment status. React handles this cleanly. TypeScript caught dozens of bugs before they ever hit production. Tailwind made responsive design fast without drowning in custom CSS.&lt;br&gt;
Django REST Framework&lt;br&gt;
DRF gives you serializers, viewsets, and a built-in admin panel out of the box. I had a fully functional backend faster than I've seen with any other framework. For REST APIs, it's still my first pick.&lt;br&gt;
PostgreSQL&lt;br&gt;
E-learning data is deeply relational — users, courses, lessons, enrollments, quiz attempts, all interconnected. PostgreSQL handles this naturally. SQLite is fine locally, but never in production.&lt;br&gt;
JWT Auth with simplejwt&lt;br&gt;
pythonREST_FRAMEWORK = {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;'DEFAULT_AUTHENTICATION_CLASSES': (
    'rest_framework_simplejwt.authentication.JWTAuthentication',
),
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;br&gt;
Access token in memory (not localStorage — that's a security risk). Refresh token handles silent re-authentication. Frontend and backend stay fully decoupled.&lt;br&gt;
Vercel + Render&lt;br&gt;
Zero-config deploys on every push with Vercel. Render for the Django backend — free tier works at early stage, scales easily when needed. Total setup time: under 2 hours.&lt;/p&gt;
&lt;p&gt;The Feature I'm Most Proud Of: AI Quiz Generation&lt;br&gt;
Instructors told me quiz creation was their biggest time sink — around 20 minutes per quiz. Writing quality multiple-choice questions is genuinely hard.&lt;br&gt;
I built AI-assisted quiz generation that brings that down to under 30 seconds.&lt;br&gt;
Here's how it works:&lt;/p&gt;
&lt;p&gt;Instructor enters topic + difficulty level&lt;br&gt;
System sends a structured prompt to the AI API&lt;br&gt;
API returns a complete quiz in JSON — questions, options, correct answers&lt;br&gt;
Platform parses and saves it directly to the database&lt;br&gt;
Instructor reviews and publishes in one click&lt;/p&gt;
&lt;p&gt;The structured prompt was the key engineering decision:&lt;br&gt;
pythondef generate_quiz_prompt(topic, num_questions, difficulty):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return f&quot;&quot;&quot;
Generate a quiz about: {topic}
Number of questions: {num_questions}
Difficulty: {difficulty}

Respond ONLY with valid JSON in this exact format:
{{
    &quot;questions&quot;: [
        {{
            &quot;question&quot;: &quot;question text&quot;,
            &quot;options&quot;: [&quot;A&quot;, &quot;B&quot;, &quot;C&quot;, &quot;D&quot;],
            &quot;correct_answer&quot;: &quot;A&quot;,
            &quot;explanation&quot;: &quot;why this is correct&quot;
        }}
    ]
}}
Do not include any text outside the JSON.
&quot;&quot;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Forcing structured output and validating before saving eliminated the inconsistency problem entirely. Early versions failed whenever the model returned extra text around the JSON — strict format constraints fixed this.&lt;/p&gt;
&lt;p&gt;RBAC: Two User Types, Zero Overlap&lt;br&gt;
Instructors and learners have completely separate capabilities. Here's the core of how that's modeled:&lt;br&gt;
python# models.py&lt;br&gt;
class UserProfile(models.Model):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ROLES = [
    ('instructor', 'Instructor'),
    ('learner', 'Learner'),
]
user = models.OneToOneField(User, on_delete=models.CASCADE)
role = models.CharField(max_length=20, choices=ROLES)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;permissions-py&quot;&gt;permissions.py&lt;/h2&gt;
&lt;p&gt;class IsInstructor(BasePermission):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def has_permission(self, request, view):
    return (
        request.user.is_authenticated and
        request.user.userprofile.role == 'instructor'
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This permission class protects every instructor-only endpoint — course creation, quiz generation, student progress. Learners can read published content and submit their own quiz attempts. Nothing more.&lt;/p&gt;
&lt;p&gt;What Broke in Production (and How I Fixed It)&lt;br&gt;
The N+1 Query Problem&lt;br&gt;
The course listing API was slow. Every course card needed the instructor name, lesson count, and enrollment count — three queries per course. With 12+ courses, that's 36+ database hits per page load.&lt;br&gt;
Fix:&lt;br&gt;
pythoncourses = Course.objects.select_related('instructor').annotate(&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lesson_count=Count('lessons'),
enrollment_count=Count('enrollments')
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;)&lt;br&gt;
One query. Page load time dropped by 70%.&lt;br&gt;
Inconsistent AI Responses&lt;br&gt;
Early quiz generation would sometimes return extra text around the JSON, breaking json.loads().&lt;br&gt;
Fix — strip everything outside the JSON object before parsing:&lt;br&gt;
pythondef parse_ai_response(response_text):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;try:
    start = response_text.index('{')
    end = response_text.rindex('}') + 1
    return json.loads(response_text[start:end])
except (ValueError, json.JSONDecodeError):
    raise ValidationError(&quot;AI response could not be parsed&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Render Cold Starts&lt;br&gt;
Render's free tier spins down inactive services. First request after inactivity: 30+ seconds. Terrible UX.&lt;br&gt;
Fix: a lightweight ping endpoint that a Vercel cron job hits every 14 minutes. Cost: zero. Cold starts for active users: eliminated.&lt;/p&gt;
&lt;p&gt;What I'd Do Differently&lt;br&gt;
Add React Query from day one. Managing server state with raw useEffect and useState gets messy fast. React Query handles caching, refetching, and loading states far more cleanly. I retrofitted it partway through — not fun.&lt;br&gt;
Map the permission model before writing code. I refactored RBAC twice because I hadn't thought through edge cases upfront. Can instructors see other instructors' content? Can learners preview unenrolled courses? Draw this out before touching code.&lt;br&gt;
Use Docker locally from the start. &quot;Works on my machine&quot; is a real problem. Docker Compose makes the jump from local to production significantly smoother.&lt;/p&gt;
&lt;p&gt;By the Numbers&lt;br&gt;
MetricResultCourses live12+Authenticated users500+Quiz generation time~20 min → under 30 secPost-launch rollbacks0Downtime since launch0&lt;/p&gt;
&lt;p&gt;What's Next&lt;br&gt;
I'm also building Letra — a custom-trained bilingual OCR mobile app for iOS and Android using TensorFlow, Keras, PaddleOCR, and OpenCV. Currently at 98% English accuracy on 800+ labeled samples, deployed to TestFlight and Google Play Beta. Arabic OCR is in active iteration targeting 85%+.&lt;/p&gt;
&lt;p&gt;The hardest part of building a real product isn't writing the code. It's the decisions you make before you write any — schema design, permission models, deployment architecture. Get those right and everything else follows.&lt;br&gt;
If you're building on Django + React or have questions about the AI quiz generation approach, drop a comment. Happy to dig into any part of this.&lt;/p&gt;
&lt;p&gt;Full-stack dev &amp;amp; AI/ML researcher based in Cairo&lt;br&gt;
 karimkhamis.com ·  qodrateman.com ·  github.com/karim-99-99&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15748/i-built-a-full-stack-e-learning-platform-with-ai-quiz-generation-heres-everything-learned</guid>
<pubDate>Fri, 24 Apr 2026 21:16:50 +0000</pubDate>
</item>
<item>
<title>Between Tradition and Modernity: Building a Full Stack App with Django and React</title>
<link>https://coderlegion.com/14774/between-tradition-and-modernity-building-a-full-stack-app-with-django-and-react</link>
<description>&lt;p&gt;If I tell you I made a full-stack app with &lt;a rel=&quot;nofollow&quot; href=&quot;https://react.dev/blog/2024/12/05/react-19&quot;&gt;React 19&lt;/a&gt; (+ &lt;a rel=&quot;nofollow&quot; href=&quot;https://react.dev/learn/react-compiler&quot;&gt;Compiler&lt;/a&gt;), &lt;a rel=&quot;nofollow&quot; href=&quot;https://tanstack.com/router/latest&quot;&gt;Tanstack Router&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://tailwindcss.com/&quot;&gt;tailwindcss&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://base-ui.com/&quot;&gt;Base UI&lt;/a&gt;, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://pnpm.io/&quot;&gt;pnpm&lt;/a&gt;, you would probably expect a &lt;a rel=&quot;nofollow&quot; href=&quot;https://hono.dev/&quot;&gt;Hono backend&lt;/a&gt; or Tanstack Start &lt;a rel=&quot;nofollow&quot; href=&quot;https://tanstack.com/start/latest/docs/framework/react/guide/server-functions&quot;&gt;Server functions&lt;/a&gt; with &lt;a rel=&quot;nofollow&quot; href=&quot;https://orm.drizzle.team/&quot;&gt;Drizzle&lt;/a&gt;, or at least some cutting-edge TS solution. Well, I've built a &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.django-rest-framework.org/&quot;&gt;Django&lt;/a&gt; backend, and it works pretty well!&lt;/p&gt;
&lt;h3 id=&quot;the-constraints-vs-the-freedom&quot;&gt;The Constraints vs. The Freedom&lt;/h3&gt;
&lt;p&gt;When starting a new project, you always face constraints. Your job as the person designing the software architecture is to find the most elegant way to build around them.&lt;/p&gt;
&lt;p&gt;Here are the constraints I was handed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Django for the backend (to match the team's existing legacy projects).&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.keycloak.org/&quot;&gt;Keycloak&lt;/a&gt; for authentication.&lt;/li&gt;
&lt;li&gt;Dockerizing the entire stack for self-hosted deployment.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The freedom? I got to decide absolutely everything else.&lt;/p&gt;
&lt;p&gt;If you’ve read my previous blog posts, you know I’ve fallen in love with the Tanstack ecosystem. I went with Tanstack Router, Query, Form, Table, and Pacer. Notice that I &lt;em&gt;didn't&lt;/em&gt; go with Tanstack Start. Given the actual goals of this app, I couldn't justify the SSR overhead, and I absolutely did not want to spend a single second fixing hydration issues (I still have nightmares about them).&lt;/p&gt;
&lt;p&gt;For the UI, I finally got to use TailwindCSS and shadcn/ui at work, freeing myself from plain CSS and the horrors of &lt;code&gt;styled-components&lt;/code&gt;. I really love the pattern of creating headless, reusable components:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-tsx&quot;&gt;export function PageHeader({ className, children, ...props }: React.HTMLAttributes&amp;lt;HTMLElement&amp;gt;) {
  return (
    &amp;lt;header
      className={cn('grid auto-rows-min items-start gap-2', className)}
      data-slot=&quot;page-header&quot;
      {...props}
    &amp;gt;
      {children}
    &amp;lt;/header&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;(Yes, it kind of looks like styled-components in a way. Maybe time is a flat circle?)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A quick shoutout to two other bangers in the frontend stack:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://inlang.com/m/gerre34r/library-inlang-paraglideJs&quot;&gt;Paraglide JS&lt;/a&gt;:&lt;/strong&gt; After fighting with &lt;code&gt;react-i18next&lt;/code&gt; (lack of type safety, fetching all keys client-side), I switched to Paraglide JS on &lt;a rel=&quot;nofollow&quot; href=&quot;https://tanstack.com/router/latest/docs/guide/internationalization-i18n#tanstack-router--paraglide-client-only&quot;&gt;Tanstack's recommendation&lt;/a&gt;. Zero downsides so far.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://knip.dev/&quot;&gt;Knip&lt;/a&gt;:&lt;/strong&gt; Analyzes your codebase for unused files, exports, and dependencies. Even with the strictest ESLint/Prettier setup, you’ll have dead code. Knip is a godsend for cleanup.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;bridging-the-gap-the-auth-weirdness&quot;&gt;Bridging the Gap: The Auth Weirdness&lt;/h3&gt;
&lt;p&gt;Let's talk about the &lt;em&gt;weird&lt;/em&gt; authentication layer in the app, that I am not a big fan of.&lt;/p&gt;
&lt;p&gt;The client authenticates with Keycloak. This means I need to check auth on the frontend (using &lt;code&gt;react-oidc-context&lt;/code&gt; and &lt;code&gt;oidc-client-ts&lt;/code&gt;, which have pretty bad documentation) and &lt;a rel=&quot;nofollow&quot; href=&quot;https://tanstack.com/router/latest/docs/how-to/setup-authentication#2-configure-router&quot;&gt;store the auth context&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I then pass the key to the backend during API calls, where Django verifies it using &lt;code&gt;jwt.decode&lt;/code&gt; against the public key. It doesn't sound that bad, except that Django has its own pre-built auth system with user tables, and Keycloak isn't designed to store app-specific user metadata. So, I had to build a weird, performant sync layer between the two (e.g., if a Keycloak email changes, reflecting it locally in the Postgres DB). It’s clunky, but it works.&lt;/p&gt;
&lt;h3 id=&quot;type-safety-is-still-possible&quot;&gt;Type Safety is still possible&lt;/h3&gt;
&lt;p&gt;I love type safety. Coming from TypeScript, and having played with C and Dart in school, dynamic typing gives me hives.&lt;/p&gt;
&lt;p&gt;Out of the box, Python can feel a bit like the Wild West compared to a strict TypeScript setup. With standard dependencies often living in a simple &lt;code&gt;.txt&lt;/code&gt; file and optional linting, it gives you a lot of freedom. But that freedom means you have to actively put in the work to enforce a strong Developer Experience, otherwise code quality can slip quickly.&lt;/p&gt;
&lt;p&gt;However, having a Python backend and a TypeScript frontend doesn't mean you have to sacrifice end-to-end type safety. Here is how I forced the two to play nice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Django properly defines the models with strict types and comments.&lt;/li&gt;
&lt;li&gt;Backend views have full documentation on response types using those models.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://drf-spectacular.readthedocs.io/en/latest/&quot;&gt;drf-spectacular&lt;/a&gt; generates the OpenAPI specs.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://orval.dev/&quot;&gt;Orval&lt;/a&gt; generates TS types and query hooks from those specs.&lt;/li&gt;
&lt;li&gt;The frontend consumes the &lt;a rel=&quot;nofollow&quot; href=&quot;https://tanstack.com/query/latest&quot;&gt;Tanstack Query Hooks&lt;/a&gt; to fetch data.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And just like that... End-to-end type safety. You know exactly what the endpoint needs, and exactly what it's going to return. When you make a change in a model, you get the feedback all the way to your frontend component.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;dx-making-the-containers-bearable&quot;&gt;DX: Making the Containers Bearable&lt;/h3&gt;
&lt;p&gt;After the users, my top priority is the developers (and code quality). Because of the strict constraints, I got to tear my hair out properly learning Docker. Setting up multiple services, ensuring they communicate, and managing deployment across two environments in a server full of existing apps was a massive headache.&lt;/p&gt;
&lt;p&gt;But once it works, Docker is magic. Starting the project takes one command. You get fully reproducible environments between local dev and production. So now you can use the &quot;But it works on my machine&quot; excuse more confidently.&lt;/p&gt;
&lt;p&gt;I also spent some time creating &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html&quot;&gt;Make commands&lt;/a&gt;. These Docker commands are quite long and spending 5 minutes going up in the terminal history trying to find the specific command to run the tests can be quite annoying. So I wrote a &lt;code&gt;Makefile&lt;/code&gt;. Now, a simple &lt;code&gt;make codegen&lt;/code&gt; spins up the OpenAPI specs and frontend types:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-makefile&quot;&gt;COMPOSE_FILE := docker-compose.dev.yml
ENV_FILE     := .env.local
COMPOSE      := docker compose -f $(COMPOSE_FILE) --env-file $(ENV_FILE)

.PHONY: codegen
codegen: schema types ## Generate both Schema and Types

.PHONY: schema
schema: ## Generate Open API schema from Backend
    $(COMPOSE) exec backend python manage.py spectacular --file openapi.yml --validate

.PHONY: types
types: ## Generate TypeScript types from Open API schema
    $(COMPOSE) exec frontend pnpm run generate-types
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also built a strong CI pipeline. It handles backend linting and formatting, 800+ Django tests, migration checks, OpenAPI schema validation, Frontend Schema Types validation, frontend type checking, frontend build, and finally Playwright tests.&lt;/p&gt;
&lt;p&gt;It sounds heavy, but it only takes ~7 minutes if &lt;em&gt;all&lt;/em&gt; steps run, thanks to aggressive caching, sharding, and parallel jobs. If I only touch backend code, the pipeline finishes in 2 minutes. Stop the pipeline early, run only what changed. It’s worth the initial setup time.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;boring-isn-t-bad&quot;&gt;Boring isn't bad&lt;/h3&gt;
&lt;p&gt;Django is not the most exciting tech, but it’s great for a CRUD app exposing APIs to Postgres. Yes, I still have to handle some complexity: RBAC, Redis caching, querying a Clickhouse DB with raw SQL, and WebSockets for live notifications, but I’m not building a crazy app for millions of users.&lt;/p&gt;
&lt;p&gt;Django is simple, predictable, and LLMs understand it perfectly. Need a cache layer? Two lines of code. It’s fast enough that running 800+ tests (including DB writes) takes 10 seconds.&lt;br&gt;
I still have some issues with it, like if there is an internal server error, an endpoint will return some html by default. So you need a custom middleware to formalize all kinds of errors. And of course, it has to be in Python. But overall: it just works.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-silo-problem-designing-apis-in-a-vacuum&quot;&gt;The Silo Problem: Designing APIs in a Vacuum&lt;/h3&gt;
&lt;p&gt;Now, for a bit of a reality check regarding backend development.&lt;/p&gt;
&lt;p&gt;In theory, backend engineers handle incredibly important tasks: complex business logic, rock-solid security, scaling, and database optimizations. This should put to shame frontend engineers like me who spend 2 hours changing the color of a button.&lt;/p&gt;
&lt;p&gt;But in my early years of corporate experience, my reality has been quite different. I have only encountered codebases where basic software engineering practices, like mandatory PR reviews, automated testing, or even basic linting, just weren't part of the culture. When teams don't put effort into those foundations, you quickly end up with messy codebases, weak RBAC, major security oversights, and poor performance. More importantly, I’ve encountered the &quot;Silo Problem.&quot;&lt;/p&gt;
&lt;p&gt;Here’s a story from a past job. We were building an AI Chat app. I was the solo frontend dev, working with a UI/UX designer, a handful of backend devs, and some AI engineers.&lt;/p&gt;
&lt;p&gt;The backend team held their planning meetings without bringing in the frontend or the UI designer. Because of that disconnect, the database relations and endpoints were designed without the actual client application in mind. The result made zero sense for the UI: they added data models for UI themes using variables that completely clashed with the design system, and created endpoints structured in a way that required six separate workarounds on the client just to render a basic view. I ended up rewriting the frontend logic three separate times to keep up with OpenAPI specs that were handed down after the fact (which, naturally, rarely matched the live responses). It was an incredibly frustrating but valuable lesson in why API design has to be collaborative.&lt;/p&gt;
&lt;p&gt;If you are a backend developer adding a new endpoint, you aren't doing it for fun. You are doing it because the user interface needs that data. Designing APIs without consulting the client-side needs is like building a steering wheel without checking what kind of car it's going into.&lt;/p&gt;
&lt;p&gt;Thankfully, on my current project, I am the frontend, backend, and DevOps engineer. Everything communicates nicely, because I actually talk to myself.&lt;/p&gt;
&lt;h3 id=&quot;the-corporate-reality&quot;&gt;The Corporate Reality&lt;/h3&gt;
&lt;p&gt;I started this new grad job in mid-2025 at a small non-tech company. I turned down a crazy offer at a massive corporation specifically because I wanted the freedom to build good products and actually care about software architecture.&lt;br&gt;
And I did get that freedom. I spent hours refining small details, optimizing DX, and over-engineering the type safety.&lt;br&gt;
But as the months go by, a grounding realization has set in:&lt;/p&gt;
&lt;p&gt;This app will likely never have more than 8 concurrent users.&lt;br&gt;
Looking at the history of internal projects, business priorities pivot fast, and apps are sometimes replaced after a year or two.&lt;/p&gt;
&lt;p&gt;It's a humbling thought. But I've come to see it less as a reason not to care, and more as a reminder to calibrate. Good architecture isn't just about scale, but also it's maintainability, handoff-ability, and not leaving a mess for the next person. Those things matter even if the app never sees a traffic spike.&lt;/p&gt;
&lt;p&gt;But in life, you make compromises. I’m getting paid to learn, I built an architecture I’m proud of, and at the end of the day, I get to log off and live in one of the &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.google.com/search?q=tromsø+aurora&amp;amp;tbm=isch&quot;&gt;most beautiful places in the world&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14774/between-tradition-and-modernity-building-a-full-stack-app-with-django-and-react</guid>
<pubDate>Tue, 14 Apr 2026 11:49:41 +0000</pubDate>
</item>
<item>
<title>How to Send Contact Form Submissions to WhatsApp (Without Building a Backend)</title>
<link>https://coderlegion.com/13364/how-to-send-contact-form-submissions-to-whatsapp-without-building-a-backend</link>
<description>&lt;p&gt;Most contact forms send submissions to email.&lt;/p&gt;
&lt;p&gt;The problem is — &lt;strong&gt;email gets missed&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If you're not checking your inbox constantly, new enquiries can sit there for hours. In many cases, the first person to respond wins the lead, so delays can cost you real opportunities.&lt;/p&gt;
&lt;p&gt;A better approach is to send form submissions somewhere you'll actually see them:&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;WhatsApp&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In this post, I’ll show you how to send contact form submissions to WhatsApp without building your own backend.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem-with-email-only-forms&quot;&gt;The problem with email-only forms&lt;/h3&gt;
&lt;p&gt;Most setups look like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User fills out a form  &lt;/li&gt;
&lt;li&gt;Submission gets emailed  &lt;/li&gt;
&lt;li&gt;You check it later  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This creates a delay — and that delay is where leads are lost.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;option-1-build-it-yourself&quot;&gt;Option 1: Build it yourself&lt;/h3&gt;
&lt;p&gt;You &lt;em&gt;can&lt;/em&gt; send form submissions to WhatsApp yourself, but it’s not simple.&lt;/p&gt;
&lt;p&gt;You’ll need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a backend (Node, Python, etc.)&lt;/li&gt;
&lt;li&gt;WhatsApp Business API access&lt;/li&gt;
&lt;li&gt;message formatting logic&lt;/li&gt;
&lt;li&gt;error handling and fallbacks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For most projects, this quickly becomes overkill.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;option-2-use-a-form-backend&quot;&gt;Option 2: Use a form backend&lt;/h3&gt;
&lt;p&gt;A simpler approach is to use a form backend that handles delivery for you.&lt;/p&gt;
&lt;p&gt;Instead of building everything:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Keep your existing HTML form  &lt;/li&gt;
&lt;li&gt;Send submissions to a backend endpoint  &lt;/li&gt;
&lt;li&gt;Receive them instantly on WhatsApp  &lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-html-form&quot;&gt;Example HTML form&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;
&amp;lt;form action=&quot;https://web2phone.co.uk/api/v1/submit/&quot; method=&quot;POST&quot;&amp;gt;
  &amp;lt;input type=&quot;hidden&quot; name=&quot;public_key&quot; value=&quot;YOUR_PUBLIC_KEY&quot;&amp;gt;

  &amp;lt;input name=&quot;name&quot; placeholder=&quot;Your name&quot; required&amp;gt;
  &amp;lt;input type=&quot;email&quot; name=&quot;email&quot; placeholder=&quot;*Emails are not allowed*&quot; required&amp;gt;
  &amp;lt;textarea name=&quot;message&quot; placeholder=&quot;How can we help?&quot; required&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/13364/how-to-send-contact-form-submissions-to-whatsapp-without-building-a-backend</guid>
<pubDate>Mon, 23 Mar 2026 09:40:10 +0000</pubDate>
</item>
<item>
<title>Meet Seendr: The App That's Bringing Genuine Connection Back to Social Media</title>
<link>https://coderlegion.com/13324/meet-seendr-the-app-thats-bringing-genuine-connection-back-to-social-media</link>
<description>&lt;p&gt;When was the last time you made a real friend online?&lt;/p&gt;
&lt;p&gt;Not a follower. Not a mutual. Not someone who liked your post once and disappeared. An actual friend — someone you talk to regularly, who shares your interests, who you'd call if you had a bad day.&lt;/p&gt;
&lt;p&gt;If you're struggling to answer that, you're not alone. And that's exactly why we built Seendr.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem-nobody-talks-about&quot;&gt;The Problem Nobody Talks About&lt;/h3&gt;
&lt;p&gt;Social media was supposed to connect us. Instead, it turned connection into a competition.&lt;/p&gt;
&lt;p&gt;Today's platforms are built around influence. Your worth is measured by follower counts, likes, and engagement metrics. Conversations happen in the shadow of invisible hierarchies — people with bigger audiences dominate, while everyone else watches from the sidelines. The platforms we use daily are optimized for attention, not for friendship.&lt;/p&gt;
&lt;p&gt;And the result? Adults are lonelier than ever.&lt;/p&gt;
&lt;p&gt;If you've ever tried to actually meet new people online, you know how frustrating it is. You join a Discord server, send a message, and wait hours for a reply that may never come. You scroll through apps that feel like cattle calls. You post on social media hoping someone will reach out — but all you get is likes from strangers who'll forget you by tomorrow.&lt;/p&gt;
&lt;p&gt;The tools we have aren't built for making friends. They're built for building audiences.&lt;/p&gt;
&lt;p&gt;We wanted to change that.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-seendr-&quot;&gt;What Is Seendr?&lt;/h3&gt;
&lt;p&gt;Seendr is a video-first app designed for one thing: helping adults make real friends through shared interests.&lt;/p&gt;
&lt;p&gt;Here's what makes it different:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No vanity metrics.&lt;/strong&gt; There are no follower counts, no likes, no public engagement stats. When you open Seendr, nobody knows if you have 10 connections or 10,000. Every conversation starts on equal ground.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Interest-based matching.&lt;/strong&gt; You don't scroll through random profiles. You select the things you care about — gaming, philosophy, fitness, entrepreneurship, mental health, whatever — and get matched with people who share those passions. The matching algorithm considers shared interests, availability, and communication style.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The 45-second vibe check.&lt;/strong&gt; Instead of committing to an awkward 30-minute call with a stranger, you start with a quick 45-second video chat. If the energy's right, you stay and keep talking. If not, no pressure — move on to the next match. It's like speed-dating for friendships.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Debate rooms and chatrooms.&lt;/strong&gt; Sometimes you don't want a 1-on-1 call. You want to jump into a conversation about something interesting. Seendr has topic-based rooms where people discuss and debate real subjects — &quot;Is AI the best thing to happen to humanity?&quot;, &quot;What's the most overrated career advice?&quot;, whatever the community cares about. It gives you a reason to talk, which kills the awkwardness of cold introductions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You control the pace.&lt;/strong&gt; Not everyone's comfortable jumping straight to video. On Seendr, you can start with text or voice and unlock video when you're ready. No pressure, no judgment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Safety first.&lt;/strong&gt; We take safety seriously. Liveness verification ensures real people are behind every profile. AI-powered moderation catches inappropriate behavior. And a robust reporting system lets the community help keep the space clean.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;who-is-seendr-for-&quot;&gt;Who Is Seendr For?&lt;/h3&gt;
&lt;p&gt;If any of these sound like you, Seendr was built with you in mind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You moved to a new city and your social circle is basically your coworkers&lt;/li&gt;
&lt;li&gt;You work remotely and realize you haven't had a non-work conversation in days&lt;/li&gt;
&lt;li&gt;You're tired of social media making you feel like a spectator instead of a participant&lt;/li&gt;
&lt;li&gt;You want to meet people who actually share your interests, not just your zip code&lt;/li&gt;
&lt;li&gt;You're introverted but still crave meaningful connection — just on your own terms&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Whether you're 18 or 40, a college student or a remote professional, Seendr is built for people who want real conversations with real people.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;try-it-right-now&quot;&gt;Try It Right Now&lt;/h3&gt;
&lt;p&gt;Seendr is live. You can use it today.&lt;/p&gt;
&lt;p&gt;Head over to &lt;strong&gt;seendr.vercel.app&lt;/strong&gt; and see for yourself. Create a profile, pick your interests, and start connecting.&lt;/p&gt;
&lt;p&gt;It's early. We're still building. There are features in our backlog that we're genuinely excited about — and we want your feedback to help shape what comes next.&lt;/p&gt;
&lt;p&gt;But the core experience is there: find someone who gets you, have a real conversation, and maybe — just maybe — make a friend you'll actually keep.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;built-by-people-who-needed-it&quot;&gt;Built By People Who Needed It&lt;/h3&gt;
&lt;p&gt;This isn't a product we built from a boardroom. My teammate and I built Seendr because we've lived the problem. We've felt the loneliness. We've scrolled through platforms that promised connection and delivered performance.&lt;/p&gt;
&lt;p&gt;Seendr is our answer to that. And we'd love for you to be part of it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Try it at &lt;a rel=&quot;nofollow&quot; href=&quot;http://seender.vercel.app&quot;&gt;seendr.vercel.app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If it resonates with you, share it with someone who needs it. The best friendships start with someone taking the first step.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;We're building Seendr in public and sharing every lesson along the way. Follow along for updates on the product, the tech, and the journey. And if you have feedback after trying the app — we genuinely want to hear it.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13324/meet-seendr-the-app-thats-bringing-genuine-connection-back-to-social-media</guid>
<pubDate>Sun, 22 Mar 2026 15:26:15 +0000</pubDate>
</item>
<item>
<title>RepoRadar - match exact stack to jobs</title>
<link>https://coderlegion.com/13062/reporadar-match-exact-stack-to-jobs</link>
<description>&lt;p&gt; I've been job hunting as a full-stack dev. I usually do freelance work but I'm not the best businessman and honestly it would be nice if somebody just told me what to build and I could get paid. But all the jobs on LinkedIn are ghost jobs — they're just accumulating resumes into a black hole and none of them ever actually get checked. You write a whole cover letter for a job that doesn't exist.&lt;/p&gt;
&lt;p&gt;I checked out a bunch of the newer tools. HiringCafe looked like the backend wasn't even hooked up, which is kinda ridiculous. StackJobs wouldn't load. Scoutify wanted money and I was like nah, I'll just build this myself with Claude. Jobright kinda works but the matching wasn't what I wanted — I wanted to be able to search for specific engineering jobs that match my exact tech stack. Like I build with Django, React, TypeScript — I don't want to read through every JD guessing if they use what I use.&lt;/p&gt;
&lt;p&gt;So I built this thing called RepoRadar. I wanted it to be click click results, no friction.&lt;/p&gt;
&lt;h3 id=&quot;how-it-works&quot;&gt;how it works&lt;/h3&gt;
&lt;p&gt;Google SSO → upload your resume → Claude API parses it and auto-selects your tech stack → hit search → see matching jobs sorted by recent. This way you avoid the ghost jobs because everything's pulled fresh. You click a job and it takes you to the actual company page to apply. No middleman.&lt;/p&gt;
&lt;h3 id=&quot;where-the-jobs-come-from&quot;&gt;where the jobs come from&lt;/h3&gt;
&lt;p&gt;After doing some research it turns out most tech companies use one of like four ATS platforms and they all have public APIs. No auth needed. You can hit them as much as you want which is pretty sweet. So what I did is I mapped over 6,000 companies to their ATS platform — Greenhouse, Lever, Ashby, Workable. I created a Celery beat task that hits all of them every morning around 6am and pulls fresh listings.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET https://boards-api.greenhouse.io/v1/boards/stripe/jobs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That gives you every open role at Stripe in JSON. All four platforms work like this.&lt;/p&gt;
&lt;p&gt;I also pull from RemoteOK, Remotive, We Work Remotely, and the monthly HN Who's Hiring thread. But it's mostly from the ATS boards — there's some ridiculous amount that show up on RepoRadar, like 185,000 jobs right now.&lt;/p&gt;
&lt;p&gt;The mapping was honestly the hardest part of the whole project. Took way longer than the actual code, and I kept having to reprocess them and losing my SSH connection to Railway in the middle of it.&lt;/p&gt;
&lt;h3 id=&quot;how-the-matching-works&quot;&gt;how the matching works&lt;/h3&gt;
&lt;p&gt;Every job description gets run through a regex extractor with ~170 keyword patterns — Django, React, PostgreSQL, TypeScript, Next.js, etc. It tags each job with what it detected. When you search it just filters where your stack overlaps with what's in the JD.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;TECH_PATTERNS = {
    'django': r'\bdjango\b',
    'react': r'\breact(?:\.js|js)?\b',
    'postgresql': r'\b(?:postgres(?:ql)?|psql)\b',
    'typescript': r'\btypescript\b',
    'next_js': r'\bnext\.?js\b',
    # ~170 more
}
 
def extract_techs(description_text):
    detected = []
    text_lower = description_text.lower()
    for tech_name, pattern in TECH_PATTERNS.items():
        if re.search(pattern, text_lower):
            detected.append(tech_name)
    return detected
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No ML, no embeddings, no vector search. If I get some traffic maybe I'll add that stuff, who knows. Is it perfect? No. But is it tailored for engineering jobs? Absolutely.&lt;/p&gt;
&lt;p&gt;I also wanted to make it so you could search by what companies are actually building with. Like I build with Claude, so I want to find companies that match my exact workflow. That's kind of the vision.&lt;/p&gt;
&lt;h3 id=&quot;the-stack&quot;&gt;the stack&lt;/h3&gt;
&lt;p&gt;Django 5 / DRF on Railway, React 19 / TypeScript / Vite / Tailwind on Netlify, PostgreSQL, Redis + Celery for background jobs, Claude API for resume parsing, Google OAuth via django-allauth. Railway runs a single container that starts gunicorn and a Celery worker from a start.sh script. Frontend proxies API requests through netlify.toml. OAuth goes straight to Railway because you can't proxy OAuth redirects — learned that the hard way.&lt;/p&gt;
&lt;h3 id=&quot;what-i-learned&quot;&gt;what I learned&lt;/h3&gt;
&lt;p&gt;ATS public APIs are a goldmine. You can hit them way more than you can hit the GitHub API (which wasn't that useful anyway since most company repos are private). There's so many jobs out there it's actually crazy once you start pulling from these endpoints.&lt;/p&gt;
&lt;p&gt;The company-to-ATS mapping took way longer than writing the application. Like, significantly.&lt;/p&gt;
&lt;p&gt;Freshness matters more than volume. The older a posting is, the less likely you are to hear back from anybody. So everything on RepoRadar is sorted recent-first and refreshed daily.&lt;/p&gt;
&lt;p&gt;Originally I had set up a GitHub integration to search repos and find organizations by tech stack, but it turns out most company repos are private so that didn't work the way I wanted. I pivoted to focusing on the ATS boards and remote platforms instead.&lt;/p&gt;
&lt;h3 id=&quot;what-s-not-great&quot;&gt;what's not great&lt;/h3&gt;
&lt;p&gt;6,200 companies is a lot but it's not everything — no Workday, iCIMS, or Taleo coverage yet. Regex misses some edge cases. And if I get concurrent user load I'm gonna have to split Celery out into its own Railway service instead of running it in the same container, but that's all doable.&lt;/p&gt;
&lt;p&gt;Right now I've got it hooked up with Sentry for monitoring. I'd love to add OpenTelemetry if I can get some real users on it. I also set up a bunch of MCP servers — Railway, Chrome MCP — but the Chrome one isn't that helpful because you can't get past the Google SSO login screen with it.&lt;/p&gt;
&lt;h3 id=&quot;try-it&quot;&gt;try it&lt;/h3&gt;
&lt;p&gt;It's free. You can try it and complain to me and tell me what I did wrong — in fact I would love that. It'd be really cool if I can get some users on this thing.&lt;/p&gt;
&lt;p&gt;direct complaint line mnraynor90atgmaildotcom gracias&lt;/p&gt;
</description>
<category>Launches</category>
<guid isPermaLink="true">https://coderlegion.com/13062/reporadar-match-exact-stack-to-jobs</guid>
<pubDate>Tue, 17 Mar 2026 19:06:22 +0000</pubDate>
</item>
<item>
<title>Escaping the Sync Trap: How I Slashed Latency by 10x in a Django-Rust API Gateway</title>
<link>https://coderlegion.com/13072/escaping-the-sync-trap-how-i-slashed-latency-by-10x-in-a-django-rust-api-gateway</link>
<description>&lt;h3 id=&quot;how-i-diagnosed-and-eliminated-synchronous-bottlenecks-in-a-django-rust-api-gateway-migrating-to-asgi-and-pre-warming-caches-for-millisecond-responses-&quot;&gt;How I diagnosed and eliminated synchronous bottlenecks in a Django-Rust API gateway, migrating to ASGI and pre-warming caches for millisecond responses.&lt;/h3&gt;
&lt;p&gt;When building a high-performance backend, the standard playbook is well-known: &lt;strong&gt;offload heavy computational tasks to faster microservices (like Rust)&lt;/strong&gt; and implement an aggressive caching strategy.&lt;/p&gt;
&lt;p&gt;Recently, I did exactly that. My architecture is built around a &lt;strong&gt;Django REST Framework gateway&lt;/strong&gt; sitting behind &lt;strong&gt;Caddy&lt;/strong&gt;, heavily monitored with &lt;strong&gt;Prometheus&lt;/strong&gt; and &lt;strong&gt;Grafana&lt;/strong&gt;.  &lt;/p&gt;
&lt;p&gt;But despite the raw speed of Rust and my caching layers, my dashboards were flashing red. Latency was spiking to brutal 10-second flatlines for my most critical endpoints. Worse, my observability itself started failing, creating &lt;em&gt;silent blind spots&lt;/em&gt; exactly when I needed data the most.&lt;/p&gt;
&lt;p&gt;Here is the detective story of how I used telemetry to hunt down synchronous traps, migrate to a non-blocking async architecture, and implement proactive pre-warming to bring response times down to the millisecond range — all while reclaiming &lt;strong&gt;30% of my idle CPU&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-architecture-a-gateway-and-its-heavy-lifters&quot;&gt;The Architecture: A Gateway and its Heavy Lifters&lt;/h3&gt;
&lt;p&gt;Before diving into the problem, here is a quick look at my setup.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;          ┌──────────────────────────────┐
          │          Nextcloud           │
          │ (Authenticated Client Calls) │
          └──────────────┬───────────────┘
                         │  JWT / API Key
                         ▼
               ┌────────────────────┐
               │  Django Gateway    │
               │ (ASGI, DRF, Caddy) │
               └──────┬─────────────┘
                      │
     ┌────────────────┴────────────────┐
     │                                 │
     ▼                                 ▼
┌───────────────┐              ┌────────────────┐
│ NDVI Service  │              │ Weather Service│
│ (Rust, 8081)  │              │ (Rust, 8090)   │
│  → Postgres    │              │  → MySQL       │
└───────────────┘              └────────────────┘

       ▲
       │ Prometheus &amp;amp; Grafana
       │ (Observability Stack)
       ▼
   System Telemetry + Metrics
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7vebc0f2a5ldvbhzfubp.png&quot; alt=&quot;The Architecture&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Django&lt;/strong&gt; stays as the public API gateway, accepting requests authenticated via &lt;strong&gt;JWT&lt;/strong&gt; or &lt;strong&gt;API keys&lt;/strong&gt;.&lt;br&gt;
It enforces a shared JSON response envelope containing &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;message&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, and &lt;code&gt;errors&lt;/code&gt; to keep all client interactions standardized.&lt;/p&gt;
&lt;p&gt;Specific traffic routes — namely &lt;code&gt;/api/v1/ndvi&lt;/code&gt; and &lt;code&gt;/api/v1/weather/*&lt;/code&gt; — are forwarded directly to my &lt;strong&gt;Rust&lt;/strong&gt; backends:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NDVI microservice&lt;/strong&gt; ingests satellite data into a dedicated &lt;strong&gt;Postgres&lt;/strong&gt; database.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Weather microservice&lt;/strong&gt; relies on a &lt;strong&gt;MySQL&lt;/strong&gt; database and communicates with external providers like &lt;strong&gt;Open-Meteo&lt;/strong&gt; and &lt;strong&gt;NASA POWER&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My &lt;strong&gt;Nextcloud&lt;/strong&gt; instance acts like any other client, presenting either an &lt;code&gt;Authorization: Bearer&lt;/code&gt; token or an &lt;code&gt;X-API-Key&lt;/code&gt;. Django manages this traffic using a specific &lt;code&gt;nextcloud_hmac&lt;/code&gt; throttle configuration before passing the authorized call down to Rust with the original headers intact.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-false-cure-the-worker-starvation-anomaly&quot;&gt;The False Cure: The Worker Starvation Anomaly&lt;/h3&gt;
&lt;p&gt;To protect the system, I implemented aggressive TTL caching (e.g., 1 hour for schema data, 5 minutes for API tokens). However, once I added traffic, my &lt;strong&gt;Grafana&lt;/strong&gt; dashboard revealed a chaotic reality.&lt;/p&gt;
&lt;p&gt;I saw brutal, perfectly flat 8–10 second latency spikes on key endpoints. Crucially, perfectly timed with these latency spikes, my internal &lt;code&gt;/metrics&lt;/code&gt; request rate dropped to zero.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-diagnosis-the-gateway-caching-itself-to-death&quot;&gt;The Diagnosis: The Gateway Caching Itself to Death&lt;/h3&gt;
&lt;p&gt;The telemetry told a story of hidden synchronous bottlenecks:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The Trigger:&lt;/strong&gt; When a high-traffic endpoint like &lt;code&gt;farm-weather-current/GET&lt;/code&gt; experienced a cache miss, the Django gateway had to fetch fresh data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Trap:&lt;/strong&gt; My Django deployment was running using &lt;strong&gt;standard synchronous workers&lt;/strong&gt;. It called the Rust service, which then called the external weather API (taking 2.2+ seconds).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Impact (Worker Starvation):&lt;/strong&gt; Because the Django worker was synchronous, it &lt;em&gt;blocked entirely&lt;/em&gt; for those 2.2 seconds. All incoming traffic got stuck in a queue.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-trap-synchronous-gateway-routing&quot;&gt;The Trap: Synchronous Gateway Routing&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import httpx
from django.http import JsonResponse
from django.views import View

async def async_weather_proxy_view(request):
    async with httpx.AsyncClient(timeout=5.0) as client:
        resp = await client.get(&quot;http://weather-service:8090/api/v1/weather-current&quot;)
        resp.raise_for_status()
        data = resp.json()
    return JsonResponse({&quot;status&quot;: &quot;success&quot;, &quot;data&quot;: data, &quot;errors&quot;: None})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I had successfully offloaded work to Rust — but my synchronous Django workers completely nullified the speed gains.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-first-fix-embracing-the-non-blocking-gateway&quot;&gt;The First Fix: Embracing the Non-Blocking Gateway&lt;/h3&gt;
&lt;p&gt;I needed to decouple the speed of the gateway from the speed of the external API calls it was routing.&lt;br&gt;
I migrated the Django deployment from &lt;strong&gt;synchronous workers&lt;/strong&gt; to an &lt;strong&gt;ASGI&lt;/strong&gt; (Asynchronous Server Gateway Interface) setup, allowing my gateway to handle requests asynchronously.&lt;/p&gt;
&lt;p&gt;I rewrote my proxy views to use asynchronous HTTP clients like &lt;strong&gt;httpx&lt;/strong&gt;:&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-fix-asynchronous-non-blocking-routing&quot;&gt;The Fix: Asynchronous Non-Blocking Routing&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import httpx
from django.http import JsonResponse

async def async_weather_proxy_view(request):
    # The event loop is freed! Django can serve other requests while waiting
    async with httpx.AsyncClient() as client:
        rust_response = await client.get(&quot;http://weather-service:8090/api/v1/weather-current&quot;)
        
    return JsonResponse({
        &quot;status&quot;: &quot;success&quot;,
        &quot;data&quot;: rust_response.json(),
        &quot;errors&quot;: None
    })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The visual evidence on my dashboards was a massive, instant victory:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Observability Restored:&lt;/strong&gt; The metrics scrape line remained unbroken. Django could finally pause a slow weather request, instantly answer the Prometheus scrape, and resume without blocking.&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Instant Internal Routing:&lt;/strong&gt; In my initial setup, a simple internal metrics scrape took ~84ms. After the ASGI migration, that duration dropped to &lt;strong&gt;11ms&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-second-fix-proactive-caching&quot;&gt;The Second Fix: Proactive Caching&lt;/h3&gt;
&lt;p&gt;While the infrastructure was now bulletproof, the end-user experience was still occasionally sluggish.&lt;/p&gt;
&lt;p&gt;With an “on-demand” caching strategy, the very first user to request the weather after a 1-hour cache expiration had to pay the &lt;strong&gt;Cache Miss Penalty&lt;/strong&gt; (waiting ~2.2 seconds for the external API).&lt;/p&gt;
&lt;p&gt;To eliminate this, I &lt;strong&gt;decoupled the data-fetching time from the user-request cycle entirely&lt;/strong&gt;.&lt;br&gt;
I implemented a &lt;strong&gt;Proactive Background Pre-warming&lt;/strong&gt; pattern using a background task (like Celery) that runs every 55 minutes, independently fetching slow data and silently overwriting the cache before it expires.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8gk9f4fawwvmmjypt9xe.png&quot; alt=&quot;caching&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-cache-pre-warmer-celery-example-&quot;&gt;The Cache Pre-Warmer (Celery Example)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from celery import shared_task
from django.core.cache import cache
import requests

@shared_task
def pre_warm_weather_cache():
    # Runs in the background every 55 minutes, shielding the user from the 2.2s wait
    response = requests.get(&quot;http://weather-service:8090/api/v1/weather-current&quot;)
    if response.status_code == 200:
        cache.set(&quot;weather_current_data&quot;, response.json(), timeout=3600)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result? The average latency for critical weather endpoints &lt;strong&gt;plummeted from seconds to milliseconds&lt;/strong&gt; as the hot cache permanently took over.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-grand-slam-optimizing-the-rust-build-pipeline&quot;&gt;The Grand Slam: Optimizing the Rust Build Pipeline&lt;/h3&gt;
&lt;p&gt;The final victory of this new architecture came from pure server efficiency.&lt;br&gt;
During deployments, compiling Rust crates (&lt;code&gt;sqlx&lt;/code&gt;, &lt;code&gt;syn&lt;/code&gt;) from scratch was pegging my 4-core server at 100% CPU, artificially causing timeouts.&lt;/p&gt;
&lt;p&gt;To fix this, I implemented &lt;strong&gt;cargo-chef&lt;/strong&gt; in a &lt;strong&gt;multi-stage Dockerfile&lt;/strong&gt; to strictly cache Rust dependencies.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;multi-stage-dockerfile-for-the-rust-microservice&quot;&gt;Multi-stage Dockerfile for the Rust Microservice&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;FROM rust:1.88-slim AS chef
USER root
RUN cargo install cargo-chef
WORKDIR /app

FROM chef AS planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json

FROM chef AS builder
COPY --from=planner /app/recipe.json recipe.json
# Docker caches this heavy dependency build!
RUN cargo chef cook --release --recipe-path recipe.json
COPY . .
RUN cargo build --release --bin weather-service

FROM debian:bookworm-slim AS runtime
WORKDIR /app
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y libssl-dev ca-certificates
COPY --from=builder /app/target/release/weather-service /usr/local/bin/
EXPOSE 8090
ENTRYPOINT [&quot;/usr/local/bin/weather-service&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Between ASGI, background caching, and Docker layer caching, my total &lt;strong&gt;Node CPU&lt;/strong&gt; now rests comfortably between &lt;strong&gt;11% and 13%&lt;/strong&gt;.&lt;br&gt;
I fundamentally reclaimed &lt;strong&gt;30% of my total server compute capacity&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion-finding-the-next-bottleneck&quot;&gt;Conclusion: Finding the Next Bottleneck&lt;/h3&gt;
&lt;p&gt;Building high-performance API gateways is an ongoing journey of shifting bottlenecks.&lt;/p&gt;
&lt;p&gt;By relying strictly on my telemetry, I proved that &lt;strong&gt;synchronous workers nullify microservice speed&lt;/strong&gt;, validated the immense power of &lt;strong&gt;ASGI&lt;/strong&gt;, and eliminated &lt;strong&gt;cache miss penalties&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;With the gateway running unburdened, my dashboards have revealed one final bottleneck — a 6-to-8 second delay on my token generation endpoint.&lt;br&gt;
Because my CPU is mostly idle, I know exactly what this is: a &lt;strong&gt;database connection pool limitation&lt;/strong&gt; in the Rust service.&lt;/p&gt;
&lt;p&gt;And thanks to my new observability baseline, I know exactly &lt;strong&gt;where to strike next&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13072/escaping-the-sync-trap-how-i-slashed-latency-by-10x-in-a-django-rust-api-gateway</guid>
<pubDate>Tue, 17 Mar 2026 17:16:00 +0000</pubDate>
</item>
<item>
<title>I Learned Python 5 Days Ago. Now I'm Building a Startup With It.</title>
<link>https://coderlegion.com/12545/i-learned-python-5-days-ago-now-im-building-a-startup-with-it</link>
<description>&lt;p&gt;I didn't pick up Python because it was trendy. I picked it up because someone challenged me to — and I don't run from challenges.&lt;/p&gt;
&lt;p&gt;Five days later, I was knee-deep in Django, Docker, and a codebase that refused to cooperate. This is the story of how a dare turned into a startup, what I learned about switching languages under pressure, and why the struggle was worth every frustrating minute.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-app-why-i-m-building-seendr&quot;&gt;The App: Why I'm Building Seendr&lt;/h3&gt;
&lt;p&gt;Before I get into the technical chaos, let me tell you why any of this matters.&lt;/p&gt;
&lt;p&gt;I've been on the other side of the loneliness problem. I've sat on Discord, scrolling through &quot;meet new people&quot; servers, sending messages to strangers and waiting hours — sometimes days — for a reply that often never comes. I've tried group chats that fizzle out after two days. I've opened social media looking for connection and found nothing but highlight reels, follower counts, and a quiet feeling that everyone has friends except me.&lt;/p&gt;
&lt;p&gt;And I know I'm not the only one.&lt;/p&gt;
&lt;p&gt;Social media was built to connect people. Somewhere along the way, it became a stage. Likes replaced conversations. Follower counts created invisible hierarchies. The platforms we use daily are optimized for engagement, not for friendship. If you don't have the followers, you don't have the voice. And if you're just looking for someone to talk to about something you care about — good luck.&lt;/p&gt;
&lt;p&gt;That's what Seendr is for.&lt;/p&gt;
&lt;p&gt;Seendr is a video-first mobile app that connects people through shared interests. There are no follower counts. No likes. No vanity metrics at all. You sign up, choose your interests, and get matched with people who share them. From there, you can do a 45-second video &quot;vibe check&quot; to see if there's chemistry, join topic-based debate rooms, or just have a real conversation with someone new.&lt;/p&gt;
&lt;p&gt;The idea is simple: what if a social platform treated every user as an equal and gave them a reason to talk?&lt;/p&gt;
&lt;p&gt;My teammate and I are building this as a school project and a real startup at the same time. The school requirement was specific — we had to use Django for the backend.&lt;/p&gt;
&lt;p&gt;Django means Python. And I had never written a single line of Python in my life.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-challenge-5-days-to-learn-a-language&quot;&gt;The Challenge: 5 Days to Learn a Language&lt;/h3&gt;
&lt;p&gt;I come from the JavaScript/TypeScript world. NestJS, Express, React, Next.js — that's my comfort zone. When I heard &quot;Django,&quot; my first reaction was resistance. Why can't I just use what I know?&lt;/p&gt;
&lt;p&gt;But then it hit me: this was exactly the kind of challenge I'm always telling other people to embrace. I've said it in conversations, I've thought it during late-night coding sessions — growth happens outside your comfort zone. So when someone essentially dared me to learn Python and build with it, I said yes.&lt;/p&gt;
&lt;p&gt;I gave myself five days.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;day-1-2-the-tutorial-phase-false-confidence-&quot;&gt;Day 1–2: The Tutorial Phase (False Confidence)&lt;/h3&gt;
&lt;p&gt;I started with a crash course on Django. Within two days, I understood the structure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;views.py&lt;/strong&gt; — where you handle requests and return responses&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;urls.py&lt;/strong&gt; — the routing layer that maps URLs to views&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;models.py&lt;/strong&gt; — your database schema, defined as Python classes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;settings.py&lt;/strong&gt; — the central configuration file for everything&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;migrations/&lt;/strong&gt; — Django's way of tracking and applying database changes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;serializers&lt;/strong&gt; — how data gets converted between Python objects and JSON (via Django REST Framework)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Django calls itself a &quot;batteries-included&quot; framework, and it earns that title. Authentication, admin panel, ORM, middleware — it's all built in. Coming from NestJS where you wire up a lot of things manually, I appreciated that.&lt;/p&gt;
&lt;p&gt;I felt good. I felt ready.&lt;/p&gt;
&lt;p&gt;I was not ready.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;day-3-the-real-python-hits&quot;&gt;Day 3: The Real Python Hits&lt;/h3&gt;
&lt;p&gt;Here's the mistake I made: I learned Django's structure but skipped how Python actually works under the hood. Specifically, I glossed over how object-oriented programming is implemented in Python.&lt;/p&gt;
&lt;p&gt;And then I tried to write real code.&lt;/p&gt;
&lt;p&gt;The syntax felt like someone took everything I knew and rearranged it just enough to be infuriating.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Functions don't use curly braces.&lt;/strong&gt; They use indentation. And not just as a style choice — the indentation IS the structure. Miss a tab and your code breaks in ways that give you no useful error message.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;self&lt;/code&gt; is everywhere.&lt;/strong&gt; In NestJS, when you write a method inside a class, &lt;code&gt;this&lt;/code&gt; is implied. In Python, you pass &lt;code&gt;self&lt;/code&gt; explicitly as the first argument to every single method. It felt redundant. It felt verbose. It felt wrong.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The syntax for basic things looks alien.&lt;/strong&gt; List comprehensions, decorators with &lt;code&gt;@&lt;/code&gt; symbols, &lt;code&gt;__init__&lt;/code&gt; as a constructor name, &lt;code&gt;def&lt;/code&gt; instead of &lt;code&gt;function&lt;/code&gt; — every small difference added friction.&lt;/p&gt;
&lt;p&gt;I won't lie. For the first few hours, I was genuinely annoyed. I kept thinking: &quot;This is doing the same thing as what I already know, just in a more annoying way.&quot;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;day-4-the-click&quot;&gt;Day 4: The Click&lt;/h3&gt;
&lt;p&gt;And then something happened.&lt;/p&gt;
&lt;p&gt;I was writing a view class in Django and realized — I'm still making a class. I'm still writing methods inside it. I'm still receiving a request, processing data, and returning a response. The authentication flow I was building was conceptually identical to the JWT auth system I had built in NestJS for a previous project (a URL shortener).&lt;/p&gt;
&lt;p&gt;The patterns were the same. Django just spoke with a different accent.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NestJS has &lt;code&gt;@Controller()&lt;/code&gt; decorators → Django has &lt;code&gt;urls.py&lt;/code&gt; routing&lt;/li&gt;
&lt;li&gt;NestJS has services with business logic → Django has the same, you just organize it differently&lt;/li&gt;
&lt;li&gt;NestJS uses TypeORM or Prisma → Django has its own ORM built in&lt;/li&gt;
&lt;li&gt;Both use middleware, both use serialization, both handle auth with tokens&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once I stopped fighting the syntax and started seeing the underlying architecture, Python stopped feeling foreign. It started feeling like a dialect of the same language I already spoke.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;day-5-docker-tried-to-end-me&quot;&gt;Day 5+: Docker Tried to End Me&lt;/h3&gt;
&lt;p&gt;Just when I thought I was over the hump, Docker entered the chat.&lt;/p&gt;
&lt;p&gt;We were using Docker Compose to run the full stack — Django API, PostgreSQL, Redis, Celery workers. Professional setup, the kind you'd see in a real production environment. I was proud of it.&lt;/p&gt;
&lt;p&gt;Then I ran &lt;code&gt;docker compose up&lt;/code&gt; and got:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ModuleNotFoundError: No module named 'django'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Django was installed. I confirmed it with &lt;code&gt;pip list&lt;/code&gt; inside the container. Python could see every other package. But when the app tried to start, Django didn't exist.&lt;/p&gt;
&lt;p&gt;I spent &lt;strong&gt;twelve hours&lt;/strong&gt; on this bug.&lt;/p&gt;
&lt;p&gt;The root cause? A combination of three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;PIPENV_VENV_IN_PROJECT=1&lt;/code&gt;&lt;/strong&gt; in the Dockerfile was silently overriding the &lt;code&gt;--system&lt;/code&gt; install flag, putting packages in a &lt;code&gt;.venv&lt;/code&gt; folder inside the project directory&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker's volume mount&lt;/strong&gt; was mapping my local &lt;code&gt;api/&lt;/code&gt; folder over the container's &lt;code&gt;/app&lt;/code&gt; directory — which wiped out that &lt;code&gt;.venv&lt;/code&gt; at runtime&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;sed&lt;/code&gt; command I used to try to fix the Dockerfile left a &lt;strong&gt;trailing backslash&lt;/strong&gt; that broke the entire ENV block, causing a cryptic build error&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So the packages were being installed during the build, then erased when the container started. Django was there. Then it wasn't. Every single time.&lt;/p&gt;
&lt;p&gt;I rebuilt with &lt;code&gt;--no-cache&lt;/code&gt; more times than I can count. I read Stack Overflow threads from 2019. I stared at logs until my eyes hurt.&lt;/p&gt;
&lt;p&gt;Eventually, I reached out to the president of Django Cameroon for help. Because sometimes the bravest thing you can do as a developer is admit you're stuck.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-i-actually-learned&quot;&gt;What I Actually Learned&lt;/h3&gt;
&lt;p&gt;This whole experience — the five-day crash course, the syntax battles, the Docker nightmare — taught me more than any tutorial could.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Frameworks are dialects, not different languages.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you understand how web applications work — routing, middleware, authentication, database operations, serialization — you can learn any framework. The concepts transfer. The syntax is just wrapping paper.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Skipping fundamentals will always catch up to you.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I jumped into Django without properly learning Python's OOP model. That shortcut cost me hours of confusion later. Learn the language, then learn the framework. Not the other way around.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Docker is simple until it isn't.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Docker Compose looks clean and elegant in tutorials. In practice, the interaction between build layers, volume mounts, environment variables, and package managers can create invisible bugs that steal entire days. Respect the tooling. Read the docs carefully. And always check what your volume mounts are overwriting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Asking for help is a skill, not a weakness.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I used to sit with broken code for hours because I didn't want to seem stupid. This project broke that habit. When you've been stuck for 12 hours and you message someone who solves it in 10 minutes, you stop caring about looking smart. You just want to learn.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Constraints breed growth.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If I'd been allowed to use NestJS, I would have. And I would have learned nothing new. Being forced into Python and Django expanded my thinking, gave me a new perspective on backend architecture, and made me a more versatile developer.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;where-seendr-is-now&quot;&gt;Where Seendr Is Now&lt;/h3&gt;
&lt;p&gt;We're still building. The Django backend is running (finally). The React Native frontend is taking shape with Expo. We've tested Agora's video SDK for the real-time calling feature. The matching algorithm is in its MVP form — simple scoring based on shared interests, age proximity, and location.&lt;/p&gt;
&lt;p&gt;There's still a mountain of work ahead: the debate rooms, the moderation system, the 45-second vibe check flow, push notifications, and a hundred other features living in our backlog.&lt;/p&gt;
&lt;p&gt;But the foundation is there. And it exists because I said yes to a challenge I wasn't ready for.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;if-you-re-thinking-about-picking-up-a-new-language&quot;&gt;If You're Thinking About Picking Up a New Language&lt;/h3&gt;
&lt;p&gt;Do it. Don't wait until you feel ready. Don't wait until you &quot;have time.&quot; Pick a project that forces you to learn, set a deadline that scares you, and start.&lt;/p&gt;
&lt;p&gt;You'll be frustrated. You'll write terrible code. You'll hit bugs that make you question your career choices. And then, somewhere around day four, something will click. The patterns will emerge. The syntax will stop fighting you. And you'll realize that you're not starting from zero — you're building on everything you've already learned.&lt;/p&gt;
&lt;p&gt;Five days ago, I didn't know Python.&lt;/p&gt;
&lt;p&gt;Now I'm building a startup with it.&lt;/p&gt;
&lt;p&gt;And honestly? I'm just getting started.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;I'm documenting my entire dev journey — from learning new languages to building Seendr — on LinkedIn and here. If you're on a similar path, let's connect. Drop a comment or reach out. We're all figuring this out together.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12545/i-learned-python-5-days-ago-now-im-building-a-startup-with-it</guid>
<pubDate>Tue, 10 Mar 2026 21:47:41 +0000</pubDate>
</item>
<item>
<title>Django formfield_for_foreignkey() function</title>
<link>https://coderlegion.com/6026/django-formfieldforforeignkey-function</link>
<description>&lt;h3 id=&quot;mastering-django-formfield-for-foreignkey-complete-guide-with-examples&quot;&gt;Mastering Django &lt;a rel=&quot;nofollow&quot; href=&quot;https://globalnewsone.com/formfield_for_foreignkey-in-django-admin/&quot;&gt;&lt;code&gt;formfield_for_foreignkey&lt;/code&gt;&lt;/a&gt;: Complete Guide with Examples&lt;/h3&gt;
&lt;p&gt;Django’s admin is one of its most powerful features. Out of the box, it gives you CRUD forms to create, edit, and manage your models. But sometimes, the &lt;strong&gt;default dropdowns for ForeignKey fields are too broad&lt;/strong&gt; — they list all related objects in the database, which may not always be secure, efficient, or user-friendly.&lt;/p&gt;
&lt;p&gt;This is where &lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://globalnewsone.com/formfield_for_foreignkey-in-django-admin/&quot;&gt;formfield_for_foreignkey&lt;/a&gt;&lt;/strong&gt; comes in.&lt;/p&gt;
&lt;p&gt;In this guide, we’ll cover everything you need to know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ What &lt;code&gt;formfield_for_foreignkey&lt;/code&gt; is&lt;/li&gt;
&lt;li&gt;✅ How Django handles ForeignKey fields by default&lt;/li&gt;
&lt;li&gt;✅ Real-world use cases for overriding it&lt;/li&gt;
&lt;li&gt;✅ Advanced examples with filtering, permissions, and tenants&lt;/li&gt;
&lt;li&gt;✅ Best practices for production apps&lt;/li&gt;
&lt;li&gt;✅ FAQs and troubleshooting tips&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By the end, you’ll be able to &lt;strong&gt;customize your admin dropdowns&lt;/strong&gt; to be smarter, safer, and tailored to your business rules.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-what-is-formfield-for-foreignkey-&quot;&gt;1. What is &lt;a rel=&quot;nofollow&quot; href=&quot;https://globalnewsone.com/formfield_for_foreignkey-in-django-admin/&quot;&gt;formfield_for_foreignkey&lt;/a&gt;?&lt;/h3&gt;
&lt;p&gt;In Django Admin, the method &lt;code&gt;formfield_for_foreignkey(self, db_field, request, **kwargs)&lt;/code&gt; is a &lt;strong&gt;hook method&lt;/strong&gt; inside &lt;code&gt;ModelAdmin&lt;/code&gt;. It allows you to &lt;strong&gt;customize the queryset&lt;/strong&gt; for ForeignKey dropdown fields in the admin form.&lt;/p&gt;
&lt;p&gt;By default, Django lists all related objects in the dropdown. But with this hook, you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Filter objects (e.g., only active ones)&lt;/li&gt;
&lt;li&gt;Restrict by logged-in user&lt;/li&gt;
&lt;li&gt;Sort objects for usability&lt;/li&gt;
&lt;li&gt;Enforce multi-tenant separation&lt;/li&gt;
&lt;li&gt;Apply role-based visibility rules&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Method signature:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def formfield_for_foreignkey(self, db_field, request, **kwargs):
    # custom logic
    return super().formfield_for_foreignkey(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-how-django-handles-foreignkey-by-default&quot;&gt;2. How Django Handles ForeignKey by Default&lt;/h3&gt;
&lt;p&gt;Consider this example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Django Admin, when creating a &lt;code&gt;Book&lt;/code&gt;, the &lt;strong&gt;author dropdown&lt;/strong&gt; will list &lt;em&gt;all authors&lt;/em&gt; in the database.&lt;/p&gt;
&lt;p&gt;That’s fine for small projects, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It can clutter the UI when there are thousands of authors.&lt;/li&gt;
&lt;li&gt;It may expose data users shouldn’t see (e.g., other tenants’ data).&lt;/li&gt;
&lt;li&gt;It can confuse staff who only need a subset.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is the default behavior — and exactly where &lt;code&gt;formfield_for_foreignkey&lt;/code&gt; helps.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-real-world-use-cases&quot;&gt;3. Real-World Use Cases&lt;/h3&gt;
&lt;h4 id=&quot;example-1-filter-by-logged-in-user&quot;&gt;Example 1: Filter by Logged-In User&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == &quot;author&quot;:
            kwargs[&quot;queryset&quot;] = Author.objects.filter(created_by=request.user)
        return super().formfield_for_foreignkey(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Only authors created by the logged-in user appear in the dropdown.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;example-2-show-only-active-records&quot;&gt;Example 2: Show Only Active Records&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def formfield_for_foreignkey(self, db_field, request, **kwargs):
    if db_field.name == &quot;author&quot;:
        kwargs[&quot;queryset&quot;] = Author.objects.filter(is_active=True)
    return super().formfield_for_foreignkey(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Inactive authors are hidden.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;example-3-sort-dropdown-alphabetically&quot;&gt;Example 3: Sort Dropdown Alphabetically&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def formfield_for_foreignkey(self, db_field, request, **kwargs):
    if db_field.name == &quot;author&quot;:
        kwargs[&quot;queryset&quot;] = Author.objects.all().order_by(&quot;name&quot;)
    return super().formfield_for_foreignkey(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Users see a clean, ordered dropdown.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;example-4-restrict-by-permissions&quot;&gt;Example 4: Restrict by Permissions&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def formfield_for_foreignkey(self, db_field, request, **kwargs):
    if db_field.name == &quot;author&quot;:
        if request.user.has_perm(&quot;app.view_all_authors&quot;):
            kwargs[&quot;queryset&quot;] = Author.objects.all()
        else:
            kwargs[&quot;queryset&quot;] = Author.objects.filter(created_by=request.user)
    return super().formfield_for_foreignkey(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Superusers/managers see all authors.&lt;br&gt;
✅ Staff see only their own.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;example-5-multi-tenant-filtering&quot;&gt;Example 5: Multi-Tenant Filtering&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def formfield_for_foreignkey(self, db_field, request, **kwargs):
    if db_field.name == &quot;customer&quot;:
        kwargs[&quot;queryset&quot;] = Customer.objects.filter(tenant=request.user.tenant)
    return super().formfield_for_foreignkey(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Each tenant only sees their own customers.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-when-to-use-it-&quot;&gt;4. When to Use It?&lt;/h3&gt;
&lt;p&gt;You should override &lt;a rel=&quot;nofollow&quot; href=&quot;https://globalnewsone.com/formfield_for_foreignkey-in-django-admin/&quot;&gt;formfield_for_foreignkey&lt;/a&gt; when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You want to &lt;strong&gt;limit foreign key options&lt;/strong&gt; based on logged-in user.&lt;/li&gt;
&lt;li&gt;You need to &lt;strong&gt;hide irrelevant or sensitive records&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;You want to &lt;strong&gt;improve usability&lt;/strong&gt; by sorting or filtering.&lt;/li&gt;
&lt;li&gt;You’re working with a &lt;strong&gt;multi-tenant app&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-best-practices&quot;&gt;5. Best Practices&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;✅ Always call &lt;code&gt;super()&lt;/code&gt; at the end.&lt;/li&gt;
&lt;li&gt;✅ Keep queries efficient — avoid heavy joins here.&lt;/li&gt;
&lt;li&gt;✅ Apply consistent filtering across forms and lists (&lt;code&gt;get_queryset&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;✅ Test with multiple roles (staff, superuser).&lt;/li&gt;
&lt;li&gt;✅ Document your filtering logic — future devs will thank you.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-common-mistakes&quot;&gt;6. Common Mistakes&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;❌ Forgetting to return &lt;code&gt;super()&lt;/code&gt; → breaks form rendering.&lt;/li&gt;
&lt;li&gt;❌ Overly complex queries → slows down admin.&lt;/li&gt;
&lt;li&gt;❌ Inconsistent logic with &lt;code&gt;get_queryset&lt;/code&gt; → users see mismatched data.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-faqs&quot;&gt;7. FAQs&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Q: Can I filter dropdowns differently for add vs. change forms?&lt;/strong&gt;&lt;br&gt;
 Yes, you can check &lt;code&gt;request.resolver_match&lt;/code&gt; or &lt;code&gt;request.path&lt;/code&gt; to see the context.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q: Can I use it for ManyToMany fields?&lt;/strong&gt;&lt;br&gt;
 Use &lt;code&gt;formfield_for_manytomany&lt;/code&gt; instead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Q: Is this the only way to filter foreign keys?&lt;/strong&gt;&lt;br&gt;
 No — you can also use custom ModelForms, but this method is the most admin-friendly.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-final-thoughts&quot;&gt;8. Final Thoughts&lt;/h3&gt;
&lt;p&gt;Django’s &lt;a rel=&quot;nofollow&quot; href=&quot;https://globalnewsone.com/formfield_for_foreignkey-in-django-admin/&quot;&gt;formfield_for_foreignkey&lt;/a&gt; is more than just a customization hook — it’s a &lt;strong&gt;security and usability tool&lt;/strong&gt;. By mastering it, you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keep admin dropdowns clean and focused.&lt;/li&gt;
&lt;li&gt;Enforce tenant and role-based restrictions.&lt;/li&gt;
&lt;li&gt;Prevent accidental data leaks.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/6026/django-formfieldforforeignkey-function</guid>
<pubDate>Fri, 03 Oct 2025 21:35:34 +0000</pubDate>
</item>
<item>
<title>'formfield_for_foreignkey()' with Django Permissions: Fine-Grained Control in Admin</title>
<link>https://coderlegion.com/6025/formfieldforforeignkey-with-django-permissions-fine-grained-control-in-admin</link>
<description>&lt;p&gt;formfield_for_foreignkey()' with Django## Mastering &lt;a rel=&quot;nofollow&quot; href=&quot;https://globalnewsone.com/formfield_for_foreignkey-in-django-admin/&quot;&gt;formfield_for_foreignkey&lt;/a&gt; with Django Permissions: Fine-Grained Control in Admin&lt;/p&gt;
&lt;p&gt;Django’s admin is famous for giving developers a fully functional interface out of the box. But real-world apps often need &lt;strong&gt;fine-grained access control&lt;/strong&gt; — different users should see different subsets of related objects.&lt;/p&gt;
&lt;p&gt;You may already know about &lt;a rel=&quot;nofollow&quot; href=&quot;https://globalnewsone.com/formfield_for_foreignkey-in-django-admin/&quot;&gt;formfield_for_foreignkey&lt;/a&gt;, a hook that lets you filter ForeignKey dropdowns in Django Admin. But here’s where it gets really powerful: &lt;strong&gt;combine it with Django’s built-in permissions and roles&lt;/strong&gt; to create a &lt;strong&gt;role-aware and secure admin&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In this article, we’ll explore:&lt;/p&gt;
&lt;p&gt;✅ Why combine &lt;a rel=&quot;nofollow&quot; href=&quot;https://globalnewsone.com/formfield_for_foreignkey-in-django-admin/&quot;&gt;formfield_for_foreignkey&lt;/a&gt; with permissions&lt;br&gt;
✅ How to restrict dropdowns using &lt;code&gt;has_perm()&lt;/code&gt;&lt;br&gt;
✅ Using groups and roles for finer control&lt;br&gt;
✅ Handling sensitive data with custom rules&lt;br&gt;
✅ Multi-tenant use cases&lt;br&gt;
✅ Best practices&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-formfield-for-foreignkey-&quot;&gt;What is &lt;a rel=&quot;nofollow&quot; href=&quot;https://globalnewsone.com/formfield_for_foreignkey-in-django-admin/&quot;&gt;formfield_for_foreignkey&lt;/a&gt;?&lt;/h3&gt;
&lt;p&gt;Django provides this method inside &lt;code&gt;ModelAdmin&lt;/code&gt; classes to customize the queryset for a ForeignKey dropdown. By default, it lists &lt;strong&gt;all related objects&lt;/strong&gt; in the database. But with &lt;code&gt;formfield_for_foreignkey&lt;/code&gt;, you can filter, sort, or restrict that list.&lt;/p&gt;
&lt;p&gt;Signature:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def formfield_for_foreignkey(self, db_field, request, **kwargs):
    # custom logic here
    return super().formfield_for_foreignkey(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-combine-with-permissions-&quot;&gt;Why Combine with Permissions?&lt;/h3&gt;
&lt;p&gt;While &lt;a rel=&quot;nofollow&quot; href=&quot;https://globalnewsone.com/formfield_for_foreignkey-in-django-admin/&quot;&gt;formfield_for_foreignkey&lt;/a&gt; controls &lt;strong&gt;what objects appear&lt;/strong&gt;, Django’s permission system controls &lt;strong&gt;who can see what&lt;/strong&gt;. Combining them means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Different roles (staff, manager, superuser) see different dropdown options&lt;/li&gt;
&lt;li&gt;Sensitive objects can be hidden unless a user has explicit permission&lt;/li&gt;
&lt;li&gt;Multi-tenant applications prevent cross-tenant data leaks&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-1-restrict-by-permission&quot;&gt;Example 1: Restrict by Permission&lt;/h3&gt;
&lt;p&gt;Suppose you have &lt;code&gt;Author&lt;/code&gt; and &lt;code&gt;Book&lt;/code&gt; models. Normally, all authors would appear in the dropdown. But maybe only staff with the &lt;code&gt;app.view_all_authors&lt;/code&gt; permission should see everyone. Others should see only their own.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.contrib import admin
from .models import Author, Book

@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == &quot;author&quot;:
            if request.user.has_perm(&quot;app.view_all_authors&quot;):
                kwargs[&quot;queryset&quot;] = Author.objects.all()
            else:
                kwargs[&quot;queryset&quot;] = Author.objects.filter(created_by=request.user)
        return super().formfield_for_foreignkey(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Superusers or staff with the permission see all authors.&lt;br&gt;
✅ Other users see only the authors they created.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-2-role-based-dropdown-filtering&quot;&gt;Example 2: Role-Based Dropdown Filtering&lt;/h3&gt;
&lt;p&gt;If you’re using Django &lt;strong&gt;groups&lt;/strong&gt; or a &lt;code&gt;role&lt;/code&gt; field on your User model, you can scope choices based on role.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def formfield_for_foreignkey(self, db_field, request, **kwargs):
    if db_field.name == &quot;author&quot;:
        if request.user.groups.filter(name=&quot;Managers&quot;).exists():
            # Managers see all active authors
            kwargs[&quot;queryset&quot;] = Author.objects.filter(is_active=True)
        else:
            # Regular staff only see their own active authors
            kwargs[&quot;queryset&quot;] = Author.objects.filter(
                created_by=request.user, is_active=True
            )
    return super().formfield_for_foreignkey(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Managers have broader visibility.&lt;br&gt;
✅ Staff stay scoped to their own data.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-3-handling-sensitive-data&quot;&gt;Example 3: Handling Sensitive Data&lt;/h3&gt;
&lt;p&gt;Sometimes you want to hide sensitive objects unless the user has explicit permission.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def formfield_for_foreignkey(self, db_field, request, **kwargs):
    if db_field.name == &quot;project&quot;:
        qs = Project.objects.filter(is_active=True)

        if not request.user.has_perm(&quot;app.can_assign_sensitive_projects&quot;):
            qs = qs.exclude(is_sensitive=True)

        kwargs[&quot;queryset&quot;] = qs
    return super().formfield_for_foreignkey(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Sensitive projects don’t even appear in the dropdown unless allowed.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-4-multi-tenant-use-case&quot;&gt;Example 4: Multi-Tenant Use Case&lt;/h3&gt;
&lt;p&gt;In SaaS apps with multiple tenants, you can combine tenant ownership with permissions.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def formfield_for_foreignkey(self, db_field, request, **kwargs):
    if db_field.name == &quot;customer&quot;:
        qs = Customer.objects.filter(tenant=request.user.tenant)

        if not request.user.has_perm(&quot;app.view_inactive_customers&quot;):
            qs = qs.filter(is_active=True)

        kwargs[&quot;queryset&quot;] = qs
    return super().formfield_for_foreignkey(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Users only see customers from their tenant.&lt;br&gt;
✅ Optional permission controls visibility of inactive customers.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;best-practices&quot;&gt;Best Practices&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Always call &lt;code&gt;super()&lt;/code&gt;&lt;/strong&gt; at the end to retain Django’s defaults.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep queries efficient&lt;/strong&gt; — avoid heavy joins inside this method.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test with multiple roles&lt;/strong&gt; (staff, manager, superuser) to avoid hidden bugs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stay consistent&lt;/strong&gt; — if you filter dropdowns here, apply the same logic in &lt;code&gt;get_queryset()&lt;/code&gt; for list views.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;formfield_for_foreignkey&lt;/code&gt; gives you object-level filtering power, while Django’s permission system gives you user-level control. Together, they create an &lt;strong&gt;admin that’s secure, role-aware, and tenant-safe&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Whether you’re managing sensitive data, building a SaaS app, or just keeping the admin clean for staff, mastering this combination will make your Django admin much more robust.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt; With these examples, you can now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use permissions to decide &lt;strong&gt;who sees what&lt;/strong&gt; in dropdowns.&lt;/li&gt;
&lt;li&gt;Implement role-based logic with groups.&lt;/li&gt;
&lt;li&gt;Keep your admin secure in multi-tenant and sensitive-data scenarios.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Web Dev</category>
<guid isPermaLink="true">https://coderlegion.com/6025/formfieldforforeignkey-with-django-permissions-fine-grained-control-in-admin</guid>
<pubDate>Fri, 03 Oct 2025 21:35:11 +0000</pubDate>
</item>
<item>
<title>Mastering `formfield_for_foreignkey` in Django Admin: A Complete Guide</title>
<link>https://coderlegion.com/6002/mastering-formfieldforforeignkey-in-django-admin-a-complete-guide</link>
<description>&lt;h3 id=&quot;mastering-formfield-for-foreignkey-in-django-admin-a-complete-guide&quot;&gt;Mastering &lt;code&gt;formfield_for_foreignkey&lt;/code&gt; in Django Admin: A Complete Guide&lt;/h3&gt;
&lt;p&gt;Django’s admin is one of its most powerful features. Out of the box, it gives you forms to create, edit, and manage your models. But sometimes, the default dropdowns for &lt;strong&gt;ForeignKey fields&lt;/strong&gt; are too permissive — they show all related objects in the database.&lt;/p&gt;
&lt;p&gt;What if you want to &lt;strong&gt;restrict the choices&lt;/strong&gt; a user can see, based on who is logged in, or some business logic?&lt;/p&gt;
&lt;p&gt;That’s where &lt;strong&gt;&lt;code&gt;formfield_for_foreignkey&lt;/code&gt;&lt;/strong&gt; comes in.&lt;/p&gt;
&lt;p&gt;Read this aticle also &lt;a rel=&quot;nofollow&quot; href=&quot;https://globalnewsone.com/formfield_for_foreignkey-in-django-admin/&quot;&gt;formfield_for_foreignkey in Django&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In this article, we’ll dive into:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ What is &lt;code&gt;formfield_for_foreignkey&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;✅ How Django handles ForeignKey fields by default&lt;/li&gt;
&lt;li&gt;✅ Real-world examples of overriding &lt;code&gt;formfield_for_foreignkey&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;✅ When and why you should use it&lt;/li&gt;
&lt;li&gt;✅ Best practices&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-formfield-for-foreignkey-in-django-admin-&quot;&gt;What is &lt;code&gt;formfield_for_foreignkey&lt;/code&gt; in Django Admin?&lt;/h3&gt;
&lt;p&gt;In Django Admin, the method &lt;strong&gt;`formfield_for_foreignkey(self, db_field, request, &lt;/strong&gt;kwargs)`&lt;strong&gt; is a &lt;/strong&gt;hook method** that lets you customize the queryset for ForeignKey dropdown fields in the admin form.&lt;/p&gt;
&lt;p&gt;By default, Django will show &lt;strong&gt;all objects&lt;/strong&gt; of the related model.&lt;br&gt;
With this method, you can &lt;strong&gt;filter, sort, or limit&lt;/strong&gt; the available options.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Method signature:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def formfield_for_foreignkey(self, db_field, request, **kwargs):
    # custom logic
    return super().formfield_for_foreignkey(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-django-handles-foreignkey-by-default&quot;&gt;How Django Handles ForeignKey by Default&lt;/h3&gt;
&lt;p&gt;Imagine we have two models:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;In the Django Admin, when creating a &lt;code&gt;Book&lt;/code&gt;, the &lt;code&gt;author&lt;/code&gt; dropdown will list &lt;strong&gt;all authors&lt;/strong&gt; in the database.&lt;/li&gt;
&lt;li&gt;This is the default behavior.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But what if you only want staff users to see &lt;strong&gt;authors they created&lt;/strong&gt;? That’s where you override &lt;code&gt;formfield_for_foreignkey&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-1-filter-foreignkey-choices-by-logged-in-user&quot;&gt;Example 1: Filter ForeignKey Choices by Logged-in User&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.contrib import admin
from .models import Book, Author

@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
    
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == &quot;author&quot;:
            # Only show authors created by the logged-in user
            kwargs[&quot;queryset&quot;] = Author.objects.filter(created_by=request.user)
        return super().formfield_for_foreignkey(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Now, when a staff user opens the &lt;strong&gt;Book form&lt;/strong&gt;, the &lt;code&gt;author&lt;/code&gt; dropdown only lists the authors they created.&lt;br&gt;
Superusers will still see everything.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-2-show-only-active-related-objects&quot;&gt;Example 2: Show Only Active Related Objects&lt;/h3&gt;
&lt;p&gt;Sometimes you want to show only &lt;em&gt;active&lt;/em&gt; records.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def formfield_for_foreignkey(self, db_field, request, **kwargs):
    if db_field.name == &quot;author&quot;:
        kwargs[&quot;queryset&quot;] = Author.objects.filter(is_active=True)
    return super().formfield_for_foreignkey(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✅ Now, inactive authors won’t appear in the dropdown.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-3-sort-the-dropdown-options&quot;&gt;Example 3: Sort the Dropdown Options&lt;/h3&gt;
&lt;p&gt;You can also &lt;strong&gt;order the queryset&lt;/strong&gt; to make the dropdown more user-friendly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def formfield_for_foreignkey(self, db_field, request, **kwargs):
    if db_field.name == &quot;author&quot;:
        kwargs[&quot;queryset&quot;] = Author.objects.all().order_by(&quot;name&quot;)
    return super().formfield_for_foreignkey(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-should-you-use-formfield-for-foreignkey-&quot;&gt;When Should You Use &lt;code&gt;formfield_for_foreignkey&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;You should override this method when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You want to &lt;strong&gt;restrict foreign key options&lt;/strong&gt; based on the logged-in user.&lt;/li&gt;
&lt;li&gt;You need to &lt;strong&gt;hide inactive or irrelevant related objects&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;You want to &lt;strong&gt;improve usability&lt;/strong&gt; by sorting or limiting dropdown choices.&lt;/li&gt;
&lt;li&gt;You are working in a &lt;strong&gt;multi-tenant application&lt;/strong&gt; where users should not see each other’s data.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-best-practices&quot;&gt;⚡ Best Practices&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Always call &lt;code&gt;super()&lt;/code&gt; at the end so Django can apply its defaults.&lt;/li&gt;
&lt;li&gt;Be careful with queries — don’t run expensive queries inside this method.&lt;/li&gt;
&lt;li&gt;Test both staff and superuser accounts to ensure permissions work as expected.&lt;/li&gt;
&lt;li&gt;Keep the logic simple — if the filtering is very complex, consider using &lt;strong&gt;custom forms&lt;/strong&gt; instead.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thoughts-1&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;&lt;code&gt;formfield_for_foreignkey&lt;/code&gt;&lt;/strong&gt; method is one of the most useful hooks in Django Admin. It gives you control over what appears in your ForeignKey dropdowns, making your admin interface &lt;strong&gt;safer, more user-friendly, and tenant-aware&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Whether you’re building a small project or a large SaaS app, mastering this hook will save you a lot of time and prevent accidental data leaks.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;✅ With this article, you can now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Explain what &lt;code&gt;formfield_for_foreignkey&lt;/code&gt; is.&lt;/li&gt;
&lt;li&gt;Customize dropdowns for different use cases.&lt;/li&gt;
&lt;li&gt;Improve your Django admin forms for better usability and security.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Read this aticle also &lt;a rel=&quot;nofollow&quot; href=&quot;https://globalnewsone.com/formfield_for_foreignkey-in-django-admin/&quot;&gt;formfield_for_foreignkey in Django&lt;/a&gt;&lt;/p&gt;
</description>
<category>Web Dev</category>
<guid isPermaLink="true">https://coderlegion.com/6002/mastering-formfieldforforeignkey-in-django-admin-a-complete-guide</guid>
<pubDate>Fri, 03 Oct 2025 11:37:42 +0000</pubDate>
</item>
<item>
<title>How to Restrict ForeignKey Choices in Django Admin</title>
<link>https://coderlegion.com/6005/how-to-restrict-foreignkey-choices-in-django-admin</link>
<description>&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;If you’ve ever built an application with &lt;strong&gt;Django Admin&lt;/strong&gt;, you’ve probably used dropdowns for related models. These dropdowns appear whenever you have a &lt;strong&gt;ForeignKey&lt;/strong&gt; field.&lt;/p&gt;
&lt;p&gt;Read this article aslo &lt;a rel=&quot;nofollow&quot; href=&quot;https://globalnewsone.com/restrict-foreignkey-choices-django-admin/&quot;&gt;How to Restrict ForeignKey Choices in Django&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;But here’s the problem:&lt;br&gt;
By default, Django Admin will show &lt;strong&gt;all related objects&lt;/strong&gt; in that dropdown.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What if you only want to show the objects created by the &lt;strong&gt;logged-in user&lt;/strong&gt;?&lt;/li&gt;
&lt;li&gt;Or maybe you want staff users to only see certain options?&lt;/li&gt;
&lt;li&gt;Or perhaps you need to filter choices based on &lt;strong&gt;business logic&lt;/strong&gt; (like only “active” records)?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you landed here by searching things like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;“Django admin limit dropdown choices”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;“How to restrict ForeignKey options per user in Django”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;“Django admin filter related objects dynamically”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;“Restrict choices in Django admin form”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;“How to show limited foreign key options in Django”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt; Then this article is exactly for you.&lt;/p&gt;
&lt;p&gt;The solution lies in a powerful Django Admin method:&lt;br&gt;
&lt;code&gt;formfield_for_foreignkey(self, db_field, request, **kwargs)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;In this article, we’ll explore:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Why Django shows all ForeignKey objects by default&lt;/li&gt;
&lt;li&gt;How to filter dropdown choices in the admin&lt;/li&gt;
&lt;li&gt;Step-by-step examples with code&lt;/li&gt;
&lt;li&gt;Alternative approaches (querysets, limit_choices_to)&lt;/li&gt;
&lt;li&gt;Best practices for performance &amp;amp; maintainability&lt;/li&gt;
&lt;li&gt;Common pitfalls &amp;amp; FAQs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By the end, you’ll know exactly how to &lt;strong&gt;control your admin dropdowns like a pro&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-does-django-show-all-foreignkey-choices-by-default-&quot;&gt;Why Does Django Show All ForeignKey Choices by Default?&lt;/h3&gt;
&lt;p&gt;When Django sees a &lt;strong&gt;ForeignKey&lt;/strong&gt; in your model, it automatically renders a dropdown in the Admin. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the &lt;strong&gt;Book Admin form&lt;/strong&gt;, Django will render a dropdown for &lt;code&gt;author&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But by default:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It will show &lt;strong&gt;all authors&lt;/strong&gt; from the database.&lt;/li&gt;
&lt;li&gt;Even if you only want to allow some of them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s fine for small projects, but in &lt;strong&gt;real-world apps&lt;/strong&gt;, you often need restrictions.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-do-you-need-to-restrict-foreignkey-choices-&quot;&gt;When Do You Need to Restrict ForeignKey Choices?&lt;/h3&gt;
&lt;p&gt;Here are some common use cases where you might want to filter ForeignKey dropdowns in Django Admin:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;User-specific data&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Example: Only show &lt;code&gt;Projects&lt;/code&gt; created by the logged-in user.&lt;/li&gt;
&lt;li&gt;Useful in multi-tenant apps.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Role-based filtering&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Example: Staff can only assign orders to employees in their department.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Status-based filtering&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Example: Only show “active” categories, not archived ones.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Security restrictions&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Example: Prevent staff users from accessing other users’ data.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Performance reasons&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you have thousands of related objects, filtering avoids &lt;strong&gt;slow dropdowns&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-django-way-using-formfield-for-foreignkey&quot;&gt;The Django Way: Using &lt;code&gt;formfield_for_foreignkey&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Django gives us a built-in method for this exact problem:&lt;br&gt;
&lt;code&gt;formfield_for_foreignkey(self, db_field, request, **kwargs)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This method is part of the &lt;strong&gt;ModelAdmin class&lt;/strong&gt; and runs whenever Django builds a form field for a ForeignKey.&lt;/p&gt;
&lt;h4 id=&quot;method-signature&quot;&gt;Method Signature&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def formfield_for_foreignkey(self, db_field, request, **kwargs):
    # your logic here
    return super().formfield_for_foreignkey(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;db_field&lt;/strong&gt; → the field object (so you can check if it’s the field you want to customize).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;request&lt;/strong&gt; → the current HTTP request (so you can check the logged-in user).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;kwargs&lt;/strong&gt; → extra options passed to the form field.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-1-restrict-choices-to-logged-in-user&quot;&gt;Example 1: Restrict Choices to Logged-in User&lt;/h3&gt;
&lt;p&gt;Let’s say each &lt;code&gt;Book&lt;/code&gt; belongs to an &lt;code&gt;Author&lt;/code&gt;, and you want a user to only select themselves as the author.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.contrib import admin
from .models import Book, Author

class BookAdmin(admin.ModelAdmin):
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == &quot;author&quot;:
            kwargs[&quot;queryset&quot;] = Author.objects.filter(user=request.user)
        return super().formfield_for_foreignkey(db_field, request, **kwargs)

admin.site.register(Book, BookAdmin)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✔ Now, the dropdown for &lt;code&gt;author&lt;/code&gt; will only show authors tied to the logged-in user.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-2-restrict-choices-based-on-user-role&quot;&gt;Example 2: Restrict Choices Based on User Role&lt;/h3&gt;
&lt;p&gt;Let’s say staff users should only see authors in their department.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class BookAdmin(admin.ModelAdmin):
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == &quot;author&quot; and not request.user.is_superuser:
            kwargs[&quot;queryset&quot;] = Author.objects.filter(department=request.user.department)
        return super().formfield_for_foreignkey(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✔ Superusers see all authors, staff see filtered ones.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-3-show-only-active-records&quot;&gt;Example 3: Show Only Active Records&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class BookAdmin(admin.ModelAdmin):
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == &quot;author&quot;:
            kwargs[&quot;queryset&quot;] = Author.objects.filter(is_active=True)
        return super().formfield_for_foreignkey(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;✔ Only active authors appear in the dropdown.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;alternative-approaches&quot;&gt;Alternative Approaches&lt;/h3&gt;
&lt;p&gt;Sometimes you don’t need &lt;code&gt;formfield_for_foreignkey&lt;/code&gt;. Here are other options:&lt;/p&gt;
&lt;h4 id=&quot;1-using-limit-choices-to-in-the-model&quot;&gt;1. Using &lt;code&gt;limit_choices_to&lt;/code&gt; in the Model&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Book(models.Model):
    author = models.ForeignKey(
        Author, 
        on_delete=models.CASCADE,
        limit_choices_to={'is_active': True}
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;⚠️ Downside: This is &lt;strong&gt;static&lt;/strong&gt; — you can’t use the logged-in user or request context.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-overriding-the-form-class&quot;&gt;2. Overriding the Form Class&lt;/h4&gt;
&lt;p&gt;You can also customize choices in the &lt;strong&gt;form&lt;/strong&gt; itself:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django import forms
from .models import Book, Author

class BookForm(forms.ModelForm):
    class Meta:
        model = Book
        fields = &quot;__all__&quot;

    def __init__(self, *args, **kwargs):
        user = kwargs.pop('request').user
        super().__init__(*args, **kwargs)
        self.fields['author'].queryset = Author.objects.filter(user=user)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then plug the form into your admin:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class BookAdmin(admin.ModelAdmin):
    form = BookForm
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;best-practices-1&quot;&gt;Best Practices&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;✅ Use &lt;code&gt;formfield_for_foreignkey&lt;/code&gt; if you need request-aware filtering.&lt;/li&gt;
&lt;li&gt;✅ Use &lt;code&gt;limit_choices_to&lt;/code&gt; for &lt;strong&gt;static filters&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;✅ Cache querysets if you expect heavy load.&lt;/li&gt;
&lt;li&gt;✅ Always allow superusers full access.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;common-pitfalls&quot;&gt;Common Pitfalls&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Forgetting &lt;code&gt;super()&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Always call the parent method, otherwise you may break Django’s defaults.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Filtering too aggressively&lt;/strong&gt;&lt;br&gt;
If you filter incorrectly, you might prevent staff from editing old data.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Performance issues&lt;/strong&gt;&lt;br&gt;
Large querysets in dropdowns can slow down admin forms — add filters wisely.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;faqs&quot;&gt;FAQs&lt;/h3&gt;
&lt;h4 id=&quot;-can-i-restrict-foreignkey-dropdowns-outside-the-admin-&quot;&gt;❓ Can I restrict ForeignKey dropdowns outside the admin?&lt;/h4&gt;
&lt;p&gt;Yes — in custom forms, override the &lt;code&gt;__init__&lt;/code&gt; method and filter querysets.&lt;/p&gt;
&lt;h4 id=&quot;-can-i-hide-the-dropdown-completely-&quot;&gt;❓ Can I hide the dropdown completely?&lt;/h4&gt;
&lt;p&gt;Yes — you can set &lt;code&gt;readonly_fields&lt;/code&gt; or replace the field with a custom widget.&lt;/p&gt;
&lt;h4 id=&quot;-what-if-i-want-autocomplete-instead-of-dropdown-&quot;&gt;❓ What if I want autocomplete instead of dropdown?&lt;/h4&gt;
&lt;p&gt;Use &lt;code&gt;autocomplete_fields&lt;/code&gt; in your &lt;code&gt;ModelAdmin&lt;/code&gt;. It works well with large datasets.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Restricting ForeignKey choices in Django Admin is a &lt;strong&gt;common real-world need&lt;/strong&gt;.&lt;br&gt;
And Django makes it easy with the &lt;code&gt;formfield_for_foreignkey&lt;/code&gt; method.&lt;/p&gt;
&lt;p&gt;So whether you searched for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;“How to filter related objects in Django admin form”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;“Django admin restrict choices dynamically per user”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;“Limit ForeignKey dropdown in Django”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt; This guide gives you the complete solution.&lt;/p&gt;
&lt;p&gt;By overriding &lt;code&gt;formfield_for_foreignkey&lt;/code&gt;, you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Filter dropdowns per user&lt;/li&gt;
&lt;li&gt;Apply role-based restrictions&lt;/li&gt;
&lt;li&gt;Improve performance&lt;/li&gt;
&lt;li&gt;Keep your admin secure&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Read this article aslo &lt;a rel=&quot;nofollow&quot; href=&quot;https://globalnewsone.com/restrict-foreignkey-choices-django-admin/&quot;&gt;How to Restrict ForeignKey Choices in Django&lt;/a&gt;&lt;/p&gt;
</description>
<category>Web Dev</category>
<guid isPermaLink="true">https://coderlegion.com/6005/how-to-restrict-foreignkey-choices-in-django-admin</guid>
<pubDate>Fri, 03 Oct 2025 11:37:19 +0000</pubDate>
</item>
<item>
<title>Demystifying db_field in Django Admin</title>
<link>https://coderlegion.com/5936/demystifying-dbfield-in-django-admin</link>
<description>&lt;p&gt;When working with Django Admin, you’ll often need to customize how fields appear in your forms. Django provides hooks such as &lt;code&gt;formfield_for_foreignkey&lt;/code&gt;, &lt;code&gt;formfield_for_choice_field&lt;/code&gt;, and &lt;code&gt;formfield_for_dbfield&lt;/code&gt; — all of which receive an argument called &lt;strong&gt;&lt;code&gt;db_field&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If you’ve ever wondered &lt;em&gt;what exactly &lt;code&gt;db_field&lt;/code&gt; is and how you can use it&lt;/em&gt;, this article is for you.&lt;/p&gt;
&lt;h3 id=&quot;for-complete-article-please-visit-db-field-in-django-admin&quot;&gt;For complete article please visit &lt;a rel=&quot;nofollow&quot; href=&quot;https://globalnewsone.com/demystifying-db_field-in-django-admin/&quot;&gt;db_field in Django Admin&lt;/a&gt;&lt;/h3&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-db-field-&quot;&gt;What is &lt;code&gt;db_field&lt;/code&gt;?&lt;/h3&gt;
&lt;p&gt;In short, &lt;strong&gt;&lt;code&gt;db_field&lt;/code&gt; is the model field object being processed&lt;/strong&gt; when Django builds the admin form.&lt;/p&gt;
&lt;p&gt;When you define a model:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Django Admin inspects each field (&lt;code&gt;title&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt;, etc.) when rendering the form. At that point, it passes the actual &lt;strong&gt;field instance&lt;/strong&gt; (a &lt;code&gt;CharField&lt;/code&gt; or &lt;code&gt;ForeignKey&lt;/code&gt;) to your override methods as &lt;code&gt;db_field&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;For &lt;code&gt;Book.title&lt;/code&gt; → &lt;code&gt;db_field&lt;/code&gt; will be a &lt;code&gt;CharField&lt;/code&gt; instance.&lt;/li&gt;
&lt;li&gt;For &lt;code&gt;Book.author&lt;/code&gt; → &lt;code&gt;db_field&lt;/code&gt; will be a &lt;code&gt;ForeignKey&lt;/code&gt; instance.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;where-is-db-field-used-&quot;&gt;Where is &lt;code&gt;db_field&lt;/code&gt; Used?&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;db_field&lt;/code&gt; argument appears in several admin customization methods:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;`formfield_for_foreignkey(self, db_field, request, &lt;/strong&gt;kwargs)&lt;code&gt;**
→ Lets you change how &lt;/code&gt;ForeignKey` fields are displayed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;`formfield_for_choice_field(self, db_field, request, &lt;/strong&gt;kwargs)&lt;code&gt;**
→ Lets you modify dropdowns created from &lt;/code&gt;choices`.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;`formfield_for_dbfield(self, db_field, request, &lt;/strong&gt;kwargs)`*&lt;em&gt;&lt;br&gt;
→ A generic hook for &lt;/em&gt;any* field type.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-1-filtering-a-foreignkey&quot;&gt;Example 1 — Filtering a ForeignKey&lt;/h3&gt;
&lt;p&gt;Imagine you want to limit which authors appear in the dropdown when creating a book.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.contrib import admin

class BookAdmin(admin.ModelAdmin):
    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        # Only customize the &quot;author&quot; field
        if db_field.name == &quot;author&quot;:
            kwargs[&quot;queryset&quot;] = Author.objects.filter(name__startswith=&quot;A&quot;)
        return super().formfield_for_foreignkey(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; What’s happening?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Django passes the &lt;code&gt;Book.author&lt;/code&gt; field as &lt;code&gt;db_field&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;We check &lt;code&gt;db_field.name == &quot;author&quot;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Then we override its queryset to only show authors whose names start with “A”.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-2-customizing-choices&quot;&gt;Example 2 — Customizing Choices&lt;/h3&gt;
&lt;p&gt;Suppose you have a field with predefined choices:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class Book(models.Model):
    GENRE_CHOICES = [
        (&quot;fiction&quot;, &quot;Fiction&quot;),
        (&quot;nonfiction&quot;, &quot;Non-Fiction&quot;),
        (&quot;poetry&quot;, &quot;Poetry&quot;),
    ]
    title = models.CharField(max_length=200)
    genre = models.CharField(max_length=20, choices=GENRE_CHOICES)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can alter how these choices appear in admin:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class BookAdmin(admin.ModelAdmin):
    def formfield_for_choice_field(self, db_field, request, **kwargs):
        if db_field.name == &quot;genre&quot;:
            # Reorder or filter choices
            kwargs[&quot;choices&quot;] = [
                (&quot;fiction&quot;, &quot;Fiction&quot;),
                (&quot;poetry&quot;, &quot;Poetry&quot;),
            ]
        return super().formfield_for_choice_field(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;example-3-catch-all-with-formfield-for-dbfield&quot;&gt;Example 3 — Catch-All with &lt;code&gt;formfield_for_dbfield&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;If you want to apply a rule to &lt;strong&gt;all fields&lt;/strong&gt;, use &lt;code&gt;formfield_for_dbfield&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;class BookAdmin(admin.ModelAdmin):
    def formfield_for_dbfield(self, db_field, request, **kwargs):
        # Example: add placeholder text to all CharFields
        if isinstance(db_field, models.CharField):
            kwargs[&quot;widget&quot;].attrs[&quot;placeholder&quot;] = f&quot;Enter {db_field.verbose_name}&quot;
        return super().formfield_for_dbfield(db_field, request, **kwargs)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, every &lt;code&gt;CharField&lt;/code&gt; in the model gets a placeholder in its admin input box.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-is-db-field-useful-&quot;&gt;Why is &lt;code&gt;db_field&lt;/code&gt; Useful?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Granular Control&lt;/strong&gt; — target individual fields like &lt;code&gt;author&lt;/code&gt; or &lt;code&gt;genre&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User-Specific Filtering&lt;/strong&gt; — restrict dropdowns per user role.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom Widgets&lt;/strong&gt; — attach custom widgets or attributes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Centralized Logic&lt;/strong&gt; — all customization stays inside &lt;code&gt;ModelAdmin&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;key-takeaways&quot;&gt;Key Takeaways&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;db_field&lt;/code&gt; is &lt;strong&gt;the actual field object&lt;/strong&gt; from your model.&lt;/li&gt;
&lt;li&gt;Django sends it to hooks so you can inspect the field and modify its admin form behavior.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;formfield_for_foreignkey&lt;/code&gt; → control &lt;code&gt;ForeignKey&lt;/code&gt; dropdowns.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;formfield_for_choice_field&lt;/code&gt; → control &lt;code&gt;choices&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;formfield_for_dbfield&lt;/code&gt; → catch-all for any field type.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;✅ By leveraging &lt;code&gt;db_field&lt;/code&gt;, you can transform the Django Admin from a basic CRUD tool into a finely tuned interface that enforces business rules and improves usability.&lt;/p&gt;
</description>
<category>Python</category>
<guid isPermaLink="true">https://coderlegion.com/5936/demystifying-dbfield-in-django-admin</guid>
<pubDate>Fri, 03 Oct 2025 11:35:39 +0000</pubDate>
</item>
<item>
<title>PWA and Django #4: Installing a PWA as a native application</title>
<link>https://coderlegion.com/880/pwa-and-django-4-installing-a-pwa-as-a-native-application</link>
<description>&lt;p&gt;Welcome to the fourth entry on the Progressive Web Application with Django series. In this chapter we will learn how to install our webapp as a native application. Very useful and really easy.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba57a900-5b98-45d0-bd38-550d5645c3df_512x512.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;allowing-native-installation&quot;&gt;Allowing native installation&lt;/h3&gt;
&lt;p&gt;With a small change to the source code, we can ask the user if they want to install our supercool webapp as a &quot;native&quot; app.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let beforeInstallPromptEvent = null;
let installed = false;

async function installPWA() {
   if (beforeInstallPromptEvent === null || installed) {
       return;
   }

   try {
       beforeInstallPromptEvent.prompt();

       const { outcome } = await beforeInstallPromptEvent.userChoice;
       if (outcome === 'accepted') {
           console.log(&quot;App install dialog accepted!&quot;);
           beforeInstallPromptEvent = null;
           installed = true;
       }

   } catch(e) {
       console.error(e);
   }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can also listen to a couple of events to customize the responses and behavior of our webapp in every step of the process:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window.addEventListener('beforeinstallprompt', (e) =&amp;gt; {
   beforeInstallPromptEvent = e;
});

window.addEventListener('appinstalled', () =&amp;gt; {
   installed = true;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The user needs to fire the event, so an install button is a good place to call the new code:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21a2596b-e8ac-4751-b745-8c8e8bd4db74_302x390.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;When clicked, the user will see a browser dialog asking for the installation like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb127466f-1047-4437-a449-2ebcae04a73e_470x204.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;If accepted, the webapp will be registered on the operating system, with its own launcher icon:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1a321fe-b3f5-446c-ab4a-2289681b6308_756x462.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;executing-as-a-standalone-app&quot;&gt;Executing as a standalone app&lt;/h3&gt;
&lt;p&gt;Once installed, the web app will launch in its own window, providing a more integrated experience with styling based on the manifest (see previous chapters). Like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8ca65df6-5df0-4aeb-a7ae-ba2ce15c5e85_507x861.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;Note that you can customize the icon, the behavior of the webapp, colors, etc. in the PWA manifest. You can read more about this here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/Manifest&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/Manifest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://web.dev/learn/pwa/web-app-manifest&quot;&gt;https://web.dev/learn/pwa/web-app-manifest&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;about-the-list&quot;&gt;About the list&lt;/h3&gt;
&lt;p&gt;Among the Python and Docker posts, I will also write about other related topics, like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Software architecture&lt;/li&gt;
&lt;li&gt;Programming environments&lt;/li&gt;
&lt;li&gt;Linux operating system&lt;/li&gt;
&lt;li&gt;Etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you found some interesting technology, programming language or whatever, please, let me know! I'm always open to learning something new!&lt;/p&gt;
&lt;h3 id=&quot;about-the-author&quot;&gt;About the author&lt;/h3&gt;
&lt;p&gt;I'm Andrés, a full-stack software developer based in Palma, on a personal journey to improve my coding skills. I'm also a self-published fantasy writer with four published novels to my name. Feel free to ask me anything!&lt;/p&gt;
</description>
<category>Getting Started</category>
<guid isPermaLink="true">https://coderlegion.com/880/pwa-and-django-4-installing-a-pwa-as-a-native-application</guid>
<pubDate>Sun, 23 Feb 2025 10:58:34 +0000</pubDate>
</item>
<item>
<title>Deploying a Django 5 Project to Vercel</title>
<link>https://coderlegion.com/1031/deploying-a-django-5-project-to-vercel</link>
<description>&lt;h1&gt;Deploying a Django 5 Project to Vercel&lt;/h1&gt;
&lt;p&gt;This guide explains how to deploy a Django 5 project to Vercel using the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/wemake-services/wemake-django-template&quot;&gt;wemake-django-template&lt;/a&gt; (as of February 2, 2025). The template is based on Django 5.1.4 and Python 3.12, emphasizing code quality and security. More details are available on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://wemake-services.github.io/wemake-django-template/&quot;&gt;documentation page&lt;/a&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Important Considerations&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Deployment Bundle Size:&lt;/strong&gt; Vercel limits Serverless Functions to an uncompressed size of 250 MB (including layers), enforced by AWS.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Dependency Management:&lt;/strong&gt; Install dependencies via a &lt;code&gt;requirements.txt&lt;/code&gt; file or a Pipfile with Pipfile.lock.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Python Version:&lt;/strong&gt; Only Python 3.9 or 3.12 are supported.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If these requirements don’t match your project needs, Vercel might not be the best fit.&lt;/p&gt;
&lt;h2&gt;Prerequisites&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Vercel Account &amp;amp; CLI:&lt;/strong&gt; Ensure a Vercel account is active and the CLI is installed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Project Setup&lt;/h2&gt;
&lt;h3&gt;1. Bootstrap Your Project&lt;/h3&gt;
&lt;p&gt;Begin by using &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/cookiecutter/cookiecutter&quot;&gt;cookiecutter&lt;/a&gt; with the wemake-django-template:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cookiecutter gh:wemake-services/wemake-django-template
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When prompted, provide values as needed. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
You've downloaded /Users/kelvin/.cookiecutters/wemake-django-template before. Is it okay to delete and re-download it? [y/n] (y): y
  [1/4] project_name (test-project): django-vercel
  [2/4] project_verbose_name (Test Project): Deploy django to vercel
  [3/4] project_domain (myapp.com): vercel.com
  [4/4] organization (wemake.services): github.com/vousmeevoyez
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Configuring the Project for Vercel&lt;/h2&gt;
&lt;h3&gt;2. Generate &lt;code&gt;requirements.txt&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Since the template uses Poetry for dependency management, export dependencies to a &lt;code&gt;requirements.txt&lt;/code&gt; file (without hashes for faster resolution):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;poetry export --without-hashes --format=requirements.txt &amp;gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Update WSGI Settings&lt;/h3&gt;
&lt;p&gt;Vercel requires the entry point to be named &lt;code&gt;app&lt;/code&gt; or &lt;code&gt;handler&lt;/code&gt;. Make the following changes:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;
    &lt;strong&gt;In &lt;code&gt;server/wsgi.py&lt;/code&gt;:&lt;/strong&gt; Replace the default callable with:
    &lt;pre&gt;&lt;code&gt;app = get_wsgi_application()
&lt;/code&gt;&lt;/pre&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;strong&gt;In &lt;code&gt;server/settings/components/common.py&lt;/code&gt;:&lt;/strong&gt; Set the WSGI application:
    &lt;pre&gt;&lt;code&gt;WSGI_APPLICATION = 'server.wsgi.app'
&lt;/code&gt;&lt;/pre&gt;
  &lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. (Optional) Database Setup&lt;/h3&gt;
&lt;p&gt;For simplicity, this guide uses SQLite for deployment. For production, consider PostgreSQL. To allow flexibility using &lt;code&gt;dj-database-url&lt;/code&gt;, update the database configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DATABASES = {
    'default': dj_database_url.config(default=config('DATABASE_URL'))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. Create &lt;code&gt;vercel.json&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Add a &lt;code&gt;vercel.json&lt;/code&gt; file at the project root with the following content to configure the runtime and entrypoint:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;version&quot;: 2,
  &quot;builds&quot;: [
    {
      &quot;src&quot;: &quot;server/wsgi.py&quot;,
      &quot;use&quot;: &quot;@vercel/python&quot;,
      &quot;config&quot;: {&quot;runtime&quot;: &quot;python3.12&quot;}
    }
  ],
  &quot;routes&quot;: [
    {
      &quot;src&quot;: &quot;/(.*)&quot;,
      &quot;dest&quot;: &quot;server/wsgi.py&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Deploying to Vercel&lt;/h2&gt;
&lt;p&gt;With the project configured, deploy using the Vercel CLI:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vercel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The CLI will prompt for details, for example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
Vercel CLI 34.3.1
? Set up and deploy “~/apps/django-vercel”? yes
? Which scope do you want to deploy to? kelvinswarna's projects
? Link to existing project? no
? What’s your project’s name? django-vercel
? In which directory is your code located? ./
...
✅  Production: https://django-vercel-d5fwpcj3u-kelvinswarnas-projects.vercel.app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To update the production deployment later, run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vercel --prod
&lt;/code&gt;&lt;/pre&gt;
&lt;div class=&quot;div-green&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Tip:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;If you plan to use a database other than SQLite, configure PostgreSQL with a managed service like Supabase or Neon for better scalability and reliability.&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;div-blue&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Note:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;Vercel's serverless environment does not support long-running background tasks. If your Django app relies on Celery or similar background task processing, consider using an external task queue service like Redis with AWS Lambda or Cloud Run.&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;div-red&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Caution:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;Vercel enforces a 250 MB uncompressed limit for serverless functions. If your Django project has many dependencies or large assets, your deployment might fail. Optimize dependencies and exclude unnecessary files to stay within this limit.&lt;/span&gt;
&lt;/div&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;By following these steps, your Django 5 project is now configured to meet Vercel’s requirements and deployed successfully. This guide covered exporting dependencies, adjusting WSGI settings, optionally setting up the database configuration, and creating the required &lt;code&gt;vercel.json&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;Enjoy your deployed Django application on Vercel!&lt;/p&gt;
</description>
<category>Deployment</category>
<guid isPermaLink="true">https://coderlegion.com/1031/deploying-a-django-5-project-to-vercel</guid>
<pubDate>Sun, 02 Feb 2025 08:59:25 +0000</pubDate>
</item>
<item>
<title>When to Choose FastAPI Over Django or Flask: A Comprehensive Guide with Practical Examples</title>
<link>https://coderlegion.com/884/when-to-choose-fastapi-over-django-or-flask-comprehensive-guide-with-practical-examples</link>
<description>&lt;p&gt;Python is home to several powerful web frameworks, each designed to cater to different needs. Django and Flask have long been dominant players, offering solutions for web development for over a decade. However, in recent years, FastAPI has emerged as a modern framework, purpose-built for APIs and asynchronous programming. Its speed, simplicity, and robust features make it an appealing choice for developers.&lt;/p&gt;
&lt;p&gt;In this guide, we’ll explore:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What makes FastAPI unique. &lt;/li&gt;
&lt;li&gt;Key differences compared to Django and Flask. &lt;/li&gt;
&lt;li&gt;Specific use cases where FastAPI excels.&lt;/li&gt;
&lt;li&gt;Detailed examples demonstrating its strengths.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;what-is-fastapi-&quot;&gt;&lt;strong&gt;What is FastAPI?&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;FastAPI is a Python framework for building APIs with modern design principles. It’s built on the Starlette framework (for the web parts) and Pydantic (for data validation). Unlike Django or Flask, FastAPI fully embraces asynchronous programming with asyncio, making it one of the fastest Python frameworks available.&lt;/p&gt;
&lt;h3 id=&quot;key-features-&quot;&gt;&lt;strong&gt;Key Features:&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;High Performance: Comparable to Node.js and Go, thanks to ASGI and&lt;br&gt;
async support.&lt;/li&gt;
&lt;li&gt;Automatic Documentation: Swagger UI and ReDoc are auto-generated.&lt;/li&gt;
&lt;li&gt;Type Safety: Input validation and response types are built-in using&lt;br&gt;
Python's type hints.&lt;/li&gt;
&lt;li&gt;Ease of Use: FastAPI is simple to use, even for beginners, while&lt;br&gt;
offering powerful capabilities for experts.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;why-choose-fastapi-&quot;&gt;Why Choose FastAPI?&lt;/h3&gt;
&lt;p&gt;FastAPI is specifically designed for building modern APIs with a focus on performance and developer experience. Let’s delve deeper into scenarios where it outshines Django and Flask:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. High-Performance APIs&lt;/strong&gt;&lt;br&gt;
If your application needs to handle thousands of simultaneous requests efficiently, FastAPI is a game-changer.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Built on ASGI, it supports asynchronous requests out of the box.&lt;/li&gt;
&lt;li&gt;Ideal for real-time applications, such as chat servers, streaming&lt;br&gt;
platforms, or IoT systems.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Example: Asynchronous API with FastAPI&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from fastapi import FastAPI
import asyncio

app = FastAPI()

@app.get(&quot;/long-task/&quot;)
async def long_task():
      await asyncio.sleep(5)  # Simulate a time-consuming operation
      return {&quot;message&quot;: &quot;Task completed after 5 seconds&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This endpoint:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Demonstrates how FastAPI handles concurrent requests without blocking&lt;br&gt;
other users.&lt;/li&gt;
&lt;li&gt;Would be cumbersome to implement in Flask or Django without&lt;br&gt;
significant modifications.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. Automatic Documentation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;FastAPI automatically generates Swagger UI and ReDoc documentation for your API. This is a huge time-saver for teams working on complex APIs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example: Auto-Generated Docs&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from fastapi import FastAPI

app = FastAPI()

@app.get(&quot;/items/{item_id}&quot;)
async def read_item(item_id: int, q: str = None):
&quot;&quot;&quot;
Fetch an item by ID.

  - **item_id**: The ID of the item to fetch.  
  - **q**: An optional query string for filtering results.  
&quot;&quot;&quot;
    return {&quot;item_id&quot;: item_id, &quot;query&quot;: q}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To access the docs:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run the app using uvicorn:&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt; uvicorn main:app --reload&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Visit:&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Swagger UI: &lt;a rel=&quot;nofollow&quot; href=&quot;http://127.0.0.1:8000/docs&quot;&gt;http://127.0.0.1:8000/docs&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ReDoc: &lt;a rel=&quot;nofollow&quot; href=&quot;http://127.0.0.1:8000/redoc&quot;&gt;http://127.0.0.1:8000/redoc&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Both provide an interactive interface to test your API endpoints.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Data Validation and Serialization&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;FastAPI uses Pydantic for data validation, which ensures robust type checking and error handling.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example: Input Validation with Pydantic&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
    username: str
    email: str
    age: int

@app.post(&quot;/users/&quot;)
async def create_user(user: User):
    return {&quot;message&quot;: &quot;User created successfully!&quot;, &quot;user&quot;: user}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What happens here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FastAPI automatically validates that the username, email, and age&lt;br&gt;
fields are present and of the correct type.&lt;/li&gt;
&lt;li&gt;If validation fails, the    API returns a detailed error message&lt;br&gt;
without any extra coding.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;4. WebSocket Support&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Real-time communication is a breeze with FastAPI, thanks to native WebSocket support.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example: Building a WebSocket Chat Server&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;from fastapi &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import FastAPI, WebSocket

app = FastAPI()

@app.websocket(&quot;/ws&quot;)
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    while True:
        data = await websocket.receive_text()
        await websocket.send_text(f&quot;You said: {data}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use this for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Chat applications. &lt;/li&gt;
&lt;li&gt;Real-time notifications or dashboards. &lt;/li&gt;
&lt;li&gt;Multiplayer    games.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;5. Ease of Scaling and Deployment&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;FastAPI integrates well with modern deployment tools like Docker and Kubernetes. Its ASGI compatibility makes it ideal for scaling microservices.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example: Dockerizing FastAPI&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM python:3.10-slim

WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
COPY . .

CMD [&quot;uvicorn&quot;, &quot;main:app&quot;, &quot;--host&quot;, &quot;0.0.0.0&quot;, &quot;--port&quot;, &quot;8000&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;when-django-or-flask-might-be-better&quot;&gt;When Django or Flask Might Be Better&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Django is unbeatable for full-stack applications with templates,&lt;br&gt;
authentication, and ORM out of the box.&lt;/li&gt;
&lt;li&gt;Flask is perfect for small projects or when you need a quick&lt;br&gt;
prototype without the complexity of Django or FastAPI.&lt;/li&gt;
&lt;li&gt;Legacy Systems: If your team already uses Django or Flask&lt;br&gt;
extensively, switching to FastAPI might not justify the learning&lt;br&gt;
curve.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;div-green&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Tip:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;When starting with FastAPI, use Uvicorn as your ASGI server for a smooth development experience. It’s lightweight, easy to configure, and perfectly complements FastAPI’s capabilities.&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;div-red&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Caution:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;If you are migrating from Django or Flask to FastAPI, note that FastAPI does not come with built-in components like an ORM (similar to Django’s) or a templating engine. Be prepared to integrate libraries like SQLAlchemy or Jinja2 for those functionalities.&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;div-blue&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Note:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;FastAPI’s auto-generated documentation supports both Swagger UI and ReDoc. These are especially useful for teams collaborating on API development as they provide an interactive interface for testing endpoints directly.&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;div-faq&quot;&gt;
&lt;span class=&quot;faq-header&quot;&gt;FAQ&lt;/span&gt;
&lt;span class=&quot;faq-body&quot;&gt;Q: Is FastAPI better than Django and Flask for all use cases?
A: Not necessarily. While FastAPI is excellent for APIs and asynchronous programming, Django is better suited for full-stack applications, and Flask is ideal for lightweight, quick prototypes. &lt;br&gt; &lt;br&gt;

Q: Does FastAPI support databases out of the box?
A: No, FastAPI doesn’t include a built-in ORM. However, you can easily integrate libraries like SQLAlchemy, Tortoise ORM, or GINO for database support. &lt;br&gt; &lt;br&gt;

Q: How does FastAPI handle validation errors?
A: FastAPI uses Pydantic for validation and automatically generates detailed error messages for invalid data inputs, saving developers significant time. &lt;br&gt; &lt;br&gt;

Q: Can I use FastAPI for real-time features like WebSockets?
A: Yes, FastAPI has built-in support for WebSockets, making it a great choice for real-time applications such as chat systems and live dashboards. &lt;br&gt; &lt;br&gt;

Q: What’s the best way to deploy FastAPI applications?
A: FastAPI integrates well with Docker for containerization and Kubernetes for scaling. Use an ASGI server like Uvicorn or Daphne for production deployment.&lt;/span&gt;
&lt;/div&gt; &lt;br&gt; &lt;br&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;FastAPI excels in modern, API-driven development, offering unparalleled performance, automatic documentation, and type safety. While Django and Flask still shine in specific scenarios, FastAPI is the future of Python APIs, designed for speed and scalability.&lt;/p&gt;
&lt;p&gt;Ready to start? Install FastAPI today and see how it transforms your API development!&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install fastapi uvicorn
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Python</category>
<guid isPermaLink="true">https://coderlegion.com/884/when-to-choose-fastapi-over-django-or-flask-comprehensive-guide-with-practical-examples</guid>
<pubDate>Wed, 22 Jan 2025 10:22:44 +0000</pubDate>
</item>
<item>
<title>PWA and Django #3: Online and offline resources in a PWA - Developing Progressive Web Applications with Django</title>
<link>https://coderlegion.com/486/pwa-django-online-offline-resources-developing-progressive-web-applications-with-django</link>
<description>&lt;p&gt;Welcome to the third entry on the Progressive Web Application with Django series. In this chapter, we'll learn how to cache resources for our PWA to be able to use them offline, without an active internet connection.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc2285582-02bc-4d1d-a6fa-f0298469c065_2048x2048.png&quot; alt=&quot;Online and offline resources in a PWA&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;implementing-offline-functionality&quot;&gt;Implementing Offline Functionality&lt;/h3&gt;
&lt;p&gt;In the previous chapters we defined an small PWA application with every required part: the manifest and the ServiceWorker. We learned how to register the PWA and developed a very simple interface with some images:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcff26966-a642-403e-9308-b5971d534994_367x302.png&quot; alt=&quot;UI for our PWA&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now we will learn how to store data in the PWA cache and how to choose from where to load every image: from the internet or from the local cache.&lt;/p&gt;
&lt;p&gt;To store one or more resources on the PWA cache we use a function like this on the ServiceWorker:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const CACHE_NAME = &quot;DJANGO_PWA_TEST&quot;
const MAIN_URL = &quot;https://laboratorio.alvarezperello.com/djangopwa/&quot;;

self.addEventListener(&quot;install&quot;, (event) =&amp;gt; {
   console.info(&quot;*** PWA event *** install&quot;, event);
   event.waitUntil(activateApp());
});

self.addEventListener(&quot;activate&quot;, (event) =&amp;gt; {
   console.info(&quot;*** PWA event *** activate&quot;, event);
   event.waitUntil(activateApp());
});

async function activateApp() {
   // When a service worker is initially registered, pages won't use it
   // until they next load. The claim() method causes those pages to be
   // controlled immediately.
   console.log('Claiming control...');
   clients.claim().then((ev) =&amp;gt; {
       console.log('...claimed!', ev);
   })

   manageCache();
}

self.addEventListener(&quot;sync&quot;, (event) =&amp;gt; {
   console.info(&quot;*** PWA event *** sync&quot;, event);
   manageCache();
});

async function manageCache() {
   const cache = await caches.open(CACHE_NAME);
   if (!cache) {
       console.error(&quot;Error storing resources in cache!&quot;);
       return;
   }

   storeResourceInCache(cache, MAIN_URL+&quot;static/demo/img/snake1.jpg&quot;);
   //storeResourceInCache(cache, MAIN_URL+&quot;static/demo/img/snake2.png&quot;);
   //storeResourceInCache(cache, MAIN_URL+&quot;static/demo/img/snake3.png&quot;);
}

async function storeResourceInCache(cache, element) {
   console.log(&quot;Storing resource in cache: &quot;+element);
   cache.add(element).then(event =&amp;gt; {
       console.info(&quot;Resource stored successfully! &quot;+element);
   }).catch(event =&amp;gt; {
       console.error(&quot;Error storing resource! &quot;+element, event);
   });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, when we execute our PWA, we can read the cache messages in the developer console:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Registering service worker...
...register completed!
The service worker is active!

serviceworker.js: Claiming control...
serviceworker.js: Resource already in cache! static/demo/img/snake1.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our PWA cache is working!&lt;/p&gt;
&lt;h3 id=&quot;choosing-from-where-to-load-each-resource&quot;&gt;Choosing from where to load each resource&lt;/h3&gt;
&lt;p&gt;When the PWA loads a resource, calls the fetch event, like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;self.addEventListener(&quot;fetch&quot;, async (event) =&amp;gt; {
   console.info(&quot;*** PWA event *** fetch&quot;, event);
  
   let url = event.request.url.toString();
   console.info(&quot;The PWA is loading a resource from: &quot;+url);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are in control of the request now, and can choose from where return the requested resource: from cache or from internet.&lt;/p&gt;
&lt;p&gt;Here is an example of how to check if we have a resource cached and return it from cache. And, if not cached, request it from the Internet instead.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;self.addEventListener(&quot;fetch&quot;, async (event) =&amp;gt; {
   let url = event.request.url.toString();
   if (!url.includes(&quot;static/demo/img/snake&quot;)) {
       return false;
   }

   const cache = await caches.open(CACHE_NAME);
   if (!cache) {
       console.error(&quot;Error loading resources from cache!&quot;);
       return false;
   }

   let fetchResponsePromise = await cache.match(url).then(async (cachedResponse) =&amp;gt; {
       if (cachedResponse &amp;amp;&amp;amp; cachedResponse.ok) {
           console.warn(&quot;Loading from cache: &quot;+url);
           return cachedResponse;

       } else {
           console.error(&quot;Error! the cache does not have this url! &quot;+url);
           console.error(cache.keys());

           remoteFetchResponsePromise = await fetch(event).then(async (networkResponse) =&amp;gt; {
               console.warn(&quot;Loading from internet: &quot;+url);
               return networkResponse;
           });

           return remoteFetchResponsePromise;
       }
   });


   return (await fetchResponsePromise);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can read the developer console to know from where every image has been loaded, like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F142f62e0-1acd-46ab-b257-6c5a64ac5bff_708x71.png&quot; alt=&quot;From where every image has been loaded&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;in-the-next-chapter&quot;&gt;In the next chapter&lt;/h3&gt;
&lt;p&gt;We now have a PWA. Now we will learn how to make an installable PWA, that will show as a native application in the operating system. That's one of the greatest functionalities of the PWAs: we can use them to create &quot;almost native&quot; applications using Django.&lt;/p&gt;
&lt;p&gt;See you in the next chapter!&lt;/p&gt;
&lt;h3 id=&quot;about-the-list-1&quot;&gt;About the list&lt;/h3&gt;
&lt;p&gt;Among the Python and Docker posts, I will also write about other related topics, like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Software architecture&lt;/li&gt;
&lt;li&gt;Programming environments&lt;/li&gt;
&lt;li&gt;Linux operating system&lt;/li&gt;
&lt;li&gt;Etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you found some interesting technology, programming language or whatever, please, let me know! I'm always open to learning something new!&lt;/p&gt;
&lt;h3 id=&quot;about-the-author-1&quot;&gt;About the author&lt;/h3&gt;
&lt;p&gt;I'm Andrés, a full-stack software developer based in Palma, on a personal journey to improve my coding skills. I'm also a self-published fantasy writer with four published novels to my name. Feel free to ask me anything!&lt;/p&gt;
</description>
<category>Web Dev</category>
<guid isPermaLink="true">https://coderlegion.com/486/pwa-django-online-offline-resources-developing-progressive-web-applications-with-django</guid>
<pubDate>Wed, 27 Nov 2024 22:52:48 +0000</pubDate>
</item>
<item>
<title>Django TemplateDoesNotExist Solved</title>
<link>https://coderlegion.com/311/django-templatedoesnotexist-solved</link>
<description>&lt;p&gt;While working in Django, you might encounter the &lt;strong&gt;&quot;TemplateDoesNotExist&quot;&lt;/strong&gt; error. This issue typically arises due to &lt;strong&gt;incorrect configuration&lt;/strong&gt; of &lt;strong&gt;template&lt;/strong&gt; &lt;strong&gt;settings&lt;/strong&gt; or &lt;strong&gt;paths&lt;/strong&gt;. Fortunately, you can easily solve this error by properly configuring your settings.py file to include the correct template directories. In this article, I will guide you through the steps to set up your template directories correctly, how to debug potential issues, and ensure your templates are correctly recognized by Django a &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/189/history-and-features-of-python?show=189#q189&quot;&gt;Python&lt;/a&gt; framework, all presented straightforwardly and practically.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://logiclair.org/?qa=blob&amp;amp;qa_blobid=14579476459226401271&quot; alt=&quot;TemplateDoesNotExist&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;configuring-django-template-settings&quot;&gt;&lt;strong&gt;Configuring Django Template Settings&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;The settings file of Django (&lt;code&gt;settings.py&lt;/code&gt;) determines where the framework should look for templates. In this sense, it is the first step towards the successful resolution of all errors related to templates, since making sure that it is correctly set up is the first step. &lt;/p&gt;
&lt;h3 id=&quot;template-directories&quot;&gt;&lt;strong&gt;Template Directories&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Ensure your &lt;code&gt;TEMPLATES&lt;/code&gt; configuration in &lt;code&gt;settings.py&lt;/code&gt; includes the correct path to your templates. For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        ...
    },
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Explanation&lt;/strong&gt;: Inside the &lt;code&gt;TEMPLATES&lt;/code&gt; setting, the value of the &lt;code&gt;DIRS&lt;/code&gt; option should be the absolute path to the directory of your templates. &lt;code&gt;BASE_DIR&lt;/code&gt; is a variable that holds the path to your project directory combined with &lt;code&gt;'templates'&lt;/code&gt; to point Django to the correct location.&lt;/p&gt;
&lt;h2 id=&quot;common-scenarios-and-solutions&quot;&gt;&lt;strong&gt;Common Scenarios and Solutions&lt;/strong&gt;&lt;/h2&gt;
&lt;h3 id=&quot;scenario-1-template-misplacement&quot;&gt;&lt;strong&gt;Scenario 1: Template Misplacement&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Your &lt;strong&gt;template&lt;/strong&gt; is not in a &lt;strong&gt;directory&lt;/strong&gt; recognized by Django.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Move your &lt;strong&gt;template&lt;/strong&gt; into one of the directories identified by the &lt;code&gt;DIRS&lt;/code&gt; option in the &lt;code&gt;TEMPLATES&lt;/code&gt; setting, or make sure the directory of your template appears in the appropriate application's &lt;code&gt;templates&lt;/code&gt; directory if &lt;code&gt;APP_DIRS&lt;/code&gt; is set to &lt;code&gt;True&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;scenario-2-typographical-errors-in-template-name&quot;&gt;&lt;strong&gt;Scenario 2: Typographical Errors in Template Name&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: There is a &lt;strong&gt;typo&lt;/strong&gt; in the template name used in your view.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Verify the template name in your view matches exactly with the filename in your templates directory, including &lt;strong&gt;case sensitivity&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;scenario-3-incorrect-app-structure&quot;&gt;&lt;strong&gt;Scenario 3: Incorrect App Structure&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Problem&lt;/strong&gt;: Django cannot find the template because the &lt;strong&gt;app structure&lt;/strong&gt; does not conform to Django's expectations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Ensure each app in your project contains a &lt;strong&gt;templates directory&lt;/strong&gt;. Django looks for templates inside the templates directory of each installed app if APP_DIRS is True.&lt;/p&gt;
&lt;h2 id=&quot;demonstrating-a-solution&quot;&gt;&lt;strong&gt;Demonstrating a Solution&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Create a Django project and start an app with the command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python manage.py startapp app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a &lt;strong&gt;templates&lt;/strong&gt; folder in the app folder and inside that create a &lt;strong&gt;my_template.html&lt;/strong&gt;. Your folder structure should look something like the following.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://logiclair.org/?qa=blob&amp;amp;qa_blobid=10929676237945187277&quot; alt=&quot;FolderStructure&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;urls-py&quot;&gt;urls.py&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;from django.contrib import admin
from django.shortcuts import render
from django.urls import path

def sample_view(request):
    return render(request, &quot;this_template_does_not_exist.html&quot;)

def my_view(request):
    return render(request, &quot;app/my_template.html&quot;)

urlpatterns = [
    path(&quot;admin/&quot;, admin.site.urls),
    path(&quot;test/&quot;, sample_view, name=&quot;test&quot;),
    path(&quot;view/&quot;, my_view, name=&quot;view&quot;),
] 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;my-template-html&quot;&gt;my_template.html&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang=&quot;en&quot;&amp;gt;
&amp;lt;head&amp;gt;
    &amp;lt;meta charset=&quot;UTF-8&quot;&amp;gt;
    &amp;lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&amp;gt;
    &amp;lt;title&amp;gt;Template&amp;lt;/title&amp;gt;
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
    This template exists
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;   &lt;/p&gt;
&lt;p&gt;Now start the server with &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python manage.py runserver
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now when you go to &lt;code&gt;http://localhost:8000/view&lt;/code&gt; you should be able to see the following output as the file exists and there are no typos involved. &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://logiclair.org/?qa=blob&amp;amp;qa_blobid=2700304601089646162&quot; alt=&quot;Correct&quot;&gt;&lt;/p&gt;
&lt;p&gt;But when you go to &lt;code&gt;http://localhost:8000/test&lt;/code&gt; you should be able to see the following output as the file does not exist.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://logiclair.org/?qa=blob&amp;amp;qa_blobid=14147655186650961319&quot; alt=&quot;Wrong&quot;&gt;&lt;/p&gt;
&lt;div class=&quot;div-red&quot;&gt;
    &lt;span class=&quot;alert-header&quot;&gt;Caution:&lt;/span&gt;
    &lt;span class=&quot;alert-body&quot;&gt;Confirm that `my_template.html` is placed in `/templates/` and that your `settings.py` correctly points to this directory as described earlier.
&lt;/span&gt;
&lt;/div&gt;
&lt;h2 id=&quot;best-practices-to-avoid-the-templatedoesnotexist-issue&quot;&gt;&lt;strong&gt;Best Practices to Avoid the &quot;TemplateDoesNotExist&quot; Issue&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;TemplateDoesNotExist&lt;/code&gt; error is a common issue in Django that can halt your development process. However, it's also one of the easiest to prevent with the right practices. You can refer various &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/257/python-debugging-techniques?show=257#q257&quot;&gt;Debugging Techniques&lt;/a&gt; in Python to know more about the type of error. Here are some tips to ensure you never run into this error again:&lt;/p&gt;
&lt;h3 id=&quot;1-verify-your-templates-configuration&quot;&gt;&lt;strong&gt;1. Verify Your TEMPLATES Configuration&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;The most common cause of this error is a &lt;strong&gt;misconfiguration&lt;/strong&gt; in the &lt;strong&gt;TEMPLATES&lt;/strong&gt; setting in your &lt;strong&gt;settings.py&lt;/strong&gt; file. Double-check that you have correctly defined the &lt;code&gt;DIRS&lt;/code&gt; list to include the paths to your templates. Use the &lt;code&gt;os.path.join(BASE_DIR, 'templates')&lt;/code&gt; pattern to ensure that your paths are correctly constructed and platform-independent.&lt;/p&gt;
&lt;h3 id=&quot;2-adhere-to-django-s-project-structure&quot;&gt;&lt;strong&gt;2. Adhere to Django's Project Structure&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Django expects your project to follow a certain structure, with a specific place for &lt;strong&gt;templates&lt;/strong&gt;. By default, Django looks for a templates directory in each app folder if &lt;strong&gt;APP_DIRS&lt;/strong&gt; is set to &lt;strong&gt;True&lt;/strong&gt; in your &lt;strong&gt;TEMPLATES&lt;/strong&gt; configuration. Ensure you place your templates in the correct directories within your apps or in the global &lt;strong&gt;templates&lt;/strong&gt; directory you specify in &lt;strong&gt;DIRS&lt;/strong&gt;. You can also use a &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/250/setting-up-virtual-environment-in-python?show=250#q250&quot;&gt;Virtual Environment&lt;/a&gt; to isolate the project.&lt;/p&gt;
&lt;h3 id=&quot;3-use-correct-template-naming-and-referencing&quot;&gt;&lt;strong&gt;3. Use Correct Template Naming and Referencing&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Always verify that the template names and paths you use in your views match exactly with your template files, including &lt;strong&gt;case sensitivity&lt;/strong&gt;, which can be an issue on some file systems. A common mistake is a &lt;strong&gt;simple typo&lt;/strong&gt; in the template path or name, leading Django to report that it cannot find the template.&lt;/p&gt;
&lt;h3 id=&quot;4-understand-template-loading-mechanism&quot;&gt;&lt;strong&gt;4. Understand Template Loading Mechanism&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Familiarize yourself with how Django loads templates, especially when using multiple apps. Django will search through the apps listed in &lt;strong&gt;INSTALLED_APPS&lt;/strong&gt; in the order they appear, looking for &lt;strong&gt;templates&lt;/strong&gt; in each app's templates directory. Knowing this order can help you troubleshoot and understand which template is being loaded when you have templates with the same name in different apps.&lt;/p&gt;
&lt;h3 id=&quot;5-regularly-check-for-deprecated-or-moved-templates&quot;&gt;&lt;strong&gt;5. Regularly Check for Deprecated or Moved Templates&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;When upgrading Django or third-party apps, templates might be &lt;strong&gt;deprecated&lt;/strong&gt; or &lt;strong&gt;moved&lt;/strong&gt;. Always check the release notes for any changes to template paths or names to ensure your project stays up-to-date and error-free.&lt;/p&gt;
&lt;h3 id=&quot;6-utilize-django-s-template-debugging-tools&quot;&gt;&lt;strong&gt;6. Utilize Django's Template Debugging Tools&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Make use of Django's built-in template debugging tools, such as setting &lt;strong&gt;DEBUG&lt;/strong&gt; to &lt;strong&gt;True&lt;/strong&gt; in your development settings to get detailed error pages that show you exactly where Django looked for your templates and where it failed to find them. This can be invaluable for quickly diagnosing and fixing path issues.&lt;/p&gt;
&lt;h3 id=&quot;7-consistent-use-of-template-extensions&quot;&gt;&lt;strong&gt;7. Consistent Use of Template Extensions&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Be consistent in your use of template file extensions. Django does not enforce a specific extension for templates &lt;strong&gt;(.html&lt;/strong&gt; is common but not required), but using different extensions inconsistently can lead to confusion and errors.&lt;/p&gt;
&lt;h3 id=&quot;8-automate-template-checks&quot;&gt;&lt;strong&gt;8. Automate Template Checks&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Consider incorporating checks into your deployment pipeline that verify the existence of templates referenced in your views. This can be as simple as a script that scans your views for &lt;strong&gt;render&lt;/strong&gt; calls and checks that the referenced templates exist in your project's template directories.&lt;/p&gt;
&lt;h2 id=&quot;conclusion-2&quot;&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;strong&gt;&quot;TemplateDoesNotExist&quot;&lt;/strong&gt; error in Django can be a stumbling block for developers. However, with a systematic approach to troubleshooting—verifying &lt;strong&gt;template locations&lt;/strong&gt;, ensuring correct settings in &lt;strong&gt;settings.py&lt;/strong&gt;, and checking for &lt;strong&gt;typos&lt;/strong&gt;—you can resolve this issue efficiently. Understanding &lt;strong&gt;Django's templating system&lt;/strong&gt; and how it locates templates is crucial for developing dynamic, error-free web applications.&lt;/p&gt;
&lt;p&gt;Remember, the key to avoiding such errors lies in adhering to Django's project structure conventions and thoroughly checking your configuration settings. With these practices, you'll find working with Django templates to be a smooth and error-free experience.&lt;/p&gt;
&lt;h3 id=&quot;additional-resources&quot;&gt;&lt;strong&gt;Additional Resources&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Django Official Documentation: Templates (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.djangoproject.com/en/stable/topics/templates/&quot;&gt;https://docs.djangoproject.com/en/stable/topics/templates/&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Django Official Documentation: Settings (&lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.djangoproject.com/en/stable/topics/settings/&quot;&gt;https://docs.djangoproject.com/en/stable/topics/settings/&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Python</category>
<guid isPermaLink="true">https://coderlegion.com/311/django-templatedoesnotexist-solved</guid>
<pubDate>Sat, 01 Jun 2024 23:24:56 +0000</pubDate>
</item>
<item>
<title>Python on the web - High cost of synchronous uWSGI</title>
<link>https://coderlegion.com/254/python-on-the-web-high-cost-of-synchronous-uwsgi</link>
<description>&lt;h3 id=&quot;introduction-1&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Python&lt;/strong&gt; is a popular choice for building programming due to its simplicity, readability, and vast ecosystem. Extending python as a webserver in a production environment is a little tricky due to the python &lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/Global_interpreter_lock&quot;&gt;&lt;strong&gt;GIL&lt;/strong&gt;&lt;/a&gt;. It basically renders most CPU bound multithreading pointless. We look into a popular option used widely and when and how it fails.&lt;/p&gt;
&lt;h3 id=&quot;python-on-web&quot;&gt;Python on Web&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Python interpreter&lt;/strong&gt; is basically an instance of our python application. We need a bridge between Python interpreter and the webserver to allow it to serve web requests. There are couple of popular standards for this bridge. We have WSGI and the latest ASGI standard. ASGI is a newer standard more suited for the latest async capabilities of python. Whereas WSGI is more suite for the traditional synchronous python.&lt;/p&gt;
&lt;p&gt;In this article, we are going to focus on how CPU bound tasks can lead to terrible user experience with WSGI.&lt;/p&gt;
&lt;div class=&quot;div-blue&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Note:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;&lt;strong&gt;WSGI Standard&lt;/strong&gt; helps us in extending python support to the web, which makes creating web applications using python possible.
One popular implementation in this standard is the &lt;strong&gt;uWSGI&lt;/strong&gt;.&lt;/span&gt;
&lt;/div&gt;
&lt;h3 id=&quot;how-uwsgi-works&quot;&gt;How uWSGI works&lt;/h3&gt;
&lt;p&gt;When we start a uWSGI instance for our Python application, it creates a worker process that runs an instance of the Python application with our application code imported. And if we have x number of worker process defined, it will fork until we have &lt;strong&gt;x&lt;/strong&gt; number of interpreters running.&lt;/p&gt;
&lt;div class=&quot;div-green&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Tip:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;We talk about &lt;strong&gt;uWSGI workers&lt;/strong&gt; as processes, but &lt;strong&gt;uWSGI workers&lt;/strong&gt; can be &lt;strong&gt;thread&lt;/strong&gt; based as well. However they hardly ever work without issues for any python web application. The main reason behind this is the ecosystem and libraries, with many being &lt;strong&gt;NOT threadsafe&lt;/strong&gt;.&lt;/span&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;NOT threadsafe&lt;/strong&gt; handles incoming HTTP requests and forward them to one of the available worker processes. Each worker process has its own &lt;strong&gt;GIL&lt;/strong&gt;, runs in its &lt;strong&gt;own memory space&lt;/strong&gt;, ensuring memory isolation and preventing issues like race conditions and data corruption that can occur when sharing state between threads.&lt;/p&gt;
&lt;h3 id=&quot;experiment-with-uwsgi&quot;&gt;Experiment with uWSGI&lt;/h3&gt;
&lt;p&gt;With our concepts clear, let's do a little experiment. We will see how incoming requests won't be served when the &lt;strong&gt;NOT threadsafe&lt;/strong&gt; workers are not available.&lt;/p&gt;
&lt;h4 id=&quot;setup&quot;&gt;Setup&lt;/h4&gt;
&lt;p&gt;We will create a &lt;strong&gt;NOT threadsafe&lt;/strong&gt; which will handle incoming requests. It is served by &lt;strong&gt;NOT threadsafe&lt;/strong&gt; with &lt;strong&gt;5 worker processes&lt;/strong&gt;. It will take &lt;strong&gt;10s&lt;/strong&gt; to respond to each request.&lt;/p&gt;
&lt;p&gt;We will create a client app while will make several (20) calls in parallel to the server at once. &lt;/p&gt;
&lt;div class=&quot;div-green&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Tip:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;You can directly get the code from the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/viv1/blog-code-examples/tree/main/uWSGI_blog&quot;&gt;repository&lt;/a&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Write a simple python application that return &quot;true&quot;. Call it &lt;strong&gt;app_main.py&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;
import time

def long_running_task():
    time.sleep(10)
    return &quot;true&quot;

def application(env, start_response):
    query_string = env.get('QUERY_STRING', '')

    query_params = {}
    for param in query_string.split('&amp;amp;'):
        key, value = param.split('=')
        query_params[key] = value

    # Get the 'param' value from the query parameters
    param_value = query_params.get('param', 'No param value provided')

    print(f&quot;Accept incoming request: {param_value}&quot;)
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return [long_running_task().encode()]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Create &lt;code&gt;Dockerfile&lt;/code&gt; to do platform &amp;amp; OS independent setup for &lt;strong&gt;uWSGI&lt;/strong&gt;&lt;br&gt;
 (Assuming you have docker installed, otherwise you can install via Docker website: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.docker.com/get-started/&quot;&gt;https://www.docker.com/get-started/&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;
    # Use an official Python runtime as a parent image
    FROM python:3.9
    
    # Set the working directory in the container
    WORKDIR /app
    
    # Install uWSGI and vim
    RUN apt-get update &amp;amp;&amp;amp; apt-get install -y build-essential vim
    RUN pip install uwsgi
    
    # Copy the app.py file into the container at /app
    COPY app_main.py /app
    
    # Make port 8000 available to the world outside this container
    EXPOSE 8000
    
    # Define environment variable
    ENV NAME World
    
    # Run uWSGI
    CMD [&quot;uwsgi&quot;, &quot;--http&quot;, &quot;0.0.0.0:8000&quot;, &quot;--wsgi-file&quot;, &quot;app_main.py&quot;, &quot;--master&quot;, &quot;--processes&quot;, &quot;5&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Create a client that will make calls in parallel to the web server. Call it &lt;strong&gt;client.py&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;
import requests
from concurrent.futures import ThreadPoolExecutor

def send_request(query_param):
    print(f&quot;Accept request: {query_param}&quot;)
    url = f'http://localhost:8000?param={query_param}'
    response = requests.get(url)
    print(f&quot;Response: {response.text}&quot;)

def send_requests():
    with ThreadPoolExecutor(max_workers=20) as executor:
        futures = [executor.submit(send_request, f'Request_{i}') for i in range(20)]

if __name__ == &quot;__main__&quot;:
    send_requests()
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;run-the-tests&quot;&gt;Run the tests&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;In one terminal, run the following to start our &lt;strong&gt;uWSGI&lt;/strong&gt; server:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;
    docker build -t blog-uwsgi -f Dockerfile .
    docker run -p 8000:8000 blog-uwsgi
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;In another terminal, run the following to setup client app:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;
python3 -m venv venv
source venv/bin/activate 
pip install uwsgi
pip install requests
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Now, run the client app:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;
python client.py
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;results&quot;&gt;Results&lt;/h4&gt;
&lt;p&gt;This is my output:&lt;br&gt;
&lt;/p&gt;&lt;pre&gt;&lt;code&gt;
&lt;code&gt;*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI master process (pid: 1)
spawned uWSGI worker 1 (pid: 6, cores: 1)
spawned uWSGI worker 2 (pid: 7, cores: 1)
spawned uWSGI worker 3 (pid: 8, cores: 1)
spawned uWSGI worker 4 (pid: 9, cores: 1)
spawned uWSGI worker 5 (pid: 10, cores: 1)
spawned uWSGI http 1 (pid: 11)
Accept incoming request: Request_6
Accept incoming request: Request_4
Accept incoming request: Request_0
Accept incoming request: Request_1
Accept incoming request: Request_7
[pid: 8|app: 0|req: 1/1] 192.168.65.1 () {32 vars in 405 bytes} [Thu Apr 11 05:04:21 2024] GET /?param=Request_0 =&amp;gt; generated 4 bytes in 10005 msecs (HTTP/1.1 200) 1 headers in 45 bytes (1 switches on core 0)
Accept incoming request: Request_9
[pid: 6|app: 0|req: 1/2] 192.168.65.1 () {32 vars in 405 bytes} [Thu Apr 11 05:04:21 2024] GET /?param=Request_4 =&amp;gt; generated 4 bytes in 10006 msecs (HTTP/1.1 200) 1 headers in 45 bytes (1 switches on core 0)
Accept incoming request: Request_10
[pid: 7|app: 0|req: 1/3] 192.168.65.1 () {32 vars in 405 bytes} [Thu Apr 11 05:04:21 2024] GET /?param=Request_7 =&amp;gt; generated 4 bytes in 10005 msecs (HTTP/1.1 200) 1 headers in 45 bytes (1 switches on core 0)
Accept incoming request: Request_8
[pid: 10|app: 0|req: 1/4] 192.168.65.1 () {32 vars in 405 bytes} [Thu Apr 11 05:04:21 2024] GET /?param=Request_6 =&amp;gt; generated 4 bytes in 10008 msecs (HTTP/1.1 200) 1 headers in 45 bytes (1 switches on core 0)
Accept incoming request: Request_2
[pid: 9|app: 0|req: 1/5] 192.168.65.1 () {32 vars in 405 bytes} [Thu Apr 11 05:04:21 2024] GET /?param=Request_1 =&amp;gt; generated 4 bytes in 10016 msecs (HTTP/1.1 200) 1 headers in 45 bytes (1 switches on core 0)
Accept incoming request: Request_11
&lt;/code&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;(Observe the &lt;strong&gt;Accept incoming request:&lt;/strong&gt; lines in the above &lt;strong&gt;uWSGI&lt;/strong&gt; server output).&lt;/p&gt;
&lt;p&gt;Notice how even when 20 requests were initiated by the client, the &lt;strong&gt;uWSGI&lt;/strong&gt; server could only handle 5 requests at once. All the 5 &lt;code&gt;worker processes&lt;/code&gt; were tied up. Until those requests were not served, no incoming request was being handled. As soon as a worker process became free, it took up another incoming request.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-3&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Python's &quot;GIL&quot; limitation is mitigated to some extent by &quot;uWSGI's&quot; ability to spawn multiple worker processes, however it is not enough when dealing with computationally intensive operations.&lt;/p&gt;
&lt;p&gt;One solution, that is used frequently, is offloading CPU-bound tasks to separate processes or services that run independently in background (example &quot;celery&quot;). This allows the uWSGI processes to handle incoming requests more efficiently.&lt;/p&gt;
&lt;p&gt;The evolution of web frameworks and standards continues to address the limitations observed in traditional &quot;WSGI&quot;-based applications, with &quot;ASGI&quot; being the latest trend.&lt;/p&gt;
&lt;p&gt;&quot;ASGI&quot; offers a solution to the synchronous limitations of &quot;WSGI&quot;, enabling Python applications to handle a large number of concurrent connections efficiently. This is particularly beneficial for I/O-bound and high-concurrency applications and specially when combined with python &quot;asyncio&quot;, where the traditional synchronous processing model of WSGI shows its limitations.&lt;/p&gt;
&lt;h3 id=&quot;references-and-further-reading&quot;&gt;References and Further Reading&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface&quot;&gt;WSGI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://uwsgi-docs.readthedocs.io/en/latest/WSGIquickstart.html&quot;&gt;uWSGI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://asgi.readthedocs.io/&quot;&gt;ASGI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://realpython.com/async-io-python/&quot;&gt;asyncio&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://realpython.com/asynchronous-tasks-with-django-and-celery/&quot;&gt;celery&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;div-blue&quot;&gt;
&lt;span class=&quot;alert-header&quot;&gt;Note:&lt;/span&gt;
&lt;span class=&quot;alert-body&quot;&gt;This article was originally published in my &lt;a rel=&quot;nofollow&quot; href=&quot;https://wewake.dev/posts/high-cost-of-sync-uwsgi/&quot;&gt;personal blog&lt;/a&gt;.&lt;/span&gt;
&lt;/div&gt;
</description>
<category>Python</category>
<guid isPermaLink="true">https://coderlegion.com/254/python-on-the-web-high-cost-of-synchronous-uwsgi</guid>
<pubDate>Tue, 30 Apr 2024 22:31:09 +0000</pubDate>
</item>
</channel>
</rss>