<?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: Chukwuemeka Andre Ozomma</title>
    <description>The latest articles on DEV Community by Chukwuemeka Andre Ozomma (@chuksandroz).</description>
    <link>https://dev.to/chuksandroz</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%2F4076879%2F7fd47793-09d0-493d-9af9-6f3d51fcdf0a.jpg</url>
      <title>DEV Community: Chukwuemeka Andre Ozomma</title>
      <link>https://dev.to/chuksandroz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chuksandroz"/>
    <language>en</language>
    <item>
      <title>A Broken Button Is Obvious. A Broken Auth Check Isn't.</title>
      <dc:creator>Chukwuemeka Andre Ozomma</dc:creator>
      <pubDate>Tue, 18 Aug 2026 13:51:11 +0000</pubDate>
      <link>https://dev.to/chuksandroz/a-broken-button-is-obvious-a-broken-auth-check-isnt-58gm</link>
      <guid>https://dev.to/chuksandroz/a-broken-button-is-obvious-a-broken-auth-check-isnt-58gm</guid>
      <description>&lt;p&gt;Here's a bug that will pass code review, pass a quick manual test, and sit in production for months before anyone notices — because it only breaks on the one input nobody thinks to try.&lt;/p&gt;

&lt;p&gt;Say you're building a delete endpoint. A user should only be able to delete their own resource — a recipe, a document, a post, doesn't matter. The obvious approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;js&lt;/span&gt;
&lt;span class="k"&gt;export&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;DELETE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;params&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;id&lt;/span&gt; &lt;span class="p"&gt;}&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;params&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;recipe&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;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&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;auth&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;recipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Forbidden&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&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="k"&gt;return&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="na"&gt;success&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="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`

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

&lt;/div&gt;



&lt;p&gt;Test it with a real recipe ID you own — works. Test it with someone else's recipe ID — correctly forbidden. Ship it.&lt;/p&gt;

&lt;p&gt;Now request a recipe ID that &lt;strong&gt;doesn't exist&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;recipe comes back null. The very next line — recipe.userId — throws, because you can't read a property off null. In a lot of setups, that unhandled exception doesn't cleanly return a 403 or a 404. Depending on your error handling upstream, it can surface as a raw 500, sometimes with a stack trace, sometimes with enough detail to tell an attacker things about your schema they shouldn't be able to see. And if your error handling is too forgiving in the wrong direction, malformed input can slip past the check that was supposed to stop it — the check never even ran, because the code crashed before reaching it.&lt;/p&gt;

&lt;p&gt;Either way: &lt;strong&gt;the ownership check silently didn't happen&lt;/strong&gt;. Not because the logic was wrong. Because of what order things ran in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this is worse than it looks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The dangerous part isn't that this crashes. Crashes get noticed. The dangerous part is how easy it is to write a slightly different version of this same mistake that doesn't crash — it just quietly returns the wrong answer instead. The moment your authorization check depends on data that might not exist yet, you've made "does this check even run" conditional on something you didn't intend to make it conditional on.&lt;/p&gt;

&lt;p&gt;A broken button is obvious the second you click it. A broken authorization check can be technically "working" for every test case you happened to think of, and wrong for the one you didn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix is about order, not logic&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;js&lt;/span&gt;
&lt;span class="k"&gt;export&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;DELETE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;params&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;id&lt;/span&gt; &lt;span class="p"&gt;}&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;params&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;session&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;auth&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;session&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;user&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="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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unauthorized&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;status&lt;/span&gt;&lt;span class="p"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recipe&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;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&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="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;recipe&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="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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Not found&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;404&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;recipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Forbidden&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recipe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&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="k"&gt;return&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="na"&gt;success&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="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Same logic. Same three questions being asked. The only thing that changed is the order: check who's asking, then check whether the thing they're asking about even exists, then check whether they're allowed to touch it. Every guard clause runs before anything that could be null gets touched.&lt;/p&gt;

&lt;p&gt;The general rule I keep coming back to: &lt;strong&gt;authorization code should be readable top to bottom as a sequence of gates, each one safe to evaluate no matter what came before it&lt;/strong&gt;. If a later check depends on data a null case could have skipped past, you don't have three independent checks — you have one check with a hole in it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where this actually bites&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This exact shape of bug shows up constantly, not just in delete routes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An "is this user an admin" check that runs after a database call that assumed the user already exists&lt;/li&gt;
&lt;li&gt;A permission check on a field that's undefined for legacy records created before that field existed&lt;/li&gt;
&lt;li&gt;A check that correctly blocks the UI button from rendering, while the actual API route behind it has no equivalent server-side check at all — the client-side gate was doing all the work, and it's trivial to bypass by just calling the endpoint directly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is worth sitting with for a second: &lt;strong&gt;hiding a button is not authorization&lt;/strong&gt;. If the check only exists in the frontend, the "check" is a UI suggestion, not a security boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The habit worth building&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you're reviewing any authorization code — your own, a teammate's, or something AI just generated for you — trace it in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;- Is identity checked first, before anything else runs?&lt;/li&gt;
&lt;li&gt;- Does the code correctly handle the resource not existing, before it tries to check ownership of that resource?&lt;/li&gt;
&lt;li&gt;- Is the check enforced on the server, not just hidden in the UI?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you can't answer all three by reading the function top to bottom, it's worth another look — regardless of how confidently it was written, by you or by an AI tool.&lt;/p&gt;

