<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ravi Gupta</title>
    <description>The latest articles on DEV Community by Ravi Gupta (@ravigupta97).</description>
    <link>https://dev.to/ravigupta97</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3784807%2F6af58333-fc08-430f-9df5-a19e579fcea9.jpg</url>
      <title>DEV Community: Ravi Gupta</title>
      <link>https://dev.to/ravigupta97</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ravigupta97"/>
    <language>en</language>
    <item>
      <title>I Thought JWTs Were Stateless. Turns Out Logout Made Me Build a Stateful Layer Anyway.</title>
      <dc:creator>Ravi Gupta</dc:creator>
      <pubDate>Mon, 06 Apr 2026 03:15:00 +0000</pubDate>
      <link>https://dev.to/ravigupta97/i-thought-jwts-were-stateless-turns-out-logout-made-me-build-a-stateful-layer-anyway-48nd</link>
      <guid>https://dev.to/ravigupta97/i-thought-jwts-were-stateless-turns-out-logout-made-me-build-a-stateful-layer-anyway-48nd</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 3 of a 4-part series on building AuthShield - a production-ready standalone authentication microservice. This post covers JWT design, the logout problem, Redis blacklisting, the two-token strategy, and refresh token rotation with reuse detection.&lt;br&gt;
Part 1 is here: &lt;a href="https://dev.to/ravigupta97/why-i-stopped-writing-auth-code-for-every-project-and-built-authshield-21cj"&gt;Why I Stopped Writing Auth Code for Every Project and Built AuthShield&lt;/a&gt;&lt;br&gt;
Part 2 is here: &lt;a href="https://dev.to/ravigupta97/i-thought-oauth-was-just-adding-a-google-button-turns-out-its-a-csrf-problem-disguised-as-a-5gek"&gt;I Thought OAuth Was Just Adding a Google Button. Turns Out It's a CSRF Problem Disguised as a Feature&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;The selling point of JWTs is that they are stateless.&lt;/p&gt;

&lt;p&gt;The server issues a token, signs it, and forgets about it. Every subsequent request includes the token. The server verifies the signature, reads the claims, and knows everything it needs - who the user is, what roles they have, when the token expires. No database lookup. No session store. No shared state between instances.&lt;/p&gt;

&lt;p&gt;For an auth microservice like AuthShield that needs to work across multiple downstream services, this is exactly the right foundation. Any service with the same signing secret can verify any token independently.&lt;/p&gt;

&lt;p&gt;Then I implemented logout.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Logout Problem
&lt;/h2&gt;

&lt;p&gt;When a user logs out, what actually happens to their JWT?&lt;/p&gt;

&lt;p&gt;Nothing. It keeps working until it expires.&lt;/p&gt;

&lt;p&gt;A JWT is a signed string. It is not stored anywhere on the server. There is nothing to delete. If a user logs out and someone else - an attacker, a browser tab left open on a shared computer, anyone - has that token, it is still valid for the remainder of its lifetime.&lt;/p&gt;

&lt;p&gt;That is not logout. That is the appearance of logout.&lt;/p&gt;

&lt;p&gt;The solution is a blacklist. When a user logs out, you store the token's unique identifier in a fast lookup store and check every incoming token against it. If the token is on the blacklist, reject it regardless of the signature being valid.&lt;/p&gt;

&lt;p&gt;AuthShield uses Redis for this, and the specific identifier used is the &lt;code&gt;jti&lt;/code&gt; claim - JWT ID - a unique value embedded in every token at creation time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;redis.asyncio&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Redis&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_access_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;jti&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;  &lt;span class="c1"&gt;# Unique token ID - used for blacklisting
&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;roles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jti&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;jti&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;# The blacklist key
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;algorithm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HS256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jti&lt;/span&gt;  &lt;span class="c1"&gt;# Return both - jti needed for blacklisting on logout
&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;blacklist_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jti&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Calculate remaining lifetime of the token
&lt;/span&gt;    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;remaining_seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;remaining_seconds&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Store in Redis with TTL matching token's remaining life
&lt;/span&gt;        &lt;span class="c1"&gt;# When the token would have expired naturally, Redis removes it automatically
&lt;/span&gt;        &lt;span class="c1"&gt;# No cleanup job needed — it is self-managing
&lt;/span&gt;        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blacklist:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;jti&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remaining_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_token_blacklisted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jti&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blacklist:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;jti&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The TTL on the Redis key is important. There is no point keeping a blacklisted token in Redis after it would have expired naturally - it is already invalid. Setting the TTL to match the remaining token lifetime means the blacklist is self-cleaning. Old entries disappear automatically without any maintenance.&lt;/p&gt;

&lt;p&gt;Every protected request checks the blacklist before doing anything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_current_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HTTPAuthorizationCredentials&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;security&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_redis&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;algorithms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HS256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Always specify explicitly — never trust the token's own alg header
&lt;/span&gt;        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExpiredSignatureError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Token expired&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvalidTokenError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Check blacklist before anything else
&lt;/span&gt;    &lt;span class="n"&gt;jti&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jti&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;is_token_blacklisted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jti&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Token has been revoked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Token is valid and not blacklisted — proceed
&lt;/span&gt;    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;user_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_by_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One detail worth calling out: &lt;code&gt;algorithms=["HS256"]&lt;/code&gt; is explicit and non-negotiable. Early JWT libraries had a vulnerability where if the token header specified &lt;code&gt;"alg": "none"&lt;/code&gt;, some libraries would skip signature verification entirely. An attacker could forge any payload they wanted. Always specify exactly which algorithms you accept and reject everything else.&lt;/p&gt;

&lt;p&gt;Now logout works. But solving logout uncovered the next problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Two-Token Problem
&lt;/h2&gt;

&lt;p&gt;If the logout blacklist works, why not just use a single long-lived token? Issue it on login, blacklist it on logout, done.&lt;/p&gt;

&lt;p&gt;Because if that token is stolen, the attacker has access for the entire duration of its lifetime. A 7-day token stolen on day one gives an attacker 7 days of undetected access. A 30-day token is worse. The longer the lifetime, the larger the window of damage from theft.&lt;/p&gt;

&lt;p&gt;So make it short-lived. 15 minutes. Now a stolen token is only useful for 15 minutes.&lt;/p&gt;

&lt;p&gt;But now the user has to log in every 15 minutes. That is not a user experience - that is punishment.&lt;/p&gt;

&lt;p&gt;The solution is two tokens with different purposes and different lifetimes.&lt;/p&gt;

&lt;p&gt;An access token is short-lived - 15 minutes in AuthShield. It is sent with every API request. If it is stolen, the damage window is 15 minutes maximum.&lt;/p&gt;

&lt;p&gt;A refresh token is long-lived - 7 days. It is used for exactly one purpose: getting a new access token when the old one expires. It is never sent to API endpoints. It never touches the application layer. It only ever goes to the &lt;code&gt;/auth/refresh&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;The user experience is seamless. The access token expires silently, the client uses the refresh token to get a new one, and the user never knows it happened. Security and UX are no longer in conflict.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_refresh_token&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="c1"&gt;# Generate a cryptographically random token
&lt;/span&gt;    &lt;span class="n"&gt;raw_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;token_urlsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Hash it before storing — same reason we hash passwords
&lt;/span&gt;    &lt;span class="c1"&gt;# If the database is breached, raw tokens are not exposed
&lt;/span&gt;    &lt;span class="n"&gt;token_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;raw_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token_hash&lt;/span&gt;  &lt;span class="c1"&gt;# Return raw to client, store hash in DB
&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;store_refresh_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;token_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;family_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;  &lt;span class="c1"&gt;# More on this shortly
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;RefreshToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;refresh_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RefreshToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;token_hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;token_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;family_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;family_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;is_used&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;is_revoked&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;refresh_token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Refresh tokens are not JWTs. They are opaque random strings, stored hashed in PostgreSQL. There is no benefit to making them JWTs because they are always verified against the database anyway - a database lookup is required regardless, so the self-contained nature of JWTs adds nothing here.&lt;/p&gt;

&lt;p&gt;Storing them hashed mirrors the same principle as password hashing. If the database is compromised, an attacker gets hashes, not usable tokens.&lt;/p&gt;

&lt;p&gt;Two tokens solved the UX problem. But it introduced another one: what happens if the refresh token is stolen?&lt;/p&gt;




&lt;h2&gt;
  
  
  Refresh Token Rotation and Reuse Detection
&lt;/h2&gt;

&lt;p&gt;A 7-day refresh token is a valuable target. If an attacker gets hold of one, they can keep generating new access tokens for 7 days without the user's password. The user might log out, the access token gets blacklisted, but the attacker still has the refresh token and can get a new access token immediately.&lt;/p&gt;

&lt;p&gt;The first defence is rotation. Every time a refresh token is used, it is immediately invalidated and a new one is issued. The client must always use the latest token. Old tokens stop working the moment they are used.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rotate_refresh_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;old_token_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;family_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="c1"&gt;# Mark the old token as used
&lt;/span&gt;    &lt;span class="n"&gt;old_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;token_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_by_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old_token_hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;old_token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_used&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Issue a new token in the same family
&lt;/span&gt;    &lt;span class="n"&gt;new_raw_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_token_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_refresh_token&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;store_refresh_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;token_hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;new_token_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;family_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;family_id&lt;/span&gt;  &lt;span class="c1"&gt;# Same family as the old token
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;new_raw_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_token_hash&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rotation limits the damage window. But it does not fully solve the theft problem. If an attacker steals the refresh token and uses it before the real user does, the attacker gets a new valid token and the real user's next refresh attempt fails - their token was already rotated.&lt;/p&gt;

&lt;p&gt;The real user gets a confusing error. The attacker continues undetected.&lt;/p&gt;

&lt;p&gt;This is where token families solve the problem completely.&lt;/p&gt;

&lt;p&gt;Every refresh token belongs to a family - a UUID assigned at login time and shared by all tokens in a rotation chain. When a token is rotated, the new token carries the same family ID. The chain is trackable.&lt;/p&gt;

&lt;p&gt;The key insight: if a token that has already been marked as used is presented again, it means one of two things. Either there is a bug in the client — unlikely. Or the token was stolen and used by a second party - the real user rotated it, then the attacker tried to use the original.&lt;/p&gt;

&lt;p&gt;When reuse is detected, the correct response is not just to reject the request. It is to revoke the entire family - every token descended from that login. Both the attacker's current token and the real user's current token become invalid simultaneously.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_refresh_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;raw_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="c1"&gt;# Hash the incoming token to look it up
&lt;/span&gt;    &lt;span class="n"&gt;token_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;token_record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;token_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_by_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token_hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;token_record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid refresh token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;token_record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_revoked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Refresh token has been revoked&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# REUSE DETECTION — this is the critical check
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;token_record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_used&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# This token was already rotated — someone is using an old token
&lt;/span&gt;        &lt;span class="c1"&gt;# Could be an attacker who stole it before the real user rotated it
&lt;/span&gt;        &lt;span class="c1"&gt;# Revoke the entire family — both attacker and real user are logged out
&lt;/span&gt;        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;token_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revoke_family&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token_record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;family_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Token reuse detected. All sessions have been revoked.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Token is valid — rotate it
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;token_record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Refresh token expired&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;new_raw_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rotate_refresh_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;old_token_hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;token_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;token_record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;token_record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;family_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;token_record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;family_id&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Issue a new access token
&lt;/span&gt;    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;user_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_by_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token_record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;new_access_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_access_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;new_access_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_raw_token&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The outcome of reuse detection is uncomfortable for the real user - they get logged out and have to re-authenticate. But that is the correct response. Something is wrong. Either the token was stolen, or the client has a bug. Either way, forcing re-authentication is the right call. The real user is inconvenienced for 30 seconds. The attacker loses access entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Lives Where and Why
&lt;/h2&gt;

&lt;p&gt;After working through all of this, the storage decisions become clear.&lt;/p&gt;

&lt;p&gt;Access tokens live in client memory - never in localStorage, which is accessible to any JavaScript on the page including third-party scripts. Memory is cleared when the tab closes, which is acceptable given the 15-minute TTL.&lt;/p&gt;

&lt;p&gt;Refresh tokens live in PostgreSQL, stored as SHA-256 hashes. The raw token is given to the client once and never stored on the server. If the database is breached, the hashes are useless without the raw tokens.&lt;/p&gt;

&lt;p&gt;The blacklist lives in Redis, keyed by JTI with TTLs matching each token's remaining lifetime. Self-cleaning, fast, and adds under 1 millisecond to every protected request.&lt;/p&gt;




&lt;h2&gt;
  
  
  What JWT Security Actually Is
&lt;/h2&gt;

&lt;p&gt;Working through all four of these problems changed how I think about token security.&lt;/p&gt;

&lt;p&gt;The stateless property of JWTs is real and valuable - it is what makes AuthShield's tokens verifiable by any downstream service without calling back to AuthShield. But statelessness and revocability are fundamentally in conflict. The solution is not to pick one. It is to understand which layer handles which concern.&lt;/p&gt;

&lt;p&gt;JWTs handle identity and authorization - who the user is and what they can do. Redis handles revocation - making logout immediate. PostgreSQL handles refresh token lifecycle - rotation, family tracking, and theft detection. Each layer does one thing.&lt;/p&gt;

&lt;p&gt;Understanding that separation is what made the implementation make sense.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is Next
&lt;/h2&gt;

&lt;p&gt;Next week: rate limiting, integration testing against real infrastructure, Docker, and what I found out about the gap between working locally and running in production.&lt;/p&gt;

&lt;p&gt;I thought finishing the code meant I was done. It did not.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AuthShield on GitHub: &lt;a href="https://github.com/ravigupta97/authshield" rel="noopener noreferrer"&gt;AuthShield-Repository&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Always learning, always observing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>security</category>
      <category>backend</category>
      <category>authentication</category>
    </item>
    <item>
      <title>I Thought OAuth Was Just Adding a Google Button. Turns Out It's a CSRF Problem Disguised as a Feature.</title>
      <dc:creator>Ravi Gupta</dc:creator>
      <pubDate>Mon, 30 Mar 2026 03:15:00 +0000</pubDate>
      <link>https://dev.to/ravigupta97/i-thought-oauth-was-just-adding-a-google-button-turns-out-its-a-csrf-problem-disguised-as-a-5gek</link>
      <guid>https://dev.to/ravigupta97/i-thought-oauth-was-just-adding-a-google-button-turns-out-its-a-csrf-problem-disguised-as-a-5gek</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 2 of a 4-part series on building AuthShield - a production-ready standalone authentication microservice. This post covers the OAuth 2.0 implementation: Authorization Code Flow, PKCE, CSRF protection, and account linking.&lt;br&gt;
Part 1 is here: &lt;a href="https://dev.to/ravigupta97/why-i-stopped-writing-auth-code-for-every-project-and-built-authshield-21cj"&gt;Why I Stopped Writing Auth Code for Every Project and Built AuthShield&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;When I started implementing OAuth 2.0 in AuthShield, I thought it was going to be one of the easier parts.&lt;/p&gt;

&lt;p&gt;Add a Google button. Redirect the user. Get their email back. Done.&lt;/p&gt;

&lt;p&gt;I was wrong - not because OAuth is complicated, but because I did not understand what it actually is. I thought it was a login mechanism. It is not. OAuth 2.0 is an authorization framework, and almost every step in the flow exists to defend against a specific attack.&lt;/p&gt;

&lt;p&gt;Once I understood that, everything clicked. But getting there took more time than I expected.&lt;/p&gt;


&lt;h2&gt;
  
  
  What OAuth 2.0 Actually Is
&lt;/h2&gt;

&lt;p&gt;Most engineers encounter OAuth for the first time through social login. Click "Sign in with Google," get redirected, come back logged in. The experience is smooth and the implementation feels like it should match that smoothness.&lt;/p&gt;

&lt;p&gt;But OAuth 2.0 was not designed for authentication. It was designed for authorization - specifically, for allowing a third-party application to access resources on a user's behalf without ever seeing the user's password.&lt;/p&gt;

&lt;p&gt;The "Sign in with Google" use case is built on top of OAuth 2.0 using OpenID Connect, which adds an identity layer. When you implement Google login, you are using both, whether you realise it or not.&lt;/p&gt;

&lt;p&gt;This distinction matters because it changes how you think about the flow. You are not just "getting the user." You are asking Google to vouch for the user's identity and hand you a verified email address. Every step in the flow is about ensuring that handoff cannot be tampered with.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Authorization Code Flow
&lt;/h2&gt;

&lt;p&gt;AuthShield uses the Authorization Code Flow, which is the most secure OAuth flow for server-side applications. Here is how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user clicks "Sign in with Google"&lt;/li&gt;
&lt;li&gt;AuthShield redirects the user to Google's authorization server with a set of parameters — client ID, requested scopes, redirect URI, and two security parameters we will come back to&lt;/li&gt;
&lt;li&gt;The user logs into Google and grants permission&lt;/li&gt;
&lt;li&gt;Google redirects the user back to AuthShield's callback URL with a short-lived authorization code&lt;/li&gt;
&lt;li&gt;AuthShield exchanges that code for an access token by making a server-to-server request to Google — this is the key step, because the exchange happens on the backend, never in the browser&lt;/li&gt;
&lt;li&gt;AuthShield uses the access token to fetch the user's profile from Google&lt;/li&gt;
&lt;li&gt;AuthShield creates or finds the user in its own database and issues its own JWT&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 5 is what makes this flow secure. The authorization code is short-lived and single-use. Even if someone intercepts the redirect in step 4, they cannot exchange the code - they do not have the client secret required for the server-to-server exchange.&lt;/p&gt;

&lt;p&gt;The implicit flow, which used to be recommended for single-page applications, skipped step 5 and returned tokens directly in the URL fragment. That meant tokens were exposed in browser history, server logs, and referrer headers. It is deprecated now. Do not use it.&lt;/p&gt;


&lt;h2&gt;
  
  
  The State Parameter: The CSRF Problem I Almost Skipped
&lt;/h2&gt;

&lt;p&gt;The first security parameter in the authorization request is the state parameter, and I almost did not implement it properly because it looked optional in the documentation.&lt;/p&gt;

&lt;p&gt;It is not optional.&lt;/p&gt;

&lt;p&gt;Here is the attack it prevents. Without a state parameter, an attacker can initiate an OAuth flow on your site and craft a malicious link to the callback URL. If they trick a victim into clicking that link — through a phishing email, a malicious website, anything - the victim's browser completes the OAuth callback and their account gets linked to the attacker's identity. The attacker now has access to the victim's account.&lt;/p&gt;

&lt;p&gt;This is a Cross-Site Request Forgery attack applied to an OAuth flow. The state parameter prevents it by creating a binding between the authorization request and the callback.&lt;/p&gt;

&lt;p&gt;Here is how AuthShield implements it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;redis.asyncio&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Redis&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_oauth_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Generate a cryptographically random state token
&lt;/span&gt;    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;token_urlsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Store in Redis with a short TTL — 10 minutes is enough
&lt;/span&gt;    &lt;span class="c1"&gt;# After that, the state is invalid and the flow must restart
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oauth_state:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# 10 minutes in seconds
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_oauth_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Check if the state exists in Redis
&lt;/span&gt;    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oauth_state:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;exists&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="c1"&gt;# Delete immediately — state tokens are single-use
&lt;/span&gt;    &lt;span class="c1"&gt;# A replayed callback with the same state will fail
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth noting here. The state is stored in Redis, not the database. It is short-lived and single-use - once it is verified in the callback, it is deleted immediately. If an attacker somehow captures the state and tries to replay the callback, the token is already gone.&lt;/p&gt;

&lt;p&gt;The callback handler verifies the state before doing anything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;google_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_redis&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# First thing — verify the state
&lt;/span&gt;    &lt;span class="c1"&gt;# If this fails, reject the request entirely
&lt;/span&gt;    &lt;span class="n"&gt;is_valid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verify_oauth_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;is_valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid or expired state parameter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Only proceed if state is valid
&lt;/span&gt;    &lt;span class="c1"&gt;# Exchange code for token, fetch user info, etc.
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  PKCE: The Part That Took Me the Longest
&lt;/h2&gt;

