<?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: Azhar Alvi </title>
    <description>The latest articles on DEV Community by Azhar Alvi  (@silentcarry).</description>
    <link>https://dev.to/silentcarry</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4032488%2F41bd09e7-acef-435e-8ec5-5d489ca389d1.gif</url>
      <title>DEV Community: Azhar Alvi </title>
      <link>https://dev.to/silentcarry</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/silentcarry"/>
    <language>en</language>
    <item>
      <title>Phase 4 [Part 2] : The Deviation</title>
      <dc:creator>Azhar Alvi </dc:creator>
      <pubDate>Sun, 02 Aug 2026 15:05:28 +0000</pubDate>
      <link>https://dev.to/silentcarry/phase-4-part-2-the-deviation-144p</link>
      <guid>https://dev.to/silentcarry/phase-4-part-2-the-deviation-144p</guid>
      <description>&lt;p&gt;&lt;strong&gt;Kill the 30-minute dead-end where an expired token traps you on a broken screen, break one bloated component into smaller ones that each own their job, and give the login and app shell their first real coat of paint — the two debts I flagged at the end of Part 1, plus the refactor that made fixing them possible.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At the close of Part 1, my app finally had a &lt;em&gt;face&lt;/em&gt;: I could log in through a real form and do full CRUD without ever opening &lt;code&gt;/docs&lt;/code&gt;. Genuinely a milestone. But I ended that write-up with two honest confessions taped to the fridge. One: the login &lt;strong&gt;silently dies after 30 minutes&lt;/strong&gt; — my JWTs expire, and when they do, the app just shows a confusing error and strands you, still pretending you're logged in. Two: it looked like &lt;strong&gt;a 1998 government form&lt;/strong&gt;. A raw list, no styling, no polish.&lt;/p&gt;

&lt;p&gt;I said I'd deal with both in "Phase 5." I changed my mind again.&lt;/p&gt;