&lt;p&gt;I write about backend patterns like this one in more depth in &lt;a href="https://leanpub.com/ai-assistedbackenddevelopment" rel="noopener noreferrer"&gt;AI-Assisted Backend Development&lt;/a&gt; — happy to dig into any specific edge case in the comments if it's useful.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>javascript</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Build First. Understand What You Build. Then Use AI to Build Faster.</title>
      <dc:creator>Chukwuemeka Andre Ozomma</dc:creator>
      <pubDate>Fri, 14 Aug 2026 02:30:57 +0000</pubDate>
      <link>https://dev.to/chuksandroz/build-first-understand-what-you-build-then-use-ai-to-build-faster-4b33</link>
      <guid>https://dev.to/chuksandroz/build-first-understand-what-you-build-then-use-ai-to-build-faster-4b33</guid>
      <description>&lt;p&gt;A few months ago I noticed something that bugged me enough to eventually write a book about it.&lt;/p&gt;

&lt;p&gt;AI coding tools are genuinely good now. Copilot, Claude, Cursor — you can describe a feature and watch working code appear in seconds. That's not hype, it's just true. But I kept running into the same pattern, in my own work and in conversations with other developers: there's a growing gap between being able to generate code and being able to tell whether that code is actually good.&lt;/p&gt;

&lt;p&gt;Not broken — broken is easy. It throws an error, you notice, you fix it. I mean the quieter failure mode: code that runs, looks reasonable, and is subtly wrong in a way you only catch if you already understand what "right" looks like. An unindexed database column that's fine at 10 rows and falls over at 100,000. An authorization check that works for the happy path and quietly leaks data the moment a null slips through. A component with three pieces of state where one would do.&lt;/p&gt;

&lt;p&gt;AI won't stop you from shipping any of that. It'll happily generate it, confidently, in about four seconds.&lt;/p&gt;

&lt;p&gt;So when I sat down to write a frontend development book, I made a deliberate call that I think goes against how most "AI coding" content is currently being written: no AI at all until the fundamentals are solid. HTML, CSS, JavaScript, React, Next.js — built by hand, understood completely, mistakes made and debugged the slow way. Only once that foundation exists does the book bring in Copilot, Claude, and Cursor — and even then, the framing isn't "here's how to prompt your way to a finished app." It's "here's how to read what the AI gives you and decide, yourself, whether it's actually right."&lt;/p&gt;

&lt;p&gt;The line I kept coming back to while writing it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build first. Understand what you build. Then use AI to build faster.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I didn't want a reader finishing the book thinking "now I know how to get AI to build things for me." I wanted them finishing it thinking "now I understand how these things get built, and I can use AI without handing over my ability to think and decide as a developer."&lt;/p&gt;

&lt;p&gt;The book's capstone is a small app — a Recipe Box — built three separate times: once by hand with vanilla HTML/CSS/JS, once in React, once in Next.js. Watching the same problem solved three different ways ended up being a better teacher than I expected going in. It makes the differences between the tools obvious instead of abstract.&lt;/p&gt;

&lt;p&gt;I just finished a second book that continues the same project into the backend — servers, databases, authentication, real deployment — applying the same rule. Backend mistakes are quieter and more expensive than frontend ones, so if "understand before you accelerate" matters anywhere, it matters there most.&lt;/p&gt;

&lt;p&gt;I'm genuinely curious how other developers here are navigating this. Are you teaching or learning AI-assisted development in a similar fundamentals-first order, or has it worked better for you to learn the tools alongside the basics from day one? I don't think there's one obviously correct answer yet — it's early enough that we're all sort of figuring it out in public.&lt;/p&gt;

&lt;p&gt;I wrote about this in more depth in a two-book series if anyone wants to dig further — &lt;a href="https://leanpub.com/ai-assisted-frontend-development" rel="noopener noreferrer"&gt;Frontend&lt;/a&gt; and &lt;a href="https://leanpub.com/ai-assistedbackenddevelopment" rel="noopener noreferrer"&gt;Backend&lt;/a&gt;, each walking through the same idea from a different layer of the stack. Would love to hear how this lands with people living this exact tension day to day.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
    </item>
  </channel>
</rss>