&lt;p&gt;The second security parameter is PKCE - Proof Key for Code Exchange, pronounced "pixie." This is the part of the OAuth implementation that surprised me the most and took the longest to understand properly.&lt;/p&gt;

&lt;p&gt;PKCE was originally designed for mobile and single-page applications that cannot securely store a client secret. But it is now recommended for all OAuth flows regardless of application type.&lt;/p&gt;

&lt;p&gt;Here is the problem it solves. In the Authorization Code Flow, the authorization code is passed through the browser in a redirect. If an attacker can intercept that redirect - through a malicious app on the same device, a compromised browser extension, or a misconfigured redirect URI - they have the code. And if they also have the client secret, they can exchange it for tokens.&lt;/p&gt;

&lt;p&gt;PKCE removes the client secret from the equation for the code exchange. Instead, the client proves it initiated the original request by solving a cryptographic challenge.&lt;/p&gt;

&lt;p&gt;Here is how it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_pkce_pair&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="c1"&gt;# Step 1: Generate a random code verifier
&lt;/span&gt;    &lt;span class="c1"&gt;# This is a long random string that only the client knows
&lt;/span&gt;    &lt;span class="n"&gt;code_verifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;token_urlsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Step 2: Hash the verifier to create the code challenge
&lt;/span&gt;    &lt;span class="c1"&gt;# SHA-256 hash, then base64url encode it
&lt;/span&gt;    &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code_verifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;code_challenge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlsafe_b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;code_verifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code_challenge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flow with PKCE works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AuthShield generates the code verifier and code challenge before redirecting to Google&lt;/li&gt;
&lt;li&gt;The code challenge is sent to Google in the authorization request&lt;/li&gt;
&lt;li&gt;The code verifier is stored temporarily - in AuthShield's case, in Redis alongside the state token&lt;/li&gt;
&lt;li&gt;When Google redirects back with the authorization code, AuthShield sends both the code and the original code verifier to exchange for tokens&lt;/li&gt;
&lt;li&gt;Google hashes the verifier, compares it to the challenge it stored from step 2, and only issues tokens if they match&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An attacker who intercepts the authorization code cannot exchange it because they do not have the code verifier. It never left the server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initiate_google_oauth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Generate state and PKCE pair
&lt;/span&gt;    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generate_oauth_state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;code_verifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code_challenge&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_pkce_pair&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Store the code verifier in Redis linked to the state
&lt;/span&gt;    &lt;span class="c1"&gt;# So we can retrieve it in the callback
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pkce_verifier:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;code_verifier&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Build the Google authorization URL
&lt;/span&gt;    &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOOGLE_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redirect_uri&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOOGLE_REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scope&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openid email profile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code_challenge&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;code_challenge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code_challenge_method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://accounts.google.com/o/oauth2/v2/auth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; for k, v in params.items())&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;exchange_code_for_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Redis&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Retrieve the code verifier stored during initiation
&lt;/span&gt;    &lt;span class="n"&gt;code_verifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pkce_verifier:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;code_verifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PKCE verifier not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Clean up
&lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pkce_verifier:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Exchange code for token — include the verifier, not the challenge
&lt;/span&gt;    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://oauth2.googleapis.com/token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOOGLE_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;client_secret&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOOGLE_CLIENT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redirect_uri&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GOOGLE_REDIRECT_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grant_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authorization_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code_verifier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;code_verifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# This is what proves we initiated the request
&lt;/span&gt;            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason this took me time to understand was not the code itself - it is not complicated once you see it. It was understanding &lt;em&gt;why&lt;/em&gt; each step exists and what happens if you skip it. The docs describe PKCE as an enhancement. In practice, for any public-facing OAuth flow, it is a requirement.&lt;/p&gt;




&lt;h2&gt;
  
  
  Account Linking: The Edge Case Nobody Thinks About
&lt;/h2&gt;

&lt;p&gt;Once the OAuth flow works, there is an edge case that is easy to miss: what happens when a user who already has an email and password account tries to sign in with Google using the same email?&lt;/p&gt;

&lt;p&gt;There are two options. Reject them with an error - "this email is already registered, please log in with your password." Or link the accounts - recognise that it is the same person and give them access.&lt;/p&gt;

&lt;p&gt;Rejecting them is the wrong call. It creates a confusing user experience and forces the user to remember which method they used to sign up. Linking them is the right call, but it requires careful handling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_or_create_oauth_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;oauth_provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;oauth_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;avatar_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# First, check if a user with this OAuth provider ID already exists
&lt;/span&gt;    &lt;span class="c1"&gt;# This handles returning OAuth users
&lt;/span&gt;    &lt;span class="n"&gt;existing_oauth_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;user_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_by_oauth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;oauth_provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oauth_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;oauth_id&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;existing_oauth_user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;existing_oauth_user&lt;/span&gt;

    &lt;span class="c1"&gt;# Check if a user with this email exists (registered with password)
&lt;/span&gt;    &lt;span class="n"&gt;existing_email_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;user_repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_by_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;existing_email_user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Link the OAuth account to the existing user
&lt;/span&gt;        &lt;span class="c1"&gt;# They can now sign in with either method
&lt;/span&gt;        &lt;span class="n"&gt;existing_email_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth_provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oauth_provider&lt;/span&gt;
        &lt;span class="n"&gt;existing_email_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oauth_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oauth_id&lt;/span&gt;
        &lt;span class="n"&gt;existing_email_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;avatar_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;avatar_url&lt;/span&gt;
        &lt;span class="c1"&gt;# Mark as verified — the OAuth provider already verified the email
&lt;/span&gt;        &lt;span class="n"&gt;existing_email_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_verified&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;existing_email_user&lt;/span&gt;

    &lt;span class="c1"&gt;# New user — create them
&lt;/span&gt;    &lt;span class="c1"&gt;# No password hash needed — they authenticate via OAuth
&lt;/span&gt;    &lt;span class="c1"&gt;# Mark as verified immediately — OAuth provider verified the email
&lt;/span&gt;    &lt;span class="n"&gt;new_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;password_hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Nullable — OAuth users have no password
&lt;/span&gt;        &lt;span class="n"&gt;oauth_provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;oauth_provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;oauth_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;oauth_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;avatar_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;avatar_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;is_verified&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Google already verified it
&lt;/span&gt;        &lt;span class="n"&gt;is_active&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;new_user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth noting in this implementation.&lt;/p&gt;

&lt;p&gt;First, the lookup order matters. We check by OAuth provider ID first, then by email. This handles the case where a user has already linked their OAuth account - we find them immediately without touching the email lookup.&lt;/p&gt;

&lt;p&gt;Second, OAuth users are marked as verified immediately. Google has already verified the email address. Requiring them to go through email verification again would be both redundant and confusing. The &lt;code&gt;password_hash&lt;/code&gt; field in the database is nullable specifically to accommodate users who authenticate only through OAuth and have no password.&lt;/p&gt;




&lt;h2&gt;
  
  
  What OAuth Actually Is
&lt;/h2&gt;

&lt;p&gt;After implementing all of this, my understanding of OAuth shifted.&lt;/p&gt;

&lt;p&gt;It is not a login feature. It is a set of security boundaries, each one defending against a specific attack. The state parameter defends against CSRF. PKCE defends against authorization code interception. The server-side code exchange defends against token exposure in the browser. Account linking defends against a fragmented user experience that pushes users toward insecure workarounds.&lt;/p&gt;

&lt;p&gt;When I thought of it as "adding a Google button," I was looking at the 1% of the implementation that is visible. The other 99% is the security reasoning underneath it.&lt;/p&gt;

&lt;p&gt;Understanding that reasoning is the difference between OAuth that works and OAuth that holds up.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is Next
&lt;/h2&gt;

&lt;p&gt;Next week: JWTs, logout, and the conflict between stateless tokens and immediate revocation.&lt;/p&gt;

&lt;p&gt;I thought JWTs were stateless and that was their whole point. Then I tried to implement logout. The solution - Redis blacklisting, token families, and reuse detection - turned out to be the most interesting engineering problem in the entire project.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AuthShield on GitHub: &lt;a href="https://github.com/ravigupta97/authshield" rel="noopener noreferrer"&gt;AuthShield-Repository&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Always learning, always observing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>oauth</category>
      <category>security</category>
      <category>backend</category>
    </item>
    <item>
      <title>Why I Stopped Writing Auth Code for Every Project and Built AuthShield</title>
      <dc:creator>Ravi Gupta</dc:creator>
      <pubDate>Mon, 23 Mar 2026 03:27:46 +0000</pubDate>
      <link>https://dev.to/ravigupta97/why-i-stopped-writing-auth-code-for-every-project-and-built-authshield-21cj</link>
      <guid>https://dev.to/ravigupta97/why-i-stopped-writing-auth-code-for-every-project-and-built-authshield-21cj</guid>
      <description>&lt;p&gt;&lt;em&gt;This is Part 1 of a 4-part series on building AuthShield - a production-ready standalone authentication microservice. This post is the origin story. No code, just the thinking.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Every backend project I have built starts the same way.&lt;/p&gt;

&lt;p&gt;New repo. Fresh database. And then before writing a single line of product code, auth.&lt;/p&gt;

&lt;p&gt;Registration. Email verification. Login. JWT tokens. Password reset. OAuth. Roles. Sessions.&lt;/p&gt;

&lt;p&gt;The first time I built it, I learned a lot. I understood JWTs, token expiry, password hashing, OAuth flows. It was genuinely valuable.&lt;/p&gt;

&lt;p&gt;Then I started the next project. And built it again.&lt;/p&gt;

&lt;p&gt;Then the next one. And built it again.&lt;/p&gt;

&lt;p&gt;At some point I stopped and looked at what I was doing. Every project needs authentication. The requirements are almost identical across all of them. The security concerns are exactly the same. Yet I was treating it as a fresh problem every single time.&lt;/p&gt;

&lt;p&gt;That is not engineering. That is repetition.&lt;/p&gt;

&lt;p&gt;So I asked myself a simple question: why am I solving the same problem again?&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Problem With Copying Auth Code
&lt;/h2&gt;

&lt;p&gt;The obvious solution is to copy it from the previous project. Most engineers do this. I did it too.&lt;/p&gt;

&lt;p&gt;But copying auth code carries a problem that only shows up later. Auth logic gets tangled with the application it was written for. It references that project's database models, that project's config structure, that project's way of handling errors. You copy it, spend a day untangling it, make adjustments to fit the new project, and ship it.&lt;/p&gt;

&lt;p&gt;Two months later you find a security issue in the original. You patch it there. The copy still has the old version. You forget. It happens.&lt;/p&gt;

&lt;p&gt;The deeper issue is that every time you touch auth code to make it fit a new project, you introduce risk. Security code is not the place for casual modifications. A small change in how tokens are validated, how refresh tokens are stored, or how OAuth state is handled can create a vulnerability that looks completely normal in a code review.&lt;/p&gt;

&lt;p&gt;Copying is not reusing. It is duplicating - and duplicating security-critical code is one of the quieter ways things go wrong in backend engineering.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Idea: A Microservice Built Once, Used Everywhere
&lt;/h2&gt;

&lt;p&gt;The question was not how to write better auth code. The question was how to write it once and never write it again.&lt;/p&gt;

&lt;p&gt;Authentication is infrastructure. It is not a feature of any specific application — it is the layer that makes features possible. And like any infrastructure, it should live in one place, be maintained in one place, and be consumed by everything that needs it.&lt;/p&gt;

&lt;p&gt;That is what microservices are for. We separate payment processing into its own service. We separate email sending into its own service. Auth deserves the same treatment, arguably more so, because the consequences of getting it wrong are more severe than a failed payment webhook.&lt;/p&gt;

&lt;p&gt;The vision was straightforward: build a standalone auth microservice that any backend project can plug into. Not import as a library. Not copy code from. Actually plug into - send it an HTTP request, get back a JWT, validate that JWT locally, done. The application never touches passwords, tokens, or OAuth flows directly.&lt;/p&gt;

&lt;p&gt;One service. Any number of consumers. One place to maintain security. One place to patch vulnerabilities.&lt;/p&gt;

&lt;p&gt;That is AuthShield.&lt;/p&gt;




&lt;h2&gt;
  
  
  What AuthShield Actually Handles
&lt;/h2&gt;

&lt;p&gt;AuthShield is a deployable FastAPI service backed by PostgreSQL and Redis. Here is what it covers out of the box:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication&lt;/strong&gt; - Email and password registration with email verification, login returning JWT access and refresh tokens, Google and GitHub OAuth 2.0 via Authorization Code Flow, and TOTP-based two-factor authentication compatible with Google Authenticator and Authy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authorisation&lt;/strong&gt; - Role-based access control with three built-in roles: user, moderator, and admin. Roles are embedded in the JWT so downstream services check permissions without a single database call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token security&lt;/strong&gt; - 15-minute access tokens, 7-day refresh tokens with rotation on every use, Redis-based blacklisting so logout takes effect on the very next request, and refresh token reuse detection that revokes an entire token family the moment theft is suspected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session management&lt;/strong&gt; - Every login creates a tracked session recording IP address and device info. Users can list all active sessions and revoke any of them individually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rate limiting&lt;/strong&gt; - Redis sliding-window rate limits on every sensitive endpoint. Not fixed-window — sliding-window, which means there is no clock boundary reset to exploit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production infrastructure&lt;/strong&gt; - Dockerised with a multi-stage build, Nginx for TLS termination and security headers on every response, structured JSON logging, and 48 integration tests running against real PostgreSQL and Redis.&lt;/p&gt;

&lt;p&gt;The integration pattern is the part that makes this actually useful as a reusable service. Any downstream project needs exactly one file and one shared environment variable. The file is 40 lines of Python. It validates the JWT locally - no runtime call to AuthShield on every request - extracts the user ID and roles from the token, and makes them available to every protected route. That is it. The application never thinks about auth again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Auth0, Clerk, or Any Existing Solution
&lt;/h2&gt;

&lt;p&gt;This is the question worth answering honestly.&lt;/p&gt;

&lt;p&gt;Managed auth services are good products. Auth0, Clerk, Supabase Auth - they are mature, well-documented, and genuinely save time. For many teams and many products, they are the right call.&lt;/p&gt;

&lt;p&gt;But I had two specific reasons for not going that route.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first is understanding.&lt;/strong&gt; When you use a managed auth service, you are trusting a black box with the most security-critical layer of your application. That is a reasonable tradeoff for speed. But it means that when something goes wrong - and in auth, something eventually always goes wrong — you are dependent on someone else's documentation, someone else's support, and someone else's timeline to fix it.&lt;/p&gt;

&lt;p&gt;I wanted to understand every decision in my auth layer. Why the token TTL is what it is. Why refresh tokens are stored hashed rather than plain. Why the OAuth state parameter exists and what happens if it is missing. Why sliding-window rate limiting behaves differently from fixed-window at the boundary. Not because I enjoy complexity, but because understanding your security layer is the difference between debugging a problem and being surprised by one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The second is control.&lt;/strong&gt; Security decisions should belong to the engineer shipping the system. What hashing algorithm, what token strategy, what revocation mechanism, what rate limit thresholds - these are decisions that have real consequences. Delegating them entirely to a third-party service means accepting their defaults without always knowing what those defaults are or why they were chosen.&lt;/p&gt;

&lt;p&gt;AuthShield gives full control over every one of those decisions. Every choice is visible, documented, and mine to change.&lt;/p&gt;

&lt;p&gt;Neither of these reasons means managed auth services are wrong. They mean they were wrong for what I was trying to build.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Series Covers
&lt;/h2&gt;

&lt;p&gt;This post is the origin story. The next three go into the parts that actually made me think.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next week -&lt;/strong&gt; OAuth and why it is more than just adding a Google button&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Week after -&lt;/strong&gt; JWTs, logout, and why stateless tokens create a stateful problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final week -&lt;/strong&gt; Rate limiting, testing, and the gap between working locally and running in production.&lt;/p&gt;

&lt;p&gt;The GitHub repo is linked below. The README has a full integration guide — how to plug AuthShield into any backend project in under 30 minutes. Contributions and feedback are welcome.&lt;/p&gt;




&lt;h2&gt;
  
  
  One Last Thought
&lt;/h2&gt;

&lt;p&gt;I built AuthShield because I saw a problem I kept solving repeatedly and decided to solve it once properly.&lt;/p&gt;

&lt;p&gt;But there is something else that came out of building it that I did not expect. The process of designing a security-focused microservice from scratch - making every decision deliberately, documenting every tradeoff, writing tests that actually run against real infrastructure - changed how I think about backend engineering in general.&lt;/p&gt;

&lt;p&gt;Not every project needs a custom auth microservice. But every engineer who has built one has a different relationship with the auth layer in every project they touch after that.&lt;/p&gt;

&lt;p&gt;That, more than anything, is why I would recommend the experience.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;AuthShield on GitHub: &lt;a href="https://github.com/ravigupta97/authshield" rel="noopener noreferrer"&gt;https://github.com/ravigupta97/authshield&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Always learning, always observing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>backend</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Faster We Build with AI, the More Dangerous Bad Auth Becomes - And the Rarer Good Auth Becomes</title>
      <dc:creator>Ravi Gupta</dc:creator>
      <pubDate>Mon, 16 Mar 2026 03:15:00 +0000</pubDate>
      <link>https://dev.to/ravigupta97/the-faster-we-build-with-ai-the-more-dangerous-bad-auth-becomes-and-the-rarer-good-auth-becomes-1oeg</link>
      <guid>https://dev.to/ravigupta97/the-faster-we-build-with-ai-the-more-dangerous-bad-auth-becomes-and-the-rarer-good-auth-becomes-1oeg</guid>
      <description>&lt;h2&gt;
  
  
  The Faster We Build with AI, the More Dangerous Bad Auth Becomes - And the Rarer Good Auth Becomes
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;While everyone races to ship with AI, understanding authentication and authorization is quietly becoming the most valuable skill in backend engineering.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Let me set a scene that probably sounds familiar.&lt;/p&gt;

&lt;p&gt;You're building a new backend service. You open Cursor, type a prompt - &lt;em&gt;"implement OAuth 2.0 login with JWT tokens"&lt;/em&gt; - and in about 8 seconds you have 150 lines of clean, readable, production-looking code. You skim it. It looks right. You move on.&lt;/p&gt;

&lt;p&gt;I've done this. Most engineers I know have done this.&lt;/p&gt;

&lt;p&gt;And here's what nobody talks about: that code is often &lt;em&gt;almost&lt;/em&gt; correct. Not obviously broken - just subtly, silently wrong in ways that don't surface until something goes wrong in production. A token that should expire doesn't. A user who should lose access still has it. An endpoint that should be protected isn't.&lt;/p&gt;

&lt;p&gt;The AI didn't fail you. You just didn't know what to check.&lt;/p&gt;

&lt;p&gt;That's what this post is about.&lt;/p&gt;




&lt;h2&gt;
  
  
  First, Let's Get the Basics Right - Because Most People Still Conflate These
&lt;/h2&gt;

&lt;p&gt;Before we go deeper, one thing I need to address: &lt;strong&gt;Authentication and Authorization are not the same thing&lt;/strong&gt;, and confusing them is the root cause of more bugs than I can count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authentication&lt;/strong&gt; answers: &lt;em&gt;Who are you?&lt;/em&gt;&lt;br&gt;
It's the process of verifying identity. When you log in with a username and password, or sign in with Google - that's authentication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authorization&lt;/strong&gt; answers: &lt;em&gt;What are you allowed to do?&lt;/em&gt;&lt;br&gt;
It's the process of determining permissions. After the system knows who you are, authorization decides what resources you can access and what actions you can take.&lt;/p&gt;