&lt;h2&gt;
  
  
  A second confession: I pulled two Phase-5 jobs forward [and picked up a third I didn't plan]
&lt;/h2&gt;

&lt;p&gt;Building a dashboard on top of a frontend that (a) traps users on a dead session, (b) crams everything into two god-components, and (c) is visually unusable felt like laying a nice floor over a cracked foundation. So before any new &lt;em&gt;features&lt;/em&gt;, I spent a session paying rent on the foundation.&lt;/p&gt;

&lt;p&gt;And here's the honest part: &lt;strong&gt;only one of these three jobs was on my list.&lt;/strong&gt; The expired-token fix, yes — I'd flagged it. But chasing that bug is what dragged the &lt;strong&gt;props refactor&lt;/strong&gt; in behind it, because the fix literally couldn't be written cleanly until I'd split my components up. One debt exposed another. That happens a lot, I'm learning.&lt;/p&gt;

&lt;p&gt;Let's call it &lt;strong&gt;PHASE 4, PART 2 — Paying the Rent:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reproduce the expired-token dead-end &lt;em&gt;on purpose&lt;/em&gt; [without waiting 30 minutes]&lt;/li&gt;
&lt;li&gt;Detect a &lt;code&gt;401&lt;/code&gt; on any request and force a clean re-login&lt;/li&gt;
&lt;li&gt;Learn &lt;strong&gt;props&lt;/strong&gt; for real — by passing a &lt;em&gt;function&lt;/em&gt; down so a child can trigger the parent's logout&lt;/li&gt;
&lt;li&gt;Split &lt;code&gt;App&lt;/code&gt; and &lt;code&gt;ExpenseList&lt;/code&gt; into &lt;code&gt;&amp;lt;LoginForm&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;AddExpenseForm&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Install &lt;strong&gt;Tailwind v4&lt;/strong&gt; [which is nothing like the tutorials you'll find]&lt;/li&gt;
&lt;li&gt;Style the login into a centered card and the app into a dashboard shell&lt;/li&gt;
&lt;li&gt;Make an &lt;em&gt;honest&lt;/em&gt; call about a lint warning instead of cargo-culting a "fix"&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Debt #1: the 30-minute dead-end [expired-token hardening]
&lt;/h2&gt;

&lt;p&gt;Here's the flaw, stated plainly. My login issues a JWT with a &lt;strong&gt;30-minute self-destruct&lt;/strong&gt; baked in [the &lt;code&gt;exp&lt;/code&gt; claim — Phase 3 me set that on purpose]. After 30 minutes the token is dead, and my FastAPI backend will reject &lt;em&gt;any&lt;/em&gt; authenticated request — list, add, edit, delete — with a &lt;code&gt;401 Unauthorized&lt;/code&gt;. That's not a bug; that's the security feature working exactly as designed.&lt;/p&gt;

&lt;p&gt;The bug was &lt;strong&gt;my reaction to it.&lt;/strong&gt; My &lt;code&gt;fetchExpenses&lt;/code&gt; caught &lt;em&gt;any&lt;/em&gt; bad response as a generic "Could not load expenses," and my mutation handlers just &lt;code&gt;console.error&lt;/code&gt;-ed the status. Meanwhile &lt;code&gt;App&lt;/code&gt; still had a truthy &lt;code&gt;token&lt;/code&gt; in state, so it kept rendering the logged-in view. The token was dead, but the app cheerfully pretended I was alive — an error message, a Logout button, and no automatic way out. A dead-end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First move, and this is the adversarial habit surviving without the GAN framing: I reproduced the failure on purpose instead of waiting half an hour.&lt;/strong&gt; DevTools → Application → Local Storage → double-click the &lt;code&gt;token&lt;/code&gt; value → jam a few junk characters onto the end → hit an endpoint. An invalid token gets the exact same &lt;code&gt;401&lt;/code&gt; an expired one would. Instant, repeatable bug. Watching it fail on demand is what made the fix obvious.&lt;/p&gt;

&lt;p&gt;Then I hit the wall that turned this into a bigger job than expected:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The logout logic lived in the wrong place to fix this.&lt;/strong&gt; My &lt;code&gt;handleLogout&lt;/code&gt; [&lt;code&gt;localStorage.removeItem('token')&lt;/code&gt; + &lt;code&gt;setToken(null)&lt;/code&gt;] sits in &lt;code&gt;App&lt;/code&gt;, the parent. But the &lt;code&gt;401&lt;/code&gt; &lt;em&gt;happens&lt;/em&gt; inside &lt;code&gt;ExpenseList&lt;/code&gt;, the child. A child component &lt;strong&gt;cannot&lt;/strong&gt; reach up and grab its parent's functions on its own. So how does the child tell the parent "the session is dead, log us out"? That question is what forced me to finally learn props — see the next section. The short version of the answer: the parent hands the child a &lt;em&gt;function&lt;/em&gt; to call.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With that wired (details below), the fix inside each authed request was tiny and identical — check for &lt;code&gt;401&lt;/code&gt; &lt;strong&gt;first&lt;/strong&gt;, and if you see it, bail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;onAuthError&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// a function App passed down; it runs handleLogout&lt;/span&gt;
  &lt;span class="k"&gt;return&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;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #9 — a &lt;code&gt;401&lt;/code&gt; is a &lt;em&gt;subset&lt;/em&gt; of "not ok," so you have to catch it &lt;em&gt;before&lt;/em&gt; your generic error branch.&lt;/strong&gt; My old code did &lt;code&gt;if (!response.ok) throw ...&lt;/code&gt;. If I'd let that run first, every expired token would just show "Could not load expenses" and &lt;em&gt;never&lt;/em&gt; log me out. By checking &lt;code&gt;response.status === 401&lt;/code&gt; first and &lt;code&gt;return&lt;/code&gt;-ing, I short-circuit: &lt;code&gt;onAuthError()&lt;/code&gt; flips &lt;code&gt;token&lt;/code&gt; to &lt;code&gt;null&lt;/code&gt; up in &lt;code&gt;App&lt;/code&gt;, &lt;code&gt;App&lt;/code&gt; stops rendering &lt;code&gt;ExpenseList&lt;/code&gt;, and the login screen takes over. Order matters. Specific case before general case.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I dropped that same three-line guard into all four authed calls — &lt;code&gt;fetchExpenses&lt;/code&gt;, &lt;code&gt;handleAdd&lt;/code&gt;, &lt;code&gt;handleDelete&lt;/code&gt;, &lt;code&gt;handleUpdate&lt;/code&gt; — and while I was crawling through &lt;code&gt;handleDelete&lt;/code&gt;, I found a landmine:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #10 — a one-letter case typo that "works" only by luck.&lt;/strong&gt; My delete URL read &lt;code&gt;http://Localhost:8000&lt;/code&gt; — capital L. It had been working the whole time because &lt;strong&gt;Windows treats hostnames case-insensitively&lt;/strong&gt;, so &lt;code&gt;Localhost&lt;/code&gt; resolves fine on my machine. The moment I deploy the backend to a Linux box [Phase 9], that could bite. Fixed it to lowercase &lt;code&gt;localhost&lt;/code&gt; now, while I was in the neighborhood. Lesson: "it works on my machine" and "it's correct" are not the same sentence.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;The payoff:&lt;/strong&gt; I re-ran my sabotage — logged in, corrupted the token, triggered a request — and instead of the stuck error screen, the app &lt;strong&gt;bounced me straight back to the login form.&lt;/strong&gt; Tested it from a reload &lt;em&gt;and&lt;/em&gt; from the add button, to prove the guard fires from more than one place. The session can no longer lie to me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cleanup I didn't plan on: props [the refactor]
&lt;/h2&gt;

&lt;p&gt;Remember the wall above — the child couldn't reach the parent's logout. The clean answer to that is the single most important React concept I'd been avoiding: &lt;strong&gt;props.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Props are how a parent hands things &lt;em&gt;down&lt;/em&gt; to a child&lt;/strong&gt; — data, config, or [the part that unlocks everything] &lt;strong&gt;functions.&lt;/strong&gt; And the mirror image is the pattern that fixed my bug: &lt;strong&gt;a child reports an event &lt;em&gt;up&lt;/em&gt; by calling a function its parent passed down.&lt;/strong&gt; That's it. Data flows down; events flow up as function calls. My &lt;code&gt;onAuthError&lt;/code&gt; was my very first one — &lt;code&gt;App&lt;/code&gt; handed &lt;code&gt;ExpenseList&lt;/code&gt; its &lt;code&gt;handleLogout&lt;/code&gt;, renamed &lt;code&gt;onAuthError&lt;/code&gt; for clarity, and the child calls it when it smells a dead session.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In App: hand the function down&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExpenseList&lt;/span&gt; &lt;span class="na"&gt;onAuthError&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleLogout&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In ExpenseList: receive it&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ExpenseList&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;onAuthError&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;That &lt;code&gt;{ onAuthError }&lt;/code&gt; in the function signature stopped me for a second, so I'll pin down what it is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;{ }&lt;/code&gt; in a component's arguments is &lt;em&gt;object destructuring&lt;/em&gt; — the cousin of the &lt;code&gt;[ ]&lt;/code&gt; I already knew from &lt;code&gt;useState&lt;/code&gt;.&lt;/strong&gt; React always calls my component with &lt;strong&gt;one&lt;/strong&gt; argument: a single object holding all its props. &lt;code&gt;{ onAuthError }&lt;/code&gt; reaches into that object and pulls out the &lt;code&gt;onAuthError&lt;/code&gt; key by &lt;em&gt;name&lt;/em&gt;. Contrast: &lt;code&gt;useState&lt;/code&gt; gives me &lt;code&gt;const [value, setter]&lt;/code&gt; — array destructuring, which unpacks &lt;strong&gt;by position&lt;/strong&gt; [that's why order matters there]. Objects unpack &lt;strong&gt;by name&lt;/strong&gt;, so the name inside the braces must exactly match the prop I passed. A typo like &lt;code&gt;{ onAuthErr }&lt;/code&gt; just silently hands me &lt;code&gt;undefined&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once I understood props, the mess I'd been living in became obvious: my &lt;strong&gt;login form&lt;/strong&gt; was welded inside &lt;code&gt;App&lt;/code&gt;, and my &lt;strong&gt;add-expense form&lt;/strong&gt; was welded inside &lt;code&gt;ExpenseList&lt;/code&gt;. Two components doing three jobs each. So I split them out, and I picked a deliberate design rule for both:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Each form owns its &lt;em&gt;own&lt;/em&gt; state and does its &lt;em&gt;own&lt;/em&gt; API call, then reports the result up through a callback prop. The parent stays thin and owns only what's genuinely shared.&lt;/strong&gt; &lt;code&gt;App&lt;/code&gt; doesn't care about my email or password keystrokes — it only cares "did we get a token?" So &lt;code&gt;LoginForm&lt;/code&gt; keeps its own &lt;code&gt;email&lt;/code&gt;/&lt;code&gt;password&lt;/code&gt; state, does the login &lt;code&gt;fetch&lt;/code&gt; itself, and calls &lt;code&gt;onLoggedIn(token)&lt;/code&gt; on success. This is &lt;em&gt;colocation&lt;/em&gt;: keep state as close as possible to where it's used, and lift up only the thing that's actually shared [the token].&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LoginForm&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;onLoggedIn&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPassword&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;formBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;username&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;formBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:8000/auth/login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&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="nf"&gt;onLoggedIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// report success UP to App&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Login Failed:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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="c1"&gt;// ...the form JSX&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;App&lt;/code&gt; shed all the login state and shrank to a coordinator — it now owns only the &lt;code&gt;token&lt;/code&gt; plus two tiny handlers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleLoginSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// the side-effect stays with the owner of the token&lt;/span&gt;
  &lt;span class="nf"&gt;setToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleLogout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&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;code&gt;&amp;lt;AddExpenseForm&amp;gt;&lt;/code&gt; came out the same way, except it needs &lt;strong&gt;two&lt;/strong&gt; callbacks: &lt;code&gt;onAdded&lt;/code&gt; [it succeeded — parent, please re-fetch the list] and &lt;code&gt;onAuthError&lt;/code&gt; [the 401 guard]. And that surfaced a nice, normal wrinkle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prop-forwarding is fine and expected.&lt;/strong&gt; &lt;code&gt;ExpenseList&lt;/code&gt; receives &lt;code&gt;onAuthError&lt;/code&gt; from &lt;code&gt;App&lt;/code&gt;, and then passes it &lt;em&gt;one level deeper&lt;/em&gt; to &lt;code&gt;AddExpenseForm&lt;/code&gt;. A prop can be handed down through multiple layers. &lt;code&gt;&amp;lt;AddExpenseForm onAdded={fetchExpenses} onAuthError={onAuthError} /&amp;gt;&lt;/code&gt; — notice &lt;code&gt;onAdded={fetchExpenses}&lt;/code&gt; is me passing &lt;code&gt;ExpenseList&lt;/code&gt;'s own re-fetch function down so the child can trigger a refresh after a successful add. Same function, reused through a prop.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two more real things fell out of the refactor:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #11 — moving JSX into a child leaves &lt;em&gt;leftovers&lt;/em&gt; in the parent.&lt;/strong&gt; The instant I extracted &lt;code&gt;&amp;lt;LoginForm&amp;gt;&lt;/code&gt;, my login screen showed the &lt;strong&gt;title twice&lt;/strong&gt;. Cause: I'd put the &lt;code&gt;&amp;lt;h1&amp;gt;Smart Expense Manager&amp;lt;/h1&amp;gt;&lt;/code&gt; inside &lt;code&gt;LoginForm&lt;/code&gt;, but &lt;code&gt;App&lt;/code&gt; was &lt;em&gt;still&lt;/em&gt; rendering its own old heading right above it. This is the exact same lesson as the stray-render leak from Part 1 — &lt;strong&gt;when you restructure, hunt the leftovers.&lt;/strong&gt; The fix doubled as a design decision: the app title is &lt;em&gt;shell&lt;/em&gt; UI, so it belongs in &lt;code&gt;App&lt;/code&gt;, exactly once. &lt;code&gt;LoginForm&lt;/code&gt; renders only the form.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha #12 — the tempting lint "fix" is an infinite-loop trap.&lt;/strong&gt; ESLint started nagging: &lt;em&gt;"React Hook useEffect has a missing dependency: 'fetchExpenses'."&lt;/em&gt; The obvious move is to obey it and write &lt;code&gt;}, [fetchExpenses])&lt;/code&gt;. &lt;strong&gt;Don't.&lt;/strong&gt; &lt;code&gt;fetchExpenses&lt;/code&gt; is a plain function, so React creates a &lt;em&gt;brand-new copy of it every render&lt;/em&gt;. Put it in the dependency array and the effect thinks its dependency changed on every render → it re-fetches → &lt;code&gt;setState&lt;/code&gt; re-renders → new function copy → fetches again → &lt;strong&gt;infinite loop.&lt;/strong&gt; My empty &lt;code&gt;[]&lt;/code&gt; is actually the behavior I want [run once on mount]. So I made a real decision instead of cargo-culting: I &lt;strong&gt;left the warning alone.&lt;/strong&gt; It's a &lt;em&gt;warning&lt;/em&gt;, not an error, and the code is correct. I didn't even add an &lt;code&gt;eslint-disable&lt;/code&gt; comment — that would just be decorating a non-problem. &lt;code&gt;[The genuinely-correct fix is wrapping the function in&lt;/code&gt;&lt;code&gt;useCallback&lt;/code&gt;&lt;code&gt;so its identity is stable — but that cascades into memoizing the callbacks I pass down too, which is more machinery than a beginner project needs right now. useCallback = "know this exists."]&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The whole app behaves &lt;em&gt;identically&lt;/em&gt; after all this — same login, same CRUD, same 401 bounce. But &lt;code&gt;App&lt;/code&gt; and &lt;code&gt;ExpenseList&lt;/code&gt; are now thin coordinators, and each form is a small thing that owns its one job. That's the entire point of the refactor: no new features, just a codebase I can actually keep building on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debt #2: it looked like a 1998 government form [Tailwind]
&lt;/h2&gt;

&lt;p&gt;Time to make it not-ugly. I chose &lt;strong&gt;Tailwind CSS&lt;/strong&gt; [utility-first styling] and a &lt;strong&gt;modern SaaS-dashboard&lt;/strong&gt; look — soft-gray canvas, white cards with subtle shadows, a top header bar. Partly because it's everywhere in industry, partly because that card-and-shell layout is a direct warm-up for the Phase 5 dashboard.&lt;/p&gt;

&lt;p&gt;First lesson before I typed a single class:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #13 — Tailwind v4 threw out the setup every tutorial still teaches.&lt;/strong&gt; Search "install Tailwind" and you'll get v3 instructions: a &lt;code&gt;tailwind.config.js&lt;/code&gt;, a &lt;code&gt;content: [...]&lt;/code&gt; array, &lt;code&gt;@tailwind base/components/utilities&lt;/code&gt; directives, PostCSS wiring. &lt;strong&gt;In v4, all of that is gone.&lt;/strong&gt; It's now a Vite plugin plus a &lt;em&gt;single&lt;/em&gt; line of CSS. I actually verified the current steps against today's docs instead of trusting my memory or an old blog — which is a habit I want to keep for fast-moving tools, because copying stale setup is a guaranteed hour lost.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The entire v4 setup, for a Vite + React app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;tailwindcss @tailwindcss/vite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.js — ADD the plugin, keep react()&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;react&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vitejs/plugin-react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;tailwindcss&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@tailwindcss/vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;react&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;tailwindcss&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* src/index.css — this ONE line replaces all the old @tailwind directives */&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A restart of the dev server later [config changes don't hot-reload — one to remember], I dropped &lt;code&gt;className="text-3xl font-bold text-blue-600"&lt;/code&gt; on my title, watched it go big and blue, and knew the pipe was live. Note it's &lt;strong&gt;&lt;code&gt;className&lt;/code&gt;, not &lt;code&gt;class&lt;/code&gt;&lt;/strong&gt; — the same JSX rule that bit me back in Part 1, now paying dividends because &lt;em&gt;every&lt;/em&gt; Tailwind style rides on &lt;code&gt;className&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mental model that made Tailwind click:&lt;/strong&gt; you don't write CSS rules in a separate file. You &lt;strong&gt;compose a look out of tiny, single-purpose utility classes right on the element&lt;/strong&gt; — &lt;code&gt;p-8&lt;/code&gt; is padding, &lt;code&gt;rounded-xl&lt;/code&gt; is corners, &lt;code&gt;shadow-md&lt;/code&gt; is a shadow, &lt;code&gt;bg-white&lt;/code&gt; is the background. Honest reaction: the &lt;code&gt;className&lt;/code&gt; strings get &lt;em&gt;long&lt;/em&gt; and repetitive, and my two login inputs share an identical one. That felt wrong at first, but it's genuinely idiomatic Tailwind. &lt;code&gt;[Later I can collapse a repeated set with&lt;/code&gt;&lt;code&gt;@apply&lt;/code&gt;&lt;code&gt;in CSS, or extract a tiny&lt;/code&gt;&lt;code&gt;&amp;lt;Input&amp;gt;&lt;/code&gt;&lt;code&gt;component. Know this exists; not now.]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Here's the login screen — a centered white card floating on a gray page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// App's logged-out branch&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"min-h-screen bg-slate-100 flex items-center justify-center"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"w-full max-w-sm bg-white rounded-xl shadow-md p-8 flex flex-col gap-6"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-2xl font-bold text-slate-800 text-center"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Smart Expense Manager&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LoginForm&lt;/span&gt; &lt;span class="na"&gt;onLoggedIn&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleLoginSuccess&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handful of utilities I'll actually reuse forever:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;flex items-center justify-center&lt;/code&gt;&lt;/strong&gt; — &lt;em&gt;the&lt;/em&gt; centering combo. &lt;code&gt;items-center&lt;/code&gt; centers vertically, &lt;code&gt;justify-center&lt;/code&gt; horizontally. Wrap it in &lt;code&gt;min-h-screen&lt;/code&gt; so there's a full viewport to center &lt;em&gt;within&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;flex flex-col gap-*&lt;/code&gt;&lt;/strong&gt; — stack children in a column with even spacing between them, no margins to hand-tune.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;focus:ring-2&lt;/code&gt; / &lt;code&gt;hover:bg-blue-700&lt;/code&gt;&lt;/strong&gt; — the &lt;code&gt;focus:&lt;/code&gt; and &lt;code&gt;hover:&lt;/code&gt; prefixes apply a style only in that &lt;em&gt;state&lt;/em&gt;. It's Tailwind's answer to CSS pseudo-classes, and it's genuinely elegant — my inputs get a blue focus ring, my button darkens on hover, all inline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the logged-in shell — a real SaaS layout with a header bar and a centered content column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"min-h-screen bg-slate-100"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;header&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"bg-white shadow-sm"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"max-w-3xl mx-auto px-6 py-4 flex items-center justify-between"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-xl font-bold text-slate-800"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Smart Expense Manager&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleLogout&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-sm text-slate-600 border border-slate-300 rounded-md px-3 py-1.5 hover:bg-slate-50 transition-colors"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Logout
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;header&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"max-w-3xl mx-auto px-6 py-8"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExpenseList&lt;/span&gt; &lt;span class="na"&gt;onAuthError&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleLogout&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth banking here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;lt;header&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt; instead of &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;s.&lt;/strong&gt; These are &lt;em&gt;semantic&lt;/em&gt; HTML — they render identically but tell browsers and screen readers what each region &lt;em&gt;is&lt;/em&gt;. A free accessibility and clarity win; a small habit that separates "works" from "professional."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;max-w-3xl mx-auto&lt;/code&gt;&lt;/strong&gt; is the centered-column pattern. &lt;code&gt;max-w-3xl&lt;/code&gt; caps the width so content isn't a painfully wide ribbon on a big monitor; &lt;code&gt;mx-auto&lt;/code&gt; sets left/right margins to auto, which centers the block. I used the &lt;em&gt;same&lt;/em&gt; &lt;code&gt;max-w-3xl mx-auto px-6&lt;/code&gt; on both the header's inner div and &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt;, which is what keeps the logo and the content aligned down the page. &lt;code&gt;justify-between&lt;/code&gt; in the header shoves the title hard-left and Logout hard-right.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And one small stumble that taught the right instinct:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #14 — reach for &lt;code&gt;gap&lt;/code&gt; on the parent, not a margin on each child.&lt;/strong&gt; My title and the email box were jammed together with no space. My first instinct was to slap a &lt;code&gt;margin-bottom&lt;/code&gt; on the heading. The cleaner fix — and the more Tailwind-idiomatic one — was to make the &lt;em&gt;card&lt;/em&gt; a &lt;code&gt;flex flex-col gap-6&lt;/code&gt; container, so the gap spaces &lt;strong&gt;every&lt;/strong&gt; child uniformly [title → form] with one declaration on the parent. Set the rhythm once, up top, instead of hand-tuning margins on each element and fighting margin quirks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Honest status: this is the pass I've &lt;em&gt;started&lt;/em&gt;, not finished.&lt;/strong&gt; The login card and the whole app shell are dressed and look legitimately decent. What's &lt;em&gt;not&lt;/em&gt; done yet: the expense-list itself is still raw inside that nice shell — the list needs to become a card, the add-expense form needs the same input/button styling as login, and each row needs tidy edit/delete buttons and clean loading/empty/error states. That's the frontier, and it flows directly into Phase 5.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of every React conversation [the model that tied it together]
&lt;/h2&gt;

&lt;p&gt;If Part 1's big mental model was "the Save button doesn't save — it &lt;em&gt;asks the backend to&lt;/em&gt;," Part 2's is smaller but just as clarifying:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data flows &lt;em&gt;down&lt;/em&gt;, events flow &lt;em&gt;up&lt;/em&gt;, and only a state change repaints the screen.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A parent passes &lt;strong&gt;data or functions down&lt;/strong&gt; to a child as &lt;strong&gt;props&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;When something happens in the child [login succeeded, a &lt;code&gt;401&lt;/code&gt; came back, an expense was added], the child &lt;strong&gt;calls a function the parent gave it&lt;/strong&gt; — that's the event flowing back up.&lt;/li&gt;
&lt;li&gt;That function, running in the parent, &lt;strong&gt;changes state&lt;/strong&gt; [&lt;code&gt;setToken(...)&lt;/code&gt;]. And state changing is the &lt;em&gt;only&lt;/em&gt; thing that makes React re-render and swap the screen.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every single thing I built this session is that loop. &lt;code&gt;LoginForm&lt;/code&gt; → &lt;code&gt;onLoggedIn(token)&lt;/code&gt; → &lt;code&gt;setToken&lt;/code&gt; → the UI flips to logged-in. &lt;code&gt;ExpenseList&lt;/code&gt; → &lt;code&gt;onAuthError()&lt;/code&gt; → &lt;code&gt;setToken(null)&lt;/code&gt; → the UI flips back to login. &lt;code&gt;AddExpenseForm&lt;/code&gt; → &lt;code&gt;onAdded()&lt;/code&gt; → re-fetch → the list re-renders. Once I saw the one shape, the whole component tree stopped being mysterious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stuff I want to remember [the honest takeaways]
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A JWT expiring is the security feature working, not a bug&lt;/strong&gt; — but &lt;em&gt;not handling&lt;/em&gt; the resulting &lt;code&gt;401&lt;/code&gt; is the bug. Detect it and force a clean re-login.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reproduce a time-based bug on purpose&lt;/strong&gt; — corrupt the token in DevTools instead of waiting 30 minutes. An invalid token gives the same &lt;code&gt;401&lt;/code&gt; as an expired one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the specific status [&lt;code&gt;401&lt;/code&gt;] &lt;em&gt;before&lt;/em&gt; the generic &lt;code&gt;!response.ok&lt;/code&gt; branch,&lt;/strong&gt; or the general case masks the special one. Specific before general.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Props are how a parent passes data — and functions — down; a child reports events up by calling a function the parent gave it.&lt;/strong&gt; Data down, events up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;{ prop }&lt;/code&gt; in a component signature is object destructuring [by name]; &lt;code&gt;useState&lt;/code&gt;'s &lt;code&gt;[value, setter]&lt;/code&gt; is array destructuring [by position].&lt;/strong&gt; Same idea, different brackets — and the prop name must match exactly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Colocate state:&lt;/strong&gt; each form owns its own state and its own API call; the parent stays thin and owns only what's shared [the token]. Lift up only the shared thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prop-forwarding is normal&lt;/strong&gt; — a prop can be handed down through several layers [&lt;code&gt;App&lt;/code&gt; → &lt;code&gt;ExpenseList&lt;/code&gt; → &lt;code&gt;AddExpenseForm&lt;/code&gt;].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When you restructure, hunt the leftovers&lt;/strong&gt; — moving JSX into a child left a duplicated &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; behind in the parent. [Same lesson as Part 1's stray render. It keeps coming back.]&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't cargo-cult a lint "fix."&lt;/strong&gt; Obeying the &lt;code&gt;exhaustive-deps&lt;/code&gt; warning naively [&lt;code&gt;[fetchExpenses]&lt;/code&gt;] creates an infinite fetch loop, because the function is recreated every render. The empty &lt;code&gt;[]&lt;/code&gt; was correct. A warning is not an error. &lt;code&gt;[Real fix = useCallback; deferred.]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind v4 is nothing like the v3 tutorials&lt;/strong&gt; — no config file, no &lt;code&gt;content&lt;/code&gt; array, no &lt;code&gt;@tailwind&lt;/code&gt; directives. Just the &lt;code&gt;@tailwindcss/vite&lt;/code&gt; plugin + &lt;code&gt;@import "tailwindcss";&lt;/code&gt;. Verify current docs for fast-moving tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind is utility-first:&lt;/strong&gt; compose a look from tiny classes on &lt;code&gt;className&lt;/code&gt; [still not &lt;code&gt;class&lt;/code&gt;]. Long, repeated class strings are normal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;flex items-center justify-center&lt;/code&gt;&lt;/strong&gt; centers; &lt;strong&gt;&lt;code&gt;flex flex-col gap-*&lt;/code&gt;&lt;/strong&gt; spaces stacked children [set spacing on the parent, not margins on each child]; &lt;strong&gt;&lt;code&gt;focus:&lt;/code&gt;/&lt;code&gt;hover:&lt;/code&gt;&lt;/strong&gt; prefixes are Tailwind's pseudo-classes; &lt;strong&gt;&lt;code&gt;max-w-* mx-auto&lt;/code&gt;&lt;/strong&gt; is the centered column.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use semantic &lt;code&gt;&amp;lt;header&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt;&lt;/strong&gt; instead of &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; soup — free clarity and accessibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The one React loop:&lt;/strong&gt; data down (props) → event up (callback) → &lt;code&gt;setState&lt;/code&gt; → re-render. Everything I built is that shape.&lt;/li&gt;
&lt;li&gt;Commit at every green checkpoint, subject + why. I committed after the 401 fix, after each extraction, and after the Tailwind install — small and often.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next up: Phase 5, for real this time. The face now has a &lt;strong&gt;pulse&lt;/strong&gt; [the session no longer dead-ends], its internals are &lt;strong&gt;organized&lt;/strong&gt; [small components that each own their job], and it's wearing its &lt;strong&gt;first real clothes&lt;/strong&gt; [a login card and a dashboard shell]. What's left is to finish dressing the expense list, and then give the app some actual &lt;em&gt;expression&lt;/em&gt;: a dashboard with spending insights — totals, spend-by-category, month-over-month — turning "it works and it's tidy" into "I'd genuinely use this." The foundation's finally solid enough to build something interesting on top of. See you there.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>frontend</category>
      <category>softwaredevelopment</category>
      <category>webdev</category>
    </item>
    <item>
      <title>PHASE 4 — The Face</title>
      <dc:creator>Azhar Alvi </dc:creator>
      <pubDate>Sat, 01 Aug 2026 09:29:14 +0000</pubDate>
      <link>https://dev.to/silentcarry/phase-4-the-face-5b25</link>
      <guid>https://dev.to/silentcarry/phase-4-the-face-5b25</guid>
      <description>&lt;p&gt;&lt;strong&gt;Scaffold a React app, log in through a real form, and do full CRUD without ever opening &lt;code&gt;/docs&lt;/code&gt; again — then close the three gaps nobody builds: loading, empty, and error.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So by the end of Phase 3 my app had a memory [a database], a mouth [HTTP endpoints], and a lock on the door [auth]. It knew who you were and refused to show you anyone else's expenses. Genuinely satisfying. There was just one small, embarrassing detail: &lt;strong&gt;the only human who could actually use it was me, poking at &lt;code&gt;/docs&lt;/code&gt;.&lt;/strong&gt; My "app" was a Swagger page and a lot of faith. No screen, no login box, nothing a normal person could look at. It had everything except a &lt;em&gt;face&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Phase 4 gives it one.&lt;/p&gt;

&lt;h2&gt;
  
  
  A confession before we start: I changed the plan
&lt;/h2&gt;

&lt;p&gt;If you read the end of my Phase 3 write-up, I promised Phase 4 would be filtering, sorting, and pagination — making the backend list &lt;em&gt;usable&lt;/em&gt; at scale. I changed my mind. I went with frontend.&lt;/p&gt;

&lt;p&gt;My reasoning, honestly: I've been living in Python for four phases and I've &lt;em&gt;never&lt;/em&gt; touched React. Building the UI now forces me to re-meet my own auth, tokens, CORS, and endpoints from the &lt;strong&gt;client's&lt;/strong&gt; side of the wire — which is the best way to find out whether I actually understood them. The known cost, which I'm accepting on purpose: &lt;code&gt;GET /expenses&lt;/code&gt; is still unbounded, so when pagination lands later I'll have to come back and revise this list UI. Flagged, accepted, moving on.&lt;/p&gt;

&lt;p&gt;Let's call it &lt;strong&gt;PHASE 4 — The Face:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scaffold a real React app with Vite&lt;/li&gt;
&lt;li&gt;Write my first component and actually understand JSX&lt;/li&gt;
&lt;li&gt;Build a login form that holds its own text [controlled inputs + &lt;code&gt;useState&lt;/code&gt;]&lt;/li&gt;
&lt;li&gt;Call the backend from the browser — and get mugged by CORS&lt;/li&gt;
&lt;li&gt;Store the token, and reckon with &lt;em&gt;where&lt;/em&gt; to store it&lt;/li&gt;
&lt;li&gt;Fetch and render the expense list [&lt;code&gt;useEffect&lt;/code&gt;]&lt;/li&gt;
&lt;li&gt;Close the three gaps: loading, empty, error&lt;/li&gt;
&lt;li&gt;Wire up add / delete / edit — full CRUD from the UI&lt;/li&gt;
&lt;li&gt;Fix the bug that made login "do nothing until I refreshed"&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  First, the new world [everything here is different from the backend]
&lt;/h2&gt;

&lt;p&gt;The backend runs on Python; you install libraries with &lt;code&gt;pip&lt;/code&gt; and freeze them into &lt;code&gt;requirements.txt&lt;/code&gt;. The frontend has an exact parallel universe, and learning the mapping made the whole thing click:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt; is the JavaScript runtime — the "Python interpreter" of the frontend. [I'm on v22.]&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm&lt;/strong&gt; is Node's package manager — this is &lt;code&gt;pip&lt;/code&gt;. [I'm on v11.]&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;package.json&lt;/code&gt;&lt;/strong&gt; is &lt;code&gt;requirements.txt&lt;/code&gt;'s counterpart — &lt;em&gt;except it updates itself automatically when you install&lt;/em&gt;. No manual &lt;code&gt;freeze&lt;/code&gt; step over here. One less habit to remember on this side of the fence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;node_modules/&lt;/code&gt;&lt;/strong&gt; is &lt;code&gt;venv/&lt;/code&gt; — enormous, machine-specific, and it must &lt;strong&gt;never&lt;/strong&gt; touch git.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vite&lt;/strong&gt; [French for "quick," said &lt;em&gt;veet&lt;/em&gt;] is the tool that scaffolds the app and runs the hot-reloading dev server. It's the frontend's &lt;code&gt;uvicorn --reload&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two forks in the road, two flagged shortcuts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vite offered me &lt;strong&gt;TypeScript or plain JavaScript&lt;/strong&gt;. I took plain JS. TypeScript is fantastic and production-common, but it's a &lt;em&gt;second&lt;/em&gt; new language stacked on top of React, and one new language at a time is plenty. &lt;code&gt;[TypeScript = "know this exists," not now.]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;It also asked which linter — &lt;strong&gt;Oxlint or ESLint&lt;/strong&gt;. [A linter is a spell-checker for code: it reads your code without running it and flags likely bugs and sloppy style.] I picked &lt;strong&gt;ESLint&lt;/strong&gt; because it's the industry default — when I hit an error and google it, ESLint results are everywhere. Oxlint is newer and faster but less documented. &lt;code&gt;[Oxlint = know this exists.]&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm create vite@latest frontend &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--template&lt;/span&gt; react
&lt;span class="nb"&gt;cd &lt;/span&gt;frontend
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last command lit up a starter page at &lt;code&gt;http://localhost:5173&lt;/code&gt; with a clicky "count is 0" button — which, I later learned, is &lt;code&gt;useState&lt;/code&gt; doing its thing. My first commit of the phase was that untouched scaffold, on its own: a clean "React works" checkpoint I could always fall back to before I started breaking things.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Habit that carried over:&lt;/strong&gt; before committing, I checked git wasn't about to swallow &lt;code&gt;node_modules&lt;/code&gt;. Turns out Vite drops its &lt;em&gt;own&lt;/em&gt; &lt;code&gt;.gitignore&lt;/code&gt; inside &lt;code&gt;frontend/&lt;/code&gt; — and yes, git supports &lt;strong&gt;multiple&lt;/strong&gt; &lt;code&gt;.gitignore&lt;/code&gt; files, one per folder. I didn't take that on faith [never, with git]; I ran &lt;code&gt;git check-ignore frontend/node_modules&lt;/code&gt; and it echoed the path back, which is git saying "yep, ignoring that." Verify, don't assume.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 1: My first component [JSX has three rules that bite]
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;React component&lt;/strong&gt; is deceptively simple: it's just a &lt;strong&gt;JavaScript function, with a Capitalized name, that returns markup.&lt;/strong&gt; That markup is &lt;strong&gt;JSX&lt;/strong&gt; — it looks like HTML but it's actually JavaScript wearing a costume. I overwrote Vite's demo &lt;code&gt;App.jsx&lt;/code&gt; with my own [safe: it's boilerplate, and I'd just committed, so &lt;code&gt;git checkout&lt;/code&gt; would bring it back instantly]:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Smart Expense Manager&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Frontend is alive.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three JSX rules that trip &lt;em&gt;everyone&lt;/em&gt;, now filed away:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One single root element.&lt;/strong&gt; You can't return two side-by-side tags; wrap them in a parent. [React will yell at you.]&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;className&lt;/code&gt;, not &lt;code&gt;class&lt;/code&gt;&lt;/strong&gt; — because &lt;code&gt;class&lt;/code&gt; is a reserved word in JavaScript.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;{ }&lt;/code&gt; to drop a JS value into markup&lt;/strong&gt; — &lt;code&gt;{2 + 2}&lt;/code&gt; renders &lt;code&gt;4&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;export default App&lt;/code&gt; is what lets &lt;code&gt;main.jsx&lt;/code&gt; do &lt;code&gt;import App from './App.jsx'&lt;/code&gt; — the JS version of exposing a name so another module can import it. I saved the file, and the browser &lt;em&gt;instantly&lt;/em&gt; swapped to my text without a refresh. That's &lt;strong&gt;hot reload&lt;/strong&gt;, and it's addictive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: A login form that holds its own text [useState]
&lt;/h2&gt;

&lt;p&gt;My instinct was to store what the user types in a normal variable — &lt;code&gt;let email = ''&lt;/code&gt;. &lt;strong&gt;Wrong, and here's the precise why:&lt;/strong&gt; React only updates the screen when it &lt;strong&gt;re-renders&lt;/strong&gt;, and a plain variable changing doesn't trigger one. The box would go stale.&lt;/p&gt;

&lt;p&gt;The fix is &lt;strong&gt;&lt;code&gt;useState&lt;/code&gt;&lt;/strong&gt;, a &lt;em&gt;hook&lt;/em&gt; — a special function that lets a component remember a value across re-renders &lt;em&gt;and&lt;/em&gt; re-render when that value changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPassword&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Logging in with:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Smart Expense Manager&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleSubmit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
          &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Email"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;
          &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Password"&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Log in&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&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;New ideas, in plain language:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;const [email, setEmail] = useState('')&lt;/code&gt;&lt;/strong&gt; returns a &lt;em&gt;pair&lt;/em&gt; — the current value and a setter. That &lt;code&gt;[...]&lt;/code&gt; unpacking is &lt;strong&gt;array destructuring&lt;/strong&gt;. The iron rule: &lt;strong&gt;never reassign &lt;code&gt;email&lt;/code&gt; directly — always call &lt;code&gt;setEmail(...)&lt;/code&gt;.&lt;/strong&gt; The setter is what tells React "this changed, re-render."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Controlled input&lt;/strong&gt; — the two-way loop that &lt;em&gt;is&lt;/em&gt; the React form pattern: &lt;code&gt;value={email}&lt;/code&gt; makes the box always display state, &lt;code&gt;onChange&lt;/code&gt; pushes every keystroke back into state. State is the single source of truth; the input just mirrors it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;import { useState }&lt;/code&gt;&lt;/strong&gt; with braces is a &lt;em&gt;named&lt;/em&gt; import; &lt;code&gt;import App&lt;/code&gt; without braces is a &lt;em&gt;default&lt;/em&gt; import. The braces are the tell.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the one that will get me someday:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #1 — &lt;code&gt;event.preventDefault()&lt;/code&gt; is not optional.&lt;/strong&gt; By default, submitting an HTML form makes the browser &lt;strong&gt;reload the entire page&lt;/strong&gt; [a genuine 1990s behavior]. In a React app that wipes all your state and blanks the screen. &lt;code&gt;preventDefault()&lt;/code&gt; cancels it so &lt;em&gt;I&lt;/em&gt; stay in control. I know for a fact I'll forget this line one day and watch my page flash — but now I'll know exactly why.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I opened DevTools, typed in both boxes [text appeared as I typed — the loop works], clicked Log in, and watched &lt;code&gt;Logging in with: ...&lt;/code&gt; print to the console with no page reload. First real interaction, done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Call the backend — and get mugged by CORS
&lt;/h2&gt;

&lt;p&gt;Now the browser had to actually &lt;em&gt;talk&lt;/em&gt; to FastAPI. Two new concepts collided here.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;&lt;code&gt;async&lt;/code&gt; / &lt;code&gt;await&lt;/code&gt; and &lt;code&gt;fetch&lt;/code&gt;.&lt;/strong&gt; Network calls take time, and JavaScript can't freeze the browser waiting. So &lt;code&gt;fetch&lt;/code&gt; [the browser's built-in HTTP client — the JS version of Python's &lt;code&gt;requests&lt;/code&gt;] returns a &lt;strong&gt;Promise&lt;/strong&gt;, an "I'll have your answer later" placeholder. &lt;strong&gt;&lt;code&gt;await&lt;/code&gt;&lt;/strong&gt; pauses &lt;em&gt;this function&lt;/em&gt; until it resolves, and any function using &lt;code&gt;await&lt;/code&gt; must be marked &lt;strong&gt;&lt;code&gt;async&lt;/code&gt;&lt;/strong&gt; [same word as FastAPI's &lt;code&gt;async def&lt;/code&gt;].&lt;/p&gt;

&lt;p&gt;Second — and this is the single biggest gotcha of the whole phase:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #2 — the login endpoint is the odd one out: it wants FORM data, not JSON.&lt;/strong&gt; My &lt;code&gt;/auth/login&lt;/code&gt; uses &lt;code&gt;OAuth2PasswordRequestForm&lt;/code&gt;, which reads &lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt;, &lt;strong&gt;not&lt;/strong&gt; JSON. Send it JSON and you get a baffling &lt;code&gt;422&lt;/code&gt;. So I built the body with &lt;code&gt;URLSearchParams&lt;/code&gt; and set the matching &lt;code&gt;Content-Type&lt;/code&gt;. Bank this: &lt;strong&gt;the OAuth2 login endpoint is form-encoded; every &lt;em&gt;other&lt;/em&gt; endpoint in my API speaks JSON.&lt;/strong&gt; Mixing these two up is &lt;em&gt;the&lt;/em&gt; classic source of 422s.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;formBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;username&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// OAuth2 calls the field "username" — I put my email there&lt;/span&gt;
  &lt;span class="nx"&gt;formBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8000/auth/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;formBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Status:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Body:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&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;My mentor &lt;em&gt;predicted&lt;/em&gt; this would fail, and it did — with a big red wall:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #3 — CORS, the thing my roadmap warned trips up everyone.&lt;/strong&gt; &lt;code&gt;Access to fetch at 'http://localhost:8000/auth/login' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header...&lt;/code&gt; My frontend runs on port &lt;strong&gt;5173&lt;/strong&gt;, my backend on &lt;strong&gt;8000&lt;/strong&gt; — different port means the browser considers them &lt;strong&gt;different origins&lt;/strong&gt;, and it refused to let my code read the response.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;CORS [Cross-Origin Resource Sharing] is a browser security feature, not a bug in my code.&lt;/strong&gt; The scenario it prevents: you're logged into your bank in one tab, you visit a sketchy site in another, and that site's JavaScript quietly fires requests at your bank using your session. To stop that, the browser won't let JS on origin A read a response from origin B &lt;em&gt;unless server B explicitly says it allows A&lt;/em&gt;. Server B says so with an &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; header — which my FastAPI wasn't sending. Getting the error actually meant my request was leaving the browser correctly; the backend just hadn't granted permission yet.&lt;/p&gt;

&lt;p&gt;The fix lives on the &lt;strong&gt;backend&lt;/strong&gt;, via FastAPI's &lt;code&gt;CORSMiddleware&lt;/code&gt; [a &lt;em&gt;middleware&lt;/em&gt; is code that wraps every request/response — perfect for stamping a header onto everything]. Bonus: it ships inside FastAPI, so &lt;strong&gt;no install, no &lt;code&gt;requirements.txt&lt;/code&gt; change&lt;/strong&gt; this 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;from&lt;/span&gt; &lt;span class="n"&gt;fastapi.middleware.cors&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CORSMiddleware&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;CORSMiddleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;allow_origins&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;http://localhost:5173&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;# the whitelist — explicit, never "*"
&lt;/span&gt;    &lt;span class="n"&gt;allow_credentials&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;allow_methods&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;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;allow_headers&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;*&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two decisions worth stating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never &lt;code&gt;allow_origins=["*"]&lt;/code&gt;.&lt;/strong&gt; It defeats the point, &lt;em&gt;and&lt;/em&gt; the browser flat-out forbids &lt;code&gt;"*"&lt;/code&gt; together with &lt;code&gt;allow_credentials=True&lt;/code&gt;. Always list explicit origins. &lt;code&gt;[Flagged shortcut: I hardcoded the dev URL. Production reads allowed origins from an env var and lists the real Vercel domain — that's my Phase 9 deploy task.]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;allow_headers=["*"]&lt;/code&gt;&lt;/strong&gt; matters because I'm about to start sending an &lt;code&gt;Authorization&lt;/code&gt; header on every protected request.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;uvicorn hot-reloaded, I clicked Log in again, and the CORS error was gone — replaced by a clean &lt;strong&gt;401&lt;/strong&gt;, because my dev database was freshly regenerated and had zero users. A 401 round-trip was a &lt;em&gt;success&lt;/em&gt;: the browser-to-backend pipe now worked end to end. I registered a test user through &lt;code&gt;/docs&lt;/code&gt;, typed the same credentials into my form, and got &lt;strong&gt;200&lt;/strong&gt; with a glorious &lt;code&gt;{ access_token: "eyJ...", token_type: "bearer" }&lt;/code&gt;. My React app had fetched its own JWT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Store the token [the one real security decision this phase]
&lt;/h2&gt;

&lt;p&gt;The token vanished the instant &lt;code&gt;handleSubmit&lt;/code&gt; finished. I needed to &lt;em&gt;keep&lt;/em&gt; it — every future request has to attach it, and a refresh shouldn't log me out. Enter &lt;strong&gt;&lt;code&gt;localStorage&lt;/code&gt;&lt;/strong&gt;: a tiny per-origin key-value store built into the browser, persists across reloads, dead-simple API [&lt;code&gt;setItem&lt;/code&gt;, &lt;code&gt;getItem&lt;/code&gt;, &lt;code&gt;removeItem&lt;/code&gt;], values are always strings.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&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="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access_token&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;code&gt;response.ok&lt;/code&gt; is a boolean that's &lt;code&gt;true&lt;/code&gt; for any 2xx — the clean way to ask "did this succeed?" instead of eyeballing status numbers.]&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's where I put the security hat back on, because it genuinely matters:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;localStorage&lt;/code&gt; is readable by &lt;strong&gt;any JavaScript running on my page.&lt;/strong&gt; So if an attacker ever injects a script into my app — that's an &lt;strong&gt;XSS&lt;/strong&gt; [cross-site scripting] attack — they can read the token straight out of storage and impersonate the user. That's the real risk, stated plainly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What I did [flagged learning shortcut]:&lt;/strong&gt; store the JWT in &lt;code&gt;localStorage&lt;/code&gt;. Simple, easy to inspect while learning, sidesteps cookie/CORS-credential complexity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What production does:&lt;/strong&gt; store it in an &lt;strong&gt;&lt;code&gt;httpOnly&lt;/code&gt;, &lt;code&gt;Secure&lt;/code&gt;, &lt;code&gt;SameSite&lt;/code&gt; cookie.&lt;/strong&gt; &lt;code&gt;httpOnly&lt;/code&gt; means JavaScript literally &lt;em&gt;cannot&lt;/em&gt; read it, so even a successful XSS can't steal it, and the browser attaches it automatically. &lt;code&gt;[Know this exists. Not building it now.]&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;React's own defense is worth knowing too: it auto-escapes any text you render, which neutralizes most XSS by default. The danger zone is a prop literally named &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; — the scary name is the warning. Don't use it and you dodge the common trap.&lt;/p&gt;

&lt;p&gt;I logged in, opened DevTools → Application → Local Storage, saw my &lt;code&gt;token&lt;/code&gt; key sitting there, refreshed the page, and it survived. That persistence is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: The expense list [useEffect, and the infinite-loop trap]
&lt;/h2&gt;

&lt;p&gt;Reading data needs &lt;strong&gt;&lt;code&gt;useEffect&lt;/code&gt;&lt;/strong&gt;, and I'm glad I learned the trap &lt;em&gt;before&lt;/em&gt; hitting it. A component function re-runs on &lt;em&gt;every&lt;/em&gt; render. So you can't just drop a &lt;code&gt;fetch&lt;/code&gt; in the component body — it'd fire on every render, hammer the API, and [if it sets state] spin into an infinite re-render loop.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;useEffect&lt;/code&gt; lets you say "run this as a &lt;em&gt;side effect&lt;/em&gt;, but only at specific times." The &lt;strong&gt;dependency array&lt;/strong&gt; [second argument] controls when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[]&lt;/code&gt; empty → run &lt;strong&gt;once&lt;/strong&gt;, right after the component first mounts. Exactly what "fetch when the page loads" wants.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[x]&lt;/code&gt; → also re-run when &lt;code&gt;x&lt;/code&gt; changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;omitted entirely&lt;/strong&gt; → run after every render. The infinite-loop trap. Don't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One quirk: the effect function itself &lt;strong&gt;can't be &lt;code&gt;async&lt;/code&gt;&lt;/strong&gt;, so the pattern is to define an async function inside it and call it immediately.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ExpenseList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setExpenses&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchExpenses&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8000/expenses&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&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="nf"&gt;setExpenses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;fetchExpenses&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="c1"&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 &lt;code&gt;`Bearer ${token}`&lt;/code&gt; is a &lt;strong&gt;template literal&lt;/strong&gt; — backticks let you embed a variable with &lt;code&gt;${...}&lt;/code&gt;. It produces the &lt;em&gt;exact&lt;/em&gt; string my &lt;code&gt;/docs&lt;/code&gt; "Authorize" button sends, which is how the app proves who it is on every protected request. The fetch came back with a clean empty array &lt;code&gt;[]&lt;/code&gt; — correct, since my test user had no expenses yet. Authentication working from the client side, first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: The three gaps nobody builds [loading, empty, error]
&lt;/h2&gt;

&lt;p&gt;This is the heart of the phase. And it starts with a gotcha that burns everyone:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #4 — &lt;code&gt;fetch&lt;/code&gt; does NOT throw on a 404 or 500.&lt;/strong&gt; It only rejects when the &lt;em&gt;network itself&lt;/em&gt; fails [server down, CORS block]. A &lt;code&gt;401&lt;/code&gt; or &lt;code&gt;500&lt;/code&gt; is, to &lt;code&gt;fetch&lt;/code&gt;, a perfectly "successful" round-trip that happens to carry a bad status. So you have to check &lt;code&gt;response.ok&lt;/code&gt; &lt;strong&gt;yourself&lt;/strong&gt; and deliberately throw on a bad status. Coming from Python's &lt;code&gt;requests&lt;/code&gt;, this genuinely surprised me.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I handled both failure kinds with &lt;strong&gt;&lt;code&gt;try&lt;/code&gt; / &lt;code&gt;catch&lt;/code&gt; / &lt;code&gt;finally&lt;/code&gt;&lt;/strong&gt; [same shape as Python]. &lt;code&gt;finally&lt;/code&gt; runs no matter what — the perfect home for &lt;code&gt;setLoading(false)&lt;/code&gt;, so a loading spinner can never get stuck on screen. And I rendered the states with &lt;strong&gt;early returns&lt;/strong&gt; in strict priority order: &lt;strong&gt;loading → error → empty → list.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setExpenses&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchExpenses&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token&lt;/span&gt;&lt;span class="dl"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8000/expenses&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Request failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&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="nf"&gt;setExpenses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Could not load expenses. Please try again.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;console&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="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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;fetchExpenses&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part tutorials skip — I &lt;em&gt;tested the error gap on purpose&lt;/em&gt;. I hit &lt;code&gt;Ctrl+C&lt;/code&gt; on the backend, refreshed the browser, and instead of a blank frozen page I got my red "Could not load expenses. Please try again." That's the adversarial habit surviving even without the GAN framing: don't just build the happy path, actively try to break it.&lt;/p&gt;

&lt;p&gt;Then rendering the list with &lt;strong&gt;&lt;code&gt;.map()&lt;/code&gt;&lt;/strong&gt; — the array method that turns each expense object into an &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt;. Every mapped element needs a &lt;strong&gt;&lt;code&gt;key&lt;/code&gt;&lt;/strong&gt; — a stable unique id so React can track rows across re-renders. &lt;strong&gt;Use the database &lt;code&gt;id&lt;/code&gt;, never the array index&lt;/strong&gt; [index breaks the moment the list reorders or an item is removed].&lt;/p&gt;

&lt;p&gt;While wiring this, a small honest scope decision surfaced: my mentor's example row showed a &lt;code&gt;category&lt;/code&gt; field. &lt;strong&gt;My &lt;code&gt;Expense&lt;/code&gt; model doesn't have one.&lt;/strong&gt; Adding it isn't a display tweak — it's a new column, a migration, and schema changes. That's literally what my roadmap's Phase 7 [AI categorization] is &lt;em&gt;for&lt;/em&gt;. So I dropped it rather than scope-creep a clean foundation phase. &lt;code&gt;[category → deferred to Phase 7.]&lt;/code&gt; My real &lt;code&gt;ExpenseRead&lt;/code&gt; returns &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;amount&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;spent_on&lt;/code&gt;, and timestamps — and a nice detail: because &lt;code&gt;amount&lt;/code&gt; is a &lt;code&gt;Decimal&lt;/code&gt;, Pydantic serializes it to a &lt;strong&gt;string&lt;/strong&gt; on purpose, to protect money from float rounding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Full CRUD from the UI [add, delete, edit]
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Add [POST].&lt;/strong&gt; Same JSON pattern I'll use everywhere except login: &lt;code&gt;Content-Type: application/json&lt;/code&gt; and &lt;code&gt;body: JSON.stringify({...})&lt;/code&gt;. On success, I clear the form and &lt;em&gt;re-fetch the list&lt;/em&gt; so the new row appears — letting the server stay the single source of truth. To make that re-fetch reusable, I pulled &lt;code&gt;fetchExpenses&lt;/code&gt; &lt;strong&gt;out&lt;/strong&gt; of &lt;code&gt;useEffect&lt;/code&gt; into the component body, so both the initial load and every post-write refresh could call it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8000/expenses&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;spent_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;spentOn&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setDescription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nf"&gt;setAmount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nf"&gt;setSpentOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;fetchExpenses&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;[A lovely bit of luck: an &lt;code&gt;&amp;lt;input type="date"&amp;gt;&lt;/code&gt; gives a date-picker whose value is already a &lt;code&gt;"YYYY-MM-DD"&lt;/code&gt; string — exactly what my Python &lt;code&gt;date&lt;/code&gt; field wants, zero conversion.]&lt;/p&gt;

&lt;p&gt;Adding the always-visible form forced a rendering upgrade. Early returns worked when the &lt;em&gt;whole&lt;/em&gt; component was one state, but the form has to show even in the empty state. So I switched to &lt;strong&gt;inline conditional rendering&lt;/strong&gt; with &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Loading expenses…&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;red&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;No expenses yet.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="cm"&gt;/* the list */&lt;/span&gt; &lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lesson worth banking: &lt;strong&gt;early-return when the entire component is one state; inline &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; when only part of the UI changes.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #5 — one missing &lt;code&gt;!&lt;/code&gt; showed two contradictory states at once.&lt;/strong&gt; For a while my app displayed "Could not load expenses" &lt;em&gt;and&lt;/em&gt; "No expenses yet" simultaneously — which is impossible; those are supposed to be mutually exclusive. The cause was hilariously small: my empty-state guard was missing the &lt;code&gt;!error&lt;/code&gt; check. One character [&lt;code&gt;!&lt;/code&gt;] fixed it. A whole render bug lived in a single missing symbol.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Delete [DELETE].&lt;/strong&gt; Two things I hadn't met:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #6 — a 204 has no body, so don't parse one.&lt;/strong&gt; My &lt;code&gt;DELETE&lt;/code&gt; returns &lt;code&gt;204 No Content&lt;/code&gt; — by definition, empty. Calling &lt;code&gt;response.json()&lt;/code&gt; on it would throw. Just check &lt;code&gt;response.ok&lt;/code&gt; and move on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha #7 — passing an argument to a handler needs an arrow wrapper.&lt;/strong&gt; To delete a specific row I wrote &lt;code&gt;onClick={() =&amp;gt; handleDelete(expense.id)}&lt;/code&gt;. If I'd written &lt;code&gt;onClick={handleDelete(expense.id)}&lt;/code&gt; [no arrow], React would &lt;strong&gt;call it immediately during render&lt;/strong&gt; — deleting everything the instant the list appeared. The arrow hands React &lt;em&gt;a function to call later&lt;/em&gt;, on click. Rule of thumb I locked in: &lt;strong&gt;no args → pass the reference [&lt;code&gt;onClick={handleLogout}&lt;/code&gt;]; need args → wrap in an arrow.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Edit [PATCH].&lt;/strong&gt; The biggest UI concept: per-row edit mode. I track &lt;em&gt;which&lt;/em&gt; row is editing with a single &lt;code&gt;editingId&lt;/code&gt; [&lt;code&gt;null&lt;/code&gt; = nobody], and render with a &lt;strong&gt;ternary&lt;/strong&gt; — "if this row is being edited, show inputs; otherwise show text + buttons." Each branch has to return one element, so I wrapped the siblings in a &lt;strong&gt;Fragment&lt;/strong&gt; [&lt;code&gt;&amp;lt;&amp;gt;...&amp;lt;/&amp;gt;&lt;/code&gt;] — an invisible grouping tag that adds no extra &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; to the page.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PATCH&lt;/code&gt; is a &lt;em&gt;partial&lt;/em&gt; update: my backend's &lt;code&gt;ExpenseUpdate&lt;/code&gt; has all-optional fields, and it saves with &lt;code&gt;payload.model_dump(exclude_unset=True)&lt;/code&gt; → &lt;code&gt;setattr&lt;/code&gt; → &lt;code&gt;db.commit()&lt;/code&gt; → &lt;code&gt;db.refresh()&lt;/code&gt;. On success I close edit mode and re-fetch, same source-of-truth habit.&lt;/p&gt;

&lt;p&gt;With edit working, I'd hit the phase's finish line: &lt;strong&gt;log in through the UI and do full create/read/update/delete without ever opening &lt;code&gt;/docs&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: The bug that made login "do nothing until I refreshed"
&lt;/h2&gt;

&lt;p&gt;This one deserves its own section because it taught me the deepest React lesson of the phase. My symptom: I'd log in, and &lt;em&gt;nothing happened&lt;/em&gt; — until I refreshed the page, at which point it worked perfectly.&lt;/p&gt;

&lt;p&gt;The cause, once I understood it, is fundamental:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #8 — React does not watch &lt;code&gt;localStorage&lt;/code&gt;.&lt;/strong&gt; My login handler wrote the token to &lt;code&gt;localStorage&lt;/code&gt;, but React only re-renders when a piece of &lt;strong&gt;state&lt;/strong&gt; changes. Writing to storage is completely invisible to it. And my &lt;code&gt;ExpenseList&lt;/code&gt; fetched only &lt;em&gt;once, on mount&lt;/em&gt; — so on a fresh load it fired with &lt;code&gt;Bearer null&lt;/code&gt;, failed, and never ran again. Logging in stashed a token, but nothing told React anything had changed. A refresh "fixed" it only because it re-mounted the whole app &lt;em&gt;after&lt;/em&gt; the token was already in storage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The fix was my first taste of &lt;strong&gt;lifting state up&lt;/strong&gt;: the token belongs in &lt;code&gt;App&lt;/code&gt; [the parent], because &lt;code&gt;App&lt;/code&gt; is what decides between "logged out" and "logged in." I put the token in React state, seeded from storage, and set it on login:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setToken&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;// survives refresh&lt;/span&gt;

&lt;span class="c1"&gt;// in the login success branch:&lt;/span&gt;
&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;setToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// &amp;lt;-- THE missing trigger. State change → re-render → instant switch.&lt;/span&gt;

&lt;span class="c1"&gt;// gate the whole UI on it:&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Smart Expense Manager&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleLogout&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Log out&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ExpenseList&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// otherwise fall through to the login form&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Logout is just login run backwards — clear &lt;em&gt;both&lt;/em&gt; copies of the token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleLogout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// so a refresh won't silently log me back in&lt;/span&gt;
  &lt;span class="nf"&gt;setToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                    &lt;span class="c1"&gt;// so the gate falls through to the login form&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two more real bugs fell out of this, and both are honest lessons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The stray-render leak.&lt;/strong&gt; After adding the gated view, logging out still showed the expense list — because an &lt;em&gt;earlier&lt;/em&gt; &lt;code&gt;&amp;lt;ExpenseList /&amp;gt;&lt;/code&gt; was still sitting in the logged-out return from a previous step. I'd added the new one without removing the old. &lt;code&gt;ExpenseList&lt;/code&gt; should appear in &lt;strong&gt;exactly one place&lt;/strong&gt;. A quick file search [it should show up twice: the import + one render] caught it. Lesson: when you restructure, hunt down the leftovers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incognito confused me for a minute.&lt;/strong&gt; Logged in on a normal window, I opened incognito, logged in &lt;em&gt;there&lt;/em&gt;, and the two windows behaved independently. That's &lt;strong&gt;correct&lt;/strong&gt; — &lt;code&gt;localStorage&lt;/code&gt; is isolated per browser profile, so incognito has its own separate store. [Imagine if it didn't; that'd be a leak.] Two &lt;em&gt;normal&lt;/em&gt; tabs share storage but not live React state, so one won't visually update until it refreshes. &lt;code&gt;[Live cross-tab sync via the storage event = know this exists.]&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bonus: so how does clicking "Save" actually change the database?
&lt;/h2&gt;

&lt;p&gt;This confused me, so I'll write down the model that fixed it: &lt;strong&gt;there are three separate programs, and none can touch the others' memory.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;My React app&lt;/strong&gt; — JavaScript in the browser. It knows &lt;em&gt;nothing&lt;/em&gt; about the database. All it can do is send HTTP requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My FastAPI backend&lt;/strong&gt; — Python in the &lt;code&gt;uvicorn&lt;/code&gt; terminal. The &lt;em&gt;only&lt;/em&gt; program that talks to the database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite&lt;/strong&gt; — the &lt;code&gt;expenses.db&lt;/code&gt; file. It only ever hears from the backend.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the Save button &lt;strong&gt;does not save to the database.&lt;/strong&gt; It sends a &lt;code&gt;PATCH&lt;/code&gt; note to the backend &lt;em&gt;asking it&lt;/em&gt; to save. The backend checks my token, finds my row [scoped to my user id — my Phase 3 ownership filter riding along], sets the new values, and runs &lt;strong&gt;&lt;code&gt;db.commit()&lt;/code&gt; — that single line is the actual write to disk.&lt;/strong&gt; Then it replies &lt;code&gt;200&lt;/code&gt;, and my browser re-fetches so the screen matches what the database now says. Every operation in the app is that same shape: &lt;strong&gt;Create&lt;/strong&gt; = POST + commit, &lt;strong&gt;Delete&lt;/strong&gt; = DELETE + commit, &lt;strong&gt;Update&lt;/strong&gt; = PATCH + commit, &lt;strong&gt;Read&lt;/strong&gt; = GET [no commit — nothing changes]. Once that one round-trip clicked, the whole app made sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stuff I want to remember [the honest takeaways]
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The frontend has an exact mirror of the backend's tools: &lt;strong&gt;Node = Python, npm = pip, &lt;code&gt;package.json&lt;/code&gt; = requirements.txt&lt;/strong&gt; [but it auto-freezes], &lt;strong&gt;&lt;code&gt;node_modules&lt;/code&gt; = venv&lt;/strong&gt;, &lt;strong&gt;Vite = uvicorn&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;component&lt;/strong&gt; is a Capitalized function returning &lt;strong&gt;JSX&lt;/strong&gt;. JSX rules that bite: one root element, &lt;code&gt;className&lt;/code&gt; not &lt;code&gt;class&lt;/code&gt;, &lt;code&gt;{ }&lt;/code&gt; for JS values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;useState&lt;/code&gt; returns a &lt;code&gt;[value, setter]&lt;/code&gt; pair. Never reassign the value — always call the setter.&lt;/strong&gt; The setter is the only thing that triggers a re-render.&lt;/li&gt;
&lt;li&gt;React &lt;strong&gt;only re-renders on state change.&lt;/strong&gt; It does &lt;em&gt;not&lt;/em&gt; watch &lt;code&gt;localStorage&lt;/code&gt;. This one fact caused my nastiest bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Controlled inputs&lt;/strong&gt; [&lt;code&gt;value&lt;/code&gt; + &lt;code&gt;onChange&lt;/code&gt;] make state the single source of truth for a form.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;event.preventDefault()&lt;/code&gt; stops the browser's default full-page reload on form submit. You will forget this once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CORS is a browser security feature, not a bug in your code.&lt;/strong&gt; Different port = different origin; the fix is &lt;code&gt;CORSMiddleware&lt;/code&gt; on the backend, with an &lt;em&gt;explicit&lt;/em&gt; origin whitelist [never &lt;code&gt;"*"&lt;/code&gt; with credentials].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The OAuth2 login endpoint is the odd one out — it wants form-encoded data. Everything else speaks JSON.&lt;/strong&gt; Mixing them up is the classic 422.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;fetch&lt;/code&gt; does NOT throw on 4xx/5xx.&lt;/strong&gt; Check &lt;code&gt;response.ok&lt;/code&gt; yourself and throw deliberately. Use &lt;code&gt;try/catch/finally&lt;/code&gt;, and put &lt;code&gt;setLoading(false)&lt;/code&gt; in &lt;code&gt;finally&lt;/code&gt; so it can never get stuck.&lt;/li&gt;
&lt;li&gt;Build the happy path, then &lt;strong&gt;close the three gaps: loading, empty, error&lt;/strong&gt; — and &lt;em&gt;actually test the error state&lt;/em&gt; by killing the backend.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.map()&lt;/code&gt; renders lists; every item needs a &lt;strong&gt;stable &lt;code&gt;key&lt;/code&gt; — use the DB id, never the array index.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; for show-or-nothing; ternary &lt;code&gt;? :&lt;/code&gt; for A-or-B; Fragments &lt;code&gt;&amp;lt;&amp;gt;...&amp;lt;/&amp;gt;&lt;/code&gt;&lt;/strong&gt; to group siblings without an extra &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Passing args to a handler needs an &lt;strong&gt;arrow wrapper&lt;/strong&gt; [&lt;code&gt;() =&amp;gt; fn(id)&lt;/code&gt;] or it fires during render. No args → pass the reference.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;204&lt;/code&gt; response has &lt;strong&gt;no body&lt;/strong&gt; — don't call &lt;code&gt;.json()&lt;/code&gt; on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT in &lt;code&gt;localStorage&lt;/code&gt; is a flagged shortcut&lt;/strong&gt; [readable by any JS → XSS risk]. Production uses an &lt;strong&gt;&lt;code&gt;httpOnly Secure SameSite&lt;/code&gt; cookie&lt;/strong&gt;. React auto-escapes text; avoid &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The Save button doesn't save — it &lt;em&gt;asks the backend to&lt;/em&gt;. &lt;strong&gt;&lt;code&gt;db.commit()&lt;/code&gt; is the real write.&lt;/strong&gt; The browser, the backend, and the database are three separate programs passing notes.&lt;/li&gt;
&lt;li&gt;When you restructure, &lt;strong&gt;hunt the leftovers&lt;/strong&gt; — a stray render leaked my list into the logged-out view.&lt;/li&gt;
&lt;li&gt;Commit at every green checkpoint, with a subject + a "why." Small and often beats one heroic commit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next up: Phase 5. The app now has a memory, a mouth, a lock, and — finally — a &lt;strong&gt;face&lt;/strong&gt; a human can log into and use. But that face is honest to a fault: it's a raw list with no styling, no summaries, and a login that just... stops working after 30 minutes with a confusing error [my JWTs expire, and I don't handle it yet]. Phase 5 is where the face gets some &lt;em&gt;expression&lt;/em&gt; — a dashboard, spending insights, and the polish that turns "it works" into "I'd actually use this." That, and I owe this app an "expired-token → send me back to login" fix I flagged and walked away from. See you there.&lt;/p&gt;

</description>
      <category>frontend</category>
      <category>react</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Teaching My Backend to Lock the Door — FastAPI Auth, Phase 3</title>
      <dc:creator>Azhar Alvi </dc:creator>
      <pubDate>Sat, 25 Jul 2026 09:06:27 +0000</pubDate>
      <link>https://dev.to/silentcarry/teaching-my-backend-to-lock-the-door-fastapi-auth-phase-3-5b9o</link>
      <guid>https://dev.to/silentcarry/teaching-my-backend-to-lock-the-door-fastapi-auth-phase-3-5b9o</guid>
      <description>&lt;p&gt;&lt;strong&gt;Hash the password, hand out a token, and make absolutely sure no one can read someone else's expenses.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So Phase 2 gave my app a mouth. It could finally &lt;em&gt;talk&lt;/em&gt; — create, read, update, and delete expenses over real HTTP endpoints, all clicking together through &lt;code&gt;/docs&lt;/code&gt;. But there was a giant, deliberately-ignored problem sitting in the middle of it: the door had no lock. Anyone who could reach the server could read, edit, or delete anything. And every expense I created was quietly stamped with the same hardcoded owner — a "dev user" whose id I'd nailed into the code with a &lt;code&gt;# TEMP&lt;/code&gt; note and a promise to fix it "in Phase 3."&lt;/p&gt;

&lt;p&gt;Well. It's Phase 3. Time to pay that debt.&lt;/p&gt;

&lt;p&gt;This is where the app grows a bouncer. The buzzword is &lt;strong&gt;auth&lt;/strong&gt;, which actually hides &lt;em&gt;two&lt;/em&gt; jobs that sound the same and aren't: &lt;strong&gt;authentication&lt;/strong&gt; ["who are you?"] and &lt;strong&gt;authorization&lt;/strong&gt; ["okay, but are you allowed to touch &lt;em&gt;this&lt;/em&gt;?"]. I went in thinking auth was "add a login form" and came out having learned about one-way hashing, signed tokens, a security bug with the excellent name &lt;em&gt;IDOR&lt;/em&gt;, and why the same password can produce two different hashes. Let me dump what I learned [and the parts that tripped me up, because — as usual — there were several].&lt;/p&gt;

&lt;p&gt;Auth is the one phase where you have to stop thinking like a builder and start thinking like the person trying to rob you. So every step below is really "here's a way an attacker wins, and here's the gap I closed to stop them."&lt;/p&gt;

&lt;p&gt;The structure. Let's call it PHASE 3 — The Lock:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give the &lt;code&gt;User&lt;/code&gt; table somewhere to store a password [a &lt;em&gt;hashed&lt;/em&gt; one, never the real thing]&lt;/li&gt;
&lt;li&gt;Password hashing helpers — turn a password into something safe to store&lt;/li&gt;
&lt;li&gt;POST /auth/register — sign up with a hashed password&lt;/li&gt;
&lt;li&gt;Understand what a JWT actually is [it's just a signed string, and it's readable]&lt;/li&gt;
&lt;li&gt;POST /auth/login — check the password, hand back a token&lt;/li&gt;
&lt;li&gt;get_current_user — the gatekeeper that turns a token back into a user&lt;/li&gt;
&lt;li&gt;Lock every expense endpoint and scope it to the logged-in owner&lt;/li&gt;
&lt;li&gt;Retire the hardcoded &lt;code&gt;DEV_USER_ID&lt;/code&gt; for good&lt;/li&gt;
&lt;li&gt;Prove two users can't see each other's data&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  First, the new ideas [and yes, this time there IS stuff to install]
&lt;/h2&gt;

&lt;p&gt;Unlike Phase 2, this phase actually adds libraries — so the &lt;code&gt;pip freeze&lt;/code&gt; habit is back on. Three new tools, in plain language:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;passlib [with bcrypt]&lt;/strong&gt; — the password shredder. &lt;strong&gt;bcrypt&lt;/strong&gt; is the actual algorithm; passlib is the friendly wrapper around it. Its whole job is to turn a password into a &lt;strong&gt;hash&lt;/strong&gt; — a scrambled string you can store safely — using a method that's &lt;em&gt;deliberately slow&lt;/em&gt; and &lt;em&gt;salted&lt;/em&gt; [more on both of those below].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PyJWT&lt;/strong&gt; — the token machine. It creates and verifies &lt;strong&gt;JWTs&lt;/strong&gt;, the signed strings that let a user prove "I'm logged in" on every request without re-sending their password. [Gotcha you'll hit: you &lt;code&gt;pip install pyjwt&lt;/code&gt; but you &lt;code&gt;import jwt&lt;/code&gt;. The names don't match. File that away now.]&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;python-multipart&lt;/strong&gt; — an unglamorous helper that lets FastAPI read &lt;em&gt;form&lt;/em&gt; data, which the standard login flow uses. You don't call it directly; FastAPI just needs it present.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"passlib[bcrypt]"&lt;/span&gt; pyjwt python-multipart
pip freeze &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The quotes around &lt;code&gt;"passlib[bcrypt]"&lt;/code&gt; matter — the brackets tell pip to pull bcrypt in as an extra. And the moment you install something, &lt;strong&gt;freeze it&lt;/strong&gt;, so the next machine [including future-you] rebuilds the identical environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Give users a place to store a password [a hashed one]
&lt;/h2&gt;

&lt;p&gt;Here's the rule that governs this entire phase, and it's worth tattooing somewhere: &lt;strong&gt;you never store the password. You store a one-way hash of it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;User&lt;/code&gt; model from Phase 1 had &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, and timestamps — but no password field at all, because I built it before auth existed. So it needs one new column:&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;hashed_password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&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="nf"&gt;mapped_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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two deliberate choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's called &lt;code&gt;hashed_password&lt;/code&gt;, not &lt;code&gt;password&lt;/code&gt;.&lt;/strong&gt; The name is a permanent reminder to everyone [me, in three weeks] that this field holds a hash, &lt;em&gt;never&lt;/em&gt; plaintext. If you ever spot raw text in there, something is badly broken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's not unique and not indexed&lt;/strong&gt; [unlike &lt;code&gt;email&lt;/code&gt;]. You never look a user up &lt;em&gt;by&lt;/em&gt; their hash — you find them by email, then verify the hash in code. Indexing it would be wasted effort.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A new column means a new migration. Same autogenerate → review → apply rhythm from Phase 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;alembic revision &lt;span class="nt"&gt;--autogenerate&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"add hashed_password to users"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I reviewed the generated file [&lt;code&gt;op.add_column(... nullable=False)&lt;/code&gt;] and then hit the wrinkle that became my first real lesson this phase.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #1 — the NOT NULL wall.&lt;/strong&gt; SQLite refuses to add a &lt;code&gt;NOT NULL&lt;/code&gt; column to a table that already has rows, because those existing rows have no value for it. My table had that old seeded dev user in it. Two honest paths: the &lt;em&gt;production&lt;/em&gt; way [add the column nullable, backfill every row, then a second migration to flip it to &lt;code&gt;NOT NULL&lt;/code&gt; — zero data loss], or the &lt;em&gt;learning&lt;/em&gt; way [my only data was throwaway, so wipe it and rebuild]. My mentor made me say out loud that we were taking a shortcut. I took it — but now I know the real move for when the data actually matters.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Destructive-command warning&lt;/strong&gt; [the first of this phase]: rebuilding from scratch &lt;em&gt;deletes every local row&lt;/em&gt;. Safe here only because it's junk dev data. Rather than delete the &lt;code&gt;.db&lt;/code&gt; file by hand, I stayed inside Alembic — which also proves my whole migration chain works end to end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;alembic downgrade base   &lt;span class="c"&gt;# drops everything back to empty [the destructive line]&lt;/span&gt;
alembic upgrade &lt;span class="nb"&gt;head&lt;/span&gt;     &lt;span class="c"&gt;# replays all migrations onto empty tables&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: The hashing helpers [hashing is NOT encryption]
&lt;/h2&gt;

&lt;p&gt;Before I could register anyone, I needed two tiny functions: one to hash a password on the way in, one to verify a login attempt later. Best practice says security code lives in its own file, so this is a new &lt;code&gt;security.py&lt;/code&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="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="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="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;The single most important concept here — and it took a beat to click:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hashing is one-way. Encryption is two-way.&lt;/strong&gt; You can &lt;em&gt;encrypt&lt;/em&gt; something and later decrypt it back. You can &lt;em&gt;hash&lt;/em&gt; a password, but you can &lt;strong&gt;never&lt;/strong&gt; turn the hash back into the password. That's the entire point: even &lt;em&gt;I&lt;/em&gt;, running the server, can't see anyone's password. If my whole database gets stolen, the attacker gets a pile of useless &lt;code&gt;$2b$...&lt;/code&gt; strings, not passwords. &lt;strong&gt;That's the first gap closed.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;bcrypt is deliberately slow.&lt;/strong&gt; Sounds like a bug; it's a feature. Slowness makes brute-forcing millions of guesses painfully expensive for an attacker while costing a real login a few harmless milliseconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;bcrypt auto-salts.&lt;/strong&gt; It mixes a random value into every hash, so two people with the same password get &lt;em&gt;different&lt;/em&gt; hashes. That kills "rainbow table" attacks [precomputed hash lookups]. I proved this to myself: hashing &lt;code&gt;"supersecret123"&lt;/code&gt; twice gave two totally different strings. Same input, different output — the salt, visibly doing its job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;deprecated="auto"&lt;/code&gt;&lt;/strong&gt; is the sneaky-clever bit. Because every hash records &lt;em&gt;which&lt;/em&gt; algorithm made it [the &lt;code&gt;$2b$&lt;/code&gt; prefix is bcrypt's ID card], I can add a newer algorithm years from now and passlib will keep verifying old hashes &lt;em&gt;and&lt;/em&gt; silently upgrade them the next time each user logs in. Nobody gets locked out, nobody resets their password. Algorithm agility, for one word of config.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I tested it in a plain &lt;code&gt;python&lt;/code&gt; shell: &lt;code&gt;hash_password("supersecret123")&lt;/code&gt; gave a long &lt;code&gt;$2b$...&lt;/code&gt; string, &lt;code&gt;verify_password("supersecret123", h)&lt;/code&gt; returned &lt;code&gt;True&lt;/code&gt;, and &lt;code&gt;verify_password("wrong", h)&lt;/code&gt; returned &lt;code&gt;False&lt;/code&gt;. Then the version gods showed up.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #2 — the 72-byte error on a 14-character password.&lt;/strong&gt; My first real hash blew up with &lt;code&gt;ValueError: password cannot be longer than 72 bytes&lt;/code&gt;. My password was 14 characters. What?! The culprit: passlib 1.7.4 is old and unmaintained, and modern bcrypt [4.1+] changed an interface it relies on. On its first run, passlib runs an internal self-test that trips the new bcrypt's 72-byte guard — &lt;em&gt;before my password is ever touched&lt;/em&gt;. The fix was to pin bcrypt to the last version passlib understands:&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"bcrypt==4.0.1"&lt;/span&gt;
pip freeze &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;This was also the moment "why do we freeze?" stopped being abstract — without the pin, a fresh install would grab the broken-for-me newest bcrypt all over again. [The modern alternative is a library called &lt;code&gt;pwdlib&lt;/code&gt;; passlib is on its way out. Noted for a future refactor.]&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 3: POST /auth/register [where the hash finally gets used]
&lt;/h2&gt;

&lt;p&gt;Two schemas, exactly like Phase 2's input/output split — but with one security rule bolted on. In &lt;code&gt;schemas.py&lt;/code&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;UserCreate&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;What a client sends to register.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;name&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;100&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="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="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;8&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;UserRead&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;What the server sends back — no password, no hash, EVER.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;name&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;email&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;span class="n"&gt;model_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ConfigDict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_attributes&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at what &lt;code&gt;UserRead&lt;/code&gt; is missing: the password &lt;em&gt;and&lt;/em&gt; the hash. This is a real gap — if the stored hash ever showed up in an API response, an attacker who can read responses gets a head start on cracking it. Leaving it out of the output schema makes that leak &lt;strong&gt;structurally impossible&lt;/strong&gt;. Same trick as &lt;code&gt;response_model=ExpenseRead&lt;/code&gt; in Phase 2, now doing security work.&lt;/p&gt;

&lt;p&gt;The endpoint, in &lt;code&gt;main.py&lt;/code&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;@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;/auth/register&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;UserRead&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_201_CREATED&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;register&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;UserCreate&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="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;existing&lt;/span&gt; &lt;span class="o"&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;scalars&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="n"&gt;email&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="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;first&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&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&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;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;409&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;Email already registered&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&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;name&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="n"&gt;name&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;payload&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;hashed_password&lt;/span&gt;&lt;span class="o"&gt;=&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;payload&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="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;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="nf"&gt;commit&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;hashed_password=hash_password(payload.password)&lt;/code&gt;&lt;/strong&gt; is the one line that makes this safe. The plaintext exists only long enough to be hashed, right here, and is never stored. What lands in the database is the &lt;code&gt;$2b$...&lt;/code&gt; string.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;/auth/&lt;/code&gt; prefix&lt;/strong&gt; namespaces auth routes together — tidy as the API grows. [The neater-still version is a separate &lt;code&gt;APIRouter&lt;/code&gt; file; leaving that for later so I'm not juggling files mid-concept.]&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;409 Conflict&lt;/strong&gt; is the precise code for "your request is fine, but it clashes with something that already exists" — here, a taken email. More specific than a generic 400. [The fully robust version wraps the insert in a &lt;code&gt;try/except IntegrityError&lt;/code&gt; to close a tiny race where two people register the same email at the same instant; the DB's unique constraint still protects integrity either way. Noted, not built.]&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tested through &lt;code&gt;/docs&lt;/code&gt;: register → &lt;strong&gt;201&lt;/strong&gt;, and the response showed &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt; and — crucially — &lt;strong&gt;no&lt;/strong&gt; &lt;code&gt;hashed_password&lt;/code&gt;. Registering the same email again → &lt;strong&gt;409&lt;/strong&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: What a JWT actually is [read this before you trust one]
&lt;/h2&gt;

&lt;p&gt;This was the concept I most needed to &lt;em&gt;get&lt;/em&gt; before writing more code. A &lt;strong&gt;JWT&lt;/strong&gt; [JSON Web Token, said "jot"] is just a &lt;strong&gt;signed string&lt;/strong&gt; the server hands you at login. You send it back on every future request, and it proves "I'm logged in" — &lt;em&gt;without&lt;/em&gt; the server having to remember sessions. The token itself carries the proof.&lt;/p&gt;

&lt;p&gt;It has &lt;strong&gt;three parts separated by dots&lt;/strong&gt;: &lt;code&gt;header.payload.signature&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Header&lt;/strong&gt; — which signing algorithm was used [I used &lt;code&gt;HS256&lt;/code&gt;].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payload&lt;/strong&gt; — the "claims," i.e. the data. I put in &lt;code&gt;sub&lt;/code&gt; [subject — who the token is about; I store the user's id] and &lt;code&gt;exp&lt;/code&gt; [expiry].&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signature&lt;/strong&gt; — the header and payload, signed with my secret key.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the thing that reframed everything for me: &lt;strong&gt;the payload is only base64-encoded, not encrypted.&lt;/strong&gt; Anyone who grabs the token can &lt;em&gt;read&lt;/em&gt; it — paste one into jwt.io and you'll see the user id in plain text. So the rule is: &lt;strong&gt;never put anything secret in a JWT.&lt;/strong&gt; What a token buys you isn't secrecy — it's &lt;strong&gt;integrity&lt;/strong&gt;. Change one character of the payload [say, to a different user's id] and the signature no longer matches, so the server rejects it. An attacker can't forge a valid signature because &lt;strong&gt;only my server knows the secret key.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Which is exactly why that key can never leak. If someone learns my &lt;code&gt;SECRET_KEY&lt;/code&gt;, they can mint valid tokens for &lt;em&gt;any&lt;/em&gt; user — a total bypass. So it lives in &lt;code&gt;.env&lt;/code&gt; [already gitignored since Phase 0], generated with real randomness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import secrets; print(secrets.token_hex(32))"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;SECRET_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;that_64_char_hex_value&lt;/span&gt;
&lt;span class="py"&gt;ACCESS_TOKEN_EXPIRE_MINUTES&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That 30-minute expiry closes another gap: a &lt;em&gt;stolen&lt;/em&gt; token stops working within half an hour instead of forever. [Real apps pair a short access token with a longer "refresh token" so users aren't logged out constantly. Not building refresh tokens this phase — that'd be over-engineering right now.]&lt;/p&gt;

&lt;p&gt;Then I wrote the token minter in &lt;code&gt;security.py&lt;/code&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;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;subject&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="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;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;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ACCESS_TOKEN_EXPIRE_MINUTES&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;subject&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="k"&gt;return&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;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;ALGORITHM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;strong&gt;timezone-aware UTC&lt;/strong&gt; expiry — same discipline as Phase 1's &lt;code&gt;timezone=True&lt;/code&gt; columns. Auth timestamps must be unambiguous across servers.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #3 — the misspelled keyword that wasn't a version bug.&lt;/strong&gt; Setting up &lt;code&gt;CryptContext&lt;/code&gt; earlier, I hit &lt;code&gt;KeyError: unknown CryptContext keyword&lt;/code&gt;. I assumed another bcrypt version mess. But my mentor made me &lt;em&gt;read the traceback bottom-up&lt;/em&gt; first — and the last line said it plainly: an argument I passed doesn't exist. I'd typed &lt;code&gt;depreciated&lt;/code&gt; [the accounting word, with an &lt;code&gt;i&lt;/code&gt;] instead of &lt;code&gt;deprecated&lt;/code&gt;. Case-and-spelling-sensitive libraries do not forgive. Reading the traceback saved me from "fixing" something that was never broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha #4 — a &lt;code&gt;.env&lt;/code&gt; file does nothing on its own.&lt;/strong&gt; My &lt;code&gt;SECRET_KEY = os.environ["SECRET_KEY"]&lt;/code&gt; crashed with a &lt;code&gt;KeyError&lt;/code&gt;. Turns out I'd been &lt;em&gt;reading&lt;/em&gt; env vars with &lt;code&gt;os.getenv&lt;/code&gt; all along but never actually &lt;em&gt;loading&lt;/em&gt; the &lt;code&gt;.env&lt;/code&gt; file into the environment — my &lt;code&gt;DATABASE_URL&lt;/code&gt; only "worked" because it had a default fallback that quietly kicked in. A &lt;code&gt;.env&lt;/code&gt; file is just text; something has to copy it into the environment first. That something is &lt;code&gt;load_dotenv()&lt;/code&gt;:&lt;/p&gt;


&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;
&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# must run BEFORE anything reads a variable
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Lesson that stuck: &lt;strong&gt;loading and reading are two different jobs.&lt;/strong&gt; &lt;code&gt;load_dotenv()&lt;/code&gt; loads; &lt;code&gt;os.getenv()&lt;/code&gt; reads. And my strict, no-fallback &lt;code&gt;SECRET_KEY&lt;/code&gt; line is what exposed the gap the fallback had been hiding — fail-fast earning its keep.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 5: POST /auth/login [check the password, hand back a token]
&lt;/h2&gt;

&lt;p&gt;This is where hashing and JWTs meet. I built login the standard FastAPI way, with &lt;code&gt;OAuth2PasswordRequestForm&lt;/code&gt; — which, bonus, lights up the green &lt;strong&gt;Authorize&lt;/strong&gt; button in &lt;code&gt;/docs&lt;/code&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;@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;/auth/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;Token&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;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;form_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OAuth2PasswordRequestForm&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;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Session&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="n"&gt;user&lt;/span&gt; &lt;span class="o"&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;scalars&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="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;form_data&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;span class="nf"&gt;first&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&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="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;form_data&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="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;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="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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WWW-Authenticate&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;Bearer&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;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;subject&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="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;access_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;access_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_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;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;ul&gt;
&lt;li&gt;
&lt;strong&gt;The quirk everyone hits:&lt;/strong&gt; the OAuth2 spec calls the field &lt;code&gt;username&lt;/code&gt;, but I put the &lt;em&gt;email&lt;/em&gt; there. So at login you type your email into the box labeled "username." Normal in FastAPI land.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The vague error is on purpose.&lt;/strong&gt; Whether the email doesn't exist &lt;em&gt;or&lt;/em&gt; the password is wrong, I return the exact same &lt;code&gt;401&lt;/code&gt; with the same message. If I distinguished them, an attacker could probe which emails are registered — a leak called &lt;em&gt;user enumeration&lt;/em&gt;. One generic message, closed. &lt;strong&gt;Do this now, it's free.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;subject=str(user.id)&lt;/code&gt;&lt;/strong&gt; stamps the user's id into the token's &lt;code&gt;sub&lt;/code&gt; claim. That breadcrumb is the whole point — it's how the next step figures out &lt;em&gt;who&lt;/em&gt; a request is from, with no password involved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The docs also show &lt;code&gt;grant_type&lt;/code&gt;, &lt;code&gt;scope&lt;/code&gt;, &lt;code&gt;client_id&lt;/code&gt;, &lt;code&gt;client_secret&lt;/code&gt; fields — all part of the full OAuth2 spec, all safely ignorable for my app. Only &lt;code&gt;username&lt;/code&gt; and &lt;code&gt;password&lt;/code&gt; matter here.&lt;/p&gt;

&lt;p&gt;Tested: right email + password → &lt;strong&gt;200&lt;/strong&gt; with a token; wrong password → &lt;strong&gt;401&lt;/strong&gt;. And it taught me a bit of Python grammar I'd been fuzzy on — &lt;code&gt;:&lt;/code&gt; &lt;em&gt;describes&lt;/em&gt; [&lt;code&gt;amount: Decimal&lt;/code&gt;, a dict pair &lt;code&gt;{"key": "value"}&lt;/code&gt;, the &lt;code&gt;:&lt;/code&gt; that opens a block] while &lt;code&gt;=&lt;/code&gt; &lt;em&gt;assigns&lt;/em&gt; [&lt;code&gt;x = 5&lt;/code&gt;, or a keyword arg &lt;code&gt;status_code=401&lt;/code&gt;]. Same word can flip: &lt;code&gt;subject: str&lt;/code&gt; in a definition vs &lt;code&gt;subject=...&lt;/code&gt; in a call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: get_current_user [the gatekeeper]
&lt;/h2&gt;

&lt;p&gt;This is the piece every locked endpoint leans on. It turns a token back into a real, live user — the concrete form of &lt;em&gt;authentication&lt;/em&gt;. First a decode helper in &lt;code&gt;security.py&lt;/code&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;def&lt;/span&gt; &lt;span class="nf"&gt;decode_access_token&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="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;dict&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;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_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;ALGORITHM&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;jwt.decode&lt;/code&gt; does two jobs at once: verifies the signature [proving I minted it, untampered] &lt;em&gt;and&lt;/em&gt; checks the &lt;code&gt;exp&lt;/code&gt; [rejecting expired tokens]. Either failure raises. Then the dependency in &lt;code&gt;main.py&lt;/code&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;oauth2_scheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OAuth2PasswordBearer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokenUrl&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/login&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;get_current_user&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="nb"&gt;str&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;oauth2_scheme&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="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;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="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;Could not validate credentials&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WWW-Authenticate&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;Bearer&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;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="nf"&gt;decode_access_token&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;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;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="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;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="n"&gt;credentials_exception&lt;/span&gt;

    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&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;get&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;int&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="n"&gt;user&lt;/span&gt; &lt;span class="ow"&gt;is&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;raise&lt;/span&gt; &lt;span class="n"&gt;credentials_exception&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OAuth2PasswordBearer&lt;/code&gt;&lt;/strong&gt; automatically pulls the token out of the incoming &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; header [401 if it's missing] and is what finally enables the Authorize button.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One &lt;code&gt;except jwt.InvalidTokenError&lt;/code&gt;&lt;/strong&gt; covers a bad signature, a malformed token, &lt;em&gt;and&lt;/em&gt; an expired one [expiry raises a subclass], all funnelling to the same generic 401. No-leak discipline again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;db.get(User, ...)&lt;/code&gt;&lt;/strong&gt; fetches the actual user rather than blindly trusting the token — so a deleted user's leftover token stops working. Returning the &lt;code&gt;User&lt;/code&gt; object is the magic: any endpoint that depends on this gets the logged-in user handed straight to it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I proved it with a throwaway &lt;code&gt;GET /auth/me&lt;/code&gt; that just returns &lt;code&gt;current_user&lt;/code&gt;: authorized → &lt;strong&gt;200&lt;/strong&gt; with my user; no token → &lt;strong&gt;401&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #5 — case-sensitivity, twice.&lt;/strong&gt; First &lt;code&gt;Oauth2PasswordBearer is not defined&lt;/code&gt;, then again — because "OAuth" is an acronym and &lt;em&gt;both&lt;/em&gt; the O and the A are uppercase: &lt;code&gt;OAuth2PasswordBearer&lt;/code&gt;. I'd lowercased the A. Python doesn't care that it's "obviously" the same word.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha #6 — order matters for dependencies.&lt;/strong&gt; I also hit &lt;code&gt;get_current_user is not defined&lt;/code&gt; on an endpoint. Not a typo this time — an &lt;em&gt;ordering&lt;/em&gt; problem. Python evaluates &lt;code&gt;Depends(get_current_user)&lt;/code&gt; in the function's default arguments &lt;em&gt;the moment it reads the &lt;code&gt;def&lt;/code&gt; line&lt;/em&gt;, so &lt;code&gt;get_current_user&lt;/code&gt; has to be &lt;strong&gt;defined above&lt;/strong&gt; any endpoint that uses it. Moved the gatekeeper up near the top of &lt;code&gt;main.py&lt;/code&gt;, above the routes. [Same reason &lt;code&gt;get_db&lt;/code&gt; has always worked — defined before its consumers.]&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 7: Lock the endpoints and scope them to the owner [the boss fight]
&lt;/h2&gt;

&lt;p&gt;This is what the whole phase was building toward — and it's the real difference between &lt;em&gt;authentication&lt;/em&gt; [we know who you are] and &lt;em&gt;authorization&lt;/em&gt; [you may only touch &lt;em&gt;your own&lt;/em&gt; rows]. Every expense endpoint gets the same two changes: add &lt;code&gt;current_user: User = Depends(get_current_user)&lt;/code&gt;, then use &lt;code&gt;current_user.id&lt;/code&gt; — for writes, stamp it; for reads, &lt;strong&gt;filter by it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Create finally retires the shortcut:&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;expense&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Expense&lt;/span&gt;&lt;span class="p"&gt;(&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;model_dump&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;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="c1"&gt;# was DEV_USER_ID
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the isolation-critical read — fetch by id &lt;strong&gt;AND&lt;/strong&gt; owner:&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;@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;/expenses/{expense_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;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ExpenseRead&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;get_one_expense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;expense_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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="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="n"&gt;expense&lt;/span&gt; &lt;span class="o"&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;scalars&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;Expense&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;Expense&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;expense_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Expense&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;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="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&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;expense&lt;/span&gt; &lt;span class="ow"&gt;is&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;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;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;Expense 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;return&lt;/span&gt; &lt;span class="n"&gt;expense&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That double &lt;code&gt;.where(...)&lt;/code&gt; closes the scariest gap in the phase — &lt;strong&gt;IDOR&lt;/strong&gt; [Insecure Direct Object Reference]: the bug where a logged-in User A simply &lt;em&gt;asks for&lt;/em&gt; User B's expense by its id. Because the query filters on &lt;code&gt;user_id&lt;/code&gt; too, A's request for B's row returns nothing → a clean &lt;strong&gt;404&lt;/strong&gt;. The ownership check lives &lt;em&gt;in the query itself&lt;/em&gt;, so I literally cannot forget it later. The list, PATCH, and DELETE endpoints all got the same treatment [filter the list by owner; fetch-scoped-by-owner before updating or deleting], so it's impossible to even &lt;em&gt;load&lt;/em&gt; someone else's row to change it.&lt;/p&gt;

&lt;p&gt;Two small but real decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;404, not 403, when the row isn't yours.&lt;/strong&gt; Saying 403 ["that exists, but you can't have it"] would leak that the id exists. 404 reveals nothing. Same no-leak instinct as the login error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;db.scalars(...)&lt;/code&gt; + &lt;code&gt;.first()&lt;/code&gt; / &lt;code&gt;.all()&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;db.scalars()&lt;/code&gt; hands me clean model objects [not row-tuples], &lt;code&gt;.first()&lt;/code&gt; gives one object or &lt;code&gt;None&lt;/code&gt; [get-one], &lt;code&gt;.all()&lt;/code&gt; gives a list [the list endpoint]. Different endings for different needs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the symbolic finish line — deleting the hardcoded owner. But not before checking it's actually unused:&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="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"DEV_USER_ID"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Habit worth keeping:&lt;/strong&gt; grep-before-delete. It turns "I &lt;em&gt;think&lt;/em&gt; it's unused" into "I've &lt;em&gt;confirmed&lt;/em&gt; it's unused." Only the definition line showed up, so I deleted it. &lt;code&gt;DEV_USER_ID&lt;/code&gt; is officially retired — 3 phases after I promised it would be.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 8: Prove it [the part tutorials skip]
&lt;/h2&gt;

&lt;p&gt;Writing auth code isn't the same as &lt;em&gt;verifying&lt;/em&gt; auth works. So I put on the attacker hat and ran the isolation proof my roadmap demanded, entirely through &lt;code&gt;/docs&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Registered a second user, Bob.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;As User A:&lt;/strong&gt; created an expense [id 1], confirmed &lt;code&gt;GET /expenses&lt;/code&gt; showed only mine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logged out, authorized as Bob&lt;/strong&gt; — the attacker:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /expenses&lt;/code&gt; → &lt;strong&gt;empty list &lt;code&gt;[]&lt;/code&gt;.&lt;/strong&gt; Bob has nothing. [If A's expense showed up here, isolation was broken.]&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /expenses/1&lt;/code&gt;, &lt;code&gt;PATCH /expenses/1&lt;/code&gt;, &lt;code&gt;DELETE /expenses/1&lt;/code&gt; → &lt;strong&gt;404, 404, 404.&lt;/strong&gt; Bob can't read, edit, or delete A's row even though he knows its id.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Back as User A:&lt;/strong&gt; &lt;code&gt;GET /expenses/1&lt;/code&gt; → &lt;strong&gt;200&lt;/strong&gt;, untouched. Bob's attacks never landed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bob is a perfectly valid, authenticated user — and still can't touch A's data. &lt;strong&gt;That's the gap closed, and it's the difference between authentication and authorization made concrete.&lt;/strong&gt; Passing this test is the moment the phase actually ended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stuff I want to remember [the honest takeaways]
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never store a password. Store a one-way hash.&lt;/strong&gt; Hashing ≠ encryption — there's no "decrypt" button, and that's the point.&lt;/li&gt;
&lt;li&gt;bcrypt is &lt;em&gt;deliberately slow&lt;/em&gt; and &lt;em&gt;auto-salted&lt;/em&gt; — same password, different hash every time.&lt;/li&gt;
&lt;li&gt;A JWT's payload is &lt;strong&gt;readable, not encrypted.&lt;/strong&gt; Never put secrets in it. What it gives you is &lt;em&gt;integrity&lt;/em&gt;, guarded by a signature only your server can produce.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;SECRET_KEY&lt;/code&gt; leaking = total auth bypass. It lives in &lt;code&gt;.env&lt;/code&gt;, never in git, and it's generated with real randomness.&lt;/li&gt;
&lt;li&gt;Short token expiry [&lt;code&gt;exp&lt;/code&gt;] limits the damage of a stolen token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;401 = "you failed to authenticate."&lt;/strong&gt; For "that's not yours," return &lt;strong&gt;404, not 403&lt;/strong&gt; — 403 leaks that the thing exists.&lt;/li&gt;
&lt;li&gt;Keep login and validation errors &lt;strong&gt;vague and identical&lt;/strong&gt; — specific ones leak which emails/rows exist [user enumeration].&lt;/li&gt;
&lt;li&gt;Put the ownership filter &lt;strong&gt;in the query&lt;/strong&gt; [&lt;code&gt;.where(user_id == current_user.id)&lt;/code&gt;], not in an afterthought check you can forget.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;os.getenv&lt;/code&gt; &lt;em&gt;reads&lt;/em&gt; env vars; &lt;code&gt;load_dotenv()&lt;/code&gt; &lt;em&gt;loads&lt;/em&gt; the &lt;code&gt;.env&lt;/code&gt; file. Different jobs — you need both.&lt;/li&gt;
&lt;li&gt;Dependencies must be &lt;strong&gt;defined above&lt;/strong&gt; the endpoints that use them — default args evaluate at &lt;code&gt;def&lt;/code&gt; time.&lt;/li&gt;
&lt;li&gt;Read tracebacks &lt;strong&gt;bottom-up&lt;/strong&gt;; the last line is the real error. It'll save you from fixing the wrong thing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;grep&lt;/code&gt; before you delete. Confirm dead code is actually dead.&lt;/li&gt;
&lt;li&gt;Freeze after every install — and sometimes you have to &lt;em&gt;pin&lt;/em&gt; a version [&lt;code&gt;bcrypt==4.0.1&lt;/code&gt;] to keep an older library happy.&lt;/li&gt;
&lt;li&gt;When you cut a corner, label it — and Phase-N-later, actually go back and pay it off. &lt;code&gt;DEV_USER_ID&lt;/code&gt;, you will not be missed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next up: Phase 4. The app now has a memory, a mouth, and a lock on the door. It knows who you are and only shows you your own stuff. What it &lt;em&gt;doesn't&lt;/em&gt; have yet is a way to make sense of all that data at scale — filtering, sorting, pagination, maybe some summaries. If Phase 3 gave the app a bouncer, Phase 4 teaches it to actually organize the room. See you there.&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>python</category>
      <category>security</category>
    </item>
    <item>
      <title>Teaching My Backend to Listen and Reply — FastAPI CRUD, Phase 2</title>
      <dc:creator>Azhar Alvi </dc:creator>
      <pubDate>Wed, 22 Jul 2026 09:38:53 +0000</pubDate>
      <link>https://dev.to/silentcarry/teaching-my-backend-to-listen-and-reply-fastapi-crud-phase-2-pjh</link>
      <guid>https://dev.to/silentcarry/teaching-my-backend-to-listen-and-reply-fastapi-crud-phase-2-pjh</guid>
      <description>&lt;p&gt;&lt;strong&gt;Validate what comes in, shape what goes out, and give every outcome its proper status code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So Phase 1 gave my app a memory — a real database that remembers users and expenses even after a restart. Small problem: the only way to actually &lt;em&gt;talk&lt;/em&gt; to it was a Python script I ran by hand. The app could remember things, but it was mute. Nobody on the internet could add an expense, list their spending, or delete a typo.&lt;/p&gt;

&lt;p&gt;Phase 2 fixes that. This is where the app grows a mouth — real HTTP endpoints you hit through the browser. The buzzword is &lt;strong&gt;CRUD&lt;/strong&gt;: Create, Read, Update, Delete. The four things basically every app does to data. I went in thinking "it's just four functions, how hard can it be" and came out having learned about request/response contracts, dependency injection, and roughly six different HTTP status codes — mostly by triggering them wrong first. Let me dump what I learned [and the parts that tripped me up, because there were, uh, several].&lt;/p&gt;

&lt;p&gt;The Structure is as follows. Lets call it PHASE 2 — The Endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set up the request/response contracts (two Pydantic schemas: one for input, one for output)&lt;/li&gt;
&lt;li&gt;Build a session dependency so every request gets a safe, auto-closing DB connection&lt;/li&gt;
&lt;li&gt;POST — create an expense&lt;/li&gt;
&lt;li&gt;GET — list them all + fetch one [with a proper 404]&lt;/li&gt;
&lt;li&gt;PATCH — update just the fields that changed&lt;/li&gt;
&lt;li&gt;DELETE — remove one cleanly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  First, the two new ideas [and no, nothing to install this time]
&lt;/h2&gt;

&lt;p&gt;Phase 1 had two new libraries. Phase 2 has two new &lt;em&gt;ideas&lt;/em&gt; — and the nice part is there's &lt;strong&gt;nothing to &lt;code&gt;pip install&lt;/code&gt;&lt;/strong&gt;, because Pydantic already ships inside FastAPI. [Which means &lt;code&gt;requirements.txt&lt;/code&gt; didn't change this phase. Still worth knowing the freeze habit is only for phases where you actually install something.]&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pydantic&lt;/strong&gt; — the bouncer at the door. Your SQLAlchemy models describe how data is &lt;em&gt;stored&lt;/em&gt;; Pydantic describes the &lt;em&gt;shape data must have to cross the border of your API&lt;/em&gt;. It checks types, rejects garbage, and turns raw JSON into a clean Python object before it gets anywhere near your database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency injection&lt;/strong&gt; — a fancy name for a simple deal. Instead of each endpoint opening its own database session and hoping to remember to close it, you write "how to get a session" &lt;strong&gt;once&lt;/strong&gt;, and every endpoint just declares "I need one." FastAPI runs it for you and cleans up after. You'll see it as &lt;code&gt;Depends(...)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Two contracts — one for input, one for output
&lt;/h2&gt;

&lt;p&gt;The single biggest lesson of this phase: &lt;strong&gt;you do not use one model for both the data coming in and the data going out.&lt;/strong&gt; Two separate Pydantic schemas, on purpose. This lives in a new file, &lt;code&gt;schemas.py&lt;/code&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="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;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&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;ConfigDict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExpenseCreate&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;What the CLIENT is allowed to send.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&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;gt&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;description&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;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;spent_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExpenseRead&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;What the SERVER sends back — includes DB-generated fields.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;
    &lt;span class="n"&gt;description&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;spent_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&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;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;

    &lt;span class="n"&gt;model_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ConfigDict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_attributes&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why two, and not one? Because input and output have genuinely different jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ExpenseCreate&lt;/code&gt; (input)&lt;/strong&gt; contains &lt;em&gt;only&lt;/em&gt; the fields a client is allowed to send. Notice what's missing: no &lt;code&gt;id&lt;/code&gt;, no &lt;code&gt;created_at&lt;/code&gt;, no &lt;code&gt;updated_at&lt;/code&gt;. Those are decided by the database, not the caller. Letting a client send its own &lt;code&gt;id&lt;/code&gt; is a classic foot-gun.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ExpenseRead&lt;/code&gt; (output)&lt;/strong&gt; is what you send back, and it &lt;em&gt;does&lt;/em&gt; include those server-generated fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Field(gt=0)&lt;/code&gt;&lt;/strong&gt; is Pydantic validating for me: an amount of &lt;code&gt;0&lt;/code&gt; or &lt;code&gt;-5&lt;/code&gt; gets auto-rejected with a clean error. &lt;code&gt;min_length&lt;/code&gt; does the same for text. The bouncer, doing its job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;model_config = ConfigDict(from_attributes=True)&lt;/code&gt;&lt;/strong&gt; is the one line that lets Pydantic build the output straight from my SQLAlchemy object [reading &lt;code&gt;.id&lt;/code&gt;, &lt;code&gt;.amount&lt;/code&gt; etc. as attributes]. In Pydantic v1 this was &lt;code&gt;orm_mode&lt;/code&gt;; v2 renamed it to &lt;code&gt;from_attributes&lt;/code&gt;. Just know it exists — you'll feel why it matters in Step 3.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And yep, &lt;code&gt;amount&lt;/code&gt; stays &lt;code&gt;Decimal&lt;/code&gt; here too. Same Phase 1 rule: money never touches a float, not even at the API boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick detour: who owns this expense?
&lt;/h2&gt;

&lt;p&gt;Before I could create anything, I hit a wall that's actually a good lesson. My &lt;code&gt;Expense&lt;/code&gt; has a &lt;code&gt;user_id&lt;/code&gt; foreign key — the database will reject any expense that doesn't point at a real user. But I haven't built login yet [that's Phase 3]. So… who's the owner?&lt;/p&gt;

&lt;p&gt;The honest answer: I cheated, on purpose, and wrote down that I cheated.&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;DEV_USER_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;  &lt;span class="c1"&gt;# TEMP: Phase 3 replaces this with the authenticated user
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I seeded one "dev user" and hardcoded its id as the owner of every expense. In a real app the owner comes from whoever's logged in. But the whole point of learning one thing at a time is &lt;em&gt;not&lt;/em&gt; tangling auth into CRUD. So I structured it so Phase 3 is a one-line swap: &lt;code&gt;DEV_USER_ID&lt;/code&gt; → &lt;code&gt;current_user.id&lt;/code&gt;. Shortcut taken, shortcut labeled.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #1:&lt;/strong&gt; My first attempt to seed the dev user blew up with &lt;code&gt;NOT NULL constraint failed: users.name&lt;/code&gt;. My &lt;code&gt;User&lt;/code&gt; model requires a &lt;code&gt;name&lt;/code&gt;, and I hadn't given one. Same rule as always — if a column is required, you have to supply it. [Bonus: the failed insert did a clean &lt;code&gt;ROLLBACK&lt;/code&gt;, so no half-broken row got saved. The transaction safety I read about in Phase 1, actually doing its thing.]&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 2: The session dependency [write it once, never leak a connection]
&lt;/h2&gt;

&lt;p&gt;Every endpoint needs a database session, and every session must close afterward — even if the request explodes halfway through. So instead of copy-pasting that logic into five endpoints, it goes in &lt;code&gt;database.py&lt;/code&gt; once:&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;from&lt;/span&gt; &lt;span class="n"&gt;collections.abc&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Generator&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;Session&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;Generator&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="bp"&gt;None&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;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SessionLocal&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;db&lt;/span&gt;
    &lt;span class="k"&gt;finally&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;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The weird-looking part is the &lt;strong&gt;&lt;code&gt;yield&lt;/code&gt; inside a &lt;code&gt;try/finally&lt;/code&gt;&lt;/strong&gt;. A normal function &lt;code&gt;return&lt;/code&gt;s and it's done. This one &lt;code&gt;yield&lt;/code&gt;s the session out to my endpoint, &lt;em&gt;pauses&lt;/em&gt; while the endpoint runs, then comes back and runs the &lt;code&gt;finally&lt;/code&gt; block no matter what. That &lt;code&gt;finally&lt;/code&gt; is the whole point: &lt;code&gt;db.close()&lt;/code&gt; is guaranteed to run on success &lt;em&gt;and&lt;/em&gt; on error, so a session can never leak. FastAPI understands this pattern and drives it for me. [I don't fully grok generators yet, and that's fine — for now: &lt;code&gt;yield&lt;/code&gt; = "hand it out, then come back and clean up."]&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: POST — create an expense [it all comes together]
&lt;/h2&gt;

&lt;p&gt;This is where Pydantic, the session dependency, and my model finally click into one endpoint. It goes in &lt;code&gt;main.py&lt;/code&gt;, where &lt;code&gt;app&lt;/code&gt; lives:&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;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;Depends&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.orm&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Session&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Expense&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;schemas&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ExpenseCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ExpenseRead&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;/expenses&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;ExpenseRead&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_201_CREATED&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;create_expense&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;ExpenseCreate&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="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;expense&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Expense&lt;/span&gt;&lt;span class="p"&gt;(&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;model_dump&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;DEV_USER_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="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expense&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;commit&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;refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expense&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;expense&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line is pulling weight, and it took me a minute to appreciate that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The &lt;code&gt;@app.post(...)&lt;/code&gt; decorator&lt;/strong&gt; is the wiring. &lt;code&gt;@&lt;/code&gt; means "attach behavior to this function" — here, "when a POST hits &lt;code&gt;/expenses&lt;/code&gt;, run me." I write the logic; the decorator handles reading the request and sending the response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;response_model=ExpenseRead&lt;/code&gt;&lt;/strong&gt; runs whatever I return &lt;em&gt;through&lt;/em&gt; the output schema before sending it. This is what stops internal fields from ever leaking — even if my model grew a secret column tomorrow, only &lt;code&gt;ExpenseRead&lt;/code&gt;'s fields go out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;status_code=201&lt;/code&gt;&lt;/strong&gt; because REST says "created" is a 201, not a plain 200. Small thing, correct thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;payload: ExpenseCreate&lt;/code&gt;&lt;/strong&gt; — just by type-hinting the parameter, FastAPI auto-reads the JSON body, validates it against the schema, and hands me a clean object. I never touch raw JSON.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Depends(get_db)&lt;/code&gt;&lt;/strong&gt; — the Step 2 dependency in action. I just get a ready-to-use &lt;code&gt;db&lt;/code&gt; that closes itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;`Expense(&lt;/strong&gt;payload.model_dump(), user_id=DEV_USER_ID)&lt;code&gt;** — &lt;/code&gt;model_dump()&lt;code&gt; turns the validated object into a dict, &lt;/code&gt;**` unpacks it into the model, and I tack on the owner. [That's the one line Phase 3 will change.]&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;add&lt;/code&gt; → &lt;code&gt;commit&lt;/code&gt; → &lt;code&gt;refresh&lt;/code&gt;&lt;/strong&gt; — stage it, write it, then re-read it so the DB-generated &lt;code&gt;id&lt;/code&gt;/timestamps get populated on my object before I return it.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #2:&lt;/strong&gt; My first POST returned a &lt;code&gt;500&lt;/code&gt; with &lt;code&gt;'ExpenseCreate' object has no attribute 'model'&lt;/code&gt;. The cause was a one-character typo: I'd written &lt;code&gt;payload.model.dump()&lt;/code&gt; [a dot] instead of &lt;code&gt;payload.model_dump()&lt;/code&gt; [an underscore]. &lt;code&gt;model_dump()&lt;/code&gt; is a &lt;em&gt;single&lt;/em&gt; Pydantic v2 method name, not two things chained with a dot. [If you see &lt;code&gt;.dict()&lt;/code&gt; in old tutorials, that's the v1 name for the same thing.]&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha #3:&lt;/strong&gt; Next &lt;code&gt;500&lt;/code&gt;: &lt;code&gt;NOT NULL constraint failed: expenses.spent_on&lt;/code&gt;. My &lt;code&gt;Expense&lt;/code&gt; requires &lt;code&gt;spent_on&lt;/code&gt;, but my input schema didn't include it, so nothing got sent for it. Fix: add &lt;code&gt;spent_on&lt;/code&gt; to &lt;code&gt;ExpenseCreate&lt;/code&gt;. The rule crystallized here — &lt;strong&gt;every required DB column must exist in the input schema.&lt;/strong&gt; Whack-a-mole ends the moment you check your model for all its &lt;code&gt;NOT NULL&lt;/code&gt; columns up front.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 4: GET — list them all, and fetch one [meet the 404]
&lt;/h2&gt;

&lt;p&gt;Two reads, two conventions: &lt;code&gt;GET /expenses&lt;/code&gt; returns a list; &lt;code&gt;GET /expenses/{id}&lt;/code&gt; returns one.&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;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;List&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="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;select&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;/expenses&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;ExpenseRead&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;list_expenses&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="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="k"&gt;return&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;scalars&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;Expense&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;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;/expenses/{expense_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;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ExpenseRead&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;get_expense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expense_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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="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;expense&lt;/span&gt; &lt;span class="o"&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Expense&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expense_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;expense&lt;/span&gt; &lt;span class="ow"&gt;is&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;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;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;Expense 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;return&lt;/span&gt; &lt;span class="n"&gt;expense&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;db.scalars(select(Expense)).all()&lt;/code&gt;&lt;/strong&gt; is the same SQLAlchemy 2.0 read from Phase 1, just returning the whole list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;db.get(Expense, id)&lt;/code&gt;&lt;/strong&gt; is the shortcut for "fetch one by primary key" — returns the object, or &lt;code&gt;None&lt;/code&gt; if there's no such id.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;raise HTTPException(404)&lt;/code&gt;&lt;/strong&gt; is the important habit. If I let &lt;code&gt;None&lt;/code&gt; sail through, the code crashes trying to serialize nothing → a &lt;code&gt;500&lt;/code&gt;, which wrongly implies &lt;em&gt;I&lt;/em&gt; broke. Instead I deliberately raise a &lt;strong&gt;404 Not Found&lt;/strong&gt;: "your request was fine, that thing just isn't here." &lt;code&gt;raise&lt;/code&gt; [not &lt;code&gt;return&lt;/code&gt;] stops the function dead and sends the error.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And here's the coolest thing I learned this phase, entirely by fat-fingering the docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ask for &lt;code&gt;/expenses/abc&lt;/code&gt; [a string where an int belongs] → you get a &lt;strong&gt;422&lt;/strong&gt;. FastAPI checks the path type &lt;em&gt;before your function even runs&lt;/em&gt; and decides the request itself is malformed.&lt;/li&gt;
&lt;li&gt;Ask for &lt;code&gt;/expenses/999999&lt;/code&gt; [a valid integer that doesn't exist] → you get a &lt;strong&gt;404&lt;/strong&gt;. The request was well-formed, so the function runs, finds nothing, and raises 404.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So: &lt;strong&gt;422 = "your request was malformed"&lt;/strong&gt; vs &lt;strong&gt;404 = "your request was fine, the resource doesn't exist."&lt;/strong&gt; I built both behaviors correctly without even meaning to. That distinction is going straight into my mental model.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #4:&lt;/strong&gt; My list endpoint gave a "cannot fetch" until I noticed I'd dropped the leading &lt;code&gt;/&lt;/code&gt; in the route path. Added the slash, instantly worked. The kind of bug that's invisible for ten minutes and obvious the second you spot it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 5: PATCH — change only what changed
&lt;/h2&gt;

&lt;p&gt;Update has two flavors, and picking the right one matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PUT&lt;/strong&gt; = "replace the whole resource." The client must resend &lt;em&gt;every&lt;/em&gt; field; anything left out gets wiped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PATCH&lt;/strong&gt; = "apply a partial change." Send only the fields you're changing; everything else stays.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For fixing a typo in one expense, forcing the user to resend amount + description + date [PUT's demand] is clumsy. So I built &lt;strong&gt;PATCH.&lt;/strong&gt; It needs its own schema where every field is optional:&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;ExpenseUpdate&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;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&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;default&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="n"&gt;gt&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;description&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="bp"&gt;None&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;default&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="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;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;spent_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&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="nd"&gt;@app.patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/expenses/{expense_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;response_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ExpenseRead&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;update_expense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expense_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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;ExpenseUpdate&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="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;expense&lt;/span&gt; &lt;span class="o"&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Expense&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expense_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;expense&lt;/span&gt; &lt;span class="ow"&gt;is&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;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;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;Expense not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;update_data&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;model_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exclude_unset&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;for&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;update_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="nf"&gt;setattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expense&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&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;commit&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;refresh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expense&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;expense&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;X | None = None&lt;/code&gt;&lt;/strong&gt; makes a field optional — "either a &lt;code&gt;Decimal&lt;/code&gt; or nothing." [Older tutorials write &lt;code&gt;Optional[Decimal]&lt;/code&gt;; same meaning, &lt;code&gt;| None&lt;/code&gt; is the modern style.] Note the validation still rides along: if &lt;code&gt;amount&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; sent, it must still be &lt;code&gt;&amp;gt; 0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;model_dump(exclude_unset=True)&lt;/code&gt;&lt;/strong&gt; is the heart of PATCH — it gives me a dict of &lt;em&gt;only the fields the client actually sent&lt;/em&gt;, ignoring the untouched ones. Without it, an omitted field would come through as &lt;code&gt;None&lt;/code&gt; and I'd accidentally erase existing data. This one flag avoids the whole trap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;setattr(expense, field, value)&lt;/code&gt;&lt;/strong&gt; sets an attribute by a name held in a variable — the dynamic version of &lt;code&gt;expense.amount = ...&lt;/code&gt;. Since I don't know in advance &lt;em&gt;which&lt;/em&gt; fields arrived, I loop and set each one.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #5:&lt;/strong&gt; Two things bit me here. First, another &lt;code&gt;500&lt;/code&gt;, because I typed &lt;code&gt;details=&lt;/code&gt; instead of &lt;code&gt;detail=&lt;/code&gt; in &lt;code&gt;HTTPException&lt;/code&gt; — a bad keyword makes the exception itself blow up. Second, and sneakier: when I hit &lt;strong&gt;Execute&lt;/strong&gt; in &lt;code&gt;/docs&lt;/code&gt; without editing the body, my &lt;code&gt;amount&lt;/code&gt; came back as some huge float. Turns out &lt;strong&gt;Swagger pre-fills the request body with placeholder sample values&lt;/strong&gt; — clicking Execute doesn't send "nothing," it sends &lt;em&gt;those placeholders&lt;/em&gt;. So PATCH dutifully saved a giant number. [Combined with SQLite storing &lt;code&gt;Numeric&lt;/code&gt; as float, it looked even uglier — that tidies up on Postgres later.] Lesson: to test a partial update, edit the body down to just the field you mean, e.g. &lt;code&gt;{"amount": 99.99}&lt;/code&gt;. &lt;code&gt;exclude_unset&lt;/code&gt; can't exclude what Swagger auto-filled for you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 6: DELETE — remove one, return nothing
&lt;/h2&gt;

&lt;p&gt;Last verb, and it introduces one more status code:&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;@app.delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/expenses/{expense_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;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_204_NO_CONTENT&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;delete_expense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expense_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&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="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;expense&lt;/span&gt; &lt;span class="o"&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Expense&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expense_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;expense&lt;/span&gt; &lt;span class="ow"&gt;is&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;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;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;Expense not found&lt;/span&gt;&lt;span class="sh"&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;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expense&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;commit&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;204 No Content&lt;/strong&gt; is the convention for a successful delete: "it worked, and there's deliberately nothing to send back." That's why this endpoint has no &lt;code&gt;response_model&lt;/code&gt; — there's no resource left to shape.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;db.delete()&lt;/code&gt; + &lt;code&gt;db.commit()&lt;/code&gt;&lt;/strong&gt; is the destructive bit. Once committed, the row is &lt;em&gt;gone&lt;/em&gt; — no undo. So I tested it on a throwaway expense, deleted it [204], then GET-ed the same id to confirm it returned a clean 404. Gone means gone.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  One non-code head-scratcher [worth writing down]
&lt;/h2&gt;

&lt;p&gt;At one point &lt;code&gt;127.0.0.1:8000/docs&lt;/code&gt; took &lt;em&gt;ages&lt;/em&gt; to load and I assumed my code was broken. It wasn't. &lt;code&gt;/docs&lt;/code&gt; is Swagger UI, and by default FastAPI tells your &lt;strong&gt;browser to download Swagger's JS/CSS from a public CDN&lt;/strong&gt; — which crawls on a slow or corporate/proxied network. My actual API was instant [hitting &lt;code&gt;/expenses&lt;/code&gt; directly proved it]. Nice reminder: a slow docs &lt;em&gt;page&lt;/em&gt; is not a slow &lt;em&gt;app&lt;/em&gt;. [The production fix is self-hosting those assets, but that's polish for a later hardening phase.]&lt;/p&gt;

&lt;h2&gt;
  
  
  Stuff I want to remember [the honest takeaways]
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One model for input, a different one for output. Never accept or expose fields you shouldn't.&lt;/li&gt;
&lt;li&gt;Every required DB column must appear in your input schema — check the model up front instead of playing 500-error whack-a-mole.&lt;/li&gt;
&lt;li&gt;Status codes carry meaning: &lt;strong&gt;201&lt;/strong&gt; create, &lt;strong&gt;200&lt;/strong&gt; read/update, &lt;strong&gt;204&lt;/strong&gt; delete, &lt;strong&gt;404&lt;/strong&gt; missing resource, &lt;strong&gt;422&lt;/strong&gt; malformed request.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;422&lt;/code&gt; vs &lt;code&gt;404&lt;/code&gt; is a real distinction: bad &lt;em&gt;shape&lt;/em&gt; vs valid-but-absent.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;raise HTTPException(...)&lt;/code&gt; [not &lt;code&gt;return&lt;/code&gt;] is how you send an error and stop.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;model_dump()&lt;/code&gt; is one method with an underscore. &lt;code&gt;model_dump(exclude_unset=True)&lt;/code&gt; is what makes PATCH safe.&lt;/li&gt;
&lt;li&gt;Write your DB-session logic once as a &lt;code&gt;Depends&lt;/code&gt; dependency with &lt;code&gt;yield&lt;/code&gt;/&lt;code&gt;finally&lt;/code&gt; — never leak a connection.&lt;/li&gt;
&lt;li&gt;Money stays &lt;code&gt;Decimal&lt;/code&gt; all the way to the API edge.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;db.delete()&lt;/code&gt; + &lt;code&gt;commit()&lt;/code&gt; is permanent. Test destructive stuff on junk data.&lt;/li&gt;
&lt;li&gt;Swagger pre-fills placeholder values — a blind "Execute" can overwrite real data.&lt;/li&gt;
&lt;li&gt;When you cut a corner [like hardcoding the owner], write it down and leave yourself the one-line path back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next up: Phase 3, Authentication — where that hardcoded &lt;code&gt;DEV_USER_ID&lt;/code&gt; finally retires. The app has a memory and a mouth; now it needs a lock on the door: registration, hashed passwords, JWT logins, and making sure no user can peek at another's expenses. If Phase 2 gave the app a mouth, Phase 3 gives it a bouncer. See you there.&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>My App Can Finally Remember Stuff! (Phase 1 — The Database)</title>
      <dc:creator>Azhar Alvi </dc:creator>
      <pubDate>Wed, 22 Jul 2026 09:23:28 +0000</pubDate>
      <link>https://dev.to/silentcarry/my-app-can-finally-remember-stuff-phase-1-the-database-58lg</link>
      <guid>https://dev.to/silentcarry/my-app-can-finally-remember-stuff-phase-1-the-database-58lg</guid>
      <description>&lt;p&gt;&lt;strong&gt;Design it on paper, model it in Python, and NEVER touch the schema by hand.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So Phase 0 got my little FastAPI app running. One route, one JSON response, a lot of pride. But here's the thing nobody warns you about: that app had the memory of a goldfish. Restart it and… nothing. No users, no data, no clue who anyone is.&lt;/p&gt;

&lt;p&gt;Phase 1 fixes that. This is where the app gets an actual database — a place to &lt;em&gt;remember&lt;/em&gt; things. And honestly? This is where "I'm following a tutorial" started turning into "oh, I kind of get how backends work now." I went in knowing zero SQL. Came out having designed tables, built models, run a real migration, and read/written actual data. Let me dump what I learned [and the parts that tripped me up, because there were a few].&lt;/p&gt;

&lt;p&gt;The Structure is as follows. Lets call it PHASE 1 — The Data Layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Get the right tools installed in the right place (SQLAlchemy + Alembic)&lt;/li&gt;
&lt;li&gt;Design the tables on paper BEFORE writing any code&lt;/li&gt;
&lt;li&gt;Turn that design into Python models&lt;/li&gt;
&lt;li&gt;Wire up the database connection&lt;/li&gt;
&lt;li&gt;Set up Alembic and run my first migration&lt;/li&gt;
&lt;li&gt;Actually write and read a row [the payoff!]&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhechw7ncijflrrioakqn.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhechw7ncijflrrioakqn.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  First, the two new toys
&lt;/h2&gt;

&lt;p&gt;Two libraries do the heavy lifting here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQLAlchemy&lt;/strong&gt; — the ORM. Fancy word, simple job: it lets me talk to the database in Python instead of writing raw SQL. I write &lt;code&gt;session.add(user)&lt;/code&gt;, it writes the &lt;code&gt;INSERT&lt;/code&gt; for me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alembic&lt;/strong&gt; — the migration tool. Think of it as git, but for the &lt;em&gt;shape&lt;/em&gt; of your database. Every time I change the table structure, it saves that change as a numbered file I can replay or roll back.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source &lt;/span&gt;venv/Scripts/activate      &lt;span class="c"&gt;# Windows/Git Bash = Scripts/, not bin/ [learned this the hard way in Phase 0]&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;sqlalchemy alembic
pip freeze &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #1:&lt;/strong&gt; I first installed these in a random throwaway folder, then wondered why my real project couldn't see them. Turns out a virtual environment is per-folder — packages installed in one venv are invisible to another. Reinstalled in the actual project. Lesson: check for &lt;code&gt;(venv)&lt;/code&gt; in your prompt and make sure it's the &lt;em&gt;right&lt;/em&gt; one before you install anything.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 1: Draw it on paper (yes, really)
&lt;/h2&gt;

&lt;p&gt;Before touching code, I sketched two boxes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;USERS                              EXPENSES
  id       (primary key)             id         (primary key)
  name                               amount     (money)
  email    (unique)                  description
  created_at / updated_at            spent_on   (the date)
                                     user_id  --&amp;gt; points to USERS.id
                                     created_at / updated_at
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two ideas that made everything click:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;primary key&lt;/strong&gt; (&lt;code&gt;id&lt;/code&gt;) is just a unique, auto-numbered tag for each row. Names repeat, IDs don't.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;foreign key&lt;/strong&gt; (&lt;code&gt;user_id&lt;/code&gt;) is a column that stores another table's id. That's the whole magic of relational databases — Azhar's expenses all carry his &lt;code&gt;user_id&lt;/code&gt;, so the DB always knows whose lunch was whose.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sounds obvious now. Was NOT obvious an hour earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Turn the drawing into models
&lt;/h2&gt;

&lt;p&gt;This is &lt;code&gt;models.py&lt;/code&gt; — basically my paper sketch, but in Python. One class = one table.&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;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;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&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;MetaData&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;DateTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Numeric&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ForeignKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;func&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;DeclarativeBase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mapped_column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;relationship&lt;/span&gt;


&lt;span class="n"&gt;NAMING_CONVENTION&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;ix&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;ix_%(column_0_label)s&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;uq&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;uq_%(table_name)s_%(column_0_name)s&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;ck&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;ck_%(table_name)s_%(constraint_name)s&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;fk&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;fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s&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;pk&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;pk_%(table_name)s&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;class&lt;/span&gt; &lt;span class="nc"&gt;Base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DeclarativeBase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MetaData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;naming_convention&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NAMING_CONVENTION&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;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="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&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;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&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="nf"&gt;mapped_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;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&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="nf"&gt;mapped_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;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&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;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&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;server_default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;func&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;updated_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&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;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&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;server_default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;func&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;onupdate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;func&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;expenses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Expense&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="nf"&gt;relationship&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;user&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;Expense&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;expenses&lt;/span&gt;&lt;span class="sh"&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;Mapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&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;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Numeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&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="n"&gt;Mapped&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="nf"&gt;mapped_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;spent_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Date&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;Mapped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&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;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;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&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;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&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;server_default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;func&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;updated_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&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;=&lt;/span&gt; &lt;span class="nf"&gt;mapped_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&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;server_default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;func&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;onupdate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;func&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;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mapped&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="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="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;expenses&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;A few things I picked up here that felt like "real developer" moments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Money is NEVER a float.&lt;/strong&gt; Ever. Floats can't hold &lt;code&gt;0.10 + 0.20&lt;/code&gt; exactly [it comes out as 0.30000000000000004, I'm not kidding], so over enough expenses your totals drift. Store it as &lt;code&gt;Numeric(10, 2)&lt;/code&gt; → Python &lt;code&gt;Decimal&lt;/code&gt;. Exact, always.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every table gets &lt;code&gt;created_at&lt;/code&gt; and &lt;code&gt;updated_at&lt;/code&gt;.&lt;/strong&gt; You will always, always want to know when something was made or changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The type annotation controls nullability.&lt;/strong&gt; &lt;code&gt;Mapped[str]&lt;/code&gt; = required. &lt;code&gt;Mapped[str | None]&lt;/code&gt; = optional. Neat.&lt;/li&gt;
&lt;li&gt;Those two &lt;code&gt;relationship(...)&lt;/code&gt; lines aren't columns — they're just Python convenience so I can write &lt;code&gt;user.expenses&lt;/code&gt; and get a list back without writing a query. The actual link is still the &lt;code&gt;user_id&lt;/code&gt; foreign key.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #2:&lt;/strong&gt; I originally named the expense date column &lt;code&gt;date&lt;/code&gt;. Bad idea — naming a column the same as its Python type (&lt;code&gt;date: Mapped[date]&lt;/code&gt;) confuses everything and breaks type inference. Renamed it &lt;code&gt;spent_on&lt;/code&gt;. Moral: don't name your stuff after builtins.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 3: The connection [kept separate on purpose]
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;database.py&lt;/code&gt; — this is &lt;em&gt;how&lt;/em&gt; to connect, deliberately kept apart from &lt;code&gt;models.py&lt;/code&gt; [which is &lt;em&gt;what&lt;/em&gt; the tables are]:&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;os&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;create_engine&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="n"&gt;DATABASE_URL&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;DATABASE_URL&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;sqlite:///./expenses.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;connect_args&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;check_same_thread&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&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;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sqlite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&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;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_engine&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;echo&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;connect_args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;connect_args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;SessionLocal&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;bind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;autoflush&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;autocommit&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why the &lt;code&gt;os.getenv&lt;/code&gt; dance instead of just hardcoding the path? Because future-me deploying to a real Postgres database will just set an environment variable and NOT have to change a single line of code. Same reason secrets live in &lt;code&gt;.env&lt;/code&gt;. One source of truth. Also — &lt;code&gt;echo=True&lt;/code&gt; prints every SQL statement the ORM generates, which is weirdly satisfying to watch.&lt;/p&gt;

&lt;p&gt;Oh, and before creating any of this I added &lt;code&gt;*.db&lt;/code&gt; and &lt;code&gt;*.sqlite3&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;. The database file is local junk data — it has no business in my repo. [And yeah, same "before it exists" trick I used for &lt;code&gt;.env&lt;/code&gt; — git can't accidentally track what it was already told to ignore.]&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Alembic and my first migration
&lt;/h2&gt;

&lt;p&gt;Here's the part I was low-key scared of, and it turned out to be the coolest bit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;alembic init alembic     &lt;span class="c"&gt;# creates the migration setup&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I pointed Alembic at my project in &lt;code&gt;alembic/env.py&lt;/code&gt; — told it where my models live and where the database is:&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;from&lt;/span&gt; &lt;span class="n"&gt;database&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DATABASE_URL&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Base&lt;/span&gt;

&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_main_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sqlalchemy.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;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;target_metadata&lt;/span&gt; &lt;span class="o"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the important bit: Alembic's autogenerate works by &lt;em&gt;comparing&lt;/em&gt; my models to the actual database and writing the difference. &lt;code&gt;target_metadata&lt;/code&gt; is how it "sees" my models. Without it, it detects nothing.&lt;/p&gt;

&lt;p&gt;I also added that &lt;code&gt;NAMING_CONVENTION&lt;/code&gt; thing (up in &lt;code&gt;models.py&lt;/code&gt;) BEFORE generating anything. Why bother? Because otherwise the database invents random names for your constraints/indexes — and SQLite often gives them no name at all. Then a future migration that needs to change one has nothing to grab onto. Set the convention early, and everything gets clean, predictable names like &lt;code&gt;fk_expenses_user_id_users&lt;/code&gt;. Set-it-and-forget-it.&lt;/p&gt;

&lt;p&gt;Then the magic command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;alembic revision &lt;span class="nt"&gt;--autogenerate&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"create users and expenses tables"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it &lt;em&gt;wrote the migration for me&lt;/em&gt; — two &lt;code&gt;create_table&lt;/code&gt; calls, the foreign key, the indexes, plus a matching &lt;code&gt;downgrade()&lt;/code&gt; that undoes it all. I read the whole file before running it [apparently this is a habit real teams enforce — autogenerate isn't perfect, so you always eyeball it first]. Looked right. So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;alembic upgrade &lt;span class="nb"&gt;head&lt;/span&gt;     &lt;span class="c"&gt;# actually builds the tables&lt;/span&gt;
alembic current          &lt;span class="c"&gt;# shows my revision with (head) = up to date&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watching the real &lt;code&gt;CREATE TABLE&lt;/code&gt; SQL scroll past because of &lt;code&gt;echo=True&lt;/code&gt;? Chef's kiss. That was the "oh, THIS is what an ORM does" moment.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #3:&lt;/strong&gt; My &lt;code&gt;-m&lt;/code&gt; message accidentally said "expense" (singular), so the migration &lt;em&gt;filename&lt;/em&gt; said expense too — and I panicked thinking the tables were wrong. They weren't. The &lt;code&gt;-m&lt;/code&gt; text is just a label; the real table names come from &lt;code&gt;__tablename__&lt;/code&gt;. Totally cosmetic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha #4:&lt;/strong&gt; When I pasted my code into an email, Gmail helpfully turned &lt;code&gt;users.id&lt;/code&gt; into a clickable link &lt;code&gt;http://users.id/&lt;/code&gt;. Nearly gave me a heart attack. It's just email being email — the real file was fine. Check your code in the editor, not in email.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 5: The payoff — write a row, read it back
&lt;/h2&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;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&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;date&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;select&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;database&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SessionLocal&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt; &lt;span class="kn"&gt;import&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;Expense&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;SessionLocal&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="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;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Azhar&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="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;azhar@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;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="n"&gt;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="c1"&gt;# NOW it's in the DB, and user.id gets assigned
&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;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Expense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;12.50&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lunch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;spent_on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;today&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&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="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="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;fetched&lt;/span&gt; &lt;span class="o"&gt;=&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;scalars&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;first&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;fetched&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;fetched&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="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="n"&gt;description&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="n"&gt;amount&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;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fetched&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expenses&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it printed my user and their lunch. My app remembers things now. 🎉&lt;/p&gt;

&lt;p&gt;Couple of things worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nothing actually hits the database until &lt;code&gt;commit()&lt;/code&gt;. That's also &lt;em&gt;when&lt;/em&gt; the primary key gets assigned — which is exactly why I commit the user before the expense that needs its &lt;code&gt;id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Decimal("12.50")&lt;/code&gt; uses quotes on purpose. &lt;code&gt;Decimal(12.50)&lt;/code&gt; [no quotes] would drag the float imprecision back in. Little detail, big deal for money.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;select(User)&lt;/code&gt; with &lt;code&gt;session.scalars(...)&lt;/code&gt; is the modern SQLAlchemy 2.0 way. If you see &lt;code&gt;session.query(...)&lt;/code&gt; in older tutorials, that's the legacy style.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha #5:&lt;/strong&gt; Ran the script a second time and it blew up with an &lt;code&gt;IntegrityError&lt;/code&gt; on the email. That's not a bug — it's my &lt;code&gt;unique&lt;/code&gt; constraint literally doing its job and refusing a duplicate. Honestly kind of reassuring to see it work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Stuff I want to remember [the honest takeaways]
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Design on paper first. The code is just the drawing, translated.&lt;/li&gt;
&lt;li&gt;Money is &lt;code&gt;Decimal&lt;/code&gt;/&lt;code&gt;Numeric&lt;/code&gt;, never float. Build it from a string.&lt;/li&gt;
&lt;li&gt;Give every table &lt;code&gt;created_at&lt;/code&gt; / &lt;code&gt;updated_at&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Never hardcode the database URL — read it from an env var.&lt;/li&gt;
&lt;li&gt;NEVER edit the database structure by hand. Models → migration, every single time.&lt;/li&gt;
&lt;li&gt;Always read an autogenerated migration before you run it.&lt;/li&gt;
&lt;li&gt;Set your naming convention before the first migration, not after.&lt;/li&gt;
&lt;li&gt;Virtual environments are per-project. Install in the right one. Freeze &lt;code&gt;requirements.txt&lt;/code&gt; after every install.&lt;/li&gt;
&lt;li&gt;Ignore &lt;code&gt;.db&lt;/code&gt; files and secrets &lt;em&gt;before&lt;/em&gt; they exist. And always commit your migration files — they're the schema's shared history.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;echo=True&lt;/code&gt; on while learning so you can actually see the SQL. It demystifies everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next up: Phase 2, where I turn all this into real API endpoints [POST an expense, GET the list, the whole CRUD gang]. If Phase 1 gave the app a memory, Phase 2 gives it a mouth. See you there.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Phase 0 : Building My First FastAPI App: Setup, Git, and Secrets Done Right</title>
      <dc:creator>Azhar Alvi </dc:creator>
      <pubDate>Thu, 16 Jul 2026 18:57:38 +0000</pubDate>
      <link>https://dev.to/silentcarry/phase-0-building-my-first-fastapi-app-setup-git-and-secrets-done-right-38m6</link>
      <guid>https://dev.to/silentcarry/phase-0-building-my-first-fastapi-app-setup-git-and-secrets-done-right-38m6</guid>
      <description>&lt;p&gt;&lt;strong&gt;Tagline: Commit small, commit often, write real messages.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Creating a hello-world app sounds easy — and it is. But doing it &lt;em&gt;the right way&lt;/em&gt;, with proper git hygiene, isolated environments, and secrets handled safely from day one? That takes a bit more care. Here's what I actually did, step by step, and why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Index
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;Structure of this log&lt;/li&gt;
&lt;li&gt;
Step 1: Project Foundations

&lt;ul&gt;
&lt;li&gt;Create the folder&lt;/li&gt;
&lt;li&gt;Initialize git first&lt;/li&gt;
&lt;li&gt;Create &lt;code&gt;.gitignore&lt;/code&gt; before installing anything&lt;/li&gt;
&lt;li&gt;Create and activate the virtual environment&lt;/li&gt;
&lt;li&gt;Install packages + freeze requirements&lt;/li&gt;
&lt;li&gt;First commit&lt;/li&gt;
&lt;li&gt;Connect to GitHub&lt;/li&gt;
&lt;li&gt;The repeatable loop&lt;/li&gt;
&lt;li&gt;The commit flow, in isolation&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Step 2: Getting FastAPI Running Locally

&lt;ul&gt;
&lt;li&gt;Ensure the virtual environment is active&lt;/li&gt;
&lt;li&gt;Install FastAPI and Uvicorn&lt;/li&gt;
&lt;li&gt;Create the app file&lt;/li&gt;
&lt;li&gt;Start the server&lt;/li&gt;
&lt;li&gt;Check the automatic docs&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Step 3: Secrets — the &lt;code&gt;.env&lt;/code&gt; File

&lt;ul&gt;
&lt;li&gt;Create &lt;code&gt;.env&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Confirm it's actually ignored&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Step 4: Start This Build Log&lt;/li&gt;
&lt;li&gt;Key Habits to Keep&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I started learning backend dev properly this week, and today I got a hello-world FastAPI app running locally. Nothing fancy — one route, one JSON response — but I wanted to write down what actually happened along the way, because "install a package and run a command" always looks simpler in hindsight than it feels in the moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structure of this log
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create project folder → init git → &lt;code&gt;.gitignore&lt;/code&gt; → virtual environment&lt;/li&gt;
&lt;li&gt;Connect to a GitHub repo&lt;/li&gt;
&lt;li&gt;Build and run a FastAPI hello-world app, explore &lt;code&gt;/docs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set up a &lt;code&gt;.env&lt;/code&gt; file for secrets and confirm it's gitignored&lt;/li&gt;
&lt;li&gt;Start this build log&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 1: Project Foundations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Create the folder
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;my-project-1
&lt;span class="nb"&gt;cd &lt;/span&gt;my-project-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Initialize git &lt;em&gt;first&lt;/em&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why first?&lt;/strong&gt; So every file created from this point on is tracked from day one — no risk of forgetting to init later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create &lt;code&gt;.gitignore&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; installing anything
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .gitignore &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
venv/
__pycache__/
.env
.DS_Store
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why before the virtual environment?&lt;/strong&gt; If you create the venv first, git could briefly see hundreds of library files before you've excluded them. Doing this first avoids ever accidentally staging things you don't want tracked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How this command works:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cat&lt;/code&gt; — normally reads and displays file content, but here it's not reading a file — it's about to receive typed input instead.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;gt;&lt;/code&gt; — redirect operator. Instead of printing to the screen, the output is written into a file. Since &lt;code&gt;.gitignore&lt;/code&gt; doesn't exist yet, this creates it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;&amp;lt; 'EOF'&lt;/code&gt; — a &lt;strong&gt;heredoc&lt;/strong&gt;. It tells bash: "don't wait for a real file — treat everything typed next, up until a line that says exactly &lt;code&gt;EOF&lt;/code&gt;, as the input text."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EOF&lt;/code&gt; (closing) — signals "stop, that's the end of the input."&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;EOF&lt;/code&gt; isn't a special keyword — it's just a convention. You could use &lt;code&gt;BANANA&lt;/code&gt; instead, and it would work identically: bash treats everything as raw text until it sees a &lt;em&gt;line&lt;/em&gt; containing exactly that word. &lt;code&gt;EOF&lt;/code&gt; ("End Of File") is used everywhere because it instantly signals intent to anyone reading the script.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This heredoc syntax is bash-specific. PowerShell uses a different syntax (&lt;code&gt;@" ... "@ | Out-File&lt;/code&gt;) — match the syntax to the shell you're actually in.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Create and activate the virtual environment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
&lt;span class="nb"&gt;source &lt;/span&gt;venv/Scripts/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;python -m venv venv&lt;/code&gt; — runs Python's built-in &lt;code&gt;venv&lt;/code&gt; module, creating a folder (&lt;code&gt;venv/&lt;/code&gt;) containing an isolated copy of Python and its own package directory.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;source venv/Scripts/activate&lt;/code&gt; — activates it, redirecting your terminal's &lt;code&gt;python&lt;/code&gt;/&lt;code&gt;pip&lt;/code&gt; to point inside &lt;code&gt;venv/&lt;/code&gt; instead of your system-wide install. You'll know it worked when &lt;code&gt;(venv)&lt;/code&gt; appears in your prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The folder is named &lt;code&gt;Scripts/&lt;/code&gt; on Windows and &lt;code&gt;bin/&lt;/code&gt; on Linux/macOS. If &lt;code&gt;source venv/bin/activate&lt;/code&gt; fails with "No such file or directory," check which one actually exists with &lt;code&gt;ls venv&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Install packages + freeze requirements
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;requests
pip freeze &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; every time you install something new, freeze it into &lt;code&gt;requirements.txt&lt;/code&gt;. This makes the exact dependency set shareable — anyone (including future-you) can recreate your environment with &lt;code&gt;pip install -r requirements.txt&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  First commit
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch &lt;/span&gt;README.md main.py      &lt;span class="c"&gt;# or: New-Item README.md, main.py   (PowerShell)&lt;/span&gt;
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Initial project setup"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What makes a good commit message:&lt;/strong&gt; it describes &lt;em&gt;what changed and why&lt;/em&gt; — not just "update" or "fix stuff."&lt;/p&gt;

&lt;h3&gt;
  
  
  Connect to GitHub
&lt;/h3&gt;

&lt;p&gt;Create a new, empty repository on GitHub first, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git remote add origin &amp;lt;your-repo-url&amp;gt;
git branch &lt;span class="nt"&gt;-M&lt;/span&gt; main
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Breaking this down:&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;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git remote add origin &amp;lt;url&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Registers a remote connection, nicknamed &lt;code&gt;origin&lt;/code&gt; (convention), pointing at your GitHub repo's URL. No data moves yet — this just registers the address.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git branch -M main&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Renames your current branch to &lt;code&gt;main&lt;/code&gt; (forcing it even if a branch with that name exists), matching what GitHub expects by default.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git push -u origin main&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Uploads your commits to GitHub for the first time. &lt;code&gt;-u&lt;/code&gt; sets up tracking between local &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;origin/main&lt;/code&gt;, so every push after this can just be &lt;code&gt;git push&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Run this sequence once per project&lt;/strong&gt; — right at the start, connecting a fresh local repo to a fresh GitHub repo.&lt;/p&gt;

&lt;h3&gt;
  
  
  The repeatable loop (used for every session after this)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source &lt;/span&gt;venv/Scripts/activate
&lt;span class="c"&gt;# ... work, install packages ...&lt;/span&gt;
pip freeze &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; requirements.txt
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"clear description of change"&lt;/span&gt;
git push
deactivate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The commit flow, in isolation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status                          &lt;span class="c"&gt;# check what changed&lt;/span&gt;
git add .gitignore                  &lt;span class="c"&gt;# stage the change&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"clear description"&lt;/span&gt;   &lt;span class="c"&gt;# commit with a real message&lt;/span&gt;
git push                            &lt;span class="c"&gt;# push to GitHub&lt;/span&gt;
git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;                &lt;span class="c"&gt;# sanity check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2: Getting FastAPI Running Locally
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Ensure the virtual environment is active
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source &lt;/span&gt;venv/Scripts/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Install FastAPI and Uvicorn
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;fastapi uvicorn
pip freeze &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FastAPI&lt;/strong&gt; — lets you define API routes/endpoints in Python. It doesn't serve requests on its own — it needs an &lt;strong&gt;ASGI&lt;/strong&gt; (Asynchronous Server Gateway Interface) server to actually run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uvicorn&lt;/strong&gt; — a lightweight ASGI server that runs your app and listens for HTTP requests.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Create the app file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;code &lt;span class="nb"&gt;.&lt;/span&gt;   &lt;span class="c"&gt;# opens the current folder in VS Code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Structure of &lt;code&gt;main.py&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Import &lt;code&gt;FastAPI&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Create an &lt;strong&gt;application instance&lt;/strong&gt; (&lt;code&gt;app = FastAPI()&lt;/code&gt;) for Uvicorn to run&lt;/li&gt;
&lt;li&gt;Use a &lt;strong&gt;decorator&lt;/strong&gt; (&lt;code&gt;@app.get("/")&lt;/code&gt;) so that a GET request to the root URL runs the function below it&lt;/li&gt;
&lt;li&gt;Define the function to handle the request&lt;/li&gt;
&lt;li&gt;Return the response (FastAPI auto-converts a Python dict to JSON)
&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="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;message&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;Hello, 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;h3&gt;
  
  
  Start the server
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvicorn main:app &lt;span class="nt"&gt;--reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;main:app&lt;/code&gt; — tells Uvicorn to look inside &lt;code&gt;main.py&lt;/code&gt; for an object named &lt;code&gt;app&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--reload&lt;/code&gt; — automatically restarts the server whenever you save a code change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Visit &lt;code&gt;http://127.0.0.1:8000&lt;/code&gt; to see the JSON response.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check the automatic docs
&lt;/h3&gt;

&lt;p&gt;Visit &lt;code&gt;http://127.0.0.1:8000/docs&lt;/code&gt; — FastAPI auto-generates a full interactive API explorer (Swagger UI / OpenAPI spec), built entirely from your route definitions. No docs code required.&lt;/p&gt;

&lt;p&gt;A read-only alternative view is available at &lt;code&gt;/redoc&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Secrets — the &lt;code&gt;.env&lt;/code&gt; File
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Create &lt;code&gt;.env&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .env &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
APP_SECRET=super-secret-value-123
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Confirm it's actually ignored — don't just assume
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status                 &lt;span class="c"&gt;# .env should not appear anywhere in the output&lt;/span&gt;
git check-ignore &lt;span class="nt"&gt;-v&lt;/span&gt; .env   &lt;span class="c"&gt;# the definitive test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;git check-ignore -v&lt;/code&gt; reports exactly which line in which file is causing the file to be ignored, e.g.:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;.&lt;span class="n"&gt;gitignore&lt;/span&gt;:&lt;span class="m"&gt;7&lt;/span&gt;:.&lt;span class="n"&gt;env&lt;/span&gt;    .&lt;span class="n"&gt;env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; &lt;code&gt;.gitignore&lt;/code&gt; only prevents git from tracking files it &lt;em&gt;hasn't already started tracking&lt;/em&gt;. If a secret was committed before being added to &lt;code&gt;.gitignore&lt;/code&gt;, it will still exist in your git history. Setting up the ignore rule &lt;strong&gt;before&lt;/strong&gt; the first &lt;code&gt;git add&lt;/code&gt; — which is what happened here — avoids this entirely.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 4: Start This Build Log
&lt;/h2&gt;

&lt;p&gt;Keep two documents, for two different audiences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;BUILD_LOG.md&lt;/code&gt;&lt;/strong&gt; (this file) — chronological, for future-me. What I did, what broke, what I learned. Terse is fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;README.md&lt;/code&gt;&lt;/strong&gt; — for other people (or future-me pretending to be someone else). What the project is and how to run it — a snapshot of current state, not a diary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most useful log entries capture the &lt;em&gt;why&lt;/em&gt;, not just the &lt;em&gt;what&lt;/em&gt; — especially for snags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## 2026-07-16&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Set up project folder, git repo, and Python virtual environment
&lt;span class="p"&gt;-&lt;/span&gt; Installed FastAPI + Uvicorn, built a hello-world app with a single GET / route
&lt;span class="p"&gt;-&lt;/span&gt; Confirmed it runs locally and explored the auto-generated /docs (Swagger UI)
&lt;span class="p"&gt;-&lt;/span&gt; Set up a .env file for secrets and confirmed it's properly gitignored
  (learned: .gitignore only blocks files git hasn't tracked yet — order matters)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit it the same way as everything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add BUILD_LOG.md
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Start build log with first entry"&lt;/span&gt;
git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Key Habits to Keep
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Commit small, commit often, write real messages&lt;/strong&gt; — a message should answer "what changed and why," not just "update."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.gitignore&lt;/code&gt; before &lt;code&gt;git add&lt;/code&gt;&lt;/strong&gt;, always — order of operations is what actually protects secrets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freeze requirements on every install&lt;/strong&gt;, not just at the end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log the "why" and the snags&lt;/strong&gt;, not just the "what" — that's the part future-you will actually forget.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>beginners</category>
      <category>git</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