&lt;p&gt;Here's a simple way to remember it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Authentication is the bouncer checking your ID at the door.&lt;br&gt;
Authorization is the VIP list that decides which rooms you can enter.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You need both. In that order. Always.&lt;/p&gt;

&lt;p&gt;Most AI-generated auth code handles authentication passably. Authorization is where things quietly go wrong — missing scope checks, over-permissioned tokens, endpoints that assume "logged in" means "allowed."&lt;/p&gt;

&lt;p&gt;Keep this distinction in your head for the rest of this post.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why This Matters More Right Now Than It Ever Has
&lt;/h2&gt;

&lt;p&gt;A few years ago, building a backend with proper auth took meaningful effort. You had to understand the flow, write the code, debug it, test it. That friction was annoying - but it also meant you spent &lt;em&gt;time&lt;/em&gt; with the code. You understood it.&lt;/p&gt;

&lt;p&gt;Today, that friction is largely gone.&lt;/p&gt;

&lt;p&gt;Copilot, ChatGPT, Claude, Cursor - these tools can scaffold an entire authentication system in minutes. That's genuinely incredible. It's also genuinely risky when the engineer shipping that code doesn't understand what's under the hood.&lt;/p&gt;

&lt;p&gt;Think about what's happening at scale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Thousands of developers are shipping AI-generated auth code every day&lt;/li&gt;
&lt;li&gt;Many of them are newer engineers who haven't been burned by auth bugs yet&lt;/li&gt;
&lt;li&gt;The code &lt;em&gt;looks&lt;/em&gt; correct - it has the right function names, the right libraries, the right structure&lt;/li&gt;
&lt;li&gt;But subtle mistakes - wrong grant type, missing token validation, no refresh rotation - slip through&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The attack surface of the internet is growing faster than collective security knowledge.&lt;/strong&gt; That's the uncomfortable reality.&lt;/p&gt;

&lt;p&gt;And here's the flip side - the opportunity: engineers who genuinely understand auth, who can look at AI-generated OAuth code and immediately spot what's missing or wrong, are increasingly rare and increasingly valuable.&lt;/p&gt;

&lt;p&gt;Let's make sure you're one of them.&lt;/p&gt;


&lt;h2&gt;
  
  
  OAuth 2.0: What It Actually Is (And What People Get Wrong About It)
&lt;/h2&gt;

&lt;p&gt;OAuth 2.0 is an &lt;strong&gt;authorization framework&lt;/strong&gt; - not an authentication protocol. This trips people up constantly. OAuth 2.0 on its own does not tell you who the user is. It tells you what a client application is allowed to do on behalf of a user.&lt;/p&gt;

&lt;p&gt;Authentication on top of OAuth 2.0 is what OpenID Connect (OIDC) adds. If you're using OAuth to log users in, you're almost certainly using OIDC on top — whether you know it or not.&lt;/p&gt;

&lt;p&gt;OAuth 2.0 works through &lt;strong&gt;flows&lt;/strong&gt;, also called grant types. Different situations call for different flows. This is where I see AI-generated code go wrong most often - it picks a flow, but not necessarily the right one for the context.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Four Flows You Need to Know
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Authorization Code Flow + PKCE&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Use this for: web apps, mobile apps, SPAs - anything where a user is present&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the most common and most secure flow for user-facing applications. Here's how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your app redirects the user to the authorization server (e.g., Google, Auth0)&lt;/li&gt;
&lt;li&gt;The user logs in and grants permission&lt;/li&gt;
&lt;li&gt;The authorization server redirects back to your app with a short-lived &lt;strong&gt;authorization code&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Your app exchanges that code for an &lt;strong&gt;access token&lt;/strong&gt; (and optionally a refresh token)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The code is single-use and expires quickly. Even if someone intercepts the redirect, they can't do much with just the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PKCE&lt;/strong&gt; (Proof Key for Code Exchange - pronounced "pixie") is the security enhancement that makes this flow safe for public clients like SPAs and mobile apps that can't securely store a client secret.&lt;/p&gt;

&lt;p&gt;How PKCE works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your app generates a random &lt;strong&gt;code verifier&lt;/strong&gt; (a long random string)&lt;/li&gt;
&lt;li&gt;It hashes that verifier to create a &lt;strong&gt;code challenge&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;It sends the code challenge with the authorization request&lt;/li&gt;
&lt;li&gt;When exchanging the code for tokens, it sends the original code verifier&lt;/li&gt;
&lt;li&gt;The authorization server verifies the verifier matches the challenge&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means even if an attacker intercepts the authorization code, they can't exchange it - they don't have the code verifier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI-generated code mistake I see here:&lt;/strong&gt; Omitting PKCE entirely for SPAs, or implementing the Authorization Code flow without PKCE for mobile apps. It looks fine. It isn't.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;2. Client Credentials Flow&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Use this for: machine-to-machine (M2M), service-to-service, backend APIs&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;No user involved here. This is for when one service needs to talk to another service. Your backend requests a token directly from the authorization server using its client ID and client secret.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Backend Service → Authorization Server: "Here's my client ID and secret"
Authorization Server → Backend Service: "Here's your access token"
Backend Service → Resource API: "Here's my access token"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple. But the mistakes here are costly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storing the client secret in version control&lt;/strong&gt; (it happens more than you think — and AI-generated setup instructions sometimes put secrets in example config files that get committed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not rotating secrets&lt;/strong&gt; - a secret that never changes is a liability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Over-scoping the token&lt;/strong&gt; - requesting all scopes "just in case" instead of the minimum required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not handling token expiry&lt;/strong&gt; - the service breaks at 2am because nobody implemented token refresh logic&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;3. Implicit Flow&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Status: Deprecated. Do not use.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I'm mentioning this because AI tools sometimes still generate it - especially if trained on older code. The Implicit Flow was designed for SPAs before PKCE existed. It returned access tokens directly in the URL fragment, which is a significant security issue. PKCE-enhanced Authorization Code Flow replaced it entirely.&lt;/p&gt;

&lt;p&gt;If you see Implicit Flow in AI-generated code, replace it.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;4. Resource Owner Password Credentials (ROPC)&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Status: Highly discouraged. Almost never appropriate.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This flow has the user send their username and password directly to your application, which then exchanges them for tokens. It defeats the entire point of OAuth - the user should never give their credentials to a third-party app.&lt;/p&gt;

&lt;p&gt;The only time this might be acceptable is for highly trusted first-party applications, and even then, there are usually better alternatives. AI tools sometimes generate this for "simplicity." It isn't worth it.&lt;/p&gt;


&lt;h2&gt;
  
  
  Token Security: Where Most Bugs Live
&lt;/h2&gt;

&lt;p&gt;Let's talk about the actual tokens - because this is where subtle mistakes cause the most damage.&lt;/p&gt;
&lt;h3&gt;
  
  
  JWT: The Good, The Bad, and The Dangerous
&lt;/h3&gt;

&lt;p&gt;JSON Web Tokens (JWTs) are everywhere. An access token in most OAuth implementations is a JWT. Here's what one looks like under the hood:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;header.payload.signature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Header&lt;/strong&gt;: algorithm used to sign the token (&lt;code&gt;RS256&lt;/code&gt;, &lt;code&gt;HS256&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload&lt;/strong&gt;: the claims - user ID, scopes, expiry, issuer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signature&lt;/strong&gt;: cryptographic proof the token hasn't been tampered with&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The most dangerous JWT mistake: accepting &lt;code&gt;alg: none&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Early JWT libraries had a flaw - if the header said &lt;code&gt;"alg": "none"&lt;/code&gt;, some libraries would skip signature verification entirely. An attacker could forge any token they wanted.&lt;/p&gt;

&lt;p&gt;The fix: always explicitly specify which algorithms your server accepts. Reject anything else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;

&lt;span class="c1"&gt;# Bad - trusting the token's own algorithm header
# Some libraries will skip verification if alg is "none"
&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Better - explicitly specify accepted algorithms
&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;algorithms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RS256&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# reject anything else
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Other JWT mistakes I've observed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No expiry (&lt;code&gt;exp&lt;/code&gt; claim missing)&lt;/strong&gt;: A token that never expires is a permanent backdoor. Always set short expiry - 15 minutes for access tokens is common.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sensitive data in the payload&lt;/strong&gt;: The payload is base64-encoded, not encrypted. Anyone can decode it. Don't put passwords, PII, or sensitive business logic in JWT claims.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not validating the issuer (&lt;code&gt;iss&lt;/code&gt;) and audience (&lt;code&gt;aud&lt;/code&gt;)&lt;/strong&gt;: A token issued for Service A should not be accepted by Service B. Always validate these claims.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weak signing secrets&lt;/strong&gt;: If you're using HMAC (HS256), your secret needs to be long and random. &lt;code&gt;secret123&lt;/code&gt; is not a secret.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Refresh Tokens: The Part Everyone Forgets to Secure
&lt;/h3&gt;

&lt;p&gt;Access tokens are short-lived. Refresh tokens are long-lived - they're used to get new access tokens without the user re-authenticating. That makes them valuable targets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refresh Token Rotation&lt;/strong&gt; is the security practice you should be using:&lt;/p&gt;

&lt;p&gt;Every time a refresh token is used, the authorization server issues a &lt;em&gt;new&lt;/em&gt; refresh token and &lt;strong&gt;invalidates the old one&lt;/strong&gt;. If an attacker steals a refresh token and tries to use it after the legitimate user already has, the server detects the reuse and can invalidate the entire session.&lt;/p&gt;

&lt;p&gt;AI-generated code often implements refresh token logic without rotation. It works - it just doesn't protect against token theft.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refresh token storage matters too:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In web apps: &lt;code&gt;HttpOnly&lt;/code&gt;, &lt;code&gt;Secure&lt;/code&gt;, &lt;code&gt;SameSite=Strict&lt;/code&gt; cookies - not localStorage (XSS can read localStorage)&lt;/li&gt;
&lt;li&gt;In mobile apps: the device's secure keychain/keystore&lt;/li&gt;
&lt;li&gt;In backend services: encrypted at rest&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Token Revocation: The Feature Nobody Implements Until They Need It
&lt;/h3&gt;

&lt;p&gt;What happens when a user reports their account was compromised? Or when an employee leaves the company? Or when you detect suspicious activity?&lt;/p&gt;

&lt;p&gt;You need to &lt;strong&gt;revoke tokens&lt;/strong&gt; - immediately invalidate them before they expire naturally.&lt;/p&gt;

&lt;p&gt;JWTs are stateless by design, which means the server doesn't track them. A revoked JWT is still cryptographically valid until it expires. There are two practical approaches:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Token blocklist&lt;/strong&gt;: Maintain a fast store (Redis works well) of revoked token IDs (&lt;code&gt;jti&lt;/code&gt; claim). Check every incoming token against this list. Overhead is low if done right.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Short expiry + refresh rotation&lt;/strong&gt;: Keep access tokens extremely short-lived (5-15 minutes). Revoke the refresh token at the authorization server. The user's access naturally expires within minutes.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most AI-generated code implements neither. It's not a bug that shows up in testing — only in an incident.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Vulnerabilities: A Quick-Reference List
&lt;/h2&gt;

&lt;p&gt;These are the ones I check for when reviewing auth code — AI-generated or otherwise:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope Creep&lt;/strong&gt;: Requesting more OAuth scopes than needed. Always use minimum required scopes. If your app only needs to read a user's email, don't request write access to their entire account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open Redirect&lt;/strong&gt;: OAuth flows use redirect URIs. If your server doesn't strictly validate the redirect URI against a whitelist, an attacker can redirect tokens to their own server. Always validate exact URI matches — not just domain matches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State Parameter Missing&lt;/strong&gt;: The &lt;code&gt;state&lt;/code&gt; parameter in OAuth flows is CSRF protection. It's a random value your app sends with the authorization request and expects back in the callback. Without it, an attacker can trick a user into completing an OAuth flow the attacker initiated. AI-generated code frequently omits this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logging Tokens&lt;/strong&gt;: Access tokens and refresh tokens should never appear in logs. One misconfigured logger that records request headers or bodies, and your tokens are in your logging system - accessible to anyone with log access. Scrub tokens from logs explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trusting Client-Provided User IDs&lt;/strong&gt;: If your API accepts a &lt;code&gt;user_id&lt;/code&gt; in the request body and acts on it without verifying it matches the authenticated token's subject claim - that's a broken authorization vulnerability. Always derive the user identity from the validated token, never from user-provided input.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Means for Your Career
&lt;/h2&gt;

&lt;p&gt;Here's what I've been observing and thinking about.&lt;/p&gt;

&lt;p&gt;AI tools are genuinely compressing the time it takes to build things. That's not going away. More backends will be built, by more people, faster than ever before.&lt;/p&gt;

&lt;p&gt;But auth is not a problem that's been solved by AI generation. It's a domain where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mistakes are invisible until they're exploited&lt;/li&gt;
&lt;li&gt;"Looks correct" and "is correct" are dangerously different things&lt;/li&gt;
&lt;li&gt;The consequences of getting it wrong fall on real users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Engineers who can look at AI-generated auth code and immediately know &lt;em&gt;what to look for&lt;/em&gt;, &lt;em&gt;why certain patterns are dangerous&lt;/em&gt;, and &lt;em&gt;what questions to ask&lt;/em&gt; - those engineers are not being replaced by AI tools. They're the ones who can actually use those tools responsibly.&lt;/p&gt;

&lt;p&gt;That's a real differentiator. Not just technically, but professionally.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Practical Checklist: What to Review in Every Auth Implementation
&lt;/h2&gt;

&lt;p&gt;Whether the code is AI-generated or hand-written, here's what I run through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Are we using the right OAuth flow for this use case?&lt;/li&gt;
&lt;li&gt;[ ] Is PKCE implemented for public clients?&lt;/li&gt;
&lt;li&gt;[ ] Are JWTs validated with explicit algorithm specification?&lt;/li&gt;
&lt;li&gt;[ ] Do access tokens have short expiry (&lt;code&gt;exp&lt;/code&gt; claim set)?&lt;/li&gt;
&lt;li&gt;[ ] Is the &lt;code&gt;iss&lt;/code&gt; (issuer) and &lt;code&gt;aud&lt;/code&gt; (audience) being validated?&lt;/li&gt;
&lt;li&gt;[ ] Is refresh token rotation implemented?&lt;/li&gt;
&lt;li&gt;[ ] Are refresh tokens stored securely (not in localStorage)?&lt;/li&gt;
&lt;li&gt;[ ] Is there a token revocation strategy?&lt;/li&gt;
&lt;li&gt;[ ] Are redirect URIs strictly validated against a whitelist?&lt;/li&gt;
&lt;li&gt;[ ] Is the &lt;code&gt;state&lt;/code&gt; parameter used to prevent CSRF?&lt;/li&gt;
&lt;li&gt;[ ] Are tokens being excluded from logs?&lt;/li&gt;
&lt;li&gt;[ ] Does each OAuth scope request follow the principle of least privilege?&lt;/li&gt;
&lt;li&gt;[ ] Is user identity derived from the token, not from request input?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Print it. Bookmark it. Run through it before every auth PR you review.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;AI didn't make auth easier to get right. It made it easier to get &lt;em&gt;almost&lt;/em&gt; right - which in security, is the most dangerous place to be.&lt;/p&gt;

&lt;p&gt;The engineers who will stand out in the next few years aren't the ones who can prompt the fastest. They're the ones who understand what the prompt produced deeply enough to know when to trust it, when to question it, and when to rewrite it.&lt;/p&gt;

&lt;p&gt;Auth is one of those areas where that judgment is everything.&lt;/p&gt;

&lt;p&gt;Go build - but build like you know what's under the hood.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found this useful? A backend engineer - always learning, always observing, and writing about what I pick up along the way. If this resonated, &lt;br&gt;
let's connect on &lt;a href="https://www.linkedin.com/in/ravigupta97/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; | &lt;a href="https://github.com/ravigupta97" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;#Authentication #Authorization #OAuth #Security #BackendDevelopment  #SoftwareEngineering&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>oauth</category>
      <category>backend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Production-Ready Task Management API with FastAPI: Testing, Deployment &amp; Production (Part 3)</title>
      <dc:creator>Ravi Gupta</dc:creator>
      <pubDate>Mon, 09 Mar 2026 03:19:27 +0000</pubDate>
      <link>https://dev.to/ravigupta97/building-a-production-ready-task-management-api-with-fastapi-testing-deployment-production-1j1c</link>
      <guid>https://dev.to/ravigupta97/building-a-production-ready-task-management-api-with-fastapi-testing-deployment-production-1j1c</guid>
      <description>&lt;h2&gt;
  
  
  Building a Production-Ready Task Management API with FastAPI: Testing, Deployment &amp;amp; Production (Part 3)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part 3 of 3:&lt;/strong&gt; After the architecture and development phases, I thought deployment would be straightforward. I was wrong. This is the final chapter of my journey from local code to live production system.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The API worked perfectly on my laptop.&lt;/p&gt;

&lt;p&gt;50+ endpoints responding.&lt;br&gt;
Authentication solid.&lt;br&gt;
Clean architecture.&lt;br&gt;
All tests passing locally.&lt;/p&gt;

&lt;p&gt;I was ready to deploy.&lt;/p&gt;

&lt;p&gt;Then I clicked "Deploy."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's when everything broke.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;CORS errors I'd never seen.&lt;br&gt;
Environment variables mysteriously missing.&lt;br&gt;
Database migrations failing for no reason.&lt;br&gt;
Rate limiting crashing the entire app.&lt;br&gt;
Cold starts taking 30 seconds.&lt;/p&gt;

&lt;p&gt;This article covers &lt;strong&gt;Phase 3: Testing, Deployment &amp;amp; Production&lt;/strong&gt; - where theory meets reality, and "working" becomes "production-ready."&lt;/p&gt;

&lt;p&gt;Not just what worked.&lt;br&gt;
What broke, what surprised me, and what I learned debugging a live system at midnight.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Quick Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📖 &lt;a href="https://dev.to/ravigupta97/building-a-production-ready-task-management-api-with-fastapi-complete-architecture-guide-25a"&gt;Part 1: Architecture &amp;amp; Design&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;a href="https://dev.to/ravigupta97/building-a-production-ready-task-management-api-with-fastapi-development-implementation-part-2-2h14"&gt;Part 2: Development &amp;amp; Implementation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🔗 &lt;a href="https://github.com/ravigupta97/task_management_api" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🚀 &lt;a href="https://task-management-api-a775.onrender.com/docs" rel="noopener noreferrer"&gt;Live API Docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  📚 What You'll Learn
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Testing async FastAPI with pytest (the setup that actually works)&lt;/li&gt;
&lt;li&gt;Docker optimization strategies (1.2GB → 380MB)&lt;/li&gt;
&lt;li&gt;Free-tier deployment with Render + Neon PostgreSQL&lt;/li&gt;
&lt;li&gt;5 production bugs that only happen after deployment&lt;/li&gt;
&lt;li&gt;Real metrics from 30 days of uptime&lt;/li&gt;
&lt;li&gt;What I'd do differently if I started over&lt;/li&gt;
&lt;li&gt;Cost breakdown of running production API ($0!)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reading time:&lt;/strong&gt; ~15 minutes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/ravigupta97/task_management_api" rel="noopener noreferrer"&gt;https://github.com/ravigupta97/task_management_api&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live API:&lt;/strong&gt; &lt;a href="https://task-management-api-a775.onrender.com/docs" rel="noopener noreferrer"&gt;https://task-management-api-a775.onrender.com/docs&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🎯 The Journey So Far
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Phase 1:&lt;/strong&gt; Spent days designing the perfect architecture.&lt;br&gt;
&lt;strong&gt;Phase 2:&lt;/strong&gt; Built features, fought async bugs, implemented JWT from scratch.&lt;br&gt;
&lt;strong&gt;Phase 3:&lt;/strong&gt; Time to deploy. How hard could it be?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Spoiler: Very hard.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/ravigupta97/building-a-production-ready-task-management-api-with-fastapi-complete-architecture-guide-25a"&gt;Part 1&lt;/a&gt;, I designed a clean architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FastAPI + PostgreSQL&lt;/li&gt;
&lt;li&gt;Repository pattern&lt;/li&gt;
&lt;li&gt;Service layer separation&lt;/li&gt;
&lt;li&gt;Clean project structure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In &lt;a href="https://dev.to/ravigupta97/building-a-production-ready-task-management-api-with-fastapi-development-implementation-part-2-2h14"&gt;Part 2&lt;/a&gt;, I built the actual system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JWT authentication&lt;/li&gt;
&lt;li&gt;50+ endpoints&lt;/li&gt;
&lt;li&gt;Rate limiting&lt;/li&gt;
&lt;li&gt;Advanced features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On paper, everything looked solid.&lt;/p&gt;

&lt;p&gt;In development, everything worked.&lt;/p&gt;

&lt;p&gt;But there's a gap between "works on my machine" and "works in production."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A massive gap.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This phase was about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Writing tests that catch real bugs (not just make CI green)&lt;/li&gt;
&lt;li&gt;Containerizing with Docker (and learning why 1.2GB is unacceptable)&lt;/li&gt;
&lt;li&gt;Deploying to actual infrastructure (free tier!)&lt;/li&gt;
&lt;li&gt;Debugging production issues at midnight&lt;/li&gt;
&lt;li&gt;Measuring real performance (not synthetic benchmarks)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The honest truth:&lt;/strong&gt; This phase took almost as long as building the features.&lt;/p&gt;

&lt;p&gt;Because deployment isn't just "push to Render."&lt;/p&gt;

&lt;p&gt;It's environment variables you forgot. CORS configurations that work locally but fail remotely. Database connection pools that exhaust mysteriously. Cold starts that make your API feel broken. Migrations that succeed locally but fail in production.&lt;/p&gt;

&lt;p&gt;Every bug taught me something tutorials never mentioned.&lt;/p&gt;

&lt;p&gt;This is where you stop being a tutorial follower and start being an engineer.&lt;/p&gt;


&lt;h2&gt;
  
  
  🧪 Testing Strategy
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The Wake-Up Call
&lt;/h3&gt;

&lt;p&gt;I thought I could skip comprehensive testing.&lt;/p&gt;

&lt;p&gt;"I manually tested everything. It works."&lt;/p&gt;

&lt;p&gt;Then I added a new feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Broke 3 existing endpoints.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Didn't notice until I tried to create a task and got a 500 error.&lt;/p&gt;

&lt;p&gt;That's when I learned: Manual testing doesn't scale.&lt;/p&gt;

&lt;p&gt;You need automated tests. Not because they're trendy.&lt;/p&gt;

&lt;p&gt;Because your memory isn't perfect.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Goal: 85%+ Coverage
&lt;/h3&gt;

&lt;p&gt;Target: &lt;strong&gt;85%+ test coverage&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not arbitrary.&lt;/p&gt;

&lt;p&gt;85% forces you to test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Happy paths (obviously)&lt;/li&gt;
&lt;li&gt;Error cases (what happens when things fail)&lt;/li&gt;
&lt;li&gt;Edge cases (empty strings, null values, boundary conditions)&lt;/li&gt;
&lt;li&gt;Integration points (do layers work together?)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Below 85%? You're probably skipping important scenarios.&lt;/p&gt;

&lt;p&gt;Above 90%? Diminishing returns (testing getters/setters, framework code).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My target:&lt;/strong&gt; 85-87%. Test what matters.&lt;/p&gt;


&lt;h3&gt;
  
  
  The Challenge: Async Testing
&lt;/h3&gt;

&lt;p&gt;Coming from Spring Boot, testing was familiar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;testGetTasks&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;taskService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTasks&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Synchronous. Simple. Works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In FastAPI with async?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Nothing worked initially.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This failed
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_get_tasks&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tasks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Error: can't await
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This also failed
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_get_tasks&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tasks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Error: client not async
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Async testing requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Async test client (not regular &lt;code&gt;TestClient&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Async fixtures&lt;/li&gt;
&lt;li&gt;Event loop management&lt;/li&gt;
&lt;li&gt;Database session handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;It's not intuitive.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But once you understand it, it's powerful.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Setup That Finally Worked
&lt;/h3&gt;

&lt;p&gt;After hours of trial and error, here's the pytest configuration that actually works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# conftest.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncClient&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.ext.asyncio&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_async_engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.orm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sessionmaker&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.main&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.database&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Base&lt;/span&gt;

&lt;span class="c1"&gt;# Test database URL
&lt;/span&gt;&lt;span class="n"&gt;TEST_DATABASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgresql+asyncpg://test_user:test_pass@localhost/test_db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Create test engine
&lt;/span&gt;&lt;span class="n"&gt;test_engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_async_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEST_DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;echo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Create test session factory
&lt;/span&gt;&lt;span class="n"&gt;TestingSessionLocal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sessionmaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;test_engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;class_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;expire_on_commit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;event_loop&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Create event loop for async tests.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_event_loop_policy&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;new_event_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt;
    &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Create fresh test database for each test.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;test_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_all&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;TestingSessionLocal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;test_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;drop_all&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Create test client with database override.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;override_get_db&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;db_session&lt;/span&gt;

    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dependency_overrides&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;override_get_db&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ac&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;ac&lt;/span&gt;

    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dependency_overrides&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this setup works:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Separate test database&lt;/strong&gt; - Never pollute your dev database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fresh database per test&lt;/strong&gt; - Isolation prevents flaky tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async fixtures&lt;/strong&gt; - Match your application's async nature&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency override&lt;/strong&gt; - FastAPI's killer feature for testing&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Time to figure this out:&lt;/strong&gt; 4 hours of Stack Overflow and trial and error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worth it?&lt;/strong&gt; Absolutely. Tests are now fast, isolated, and reliable.&lt;/p&gt;




&lt;h3&gt;
  
  
  Testing Authentication: The Fixture Pattern
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Challenge:&lt;/strong&gt; How do you test protected endpoints without logging in every time?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Create authenticated test client fixture.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# conftest.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.core.security&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_access_token&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.models.user&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Create a test user.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;testuser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;hashed_password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$2b$12$...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# hashed "testpassword"
&lt;/span&gt;        &lt;span class="n"&gt;is_active&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;is_verified&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;

&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authenticated_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_user&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Create authenticated test client.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;access_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_access_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;test_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;access_token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Now testing is clean:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest.mark.asyncio&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authenticated_client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Test creating a task.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;authenticated_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/v1/tasks/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Test Task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Test Description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TODO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HIGH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Test Task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HIGH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;No manual login. No token management. Just test the business logic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This pattern saved me hundreds of lines of repetitive setup code.&lt;/p&gt;




&lt;h3&gt;
  
  
  Integration Tests: The Full Journey
&lt;/h3&gt;

&lt;p&gt;Integration tests verify the entire flow works together.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest.mark.asyncio&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_complete_task_lifecycle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authenticated_client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Test full CRUD flow for tasks.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="c1"&gt;# Create
&lt;/span&gt;    &lt;span class="n"&gt;create_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;authenticated_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/v1/tasks/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Integration Test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TODO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LOW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;create_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;201&lt;/span&gt;
    &lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Read
&lt;/span&gt;    &lt;span class="n"&gt;get_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;authenticated_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/v1/tasks/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;get_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;get_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Integration Test&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# Update
&lt;/span&gt;    &lt;span class="n"&gt;update_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;authenticated_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/v1/tasks/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;COMPLETED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;update_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;update_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;COMPLETED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# Delete
&lt;/span&gt;    &lt;span class="n"&gt;delete_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;authenticated_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/v1/tasks/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;delete_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;204&lt;/span&gt;

    &lt;span class="c1"&gt;# Verify deletion
&lt;/span&gt;    &lt;span class="n"&gt;verify_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;authenticated_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/v1/tasks/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;verify_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What this caught:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forgot to add CASCADE DELETE (got constraint violation errors)&lt;/li&gt;
&lt;li&gt;Token expiry during long test runs&lt;/li&gt;
&lt;li&gt;Session closure issues in UPDATE operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Integration tests = real-world scenarios = real bugs found.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Test Coverage: The Results
&lt;/h3&gt;

&lt;p&gt;After writing tests for all critical paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;pytest &lt;span class="nt"&gt;--cov&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;app tests/

&lt;span class="nt"&gt;----------&lt;/span&gt; coverage: platform linux, python 3.11.16 &lt;span class="nt"&gt;-----------&lt;/span&gt;
Name                              Stmts   Miss  Cover
&lt;span class="nt"&gt;-----------------------------------------------------&lt;/span&gt;
app/main.py                          45      2    96%
app/api/v1/auth.py                   89      8    91%
app/api/v1/tasks.py                 102     10    90%
app/services/task_service.py        124     15    88%
app/core/security.py                 52      3    94%
&lt;span class="nt"&gt;-----------------------------------------------------&lt;/span&gt;
TOTAL                              1247    158    87%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;87% coverage achieved.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I didn't test (the remaining 13%):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some error handling edge cases&lt;/li&gt;
&lt;li&gt;Specific database constraint failures&lt;/li&gt;
&lt;li&gt;Rate limiter timing dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why that's okay:&lt;/strong&gt; 87% covers all critical paths. The remaining 13% is mostly defensive code and edge cases that are hard to reliably test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time invested in testing:&lt;/strong&gt; 1 week&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bugs caught before production:&lt;/strong&gt; 15+&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worth it?&lt;/strong&gt; Absolutely.&lt;/p&gt;




&lt;h2&gt;
  
  
  🐳 Docker: From 1.2GB to 380MB
&lt;/h2&gt;

&lt;h3&gt;
  
  
  My First Docker Image Was Embarrassing
&lt;/h3&gt;

&lt;p&gt;1.2GB.&lt;/p&gt;

&lt;p&gt;For a Python API.&lt;/p&gt;

&lt;p&gt;I pushed it to Docker Hub, proud of myself.&lt;/p&gt;

&lt;p&gt;Then I tried to deploy it to Render.&lt;/p&gt;

&lt;p&gt;"Build timeout: Image too large"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Panic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's when I learned: Docker image size matters.&lt;/p&gt;

&lt;p&gt;Not just for build time.&lt;br&gt;
For deployment speed.&lt;br&gt;
For resource usage.&lt;br&gt;
For everything.&lt;/p&gt;

&lt;p&gt;Time to optimize.&lt;/p&gt;


&lt;h3&gt;
  
  
  The Naive Approach
&lt;/h3&gt;

&lt;p&gt;My first Dockerfile was... simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.11&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . /app&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Image size:&lt;/strong&gt; 1.2GB&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problems:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Includes full Python image (300MB+ of unnecessary tools)&lt;/li&gt;
&lt;li&gt;Build dependencies stay in final image&lt;/li&gt;
&lt;li&gt;pip cache included&lt;/li&gt;
&lt;li&gt;Unnecessary files copied&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Build time:&lt;/strong&gt; 8 minutes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deployment:&lt;/strong&gt; Timed out on Render's free tier&lt;/p&gt;

&lt;p&gt;Not acceptable.&lt;/p&gt;




&lt;h3&gt;
  
  
  Multi-Stage Builds: The Solution
&lt;/h3&gt;

&lt;p&gt;After researching Docker best practices, I discovered multi-stage builds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The concept:&lt;/strong&gt; Build in one image, run in another.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Stage 1: Builder - Install dependencies&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;python:3.11-slim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Install build dependencies&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    gcc &lt;span class="se"&gt;\
&lt;/span&gt;    postgresql-client &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="c"&gt;# Copy and install Python dependencies&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="c"&gt;# Stage 2: Runtime - Small, clean image&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.11-slim&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Install only runtime dependencies&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    libpq5 &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="c"&gt;# Copy installed packages from builder&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /root/.local /root/.local&lt;/span&gt;

&lt;span class="c"&gt;# Copy application code&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./app ./app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; alembic.ini .&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; ./alembic ./alembic&lt;/span&gt;

&lt;span class="c"&gt;# Ensure pip packages are in PATH&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PATH=/root/.local/bin:$PATH&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8000&lt;/span&gt;

&lt;span class="c"&gt;# Run migrations and start server&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; alembic upgrade head &amp;amp;&amp;amp; uvicorn app.main:app --host 0.0.0.0 --port 8000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Final image size:&lt;/strong&gt; 380MB&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reduction:&lt;/strong&gt; 68% smaller!&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Multi-stage build&lt;/strong&gt; - Builder stage separate from runtime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slim base image&lt;/strong&gt; - &lt;code&gt;python:3.11-slim&lt;/code&gt; instead of full &lt;code&gt;python:3.11&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cache in pip&lt;/strong&gt; - &lt;code&gt;--no-cache-dir&lt;/code&gt; flag&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean apt lists&lt;/strong&gt; - &lt;code&gt;rm -rf /var/lib/apt/lists/*&lt;/code&gt; after installs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only runtime dependencies&lt;/strong&gt; - gcc and build tools left in builder stage&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Build time:&lt;/strong&gt; 3 minutes (down from 8!)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deployment:&lt;/strong&gt; Successful on Render free tier&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That felt good.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Local Development: docker-compose
&lt;/h3&gt;

&lt;p&gt;For local development, I needed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL database&lt;/li&gt;
&lt;li&gt;API server&lt;/li&gt;
&lt;li&gt;Hot reload&lt;/li&gt;
&lt;li&gt;Easy setup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;docker-compose made it simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;taskapi&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;taskapi&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;taskapi_dev&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432:5432"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres_data:/var/lib/postgresql/data&lt;/span&gt;

  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./app:/app/app&lt;/span&gt;  &lt;span class="c1"&gt;# Hot reload&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8000:8000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql+asyncpg://taskapi:taskapi@db:5432/taskapi_dev&lt;/span&gt;
      &lt;span class="na"&gt;SECRET_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev-secret-key&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;One command to start everything:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consistent environment across team&lt;/li&gt;
&lt;li&gt;No "works on my machine" issues&lt;/li&gt;
&lt;li&gt;Easy onboarding for new developers&lt;/li&gt;
&lt;li&gt;Matches production setup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;This decision saved hours of environment debugging.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Deployment: Free Tier Reality
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Platform Choice: The $0 Constraint
&lt;/h3&gt;

&lt;p&gt;I had a strict budget for infrastructure.&lt;/p&gt;

&lt;p&gt;$0.&lt;/p&gt;

&lt;p&gt;Not "limited." Zero dollars.&lt;/p&gt;

&lt;p&gt;This eliminated most options immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Contenders:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Free Tier&lt;/th&gt;
&lt;th&gt;PostgreSQL&lt;/th&gt;
&lt;th&gt;Catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Render&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Neon integration&lt;/td&gt;
&lt;td&gt;Sleeps after 15min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heroku&lt;/td&gt;
&lt;td&gt;❌ No longer free&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;Killed free tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;td&gt;⚠️ 12 months only&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;Complex setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Railway&lt;/td&gt;
&lt;td&gt;✅ Limited hours&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;$5 credit/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fly.io&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;Credit system&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Decision: Render&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Truly free forever (not trial)&lt;/li&gt;
&lt;li&gt;Docker support&lt;/li&gt;
&lt;li&gt;Auto-deploy from GitHub&lt;/li&gt;
&lt;li&gt;Simple configuration&lt;/li&gt;
&lt;li&gt;Good documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Trade-off:&lt;/strong&gt; Cold starts after 15 minutes of inactivity.&lt;/p&gt;

&lt;p&gt;Acceptable for a learning project.&lt;/p&gt;




&lt;h3&gt;
  
  
  Database: Neon PostgreSQL
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; Most free PostgreSQL offerings have catches.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Heroku:&lt;/strong&gt; Limited to 10K rows (not enough)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase:&lt;/strong&gt; Pauses after 7 days inactivity (annoying)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS RDS:&lt;/strong&gt; Only 12 months free (not sustainable)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Neon's free tier:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Actually free forever&lt;/li&gt;
&lt;li&gt;✅ 3GB storage&lt;/li&gt;
&lt;li&gt;✅ Serverless (auto-scales)&lt;/li&gt;
&lt;li&gt;✅ Branch-based development (like git!)&lt;/li&gt;
&lt;li&gt;✅ No sleep/pause&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Setup time:&lt;/strong&gt; 5 minutes&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create Neon account&lt;/li&gt;
&lt;li&gt;Create database&lt;/li&gt;
&lt;li&gt;Copy connection string&lt;/li&gt;
&lt;li&gt;Add to Render environment variables&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This was the easiest part of deployment.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The rest? Not so much.&lt;/p&gt;




&lt;h3&gt;
  
  
  First Deployment: Everything Broke
&lt;/h3&gt;

&lt;p&gt;I pushed to GitHub.&lt;/p&gt;

&lt;p&gt;Render detected the Dockerfile.&lt;/p&gt;

&lt;p&gt;Build started.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Environment variable DATABASE_URL not found
Error: SECRET_KEY is None
Error: Application failed to start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What I forgot:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Environment variables don't automatically transfer from &lt;code&gt;.env&lt;/code&gt; file to Render.&lt;/p&gt;

&lt;p&gt;You have to set them manually in Render's dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; Environment configuration is separate from code.&lt;/p&gt;

&lt;p&gt;Never assume.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Health Check Endpoint
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Critical for free tier deployments.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Render's free tier sleeps after 15 minutes of inactivity.&lt;/p&gt;

&lt;p&gt;Health checks can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wake it up automatically&lt;/li&gt;
&lt;li&gt;Verify it's actually running&lt;/li&gt;
&lt;li&gt;Test database connectivity
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/main.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTP_200_OK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;health_check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Health check endpoint.
    Tests database connectivity.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;healthy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;database&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;environment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ENVIRONMENT&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unhealthy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;database&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;disconnected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Confirms app is running&lt;/li&gt;
&lt;li&gt;Tests database connection&lt;/li&gt;
&lt;li&gt;Used by monitoring services&lt;/li&gt;
&lt;li&gt;Simple debugging tool&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;This endpoint became my best friend during deployment debugging.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🚧 Production Bugs: The 2 AM Stories
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Bug #1: The 30-Second Cold Start
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I sent the API link to a friend.&lt;/p&gt;

&lt;p&gt;"Check out my live project!"&lt;/p&gt;

&lt;p&gt;I waited.&lt;/p&gt;

&lt;p&gt;30 seconds passed.&lt;/p&gt;

&lt;p&gt;Still loading.&lt;/p&gt;

&lt;p&gt;I refreshed. 30 more seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Internal panic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Is it down? Did I break something??"&lt;/p&gt;

&lt;p&gt;Checked Render logs: "Spinning up service from sleep..."&lt;/p&gt;

&lt;p&gt;Oh.&lt;/p&gt;

&lt;p&gt;Free tier sleeps after 15 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every. Single. Time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Can't eliminate cold starts on free tier.&lt;/p&gt;

&lt;p&gt;But I could handle the UX:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Health check endpoint&lt;/strong&gt; - Wake it up faster&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend loading state&lt;/strong&gt; - "Server waking up, please wait..."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set expectations&lt;/strong&gt; - Document it's free tier&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Better solution:&lt;/strong&gt; Upgrade to paid tier ($7/month for always-on).&lt;/p&gt;

&lt;p&gt;But this is a learning project. Free tier it is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; Constraints force creative solutions. Or at least, creative messaging.&lt;/p&gt;




&lt;h3&gt;
  
  
  Bug #2: The Case-Sensitive Environment Variable
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;API deployed successfully.&lt;/p&gt;

&lt;p&gt;Opened the docs page.&lt;/p&gt;

&lt;p&gt;Everything loaded.&lt;/p&gt;

&lt;p&gt;Tried to login.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Internal Server Error"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checked logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AttributeError: 'NoneType' object has no attribute 'decode'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Spent an hour debugging.&lt;/p&gt;

&lt;p&gt;Checked JWT code. Looked fine.&lt;br&gt;
Checked database connection. Working.&lt;br&gt;
Checked everything. Nothing made sense.&lt;/p&gt;

&lt;p&gt;Then I saw it.&lt;/p&gt;

&lt;p&gt;Environment variable in Render dashboard:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SECRECT_KEY&lt;/code&gt; instead of &lt;code&gt;SECRET_KEY&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I misspelled "SECRET" as "SECRECT".&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;JWT library tried to decode with &lt;code&gt;None&lt;/code&gt; as the key.&lt;/p&gt;

&lt;p&gt;Crashed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fixed the typo&lt;/li&gt;
&lt;li&gt;Added startup validation:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/main.py
&lt;/span&gt;&lt;span class="nd"&gt;@app.on_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;startup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_config&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Validate required configuration on startup.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SECRET_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REFRESH_SECRET_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Missing required config: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✅ Configuration validated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Now if a required variable is missing, the app fails immediately on startup.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not after the first request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; Fail fast. Validate early. Save yourself hours of debugging.&lt;/p&gt;


&lt;h3&gt;
  
  
  Bug #3: Database Connection Pool Exhaustion
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;API ran fine for a few hours.&lt;/p&gt;

&lt;p&gt;Then started throwing errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;asyncpg.exceptions.TooManyConnectionsError: too many connections
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;I wasn't doing anything different.&lt;/p&gt;

&lt;p&gt;No traffic spike.&lt;/p&gt;

&lt;p&gt;Just... ran out of connections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Root Cause:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some error paths weren't closing database sessions.&lt;/p&gt;

&lt;p&gt;Session leaked.&lt;br&gt;
Connection leaked.&lt;br&gt;
Pool exhausted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ensure session cleanup in dependency:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/database.py
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;AsyncSessionLocal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Always close, even on error
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Configure connection pool limits:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/database.py
&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_async_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pool_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# Max connections
&lt;/span&gt;    &lt;span class="n"&gt;max_overflow&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# Overflow allowed
&lt;/span&gt;    &lt;span class="n"&gt;pool_pre_ping&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# Verify before use
&lt;/span&gt;    &lt;span class="n"&gt;pool_recycle&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;     &lt;span class="c1"&gt;# Recycle after 1 hour
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;After the fix:&lt;/strong&gt; No more connection errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; Connection pools are finite resources. Manage them carefully.&lt;/p&gt;


&lt;h3&gt;
  
  
  Bug #4: Alembic Migration Conflicts
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Deployed new code with database migration.&lt;/p&gt;

&lt;p&gt;Render build failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alembic.util.exc.CommandError: 
Can't locate revision identified by 'abc123'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I created migrations locally.&lt;br&gt;
Pushed code.&lt;br&gt;
But Render's database didn't have the previous migrations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run migrations as part of deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# In Dockerfile CMD&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; alembic upgrade head &amp;amp;&amp;amp; uvicorn app.main:app --host 0.0.0.0 --port 8000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;This ensures:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Migrations run before app starts&lt;/li&gt;
&lt;li&gt;Database is always up to date&lt;/li&gt;
&lt;li&gt;No manual migration steps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Also added:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Before creating new migration locally&lt;/span&gt;
alembic current  &lt;span class="c"&gt;# Check current state&lt;/span&gt;
alembic upgrade &lt;span class="nb"&gt;head&lt;/span&gt;  &lt;span class="c"&gt;# Apply all pending&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; Migrations are sequential. Treat them like git commits. Linear history matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 Performance: 30 Days of Real Data
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Numbers
&lt;/h3&gt;

&lt;p&gt;After 30 days of production traffic, here's how it actually performed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Response Times:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Average&lt;/th&gt;
&lt;th&gt;P95&lt;/th&gt;
&lt;th&gt;P99&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Health check&lt;/td&gt;
&lt;td&gt;45ms&lt;/td&gt;
&lt;td&gt;89ms&lt;/td&gt;
&lt;td&gt;142ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Login&lt;/td&gt;
&lt;td&gt;156ms&lt;/td&gt;
&lt;td&gt;278ms&lt;/td&gt;
&lt;td&gt;421ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Get tasks&lt;/td&gt;
&lt;td&gt;87ms&lt;/td&gt;
&lt;td&gt;156ms&lt;/td&gt;
&lt;td&gt;289ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create task&lt;/td&gt;
&lt;td&gt;112ms&lt;/td&gt;
&lt;td&gt;198ms&lt;/td&gt;
&lt;td&gt;334ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Overall average:&lt;/strong&gt; ~190ms&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My initial target:&lt;/strong&gt; 100ms&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reality:&lt;/strong&gt; Almost 2x slower.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Neon serverless adds latency (~20ms)&lt;/li&gt;
&lt;li&gt;Free tier CPU is shared&lt;/li&gt;
&lt;li&gt;No caching layer&lt;/li&gt;
&lt;li&gt;Some queries not optimized&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Is 190ms acceptable?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For a free-tier learning project? Yes.&lt;/p&gt;

&lt;p&gt;For a production SaaS? No. I'd need caching and optimization.&lt;/p&gt;




&lt;h3&gt;
  
  
  Uptime Tracking
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Tool:&lt;/strong&gt; UptimeRobot (free tier)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;30-day results:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;99.2% uptime&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;3 outages&lt;/strong&gt; (total: 5 hours 47 minutes)&lt;/li&gt;
&lt;li&gt;⏱️ &lt;strong&gt;Average response:&lt;/strong&gt; 180ms&lt;/li&gt;
&lt;li&gt;📈 &lt;strong&gt;Checks performed:&lt;/strong&gt; 43,200&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Outage breakdown:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Render platform maintenance (2 hours)&lt;/li&gt;
&lt;li&gt;Neon database scaling issue (30 minutes)&lt;/li&gt;
&lt;li&gt;My deployment bug (15 minutes - the ENV variable typo)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;99.2% uptime on $0 infrastructure?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Honestly better than I expected.&lt;/p&gt;

&lt;p&gt;For comparison, AWS promises 99.99% (that's 4 minutes downtime per month).&lt;/p&gt;

&lt;p&gt;I had ~5 hours downtime in 30 days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not bad for free.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Database Query Performance
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Biggest optimization:&lt;/strong&gt; Fixing N+1 queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Get tasks - N+1 problem
&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;task_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_by_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Separate query for each!
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Eager load relationships
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.orm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;selectinload&lt;/span&gt;

&lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;selectinload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Performance improvement:&lt;/strong&gt; 3x faster for lists with categories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; Async doesn't make bad queries good. Optimize your database access.&lt;/p&gt;




&lt;h2&gt;
  
  
  💰 Cost Analysis: The $0 Stack
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Monthly Infrastructure Costs
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Render&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Neon PostgreSQL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Domain&lt;/strong&gt; (optional)&lt;/td&gt;
&lt;td&gt;Namecheap&lt;/td&gt;
&lt;td&gt;~$1/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Monitoring&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;UptimeRobot free&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Yearly cost:&lt;/strong&gt; $0 (or $12 if you buy a domain)&lt;/p&gt;




&lt;h3&gt;
  
  
  Free Tier Constraints
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Render Free Tier:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ 750 hours/month (enough for one service)&lt;/li&gt;
&lt;li&gt;✅ Auto-deploy from Git&lt;/li&gt;
&lt;li&gt;✅ SSL certificate included&lt;/li&gt;
&lt;li&gt;❌ Sleeps after 15 min inactivity&lt;/li&gt;
&lt;li&gt;❌ Limited to 512MB RAM&lt;/li&gt;
&lt;li&gt;❌ Shared CPU&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Neon Free Tier:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ 3GB storage&lt;/li&gt;
&lt;li&gt;✅ Unlimited databases&lt;/li&gt;
&lt;li&gt;✅ Serverless auto-scaling&lt;/li&gt;
&lt;li&gt;❌ Limited compute units&lt;/li&gt;
&lt;li&gt;❌ Shared resources&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  When to Upgrade?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Stay on free tier if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Learning project&lt;/li&gt;
&lt;li&gt;Portfolio piece&lt;/li&gt;
&lt;li&gt;Low traffic (&amp;lt;1000 requests/day)&lt;/li&gt;
&lt;li&gt;Can tolerate cold starts&lt;/li&gt;
&lt;li&gt;No SLA requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Upgrade to paid ($7-10/month) if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real users depending on it&lt;/li&gt;
&lt;li&gt;Need consistent performance&lt;/li&gt;
&lt;li&gt;Can't afford downtime&lt;/li&gt;
&lt;li&gt;Traffic grows&lt;/li&gt;
&lt;li&gt;Professional project&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For this project:&lt;/strong&gt; Free tier is perfect.&lt;/p&gt;

&lt;p&gt;It does everything I need for a portfolio piece.&lt;/p&gt;




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

&lt;h3&gt;
  
  
  1. Tests Save More Time Than They Take
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;My initial thought:&lt;/strong&gt; "Tests slow me down. I'll skip them."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reality:&lt;/strong&gt; Tests caught 15+ bugs before production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time spent writing tests:&lt;/strong&gt; 1 week&lt;br&gt;
&lt;strong&gt;Time saved debugging production:&lt;/strong&gt; Probably 2+ weeks&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ROI:&lt;/strong&gt; Massively positive.&lt;/p&gt;

&lt;p&gt;Tests aren't overhead. They're insurance.&lt;/p&gt;


&lt;h3&gt;
  
  
  2. Docker Isn't Optional
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why Docker matters:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reason 1:&lt;/strong&gt; "Works on my machine" becomes "works everywhere"&lt;br&gt;
&lt;strong&gt;Reason 2:&lt;/strong&gt; Deployment is just "push image"&lt;br&gt;
&lt;strong&gt;Reason 3:&lt;/strong&gt; Forces you to think about dependencies&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time invested learning Docker:&lt;/strong&gt; 2 days&lt;br&gt;
&lt;strong&gt;Time saved in deployment issues:&lt;/strong&gt; Countless hours&lt;/p&gt;

&lt;p&gt;Every modern backend project should use Docker.&lt;/p&gt;

&lt;p&gt;Not optional.&lt;/p&gt;


&lt;h3&gt;
  
  
  3. Monitoring Isn't Optional Either
&lt;/h3&gt;

&lt;p&gt;Without monitoring, you're blind.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Questions you can't answer:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the API down?&lt;/li&gt;
&lt;li&gt;Why is it slow?&lt;/li&gt;
&lt;li&gt;Where are errors happening?&lt;/li&gt;
&lt;li&gt;What's the actual performance?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;With basic monitoring:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Health check endpoint&lt;/li&gt;
&lt;li&gt;Logging to stdout&lt;/li&gt;
&lt;li&gt;UptimeRobot for uptime&lt;/li&gt;
&lt;li&gt;Manual log review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; $0&lt;br&gt;
&lt;strong&gt;Value:&lt;/strong&gt; Priceless&lt;/p&gt;

&lt;p&gt;You can't fix what you can't measure.&lt;/p&gt;


&lt;h3&gt;
  
  
  4. Free Tier Forces Better Decisions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Constraints I faced:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No Redis → In-memory rate limiting&lt;/li&gt;
&lt;li&gt;Cold starts → UX handling + health checks&lt;/li&gt;
&lt;li&gt;Limited resources → Query optimization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Better architecture.&lt;/p&gt;

&lt;p&gt;Paid tiers let you throw money at problems.&lt;/p&gt;

&lt;p&gt;Free tiers force you to solve them properly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The best learning happens under constraints.&lt;/strong&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  5. Production Is the Real Teacher
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Local development teaches:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to write code&lt;/li&gt;
&lt;li&gt;How to structure projects&lt;/li&gt;
&lt;li&gt;How to use frameworks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Production teaches:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How systems fail&lt;/li&gt;
&lt;li&gt;How to debug without IDE&lt;/li&gt;
&lt;li&gt;How to handle real constraints&lt;/li&gt;
&lt;li&gt;How to think about users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gap between these two is where engineers are made.&lt;/p&gt;


&lt;h2&gt;
  
  
  🔄 What I'd Do Differently
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Write Tests Earlier (TDD)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What I did:&lt;/strong&gt; Built features, then wrote tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Had to refactor code to make it testable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better approach:&lt;/strong&gt; Test-Driven Development&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write test first&lt;/li&gt;
&lt;li&gt;Implement feature&lt;/li&gt;
&lt;li&gt;Refactor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; Forces good design from the start.&lt;/p&gt;


&lt;h3&gt;
  
  
  2. Set Up CI/CD Immediately
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What I did:&lt;/strong&gt; Manually ran tests before deploying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Forgot twice. Deployed broken code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better approach:&lt;/strong&gt; GitHub Actions&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Simple workflow&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Tests&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v2&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3.11&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pytest --cov=app tests/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Benefit:&lt;/strong&gt; Tests run automatically. Can't merge broken code.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Add Caching Earlier
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What I did:&lt;/strong&gt; Direct database queries for everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Repeated queries for same data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better approach:&lt;/strong&gt; Redis caching for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User sessions&lt;/li&gt;
&lt;li&gt;Frequently accessed data&lt;/li&gt;
&lt;li&gt;API rate limit counters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why I didn't:&lt;/strong&gt; Free tier doesn't include Redis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workaround:&lt;/strong&gt; &lt;code&gt;functools.lru_cache&lt;/code&gt; for config/static data.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Implement Database Backups
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What I did:&lt;/strong&gt; Relied on Neon's automatic backups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; No control over backup schedule or retention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better approach:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scheduled &lt;code&gt;pg_dump&lt;/code&gt; to S3&lt;/li&gt;
&lt;li&gt;Version-controlled schema&lt;/li&gt;
&lt;li&gt;Tested restore process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Data loss is unacceptable. Even for learning projects.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. Use Feature Flags
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What I did:&lt;/strong&gt; Deploy features directly to production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Can't disable buggy features without rollback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better approach:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Simple feature flags
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FeatureFlags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ENABLE_EMAIL_VERIFICATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FEATURE_EMAIL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;ENABLE_ADVANCED_SEARCH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FEATURE_SEARCH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Usage
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;FeatureFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ENABLE_ADVANCED_SEARCH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# New feature logic
&lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Old feature logic
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Benefit:&lt;/strong&gt; Enable/disable features without deploying.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 Final Thoughts
&lt;/h2&gt;

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

&lt;p&gt;&lt;strong&gt;1. Architecture matters&lt;/strong&gt; — but implementation teaches you why&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Tests aren't optional&lt;/strong&gt; — they're the difference between hobby and professional&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Production is different&lt;/strong&gt; — cold starts, CORS, connection pools, migrations... none of this shows up in tutorials&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Free tier is enough&lt;/strong&gt; — to learn, to build portfolio, to prove you can ship&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Building in public works&lt;/strong&gt; — documenting this journey kept me accountable&lt;/p&gt;




&lt;h3&gt;
  
  
  The Numbers
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What I built:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ 50+ API endpoints&lt;/li&gt;
&lt;li&gt;✅ JWT authentication&lt;/li&gt;
&lt;li&gt;✅ 87% test coverage&lt;/li&gt;
&lt;li&gt;✅ Clean architecture&lt;/li&gt;
&lt;li&gt;✅ Dockerized&lt;/li&gt;
&lt;li&gt;✅ Deployed (99.2% uptime)&lt;/li&gt;
&lt;li&gt;✅ $0 infrastructure cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Way more than any tutorial could teach&lt;/li&gt;
&lt;li&gt;The gap between "working" and "production-ready"&lt;/li&gt;
&lt;li&gt;How to debug systems you can't see&lt;/li&gt;
&lt;li&gt;How to make architectural decisions under constraints&lt;/li&gt;
&lt;li&gt;How to ship something real&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Was It Worth It?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Absolutely.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not just for the portfolio.&lt;/p&gt;

&lt;p&gt;Not just for the resume.&lt;/p&gt;

&lt;p&gt;For the learning.&lt;/p&gt;

&lt;p&gt;There's a massive difference between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Following a tutorial&lt;/li&gt;
&lt;li&gt;Building from scratch&lt;/li&gt;
&lt;li&gt;Deploying to production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This project forced me through all three.&lt;/p&gt;

&lt;p&gt;Every bug taught me something.&lt;br&gt;
Every constraint forced creativity.&lt;br&gt;
Every deployment taught me production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's the real value.&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🚀 Try It Yourself
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Live API:&lt;/strong&gt; &lt;a href="https://task-management-api-a775.onrender.com/docs" rel="noopener noreferrer"&gt;https://task-management-api-a775.onrender.com/docs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(First request might take 30 seconds if it's sleeping - free tier!)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/ravigupta97/task_management_api" rel="noopener noreferrer"&gt;https://github.com/ravigupta97/task_management_api&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clone and run:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/ravigupta97/task_management_api
&lt;span class="nb"&gt;cd &lt;/span&gt;task_management_api
docker-compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;a href="http://localhost:8000/docs" rel="noopener noreferrer"&gt;http://localhost:8000/docs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything you need is in the README.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  💬 Discussion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Questions for you:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What's your testing strategy for async APIs?&lt;/li&gt;
&lt;li&gt;How do you handle cold starts on free tiers?&lt;/li&gt;
&lt;li&gt;What was your biggest production surprise?&lt;/li&gt;
&lt;li&gt;What free tier tools do you use?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's learn together - comment below! 👇&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This completes the 3-part series on building a production-ready FastAPI application.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/ravigupta97/building-a-production-ready-task-management-api-with-fastapi-complete-architecture-guide-25a"&gt;Part 1: Architecture &amp;amp; Design&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/ravigupta97/building-a-production-ready-task-management-api-with-fastapi-development-implementation-part-2-2h14"&gt;Part 2: Development &amp;amp; Implementation&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Part 3: Testing, Deployment &amp;amp; Production (you just read it)&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What's next for me?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Follow along:&lt;/strong&gt; &lt;a href="https://www.linkedin.com/in/ravigupta97/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; | &lt;a href="https://github.com/ravigupta97" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;#FastAPI #Python #Testing #Docker #Deployment #DevOps #BackendDevelopment #BuildInPublic #SoftwareEngineering&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>fastapi</category>
      <category>python</category>
      <category>docker</category>
      <category>devops</category>
    </item>
    <item>
      <title>Building a Production-Ready Task Management API with FastAPI: Development &amp; Implementation (Part 2)</title>
      <dc:creator>Ravi Gupta</dc:creator>
      <pubDate>Mon, 02 Mar 2026 03:10:00 +0000</pubDate>
      <link>https://dev.to/ravigupta97/building-a-production-ready-task-management-api-with-fastapi-development-implementation-part-2-2h14</link>
      <guid>https://dev.to/ravigupta97/building-a-production-ready-task-management-api-with-fastapi-development-implementation-part-2-2h14</guid>
      <description>&lt;h2&gt;
  
  
  Building a Production-Ready Task Management API with FastAPI: Development &amp;amp; Implementation (Part 2)
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Part 2 of 3:&lt;/strong&gt; Architecture is comfortable. Implementation is humbling.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In Part 1, everything looked clean.&lt;/p&gt;

&lt;p&gt;The layers were well separated.&lt;br&gt;&lt;br&gt;
The database schema made sense.&lt;br&gt;&lt;br&gt;
The architecture diagram looked impressive.&lt;/p&gt;

&lt;p&gt;And then I started writing code.&lt;br&gt;
That’s when things stopped being theoretical.&lt;br&gt;
Not just what worked — but what broke, what surprised me, and that forced me to level up.&lt;br&gt;
This article covers JWT authentication, CRUD operations, advanced features, and the real challenges of building production-ready APIs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📖 &lt;a href="https://dev.to/ravigupta97/building-a-production-ready-task-management-api-with-fastapi-complete-architecture-guide-25a"&gt;Part 1: Architecture &amp;amp; Design&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🔗 &lt;a href="https://github.com/ravigupta97/task_management_api" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🚀 &lt;a href="https://task-management-api-a775.onrender.com/docs" rel="noopener noreferrer"&gt;Live API Docs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


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

&lt;ol&gt;
&lt;li&gt;Introduction &amp;amp; Recap&lt;/li&gt;
&lt;li&gt;JWT Authentication Implementation&lt;/li&gt;
&lt;li&gt;Repository Pattern in Practice&lt;/li&gt;
&lt;li&gt;Service Layer Development&lt;/li&gt;
&lt;li&gt;CRUD Operations&lt;/li&gt;
&lt;li&gt;Advanced Features&lt;/li&gt;
&lt;li&gt;Real Challenges &amp;amp; Solutions&lt;/li&gt;
&lt;li&gt;Code Examples &amp;amp; Patterns&lt;/li&gt;
&lt;li&gt;Lessons Learned&lt;/li&gt;
&lt;li&gt;What's Next&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Reading time:&lt;/strong&gt; ~12 minutes&lt;/p&gt;


&lt;h2&gt;
  
  
  🎯 Introduction &amp;amp; Recap
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/ravigupta97/building-a-production-ready-task-management-api-with-fastapi-complete-architecture-guide-25a"&gt;Part 1&lt;/a&gt;, I designed the architecture for a production-ready Task Management API using FastAPI and PostgreSQL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we covered:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Technology stack selection (FastAPI, PostgreSQL, SQLAlchemy 2.0)&lt;/li&gt;
&lt;li&gt;Database schema design (Users, Tasks, Categories)&lt;/li&gt;
&lt;li&gt;Clean architecture layers (API → Service → Repository → Database)&lt;/li&gt;
&lt;li&gt;Project structure organization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Phase 1 Result:&lt;/strong&gt; A solid foundation ready for implementation.&lt;br&gt;
On paper, it was solid.&lt;/p&gt;

&lt;p&gt;But architecture doesn’t test your assumptions, Implementation does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 Goal:&lt;/strong&gt; Build the actual features while maintaining architectural integrity.&lt;br&gt;
And discovering that “working” is not the same as “production-ready.”&lt;/p&gt;

&lt;p&gt;This article covers the development journey - the features I built, patterns I implemented, and challenges I faced turning architecture diagrams into working code.&lt;/p&gt;


&lt;h2&gt;
  
  
  🔐 JWT Authentication Implementation
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The Challenge
&lt;/h3&gt;

&lt;p&gt;Coming from Spring Boot, I was used to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;
&lt;span class="nd"&gt;@EnableWebSecurity&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SecurityConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Spring Security handles everything&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In FastAPI, there's no magic &lt;code&gt;@EnableWebSecurity&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You build authentication from the ground up.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Complete authentication system with:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;User Registration&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Email validation&lt;/li&gt;
&lt;li&gt;Password strength requirements&lt;/li&gt;
&lt;li&gt;Duplicate email/username checking&lt;/li&gt;
&lt;li&gt;Automatic password hashing&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Login Flow&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Credential verification&lt;/li&gt;
&lt;li&gt;Access token generation (15 min expiry)&lt;/li&gt;
&lt;li&gt;Refresh token generation (7 days expiry)&lt;/li&gt;
&lt;li&gt;Token storage strategy&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Token Management&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Token validation middleware&lt;/li&gt;
&lt;li&gt;Token refresh endpoint&lt;/li&gt;
&lt;li&gt;Automatic token expiry handling&lt;/li&gt;
&lt;li&gt;Logout mechanism&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Security Features&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Email verification flow&lt;/li&gt;
&lt;li&gt;Password reset mechanism&lt;/li&gt;
&lt;li&gt;Account activation&lt;/li&gt;
&lt;li&gt;Rate limiting on auth endpoints&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Implementation Deep Dive
&lt;/h3&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;1. Password Hashing with bcrypt&lt;/strong&gt;
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/core/security.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;passlib.context&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CryptContext&lt;/span&gt;

&lt;span class="n"&gt;pwd_context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CryptContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schemes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bcrypt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;deprecated&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hash_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Hash a password using bcrypt.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pwd_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plain_password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashed_password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Verify a password against a hashed password.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pwd_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plain_password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashed_password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why bcrypt?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Industry standard for password hashing&lt;/li&gt;
&lt;li&gt;Built-in salt generation&lt;/li&gt;
&lt;li&gt;Adaptive cost factor (future-proof against hardware improvements)&lt;/li&gt;
&lt;li&gt;Slow by design (prevents brute force)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was my first mindset shift:&lt;/p&gt;

&lt;p&gt;FastAPI gives flexibility.&lt;br&gt;
Security becomes your responsibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spring Boot comparison:&lt;/strong&gt; Similar to &lt;code&gt;BCryptPasswordEncoder&lt;/code&gt;, but more explicit configuration.&lt;/p&gt;


&lt;h4&gt;
  
  
  &lt;strong&gt;2. JWT Token Generation&lt;/strong&gt;
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/core/security.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;jose&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_access_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expires_delta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Create JWT access token.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;to_encode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;expires_delta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;expire&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;expires_delta&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;expire&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;to_encode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;expire&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;access&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;encoded_jwt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;to_encode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;algorithm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ALGORITHM&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;encoded_jwt&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_refresh_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Create JWT refresh token.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;to_encode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;expire&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;to_encode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;expire&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refresh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;encoded_jwt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;to_encode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REFRESH_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;algorithm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ALGORITHM&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;encoded_jwt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Design decisions here matter.&lt;/p&gt;

&lt;p&gt;JWT is just a token format.&lt;/p&gt;

&lt;p&gt;Security comes from validation.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Two token types:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Access tokens (short-lived, 15 min)&lt;/li&gt;
&lt;li&gt;Refresh tokens (long-lived, 7 days)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Separate secrets:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Different keys for access vs refresh tokens&lt;/li&gt;
&lt;li&gt;Compromised access token ≠ compromised refresh token&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Token payload:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1234567890&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"access"&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;strong&gt;3. Token Validation Dependency&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Extract JWT from &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; header&lt;/li&gt;
&lt;li&gt;Decode and validate token signature&lt;/li&gt;
&lt;li&gt;Check token type (must be "access")&lt;/li&gt;
&lt;li&gt;Fetch user from database&lt;/li&gt;
&lt;li&gt;Verify user is active&lt;/li&gt;
&lt;li&gt;Return user object&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Usage in routes:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/me&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;UserResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_current_user_profile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_current_user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get current user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s profile.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. Type-safe. Reusable.&lt;/p&gt;




&lt;h4&gt;
  
  
  &lt;strong&gt;4. Login Endpoint Implementation&lt;/strong&gt;
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/api/v1/auth.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;APIRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.schemas.token&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TokenResponse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.schemas.user&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;UserLogin&lt;/span&gt;

&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;APIRouter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/auth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authentication&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="nd"&gt;@router.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TokenResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UserLogin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Authenticate user and return access + refresh tokens.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# Get user by email
&lt;/span&gt;    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;user_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_by_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTP_401_UNAUTHORIZED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Incorrect email or password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Verify password
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;verify_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hashed_password&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTP_401_UNAUTHORIZED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Incorrect email or password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Check if user is active
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTP_403_FORBIDDEN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User account is inactive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Generate tokens
&lt;/span&gt;    &lt;span class="n"&gt;access_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_access_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
    &lt;span class="n"&gt;refresh_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_refresh_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;TokenResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;access_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;refresh_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;token_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bearer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Security considerations:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;✅ Generic error messages (don't reveal if email exists)&lt;/li&gt;
&lt;li&gt;✅ Password verification before any other checks&lt;/li&gt;
&lt;li&gt;✅ Account status validation&lt;/li&gt;
&lt;li&gt;✅ Proper HTTP status codes&lt;/li&gt;
&lt;/ol&gt;




&lt;h4&gt;
  
  
  &lt;strong&gt;5. Token Refresh Flow&lt;/strong&gt;
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@router.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/refresh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TokenResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;refresh_access_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Generate new access token using refresh token.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;credentials_exception&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTP_401_UNAUTHORIZED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Could not validate refresh token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Decode refresh token
&lt;/span&gt;        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REFRESH_SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;algorithms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ALGORITHM&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;token_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;token_type&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refresh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;credentials_exception&lt;/span&gt;

    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;JWTError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;credentials_exception&lt;/span&gt;

    &lt;span class="c1"&gt;# Verify user still exists and is active
&lt;/span&gt;    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;user_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_by_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;credentials_exception&lt;/span&gt;

    &lt;span class="c1"&gt;# Generate new access token
&lt;/span&gt;    &lt;span class="n"&gt;new_access_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_access_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sub&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;TokenResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;access_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;new_access_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;refresh_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Reuse refresh token
&lt;/span&gt;        &lt;span class="n"&gt;token_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bearer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this approach?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Access tokens expire quickly (security)&lt;/li&gt;
&lt;li&gt;Refresh tokens last longer (user experience)&lt;/li&gt;
&lt;li&gt;User doesn't need to re-login every 15 minutes&lt;/li&gt;
&lt;li&gt;Compromised access token has limited damage window&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Real Challenge: Token Storage Strategy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Question:&lt;/strong&gt; Where do clients store JWT tokens?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Options I considered:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Storage Method&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;th&gt;My Choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;localStorage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simple, persists across tabs&lt;/td&gt;
&lt;td&gt;XSS vulnerable&lt;/td&gt;
&lt;td&gt;✅ Used (with CSP headers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;httpOnly Cookies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;XSS safe&lt;/td&gt;
&lt;td&gt;Needs CSRF protection, CORS complexity&lt;/td&gt;
&lt;td&gt;Future improvement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory (state)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Most secure&lt;/td&gt;
&lt;td&gt;Lost on refresh, UX issues&lt;/td&gt;
&lt;td&gt;Not practical&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;For this project:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Documented the trade-offs in code comments&lt;/li&gt;
&lt;li&gt;Implemented localStorage for simplicity&lt;/li&gt;
&lt;li&gt;Added Content Security Policy headers&lt;/li&gt;
&lt;li&gt;Noted httpOnly cookies as production improvement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; Perfect security doesn't exist. Understand trade-offs, document decisions, implement appropriate for context.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Bug That Taught Me the Most
&lt;/h3&gt;

&lt;p&gt;My first refresh token implementation worked.&lt;/p&gt;

&lt;p&gt;It decoded the token.&lt;br&gt;
It generated a new access token.&lt;br&gt;
It returned 200 OK.&lt;/p&gt;

&lt;p&gt;It was also insecure.&lt;/p&gt;

&lt;p&gt;I forgot to check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the user still existed&lt;/li&gt;
&lt;li&gt;If the user was still active&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Meaning a banned user could still refresh tokens.&lt;/p&gt;

&lt;p&gt;The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;user_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_by_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That bug changed how I think about "working code."&lt;/p&gt;

&lt;p&gt;Working ≠ Secure&lt;br&gt;
Passing tests ≠ Safe&lt;/p&gt;


&lt;h2&gt;
  
  
  📦 Repository Pattern in Practice
&lt;/h2&gt;

&lt;p&gt;In Spring Boot, repositories are generated for you.&lt;/p&gt;

&lt;p&gt;In FastAPI, you write them.&lt;/p&gt;

&lt;p&gt;Explicitly.&lt;/p&gt;

&lt;p&gt;I implemented a generic base repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Generic&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ModelType&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_by_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scalar_one_or_none&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then extended it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseRepository&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_by_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scalars&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Centralized query logic&lt;/li&gt;
&lt;li&gt;Cleaner services&lt;/li&gt;
&lt;li&gt;Easier unit testing&lt;/li&gt;
&lt;li&gt;Reusable across features&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first, it felt verbose.&lt;/p&gt;

&lt;p&gt;Later, it felt disciplined.&lt;/p&gt;




&lt;h3&gt;
  
  
  Real Challenge: Async Session Management
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem I Hit:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# WRONG - This caused session closure errors
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_tasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;task_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_by_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Session closes here
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ERROR: Session closed!
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt; SQLAlchemy's async sessions close after the query completes. Accessing related objects (like &lt;code&gt;task.category&lt;/code&gt;) requires the session to still be open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Eager loading with &lt;code&gt;selectinload&lt;/code&gt;:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.orm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;selectinload&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_by_user_with_category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get tasks with categories eagerly loaded.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;selectinload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scalars&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Access relationships within session scope:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_task_with_details&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get task with all related data.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;task_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_by_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="c1"&gt;# Access relationships while session is active
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; Async requires explicit relationship loading. Plan your queries based on what data you'll need.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ Service Layer Development
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Purpose of Service Layer
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why not put business logic in routes?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# BAD - Business logic in routes
&lt;/span&gt;&lt;span class="nd"&gt;@router.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tasks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TaskCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="c1"&gt;# Validation logic here
&lt;/span&gt;    &lt;span class="c1"&gt;# Database operations here
&lt;/span&gt;    &lt;span class="c1"&gt;# Error handling here
&lt;/span&gt;    &lt;span class="c1"&gt;# All mixed together!
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Better - Service layer:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# GOOD - Clean separation
&lt;/span&gt;&lt;span class="nd"&gt;@router.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tasks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TaskResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;task_in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TaskCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_current_user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;task_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Routes handle HTTP concerns only&lt;/li&gt;
&lt;li&gt;Services contain business logic&lt;/li&gt;
&lt;li&gt;Easier to test services independently&lt;/li&gt;
&lt;li&gt;Reusable business logic&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Business logic doesn't belong in routes.&lt;/p&gt;

&lt;p&gt;So I moved rules into services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;task_in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TaskCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;task_in&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;due_date&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;task_in&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;due_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Due date must be in the future&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;task_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task_in&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;task_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;task_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Routes handle HTTP&lt;/li&gt;
&lt;li&gt;Services enforce rules&lt;/li&gt;
&lt;li&gt;Repositories manage data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture from Part 1 finally paid off here.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔧 CRUD Operations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Routes → Services → Repositories pattern in action.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What makes this production-ready?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;✅ &lt;strong&gt;Clear documentation&lt;/strong&gt; (auto-generated in Swagger)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Input validation&lt;/strong&gt; (Pydantic schemas)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Authorization&lt;/strong&gt; (user can only access their tasks)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Proper HTTP status codes&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Type hints&lt;/strong&gt; (IDE autocomplete, catch errors)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Pagination&lt;/strong&gt; (prevent large response payloads)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Filtering &amp;amp; search&lt;/strong&gt; (flexible queries)&lt;/li&gt;
&lt;/ol&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TaskResponse&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_tasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TaskStatus&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_current_user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;task_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_user_tasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Underneath that simple endpoint is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query composition&lt;/li&gt;
&lt;li&gt;Security enforcement&lt;/li&gt;
&lt;li&gt;Async session handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"Simple" rarely stays simple at scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Advanced Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Rate Limiting with SlowAPI
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why rate limiting?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prevent brute force attacks on auth endpoints&lt;/li&gt;
&lt;li&gt;Protect against API abuse&lt;/li&gt;
&lt;li&gt;Ensure fair resource usage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Configuration by endpoint type:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auth endpoints: 5 requests/minute&lt;/li&gt;
&lt;li&gt;Regular endpoints: 100 requests/minute&lt;/li&gt;
&lt;li&gt;Global limit: Configurable per environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real challenge:&lt;/strong&gt; SlowAPI needed custom middleware configuration on Render. Spent time debugging connection pooling and Redis integration options.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Advanced Filtering &amp;amp; Pagination
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Requirements:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Filter by status, priority, category&lt;/li&gt;
&lt;li&gt;Search in title/description&lt;/li&gt;
&lt;li&gt;Date range queries&lt;/li&gt;
&lt;li&gt;Pagination with metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flexible filtering combinations&lt;/li&gt;
&lt;li&gt;Consistent pagination structure&lt;/li&gt;
&lt;li&gt;Metadata for building UI pagination&lt;/li&gt;
&lt;li&gt;Type-safe responses&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  3. Full-Text Search
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Simple but effective search implementation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_tasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;search_query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;skip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Search tasks by title or description.
    Case-insensitive partial match.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;search_pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;search_query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nf"&gt;and_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nf"&gt;or_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ilike&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search_pattern&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ilike&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search_pattern&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scalars&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;For future improvement:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL full-text search with &lt;code&gt;tsvector&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Search ranking/relevance scoring&lt;/li&gt;
&lt;li&gt;Elasticsearch integration for large datasets&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  4. Error Handling &amp;amp; Logging
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Centralized exception handling:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/core/exceptions.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskNotFoundError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Task not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UnauthorizedTaskAccessError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Not authorized to access this task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# app/main.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi.responses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;JSONResponse&lt;/span&gt;

&lt;span class="nd"&gt;@app.exception_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;global_exception_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Global exception handler for unexpected errors.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unhandled exception: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_info&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Internal server error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Request/Response logging:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/core/monitoring.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;log_requests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Log all HTTP requests with timing.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;start_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c1"&gt;# Log request
&lt;/span&gt;    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Process request
&lt;/span&gt;    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;call_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Calculate duration
&lt;/span&gt;    &lt;span class="n"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start_time&lt;/span&gt;

    &lt;span class="c1"&gt;# Log response
&lt;/span&gt;    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; - &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consistent error responses&lt;/li&gt;
&lt;li&gt;Request tracing for debugging&lt;/li&gt;
&lt;li&gt;Performance monitoring&lt;/li&gt;
&lt;li&gt;Production-ready logging&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🚧 Real Challenges &amp;amp; Solutions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Challenge 1: Async Session Closure
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This caused "Session is already closed" errors
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_task_with_category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;task_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_by_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Session closes after repository call
&lt;/span&gt;    &lt;span class="n"&gt;category_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;  &lt;span class="c1"&gt;# ERROR!
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why it happened:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQLAlchemy async sessions have shorter lifecycle&lt;/li&gt;
&lt;li&gt;Relationships not loaded by default&lt;/li&gt;
&lt;li&gt;Accessing unloaded relationships after session closes → error&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Eager loading:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.orm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;selectinload&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_by_id_with_relations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nf"&gt;selectinload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nf"&gt;selectinload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scalar_one_or_none&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Access within session scope:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_task_details&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;task_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_by_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="c1"&gt;# Access all relationships while session is active
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Time lost:&lt;/strong&gt; ~3 hours debugging various "session closed" errors&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; With async SQLAlchemy, plan your data access patterns. Decide upfront what relationships you'll need.&lt;/p&gt;




&lt;h3&gt;
  
  
  Challenge 2: Repository Pattern Adaptation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Struggle:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Coming from Spring Boot's &lt;code&gt;JpaRepository&lt;/code&gt;, I initially wrote this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# WRONG - Trying to replicate Spring Boot directly
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskRepository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_by_user_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# Sync!
&lt;/span&gt;        &lt;span class="c1"&gt;# How do I make this async?
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spring Boot's repository interfaces work because of proxy magic at runtime.&lt;/p&gt;

&lt;p&gt;FastAPI doesn't have that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Accept that Python is explicit, not magic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# RIGHT - Explicit async
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskRepository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_by_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Explicit session
&lt;/span&gt;        &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scalars&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; Don't fight the language. FastAPI's explicitness is a feature, not a bug. It makes dependencies clear and testable.&lt;/p&gt;




&lt;h3&gt;
  
  
  Challenge 3: Token Refresh Logic
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;First implementation of token refresh had a security flaw:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's wrong?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Doesn't check if user still exists&lt;/li&gt;
&lt;li&gt;Doesn't check if user is still active&lt;/li&gt;
&lt;li&gt;Could issue tokens for deleted/banned users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Time to discover:&lt;/strong&gt; 2 days (caught during security review)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; JWT tokens are stateless, but security checks shouldn't be. Always validate against source of truth (database).&lt;/p&gt;




&lt;h3&gt;
  
  
  Challenge 4: Rate Limiting on Render
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SlowAPI worked perfectly locally, but failed on Render:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SlowAPI: Failed to connect to Redis
Rate limiting disabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Render's free tier doesn't include Redis.&lt;br&gt;
SlowAPI uses Redis for distributed rate limiting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Switch to in-memory rate limiting for free tier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/core/rate_limiter.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;slowapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Limiter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;slowapi.util&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_remote_address&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;slowapi.middleware&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SlowAPIMiddleware&lt;/span&gt;

&lt;span class="c1"&gt;# Use in-memory storage (single instance)
&lt;/span&gt;&lt;span class="n"&gt;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Limiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;key_func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;get_remote_address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;storage_uri&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;memory://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# In-memory instead of Redis
&lt;/span&gt;    &lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# In main.py
&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;limiter&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SlowAPIMiddleware&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Trade-offs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Works on free tier&lt;/li&gt;
&lt;li&gt;✅ No external dependencies&lt;/li&gt;
&lt;li&gt;❌ Not distributed (single instance only)&lt;/li&gt;
&lt;li&gt;❌ Resets on deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For production:&lt;/strong&gt; Upgrade to Redis for distributed rate limiting across multiple instances.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; Free tiers have constraints. Design for your deployment environment, not just local development.&lt;/p&gt;




&lt;h3&gt;
  
  
  Challenge 5: Alembic Migration Conflicts
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After developing features in parallel (tasks + categories), Alembic migrations conflicted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alembic revision --autogenerate
Error: Multiple head revisions present
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Created migrations from different branches without syncing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Merge heads&lt;/span&gt;
alembic merge heads &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"merge task and category features"&lt;/span&gt;

&lt;span class="c"&gt;# Apply merged migration&lt;/span&gt;
alembic upgrade &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Prevention strategy:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Always pull latest migrations before creating new ones&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;alembic upgrade head&lt;/code&gt; before creating new migration&lt;/li&gt;
&lt;li&gt;Use linear migration history (avoid parallel migrations)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Time lost:&lt;/strong&gt; 1 hour debugging + fixing migration history&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; Database migrations are sequential, not parallel. Coordinate with team (or yourself across branches).&lt;/p&gt;




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

&lt;h3&gt;
  
  
  1. Async Requires Discipline
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Spring Boot (blocking):&lt;/strong&gt; Forgiving. Mix sync/async, it'll work (maybe slower).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FastAPI (async):&lt;/strong&gt; Strict. One blocking call blocks the entire event loop.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Always &lt;code&gt;await&lt;/code&gt; database calls&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;AsyncSession&lt;/code&gt;, not &lt;code&gt;Session&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Don't mix sync and async carelessly&lt;/li&gt;
&lt;li&gt;Plan async from the start&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Explicit is Better Than Magic
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Spring Boot:&lt;/strong&gt; Annotations do magic (&lt;code&gt;@Autowired&lt;/code&gt;, &lt;code&gt;@Transactional&lt;/code&gt;)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FastAPI:&lt;/strong&gt; Dependency injection is explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_tasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_current_user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Dependencies explicit in function signature
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Initially:&lt;/strong&gt; Felt verbose&lt;br&gt;
&lt;strong&gt;After building:&lt;/strong&gt; Appreciate the clarity&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Benefit:&lt;/strong&gt; Testability. Mock dependencies easily.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Repository Pattern Pays Off
&lt;/h3&gt;

&lt;p&gt;Centralizing database logic in repositories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Made service layer cleaner&lt;/li&gt;
&lt;li&gt;✅ Enabled easy testing (mock repositories)&lt;/li&gt;
&lt;li&gt;✅ Reused queries across services&lt;/li&gt;
&lt;li&gt;✅ Single source of truth for data access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Worth the upfront effort.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Security is a Process
&lt;/h3&gt;

&lt;p&gt;Built authentication in iterations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Basic password check&lt;/li&gt;
&lt;li&gt;Added JWT tokens&lt;/li&gt;
&lt;li&gt;Added token refresh&lt;/li&gt;
&lt;li&gt;Added rate limiting&lt;/li&gt;
&lt;li&gt;Added email verification&lt;/li&gt;
&lt;li&gt;Added password reset&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Each iteration revealed new attack vectors to defend against.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Security isn't a feature you add once. It's continuous hardening.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. Documentation Writes Itself
&lt;/h3&gt;

&lt;p&gt;FastAPI's auto-generated docs are incredible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@router.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tasks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TaskResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;task_in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TaskCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_current_user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Create a new task.

    - **title**: Required, 1-200 characters
    - **priority**: LOW, MEDIUM, HIGH, URGENT
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Interactive Swagger UI with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All endpoints documented&lt;/li&gt;
&lt;li&gt;Request/response schemas&lt;/li&gt;
&lt;li&gt;Try-it-out functionality&lt;/li&gt;
&lt;li&gt;Type information&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;No separate documentation tool needed.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🔮 What's Next: Phase 3
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Coming in Part 3:&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;pytest setup with async support&lt;/li&gt;
&lt;li&gt;Unit tests for services&lt;/li&gt;
&lt;li&gt;Integration tests for API endpoints&lt;/li&gt;
&lt;li&gt;Test coverage &amp;gt; 85%&lt;/li&gt;
&lt;li&gt;Testing async code patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Deployment
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Docker multi-stage builds&lt;/li&gt;
&lt;li&gt;Render.com deployment&lt;/li&gt;
&lt;li&gt;Neon PostgreSQL setup&lt;/li&gt;
&lt;li&gt;Environment variable management&lt;/li&gt;
&lt;li&gt;Health checks &amp;amp; monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Production Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Performance metrics&lt;/li&gt;
&lt;li&gt;Uptime tracking&lt;/li&gt;
&lt;li&gt;Cost analysis ($0 free tier!)&lt;/li&gt;
&lt;li&gt;CI/CD pipeline&lt;/li&gt;
&lt;li&gt;Production best practices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Follow me to get notified when Part 3 drops!&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  📝 Complete Code &amp;amp; Resources
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔗 &lt;a href="https://github.com/ravigupta97/task_management_api" rel="noopener noreferrer"&gt;https://github.com/ravigupta97/task_management_api&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Fully documented code&lt;/li&gt;
&lt;li&gt;Clean commit history&lt;/li&gt;
&lt;li&gt;README with setup instructions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Live API Documentation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🚀 &lt;a href="https://task-management-api-a775.onrender.com/docs" rel="noopener noreferrer"&gt;https://task-management-api-a775.onrender.com/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Interactive Swagger UI&lt;/li&gt;
&lt;li&gt;Try all endpoints&lt;/li&gt;
&lt;li&gt;See request/response schemas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Files to Explore:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;app/core/security.py&lt;/code&gt; - JWT implementation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app/repositories/&lt;/code&gt; - Repository pattern&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app/services/&lt;/code&gt; - Business logic layer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app/api/v1/&lt;/code&gt; - Route handlers&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🙏 Acknowledgments
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tools &amp;amp; Resources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FastAPI documentation (exceptional quality)&lt;/li&gt;
&lt;li&gt;SQLAlchemy 2.0 docs (comprehensive)&lt;/li&gt;
&lt;li&gt;AI assistants (concept clarification)&lt;/li&gt;
&lt;li&gt;Open-source community&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What helped most:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reading production FastAPI codebases on GitHub&lt;/li&gt;
&lt;li&gt;AI tools for understanding complex concepts&lt;/li&gt;
&lt;li&gt;Trial and error (every bug taught something)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  💬 Discussion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Questions for you:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;How do you handle async session management?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What's your approach to API authentication?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repository pattern: overkill or essential?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Biggest challenge you've faced with FastAPI?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's learn together - comment below! 👇&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is Part 2 of 3 in my FastAPI journey.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/ravigupta97/building-a-production-ready-task-management-api-with-fastapi-complete-architecture-guide-25a"&gt;Part 1: Architecture &amp;amp; Design&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/ravigupta97/building-a-production-ready-task-management-api-with-fastapi-testing-deployment-production-1j1c"&gt;Part 3: Testing &amp;amp; Deployment&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Connect with me:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;LinkedIn: [&lt;a href="https://www.linkedin.com/in/ravigupta97/" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/ravigupta97/&lt;/a&gt;]&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;GitHub: [&lt;a href="https://github.com/ravigupta97" rel="noopener noreferrer"&gt;https://github.com/ravigupta97&lt;/a&gt;]&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;#FastAPI #Python #Backend #JWT #Authentication #API #CleanArchitecture #WebDevelopment #SoftwareEngineering&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>fastapi</category>
      <category>python</category>
      <category>api</category>
      <category>backend</category>
    </item>
    <item>
      <title>Building a Production-Ready Task Management API with FastAPI: Complete Architecture Guide</title>
      <dc:creator>Ravi Gupta</dc:creator>
      <pubDate>Mon, 23 Feb 2026 03:31:21 +0000</pubDate>
      <link>https://dev.to/ravigupta97/building-a-production-ready-task-management-api-with-fastapi-complete-architecture-guide-25a</link>
      <guid>https://dev.to/ravigupta97/building-a-production-ready-task-management-api-with-fastapi-complete-architecture-guide-25a</guid>
      <description>&lt;h2&gt;
  
  
  Building a Production-Ready Task Management API with FastAPI: Complete Architecture Guide
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Context:&lt;/strong&gt; After 3 years building backend services with Spring Boot, I decided to explore Python's modern web framework ecosystem. This is Part 1 of a 3-part series documenting my journey building a production-grade REST API with FastAPI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three weeks ago, I started a new learning project: build a production-ready Task Management API using FastAPI.&lt;/p&gt;

&lt;p&gt;Not a tutorial-level "TODO app in 100 lines."&lt;/p&gt;

&lt;p&gt;A real, production-grade application with authentication, proper architecture, database migrations, and deployment-ready code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The goal:&lt;/strong&gt; Learn modern Python web development while applying enterprise patterns I knew from Spring Boot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; A fully functional API with 50+ endpoints, JWT authentication, and clean architecture - deployed on free tier infrastructure.&lt;/p&gt;

&lt;p&gt;This article covers &lt;strong&gt;Phase 1: Architecture &amp;amp; Design&lt;/strong&gt; - the foundation that made everything else possible.&lt;/p&gt;




&lt;h2&gt;
  
  
  📚 What You'll Learn
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Why FastAPI over Flask/Django (from a Spring Boot developer's perspective)&lt;/li&gt;
&lt;li&gt;Designing a clean, scalable project architecture&lt;/li&gt;
&lt;li&gt;Database schema design with PostgreSQL&lt;/li&gt;
&lt;li&gt;Key technology decisions and trade-offs&lt;/li&gt;
&lt;li&gt;Mistakes I made and how I fixed them&lt;/li&gt;
&lt;li&gt;Resources that accelerated my learning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reading time:&lt;/strong&gt; ~12 minutes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/ravigupta97/task_management_api" rel="noopener noreferrer"&gt;GitHub Repo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live API Docs:&lt;/strong&gt; &lt;a href="https://task-management-api-a775.onrender.com/docs" rel="noopener noreferrer"&gt;API&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🎯 The Challenge: From Tutorials to Production
&lt;/h2&gt;

&lt;p&gt;Most FastAPI tutorials teach you this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# main.py - Everything in one file
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_root&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;World&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;150 lines. No auth. SQLite. Single file.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But production applications need:&lt;/p&gt;

&lt;p&gt;✅ 50+ endpoints organized logically&lt;br&gt;
✅ User authentication &amp;amp; authorization&lt;br&gt;
✅ Database migrations&lt;br&gt;
✅ Proper error handling&lt;br&gt;
✅ Rate limiting&lt;br&gt;
✅ Clean, testable architecture&lt;br&gt;
✅ Docker deployment&lt;br&gt;
✅ API documentation&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The gap between tutorials and production is massive.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This article bridges that gap.&lt;/p&gt;
&lt;h2&gt;
  
  
  🚀 My Background &amp;amp; Motivation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Experience:&lt;/strong&gt; Around 3 years backend development with Spring Boot (Java)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why FastAPI?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Coming from Spring Boot, I was curious about Python's modern web frameworks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Django: Too heavy for an API-first project&lt;/li&gt;
&lt;li&gt;Flask: Popular but felt dated (no native async)&lt;/li&gt;
&lt;li&gt;FastAPI: Modern, async-first, great docs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The learning goal:&lt;/strong&gt; Not just "learn FastAPI" but understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How to structure large Python projects&lt;/li&gt;
&lt;li&gt;Modern async patterns in Python&lt;/li&gt;
&lt;li&gt;Production-ready API design&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Coming from Spring Boot&lt;/strong&gt;, I wanted to see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How Python's simplicity compares to Java's verbosity&lt;/li&gt;
&lt;li&gt;Whether FastAPI's async is easier than Spring WebFlux&lt;/li&gt;
&lt;li&gt;If I could apply enterprise patterns (service layer, repositories) in Python&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Note: This isn't about "which is better" - both Spring Boot and FastAPI are excellent. This was about expanding my toolkit.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  🔍 Phase 1 Research: FastAPI vs Flask vs Django
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Time spent:&lt;/strong&gt; A few days researching frameworks, reading docs, watching talks&lt;/p&gt;
&lt;h3&gt;
  
  
  Decision Matrix
&lt;/h3&gt;

&lt;p&gt;I created this framework for choosing technologies:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criteria&lt;/th&gt;
&lt;th&gt;FastAPI&lt;/th&gt;
&lt;th&gt;Flask&lt;/th&gt;
&lt;th&gt;Django&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Async Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Native&lt;/td&gt;
&lt;td&gt;⚠️ Via extensions&lt;/td&gt;
&lt;td&gt;⚠️ Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Very fast&lt;/td&gt;
&lt;td&gt;🟡 Moderate&lt;/td&gt;
&lt;td&gt;🟡 Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auto Docs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Swagger/ReDoc&lt;/td&gt;
&lt;td&gt;❌ Manual&lt;/td&gt;
&lt;td&gt;❌ DRF only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data Validation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Pydantic built-in&lt;/td&gt;
&lt;td&gt;🟡 Libraries needed&lt;/td&gt;
&lt;td&gt;🟡 DRF serializers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Learning Curve&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;🟡 Moderate&lt;/td&gt;
&lt;td&gt;✅ Easy&lt;/td&gt;
&lt;td&gt;🔴 Steep&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Type Hints&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Required&lt;/td&gt;
&lt;td&gt;🟡 Optional&lt;/td&gt;
&lt;td&gt;🟡 Optional&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API-First&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;🟡 Flexible&lt;/td&gt;
&lt;td&gt;❌ Full-stack focus&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Why FastAPI Won
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Native Async/Await&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# FastAPI - async is first-class
&lt;/span&gt;&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tasks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_tasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scalars&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean, intuitive async. No additional configuration needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Auto-Generated API Documentation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;FastAPI generates interactive Swagger UI automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/docs&lt;/code&gt; - Swagger UI&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/redoc&lt;/code&gt; - ReDoc&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/openapi.json&lt;/code&gt; - OpenAPI schema&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zero configuration. Just write your code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Built-in Data Validation with Pydantic&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^(LOW|MEDIUM|HIGH)$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Validation happens automatically
&lt;/span&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tasks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TaskCreate&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# task is validated, type-safe
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reminded me of Bean Validation in Spring Boot, but more Pythonic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final Decision
&lt;/h3&gt;

&lt;p&gt;✅ &lt;strong&gt;FastAPI&lt;/strong&gt; for this project because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API-first design (perfect for backend learning)&lt;/li&gt;
&lt;li&gt;Modern Python features (async, type hints)&lt;/li&gt;
&lt;li&gt;Great documentation&lt;/li&gt;
&lt;li&gt;Growing ecosystem and job market demand&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🏗️ Designing Clean Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Challenge:&lt;/strong&gt; How do you structure a FastAPI project for scale?&lt;/p&gt;

&lt;h3&gt;
  
  
  Coming from Spring Boot
&lt;/h3&gt;

&lt;p&gt;In Spring Boot, I was used to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;TaskService&lt;/span&gt; &lt;span class="n"&gt;taskService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/tasks"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getTasks&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;taskService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAll&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clear separation: Controller → Service → Repository → Entity&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Could I apply the same pattern in FastAPI?&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Architecture I Designed
&lt;/h3&gt;

&lt;p&gt;After researching production FastAPI projects, I landed on this structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;task_management_api/
├── app/
│   ├── models/          # SQLAlchemy models (like @Entity)
│   ├── schemas/         # Pydantic schemas (DTOs)
│   ├── repositories/    # Data access layer
│   ├── services/        # Business logic
│   ├── api/             # Route handlers
│   │   └── v1/          # API versioning
│   ├── core/            # Security, config
│   └── main.py          # App initialization
├── tests/
├── alembic/             # Database migrations
└── requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this structure?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Separation of Concerns&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Routes handle HTTP&lt;/li&gt;
&lt;li&gt;Services contain business logic&lt;/li&gt;
&lt;li&gt;Repositories handle database&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Testability&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each layer can be tested independently&lt;/li&gt;
&lt;li&gt;Mock repositories in service tests&lt;/li&gt;
&lt;li&gt;Mock services in route tests&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Scalability&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Easy to add new features&lt;/li&gt;
&lt;li&gt;Clear place for everything&lt;/li&gt;
&lt;li&gt;Team can work on different layers&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Layer-by-Layer Breakdown
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;📁 Models (app/models/)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# SQLAlchemy ORM models (database tables)
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;__tablename__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tasks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ForeignKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;users.id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Like Spring's &lt;code&gt;@Entity&lt;/code&gt; classes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📁 Schemas (app/schemas/)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Pydantic models (request/response validation)
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TaskPriority&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Like Spring's DTOs, but with automatic validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📁 Repositories (app/repositories/)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Data access layer
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskRepository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_by_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scalar_one_or_none&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Like Spring's &lt;code&gt;@Repository&lt;/code&gt; classes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📁 Services (app/services/)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Business logic
&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TaskCreate&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Validation logic
&lt;/span&gt;        &lt;span class="c1"&gt;# Business rules
&lt;/span&gt;        &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;task_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Like Spring's &lt;code&gt;@Service&lt;/code&gt; classes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;📁 API Routes (app/api/v1/)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# HTTP handlers
&lt;/span&gt;&lt;span class="nd"&gt;@router.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/tasks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TaskResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;task_in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TaskCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_current_user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;task_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_in&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Like Spring's &lt;code&gt;@RestController&lt;/code&gt; classes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture Diagram
&lt;/h3&gt;

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

&lt;p&gt;&lt;strong&gt;Detailed Architecture Diagram:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────┐
│                  Client Request                     │
└──────────────────────┬──────────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────────┐
│              API Layer (Routes)                     │
│  • Route handlers                                   │
│  • Request validation (Pydantic)                    │
│  • Response serialization                           │
│  • Dependency injection                             │
└──────────────────────┬──────────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────────┐
│           Service Layer (Business Logic)            │
│  • Business rules                                   │
│  • Data transformation                              │
│  • Error handling                                   │
│  • Transaction management                           │
└──────────────────────┬──────────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────────┐
│         Repository Layer (Data Access)              │
│  • Database queries                                 │
│  • ORM operations                                   │
│  • Data mapping                                     │
└──────────────────────┬──────────────────────────────┘
                       │
┌──────────────────────▼──────────────────────────────┐
│              Database (PostgreSQL)                  │
│  • Tables: Users, Tasks, Categories                 │
│  • Relationships &amp;amp; constraints                      │
└─────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Benefits of this architecture:&lt;/strong&gt;&lt;br&gt;
✅ Clear separation of concerns&lt;br&gt;
✅ Easy to test each layer&lt;br&gt;
✅ Scales with team size&lt;br&gt;
✅ Familiar to developers from other frameworks&lt;/p&gt;
&lt;h2&gt;
  
  
  🗄️ Database Schema Design
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Requirements Analysis
&lt;/h3&gt;

&lt;p&gt;Before designing the schema, I listed what the API needed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User Management:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users can register and login&lt;/li&gt;
&lt;li&gt;Email verification&lt;/li&gt;
&lt;li&gt;Password reset functionality&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Users create tasks&lt;/li&gt;
&lt;li&gt;Tasks have status (TODO, IN_PROGRESS, COMPLETED)&lt;/li&gt;
&lt;li&gt;Tasks have priority levels&lt;/li&gt;
&lt;li&gt;Optional due dates&lt;/li&gt;
&lt;li&gt;Tasks belong to users&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Organization:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users can create categories&lt;/li&gt;
&lt;li&gt;Tasks can be assigned to categories&lt;/li&gt;
&lt;li&gt;Categories have colors for UI&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Entity-Relationship Design
&lt;/h3&gt;

&lt;p&gt;After a few iterations, here's the final schema:&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Entity-Relationship Diagram:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────┐
│             USERS                    │
├──────────────────────────────────────┤
│ 🔑 id (UUID, PK)                     │
│ 📧 email (UNIQUE, NOT NULL)          │
│ 👤 username (UNIQUE, NOT NULL)       │
│ 🔒 hashed_password (NOT NULL)        │
│ ✅ is_active (BOOLEAN,DEFAULT TRUE)  │
│ ✉️ is_verified(BOOLEAN,DEFAULT FALSE)│
│ 📝 full_name (TEXT, NULLABLE)        │
│ 📅 created_at (TIMESTAMP)            │
│ 📅 updated_at (TIMESTAMP)            │
└──────────────┬───────────────────────┘
               │ 1
               │
               │ N
┌──────────────▼───────────────────────┐
│             TASKS                    │
├──────────────────────────────────────┤
│ 🔑 id (UUID, PK)                    │
│ 📝 title (VARCHAR(200), NOT NULL)   │
│ 📄 description (TEXT, NULLABLE)     │
│ 📊 status (ENUM, NOT NULL)          │
│    └─ TODO, IN_PROGRESS, COMPLETED  │
│ ⭐ priority (ENUM, NOT NULL)        │
│    └─ LOW, MEDIUM, HIGH, URGENT     │
│ 📆 due_date (TIMESTAMP, NULLABLE)   │
│ 🔗 user_id (UUID, FK → users.id)    │
│ 🏷️ category_id (UUID, FK→categories)│
│ 📅 created_at (TIMESTAMP)           │
│ 📅 updated_at (TIMESTAMP)           │
└──────────────┬───────────────────────┘
               │ N
               │
               │ 1
┌──────────────▼───────────────────────┐
│           CATEGORIES                 │
├──────────────────────────────────────┤
│ 🔑 id (UUID, PK)                    │
│ 🏷️ name (VARCHAR(50), NOT NULL)     │
│ 🎨 color (VARCHAR(7),DEFAULT '#...')│
│ 🔗 user_id (UUID, FK → users.id)    │
│ 📅 created_at (TIMESTAMP)           │
│ 📅 updated_at (TIMESTAMP)           │
│                                      │
│ UNIQUE CONSTRAINT (user_id, name)    │
└───────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Design Decisions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. UUID Primary Keys&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;as_uuid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why UUID over auto-increment integers?&lt;/strong&gt;&lt;br&gt;
✅ Better security (can't enumerate resources)&lt;br&gt;
✅ Globally unique (helpful for distributed systems)&lt;br&gt;
✅ Can generate client-side&lt;br&gt;
❌ Slightly larger storage footprint&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-off accepted:&lt;/strong&gt; Security and flexibility &amp;gt; minor storage increase&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Enum Types for Status &amp;amp; Priority&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TaskStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;TODO&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TODO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;IN_PROGRESS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IN_PROGRESS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;COMPLETED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;COMPLETED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;ARCHIVED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ARCHIVED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database-level constraint validation&lt;/li&gt;
&lt;li&gt;Type safety in Python code&lt;/li&gt;
&lt;li&gt;Clear API documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Soft Timestamps&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every table has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;created_at&lt;/code&gt; - Automatically set on insert&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;updated_at&lt;/code&gt; - Automatically updated on modification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Implementation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onupdate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Cascade Behaviors&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# If user is deleted, delete their tasks
&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;relationship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;back_populates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cascade&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all, delete-orphan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# If category is deleted, set task category_id to NULL
&lt;/span&gt;&lt;span class="n"&gt;category_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ForeignKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;categories.id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ondelete&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET NULL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt; Data integrity while preserving user intent.&lt;/p&gt;

&lt;h3&gt;
  
  
  SQLAlchemy Model Example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ForeignKey&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.dialects.postgresql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sqlalchemy.orm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;relationship&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;__tablename__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;users&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;as_uuid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;primary_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;unique&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;unique&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;hashed_password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;is_verified&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;full_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Timestamps
&lt;/span&gt;    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;onupdate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Relationships
&lt;/span&gt;    &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;relationship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;back_populates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cascade&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all, delete-orphan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;categories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;relationship&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;back_populates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;owner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cascade&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all, delete-orphan&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean, readable, type-safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠️ Complete Tech Stack &amp;amp; Justification
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Final Stack
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Why?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Framework&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;FastAPI&lt;/td&gt;
&lt;td&gt;Async-first, auto docs, Pydantic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Language&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Python 3.11&lt;/td&gt;
&lt;td&gt;Latest stable, performance improvements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Database&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PostgreSQL 16&lt;/td&gt;
&lt;td&gt;Advanced features, JSON support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ORM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SQLAlchemy 2.0&lt;/td&gt;
&lt;td&gt;Async support, mature ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Migrations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Alembic&lt;/td&gt;
&lt;td&gt;Industry standard for SQLAlchemy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JWT (python-jose)&lt;/td&gt;
&lt;td&gt;Stateless, scalable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Validation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pydantic v2&lt;/td&gt;
&lt;td&gt;Built into FastAPI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Container&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;Consistent environments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Uvicorn&lt;/td&gt;
&lt;td&gt;ASGI server optimized for FastAPI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DB Hosting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Neon (Serverless PostgreSQL)&lt;/td&gt;
&lt;td&gt;Free tier, excellent for learning&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;App Hosting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Render&lt;/td&gt;
&lt;td&gt;Free tier, easy deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Decision Process for Each
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;PostgreSQL vs MySQL vs MongoDB&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criteria&lt;/th&gt;
&lt;th&gt;PostgreSQL&lt;/th&gt;
&lt;th&gt;MySQL&lt;/th&gt;
&lt;th&gt;MongoDB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JSON Support&lt;/td&gt;
&lt;td&gt;✅ Excellent (JSONB)&lt;/td&gt;
&lt;td&gt;🟡 Basic&lt;/td&gt;
&lt;td&gt;✅ Native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transactions&lt;/td&gt;
&lt;td&gt;✅ Full ACID&lt;/td&gt;
&lt;td&gt;✅ Full ACID&lt;/td&gt;
&lt;td&gt;🟡 Limited&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Advanced Features&lt;/td&gt;
&lt;td&gt;✅ Rich&lt;/td&gt;
&lt;td&gt;🟡 Good&lt;/td&gt;
&lt;td&gt;❌ Different paradigm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free Hosting&lt;/td&gt;
&lt;td&gt;✅ Neon, Supabase&lt;/td&gt;
&lt;td&gt;✅ PlanetScale&lt;/td&gt;
&lt;td&gt;✅ Atlas&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Winner: PostgreSQL&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better for structured data (tasks, users)&lt;/li&gt;
&lt;li&gt;JSONB for future flexibility&lt;/li&gt;
&lt;li&gt;Excellent free tier options&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SQLAlchemy vs Raw SQL vs other ORMs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQLAlchemy 2.0 won because:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Native async support&lt;/li&gt;
&lt;li&gt;Type hints and IDE autocomplete&lt;/li&gt;
&lt;li&gt;Migration management (Alembic)&lt;/li&gt;
&lt;li&gt;Mature, battle-tested&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The learning curve&lt;/strong&gt; was worth it for long-term maintainability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker: Day 1 Decision&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Why containerize from the start?&lt;br&gt;
✅ Consistent dev/prod environments&lt;br&gt;
✅ Easy onboarding for collaborators&lt;br&gt;
✅ Deployment simplicity&lt;br&gt;
✅ Learning industry standard practice&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No regrets on this decision.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Decision Matrix Template
&lt;/h3&gt;

&lt;p&gt;For each technology choice, I asked:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Does it solve my problem?&lt;/strong&gt; (must-have features)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it actively maintained?&lt;/strong&gt; (community, updates)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can I learn it reasonably?&lt;/strong&gt; (documentation, resources)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does it scale?&lt;/strong&gt; (production-ready)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is it resume-worthy?&lt;/strong&gt; (job market demand)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This framework helped avoid analysis paralysis.&lt;/p&gt;
&lt;h2&gt;
  
  
  📚 Resources That Helped Me
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Official Documentation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. FastAPI Docs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Link: &lt;a href="https://fastapi.tiangolo.com/" rel="noopener noreferrer"&gt;https://fastapi.tiangolo.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;⭐⭐⭐⭐⭐ (Exceptional quality)&lt;/li&gt;
&lt;li&gt;Start here. Seriously. Best framework docs I've seen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. SQLAlchemy 2.0 Docs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Link: &lt;a href="https://docs.sqlalchemy.org/" rel="noopener noreferrer"&gt;https://docs.sqlalchemy.org/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;⭐⭐⭐⭐ (Comprehensive but dense)&lt;/li&gt;
&lt;li&gt;The async section took multiple readings to understand&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Pydantic Docs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Link: &lt;a href="https://docs.pydantic.dev/" rel="noopener noreferrer"&gt;https://docs.pydantic.dev/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;⭐⭐⭐⭐⭐ (Clear examples)&lt;/li&gt;
&lt;li&gt;Great migration guide from v1 to v2&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Video Courses / Tutorials
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. FastAPI - The Complete Course (YouTube)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Covers basics to advanced patterns&lt;/li&gt;
&lt;li&gt;Helped visualize concepts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. SQLAlchemy 2.0 Tutorial (Official)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Essential for understanding async patterns&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Tools I Used Daily
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Development:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VS Code&lt;/strong&gt; + Python extension&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postman&lt;/strong&gt; for API testing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pgAdmin&lt;/strong&gt; for database management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Design:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;dbdiagram.io&lt;/strong&gt; - Database schema design&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Excalidraw&lt;/strong&gt; - Architecture diagrams&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Markdown editors&lt;/strong&gt; - Documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Learning:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ChatGPT / Claude&lt;/strong&gt; - Explaining complex concepts

&lt;ul&gt;
&lt;li&gt;"Explain SQLAlchemy async sessions like I'm coming from Spring Boot"&lt;/li&gt;
&lt;li&gt;"What's the Python equivalent of @Autowired dependency injection?"&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt; - Studying production FastAPI projects

&lt;ul&gt;
&lt;li&gt;fastapi/full-stack-fastapi-postgresql (official template)&lt;/li&gt;
&lt;li&gt;tiangolo's examples&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Learning Strategy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What worked:&lt;/strong&gt;&lt;br&gt;
✅ Read docs first, code second&lt;br&gt;
✅ Study production codebases&lt;br&gt;
✅ Use AI for concept clarification (not copy-paste)&lt;br&gt;
✅ Build incrementally, test often&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What didn't work:&lt;/strong&gt;&lt;br&gt;
❌ Jumping straight to coding without planning&lt;br&gt;
❌ Following outdated tutorials (SQLAlchemy 1.x vs 2.0)&lt;br&gt;
❌ Trying to learn everything at once&lt;/p&gt;
&lt;h2&gt;
  
  
  🚧 Real Challenges I Faced
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Challenge 1: Environment Setup
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Setting up Python, PostgreSQL, virtual environments, and managing dependencies was more complex than expected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What went wrong:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# This shouldn't have taken 2 hours, but it did&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;asyncpg
&lt;span class="c"&gt;# Error: pg_config not found&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Documented every step for reproducibility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Python 3.11 installation&lt;/span&gt;
&lt;span class="c"&gt;# 2. Virtual environment&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate  &lt;span class="c"&gt;# or .\venv\Scripts\activate on Windows&lt;/span&gt;

&lt;span class="c"&gt;# 3. PostgreSQL installation&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;postgresql@16  &lt;span class="c"&gt;# macOS&lt;/span&gt;
&lt;span class="c"&gt;# Or use Neon for cloud PostgreSQL (easier)&lt;/span&gt;

&lt;span class="c"&gt;# 4. Dependencies&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; Good documentation saves hours of debugging later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 2: Async/Await Debugging
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Spent 4 hours debugging this error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RuntimeError: Task &amp;lt;Task pending&amp;gt; attached to a different loop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What went wrong:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mixed synchronous and asynchronous session calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# WRONG - mixing sync and async
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# Sync session
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;  &lt;span class="c1"&gt;# Async call
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consistency is key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# CORRECT - all async
&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How AI helped:&lt;/strong&gt;&lt;br&gt;
I asked Claude: "Why am I getting 'attached to a different loop' error in SQLAlchemy async?"&lt;/p&gt;

&lt;p&gt;Got a clear explanation and code examples that saved me hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; Async is all-or-nothing. Mix carefully.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 3: SQLAlchemy 2.0 Documentation Overwhelm
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SQLAlchemy docs are comprehensive but overwhelming for beginners.&lt;/p&gt;

&lt;p&gt;The async section felt like reading a PhD thesis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What helped:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Used AI as a tutor&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Explain SQLAlchemy AsyncSession vs Session"&lt;/li&gt;
&lt;li&gt;"Show me a complete example of async CRUD operations"&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Studied working examples&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Found production codebases on GitHub&lt;/li&gt;
&lt;li&gt;Copied patterns, then understood them&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Started simple&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;   &lt;span class="c1"&gt;# Simple async query - master this first
&lt;/span&gt;   &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AsyncSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
       &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
           &lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scalar_one_or_none&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then built complexity gradually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; Don't try to understand everything. Master the basics, build on them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenge 4: JWT Token Security
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Confused about where to store JWT tokens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;localStorage? (XSS vulnerable)&lt;/li&gt;
&lt;li&gt;Cookies? (CSRF vulnerable)&lt;/li&gt;
&lt;li&gt;Memory only? (lost on refresh)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Researched trade-offs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;localStorage&lt;/td&gt;
&lt;td&gt;Simple, survives refresh&lt;/td&gt;
&lt;td&gt;XSS vulnerable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;httpOnly Cookie&lt;/td&gt;
&lt;td&gt;XSS safe&lt;/td&gt;
&lt;td&gt;Needs CSRF protection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory only&lt;/td&gt;
&lt;td&gt;Most secure&lt;/td&gt;
&lt;td&gt;Lost on refresh&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;My choice:&lt;/strong&gt; httpOnly cookies + CSRF tokens&lt;/p&gt;

&lt;p&gt;But for this learning project: Documented the trade-offs, implemented localStorage (simpler frontend), added strong CSP headers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned:&lt;/strong&gt; Perfect security doesn't exist. Understand trade-offs, document decisions.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'd Do Differently
&lt;/h3&gt;

&lt;p&gt;If I started over:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;✅ Plan architecture BEFORE writing code&lt;/li&gt;
&lt;li&gt;✅ Set up Docker from Day 1 (not Day 5)&lt;/li&gt;
&lt;li&gt;✅ Write tests alongside features (not after)&lt;/li&gt;
&lt;li&gt;✅ Study one production codebase deeply&lt;/li&gt;
&lt;li&gt;✅ Ask for help sooner (AI, communities, docs)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Real talk:&lt;/strong&gt; Every "mistake" taught me something. The frustration was part of learning.&lt;/p&gt;

&lt;h2&gt;
  
  
  ✅ Phase 1 Results
&lt;/h2&gt;

&lt;p&gt;After this planning phase, I had:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation:&lt;/strong&gt;&lt;br&gt;
✅ Database schema designed and documented&lt;br&gt;
✅ Architecture patterns decided&lt;br&gt;
✅ Technology stack finalized&lt;br&gt;
✅ Project structure defined&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical Foundation:&lt;/strong&gt;&lt;br&gt;
✅ Docker environment configured&lt;br&gt;
✅ PostgreSQL database set up&lt;br&gt;
✅ SQLAlchemy models defined&lt;br&gt;
✅ Alembic migrations initialized&lt;br&gt;
✅ Project structure created&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learning Outcomes:&lt;/strong&gt;&lt;br&gt;
✅ Deep understanding of FastAPI vs other frameworks&lt;br&gt;
✅ SQLAlchemy 2.0 async patterns&lt;br&gt;
✅ Clean architecture principles in Python&lt;br&gt;
✅ Production-ready project organization&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time invested:&lt;/strong&gt; A few days of focused learning and planning&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Was it worth it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Absolutely. This foundation made Phase 2 (development) much smoother.&lt;/p&gt;

&lt;p&gt;The hours spent planning saved days of refactoring later.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 What's Next: Phase 2
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Coming in Part 2:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;⏳ JWT Authentication Implementation&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Access + refresh token flow&lt;/li&gt;
&lt;li&gt;Password hashing with bcrypt&lt;/li&gt;
&lt;li&gt;Token validation middleware&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⏳ Repository Pattern in Practice&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generic CRUD operations&lt;/li&gt;
&lt;li&gt;User-specific queries&lt;/li&gt;
&lt;li&gt;Transaction management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⏳ Service Layer Development&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Business logic separation&lt;/li&gt;
&lt;li&gt;Error handling strategies&lt;/li&gt;
&lt;li&gt;Dependency injection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⏳ Real Development Challenges&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bugs I encountered&lt;/li&gt;
&lt;li&gt;Performance optimizations&lt;/li&gt;
&lt;li&gt;Testing strategies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Follow me&lt;/strong&gt; to get notified when Part 2 drops!&lt;/p&gt;

&lt;h2&gt;
  
  
  📝 Complete Code &amp;amp; Resources
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔗 &lt;a href="https://github.com/ravigupta97/task_management_api" rel="noopener noreferrer"&gt;https://github.com/ravigupta97/task_management_api&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Fully documented code&lt;/li&gt;
&lt;li&gt;Clean commit history&lt;/li&gt;
&lt;li&gt;Setup instructions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Additional Resources:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Architecture diagrams (in this article)&lt;/li&gt;
&lt;li&gt;Database schema (ER diagram above)&lt;/li&gt;
&lt;li&gt;Technology decision matrix&lt;/li&gt;
&lt;li&gt;Learning resources list&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Have questions?&lt;/strong&gt; Drop them in the comments below!&lt;/p&gt;




&lt;h2&gt;
  
  
  🙏 Acknowledgments
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Resources that helped:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FastAPI documentation (exceptional quality)&lt;/li&gt;
&lt;li&gt;SQLAlchemy docs (comprehensive)&lt;/li&gt;
&lt;li&gt;AI tools (ChatGPT, Claude) for concept clarification&lt;/li&gt;
&lt;li&gt;FastAPI community on Discord&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Special thanks to:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anyone reading this and learning alongside me&lt;/li&gt;
&lt;li&gt;The open-source community making these tools free&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  💬 Discussion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Questions for you:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Are you considering FastAPI for your next project?&lt;/li&gt;
&lt;li&gt;What's your biggest challenge with async Python?&lt;/li&gt;
&lt;li&gt;Any architectural patterns you'd recommend?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's learn together - comment below! 👇&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is Part 1 of 3 in my FastAPI journey. Follow for:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Part 2: Development &amp;amp; Implementation&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Part 3: Testing, Deployment &amp;amp; Results&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Find me on:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;LinkedIn: [&lt;a href="https://www.linkedin.com/in/ravigupta97/" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/ravigupta97/&lt;/a&gt;]&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;GitHub: [&lt;a href="https://github.com/ravigupta97" rel="noopener noreferrer"&gt;https://github.com/ravigupta97&lt;/a&gt;]&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;#FastAPI #Python #Backend #WebDevelopment #Learning #API #SoftwareEngineering&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>fastapi</category>
      <category>python</category>
      <category>backend</category>
      <category>api</category>
    </item>
  </channel>
</rss>
