<?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: ahmad hassan</title>
    <description>The latest articles on DEV Community by ahmad hassan (@ahmad_hassan_6433415b38c6).</description>
    <link>https://dev.to/ahmad_hassan_6433415b38c6</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%2F4185081%2F2395fb9b-fbec-408a-b81e-614b03f6d915.png</url>
      <title>DEV Community: ahmad hassan</title>
      <link>https://dev.to/ahmad_hassan_6433415b38c6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ahmad_hassan_6433415b38c6"/>
    <language>en</language>
    <item>
      <title>RAG Tenant Isolation Inside the Vector Index</title>
      <dc:creator>ahmad hassan</dc:creator>
      <pubDate>Wed, 14 Oct 2026 16:28:27 +0000</pubDate>
      <link>https://dev.to/ahmad_hassan_6433415b38c6/rag-tenant-isolation-inside-the-vector-index-1moi</link>
      <guid>https://dev.to/ahmad_hassan_6433415b38c6/rag-tenant-isolation-inside-the-vector-index-1moi</guid>
      <description>&lt;p&gt;Put the user filter inside the vector search itself. If you search everything and remove other people’s chunks afterwards, those chunks still use up the slots you asked for, and one forgotten line of code puts a stranger’s document into someone else’s answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The version most apps ship first
&lt;/h2&gt;

&lt;p&gt;The obvious way to build a multi user RAG app is to search the whole collection, take the best matches, and throw away anything that does not belong to the current user. It passes every test you write on your own laptop, because on your laptop there is only you.&lt;/p&gt;

&lt;p&gt;Now do the arithmetic with real users. Fifty people have uploaded contracts, and every one of those contracts has a termination clause. You ask for the 8 nearest chunks to “how do I terminate early?”. Those 8 chunks are scattered across many users, because that is what “nearest” means. After the filter you keep one or two, or none, and the assistant answers from almost nothing. Nothing crashed, so nothing alerts you. Answers just get worse as you grow.&lt;/p&gt;

&lt;p&gt;There is a second problem. The only wall between your users is a filter that lives in application code, and application code gets refactored by tired people.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the filter inside the search
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://ahmadhsn1.github.io/work/retrivo-vault/" rel="noopener noreferrer"&gt;Retrivo Vault&lt;/a&gt; the userId filter is part of the Atlas Vector Search index definition and runs inside the nearest neighbour search. Vectors that belong to someone else are never candidates, so the search returns the 8 best chunks from your documents because those are the only chunks it looks at.&lt;/p&gt;

&lt;p&gt;It takes two pieces. The index declares userId as a filter field next to the vector field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vector"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"embedding"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"numDimensions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"similarity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cosine"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"filter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"userId"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the query passes the filter to the same stage that does the searching. numDimensions has to match your embedding model, so 768 here is only an example.&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="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;searchChunks&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;queryVector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&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="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;userId&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;searchChunks needs a userId&lt;/span&gt;&lt;span class="dl"&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;chunks&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;aggregate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;$vectorSearch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chunks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;embedding&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;queryVector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;numCandidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;$meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vectorSearchScore&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="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;toArray&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 details matter. The function refuses to run without a userId, so a bug upstream fails loudly instead of searching everyone. And numCandidates is how many nearest neighbours the engine considers before returning the top limit. A larger number improves recall and costs latency, so tune it with your own data rather than copying mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to prove isolation works
&lt;/h2&gt;

&lt;p&gt;Do not trust this because a blog post said so. Write the test that would catch the failure, and keep it in CI.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create two users and upload the same document to both. Identical text gives identical vectors, which is the hardest case for isolation.&lt;/li&gt;
&lt;li&gt;Search as user A and assert that every returned chunk has A’s userId. Assert it for B as well.&lt;/li&gt;
&lt;li&gt;Give user B one tiny document and search with limit 8. You should get a few results, not eight and not zero. This catches post filtering, which returns fewer results for small accounts.&lt;/li&gt;
&lt;li&gt;Call the search function with no userId and assert that it throws.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Chunking that survives messy uploads
&lt;/h2&gt;

&lt;p&gt;People upload scanned contracts, CSV files with one enormous line, and pages with no paragraph breaks. A splitter that only cuts on blank lines hands you one giant chunk and useless retrieval, so Retrivo Vault splits in three tiers: paragraphs first, then sentences, then a fixed width cut as the last resort. Chunks overlap by 150 characters, so a fact that sits on a boundary still appears whole in at least one chunk.&lt;/p&gt;

&lt;p&gt;Here is a sketch of the idea. The real thing also packs small pieces together up to the size limit, but the fallback order is the part worth copying.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;OVERLAP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="k"&gt;for &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;paragraph&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\n{2,}&lt;/span&gt;&lt;span class="sr"&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="nx"&gt;paragraph&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;lt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paragraph&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for &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;sentence&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;paragraph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(?&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;=&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;.!?&lt;/span&gt;&lt;span class="se"&gt;])\s&lt;/span&gt;&lt;span class="sr"&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="nx"&gt;sentence&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;lt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sentence&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;sentence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;MAX&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;OVERLAP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sentence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;MAX&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;piece&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;piece&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&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;h2&gt;
  
  
  Let the assistant say it does not know
&lt;/h2&gt;

&lt;p&gt;Answers come only from retrieved passages. Every citation shows the exact passage and its match score, and when the answer is not in your documents the assistant says exactly that. A user who has seen it admit ignorance once will believe it the next time it sounds sure. A confident wrong answer costs more trust than ten honest ones earn.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same idea shows up in tokens
&lt;/h2&gt;

&lt;p&gt;Refresh tokens follow the same rule. They are issued in families and each rotation is stamped. If an old token turns up again within 15 seconds it is treated as a client retry. Anything older revokes the whole family, because by then someone else probably has a copy. Clients misbehave, so the safe outcome has to be the default one.&lt;/p&gt;

&lt;p&gt;The rest of the engineering notes, and the 142 tests behind them, are in the &lt;a href="https://ahmadhsn1.github.io/work/retrivo-vault/" rel="noopener noreferrer"&gt;Retrivo Vault case study&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://ahmadhsn1.github.io/blog/rag-tenant-isolation-vector-index/" rel="noopener noreferrer"&gt;my portfolio blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>postgres</category>
      <category>architecture</category>
    </item>
    <item>
      <title>AI Coding Agents: A Workflow That Holds Up</title>
      <dc:creator>ahmad hassan</dc:creator>
      <pubDate>Wed, 14 Oct 2026 16:26:50 +0000</pubDate>
      <link>https://dev.to/ahmad_hassan_6433415b38c6/ai-coding-agents-a-workflow-that-holds-up-29od</link>
      <guid>https://dev.to/ahmad_hassan_6433415b38c6/ai-coding-agents-a-workflow-that-holds-up-29od</guid>
      <description>&lt;p&gt;Treat a coding agent like a fast colleague who needs a clear ticket and a way to check its own work. Write the task with acceptance criteria, ask for a plan before any code, give the agent commands that prove the change works, keep every diff small, and review it as the owner. Speed is the agent’s job. Judgment stays with you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the time goes now
&lt;/h2&gt;

&lt;p&gt;Coding agents write and change code quickly, and they follow instructions well. That shifts where the effort goes in a project. Less of it is typing. More of it lands in two places: saying exactly what you want, and checking that you got it.&lt;/p&gt;

&lt;p&gt;Both are old engineering skills. Writing a clear ticket and reviewing a change were always part of the job, and they matter more now because the cost of producing a change has dropped while the cost of understanding it has not. The workflow below does not depend on any vendor. It works whether your agent lives in a terminal, an editor or a pull request bot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write the task like a ticket
&lt;/h2&gt;

&lt;p&gt;An agent fills every gap you leave with its own best guess. A vague request returns a confident answer to a question you did not quite ask. A useful task has four parts: the goal, the constraints, how to tell it is done, and what is out of scope.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Goal
Add a "resend verification email" button to the account page.

Constraints
Reuse the existing sendVerificationEmail service. Do not add dependencies.
Limit resends to 3 per hour per user.

Done when
* A signed in user with an unverified email sees the button.
* The fourth request within an hour returns a 429 and shows a clear message.
* Tests cover the limit and the already verified case.

Out of scope
Email template changes, the signup flow, anything under /billing.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last section matters more than it looks. Without it, an agent will helpfully tidy things you never mentioned, and you end up reviewing a larger change than the one you wanted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask for a plan before any code
&lt;/h2&gt;

&lt;p&gt;For anything beyond a small edit, ask the agent to read the code and propose a plan, and do not let it change files until you have read that plan. Most agents have a planning or read only mode for exactly this. A plan is cheap to read and cheap to correct. A wrong plan caught here saves a wrong implementation and the review that would have followed.&lt;/p&gt;

&lt;p&gt;When I read a plan I check three things. Does it touch only the files I expect? Does it reuse code that already exists instead of writing a parallel copy? Does it say how the change will be tested? If a plan proposes a new dependency or a new pattern, I ask why before it writes a line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make correctness something the agent can run
&lt;/h2&gt;

&lt;p&gt;The biggest single improvement to agent output is giving it a way to check itself. If tests, type checks and linting run with one command, the agent can change code, run the command, read the failures and fix them without you in the loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"check"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eslint . &amp;amp;&amp;amp; tsc --noEmit &amp;amp;&amp;amp; vitest run"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tell the agent in plain words to run that command before it says the task is finished. Better still, start from a failing test that describes the behaviour you want and let the agent make it pass. A test you wrote is a specification. A test the agent wrote for its own code can quietly confirm its own assumptions, so read those more closely than the code itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep every change small
&lt;/h2&gt;

&lt;p&gt;Review quality falls quickly as a diff grows. A change you can read in a few minutes gets a real review. A change spread over thirty files gets a skim. Ask for one concern at a time, commit at checkpoints, and treat a revert as an ordinary tool instead of a failure.&lt;/p&gt;

&lt;p&gt;You can enforce this instead of hoping for it. This script fails the build when a change is bigger than you agreed to review. Run it in CI against the branch you are merging.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execFileSync&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;node:child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BASE_REF&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;origin/main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxLines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;git&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;diff&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;--numstat&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...HEAD&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;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&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;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\t&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;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;added&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;removed&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&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;added&lt;/span&gt;&lt;span class="p"&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="o"&gt;+&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;removed&lt;/span&gt;&lt;span class="p"&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="mi"&gt;0&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;files&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;maxFiles&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;maxLines&lt;/span&gt;&lt;span class="p"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Diff budget exceeded: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; files, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; changed lines. Split this change.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&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;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;Diff within budget: &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; files, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; lines&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fifteen files and four hundred lines are a starting point, not a rule. Pick limits your team can genuinely review, and exclude lockfiles and generated files once they start causing false alarms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Teach the repository with an instructions file
&lt;/h2&gt;

&lt;p&gt;An agent begins each session knowing nothing about your project. Most tools read a plain text instructions file from the root of the repository. AGENTS.md has become a common shared name that several tools understand, and some tools also read a file of their own, such as CLAUDE.md. If your team uses more than one tool, keeping the shared rules in one place avoids them drifting apart.&lt;/p&gt;

&lt;p&gt;What goes in it is short and practical: the commands to build and test, the conventions that are not obvious from reading the code, the folders that are off limits, and the mistakes that have happened before. Start small. Add a line when you see the same mistake twice, and where you can, add a test that fails if it returns, so the rule is enforced and not merely written down.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# AGENTS.md

## Commands
* Run everything with: npm run check
* Never run the seed script against a shared database.

## Conventions
* Money is stored as integer minor units. Never use floats for amounts.
* New endpoints validate input with the shared schemas in src/schemas.

## Off limits
* Do not edit src/generated or any migration that has already run.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Review it as the owner
&lt;/h2&gt;

&lt;p&gt;When a pull request arrives, the reflex is to open the changed files. Start one step earlier. Read the task, decide what a correct change would look like, and then compare. Reviewing against intent catches the most expensive problem of all, a change that is neat and well tested and solves the wrong thing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scope. Did it do what the task asked and nothing more?&lt;/li&gt;
&lt;li&gt;Edge cases. Empty input, a repeated click, a missing record, a slow network.&lt;/li&gt;
&lt;li&gt;Security. Who is allowed to call this? Is every input validated? Are secrets kept out of the code and the logs?&lt;/li&gt;
&lt;li&gt;Dependencies. Did it add a package, and is that justified?&lt;/li&gt;
&lt;li&gt;Performance. Queries inside loops, lists with no limit, whole files loaded into memory.&lt;/li&gt;
&lt;li&gt;Tests. Would they fail if the feature were broken?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This kind of review rests on fundamentals. If you are earlier in your career, the guide on &lt;a href="https://ahmadhsn1.github.io/blog/vibe-coding-vs-learning-to-code-freshers/" rel="noopener noreferrer"&gt;vibe coding versus learning to code&lt;/a&gt; covers the basics that make it possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know when to take the keyboard back
&lt;/h2&gt;

&lt;p&gt;Some situations are better handled by hand, or at least with you driving closely.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The same error has come back three times in a row.&lt;/li&gt;
&lt;li&gt;The requirement is unclear and the agent is guessing between interpretations.&lt;/li&gt;
&lt;li&gt;The code is security sensitive: authentication, permissions or payments.&lt;/li&gt;
&lt;li&gt;The action cannot be undone, such as a data migration or a delete.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Apply the same least privilege you would give a new teammate. Work on a branch, keep production credentials out of reach, and restrict destructive commands so a bad guess costs you a revert and not a recovery.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop in one place
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Write the ticket with a goal, constraints, a definition of done and what is out of scope.&lt;/li&gt;
&lt;li&gt;Ask for a plan and read it before any file changes.&lt;/li&gt;
&lt;li&gt;Have the agent run the check command and fix what fails.&lt;/li&gt;
&lt;li&gt;Keep the diff small enough to review properly.&lt;/li&gt;
&lt;li&gt;Review against intent, then against the checklist.&lt;/li&gt;
&lt;li&gt;Record any repeated mistake in the instructions file, ideally with a test.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Expect the first few tasks to feel slower while you write tickets and set up checks. The payoff is that the output starts arriving in a state you can trust and review quickly, and that is where the time saving from an agent actually comes from.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://ahmadhsn1.github.io/blog/ai-coding-agents-workflow-that-holds-up/" rel="noopener noreferrer"&gt;my portfolio blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Vibe Coding vs Learning to Code: Freshers Guide</title>
      <dc:creator>ahmad hassan</dc:creator>
      <pubDate>Wed, 14 Oct 2026 16:25:44 +0000</pubDate>
      <link>https://dev.to/ahmad_hassan_6433415b38c6/vibe-coding-vs-learning-to-code-freshers-guide-4djd</link>
      <guid>https://dev.to/ahmad_hassan_6433415b38c6/vibe-coding-vs-learning-to-code-freshers-guide-4djd</guid>
      <description>&lt;p&gt;Yes, you should still learn to code in 2026, but learn it differently. Use AI as a fast pair and a patient tutor, and keep the judgment for yourself. Typing code is now cheap, so the skills that pay are reading code, debugging, testing and thinking about how a system fails. Learn the fundamentals by hand first, then build real projects with AI and be able to explain every line you ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you still learn to code in 2026?
&lt;/h2&gt;

&lt;p&gt;Yes. But the thing worth learning has changed, and most advice you will find is still describing the old version.&lt;/p&gt;

&lt;p&gt;Five years ago learning to code meant memorising syntax and grinding exercises until your fingers knew the shapes. That part is now mostly handled by tools. What is not handled is deciding what to build, breaking a vague problem into small ones, noticing that generated code is wrong, finding out why something broke, and designing so it keeps working when real people use it. Those are the skills that were always the real job. They used to be hidden behind the typing, and now they are in plain view.&lt;/p&gt;

&lt;p&gt;So the question for a fresher is not whether to learn to code. It is whether to learn to code the way that builds judgment, and the rest of this guide is about how.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is vibe coding, and why does it break?
&lt;/h2&gt;

&lt;p&gt;Vibe coding is building software by describing what you want to an AI, accepting what it gives back, and steering by feel instead of by reading the code. The name was popularised by Andrej Karpathy in early 2025, and it is a genuinely good way to get a prototype on screen in an afternoon.&lt;/p&gt;

&lt;p&gt;It breaks when the app meets the real world. A demo has one user who behaves. Production has thousands who do not: they double click, lose signal halfway through a payment, paste emoji into a number field and try the URL of someone else’s page. Code you did not read is code you cannot debug, secure or change safely, and when it fails you have nothing to hold on to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four people, one confusion
&lt;/h2&gt;

&lt;p&gt;Talk to enough people who are starting out and you keep hearing four different stories.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The first person ships apps by prompting and has never written a loop by hand. It works until the first bug the AI cannot fix. Then they are stuck, because they cannot read what is in front of them.&lt;/li&gt;
&lt;li&gt;The second person knows the syntax well but has never asked where the data lives, who may see it, or what happens when two requests arrive together. They write correct lines inside a design that falls over.&lt;/li&gt;
&lt;li&gt;The third person says AI does everything now, so why learn at all.&lt;/li&gt;
&lt;li&gt;The fourth person says AI can write it, but it cannot scale.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of them holds a piece of the truth and draws the wrong conclusion from it.&lt;/p&gt;

&lt;p&gt;The third person is right that typing code is no longer the bottleneck. The wrong conclusion is that understanding code stopped mattering. When typing is nearly free, the scarce thing is knowing whether the output is correct, and you cannot judge what you do not understand.&lt;/p&gt;

&lt;p&gt;The fourth person is half right. A model will write code that scales if you ask for the right design, and code that collapses at 500 users if you do not. It does not know your traffic, your data or your budget. You do. Scale is a set of decisions about data models, indexes, caching and queues, and somebody has to make those decisions and then check them.&lt;/p&gt;

&lt;p&gt;The second person should be a little worried, kindly. Syntax was always the easy part, and it is exactly the part AI took over. What is left is the part that was being skipped.&lt;/p&gt;

&lt;p&gt;And the first person has found a great way to build a prototype this afternoon and a risky way to build something other people depend on. Both are true, and you should know which one you are doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI quietly fails
&lt;/h2&gt;

&lt;p&gt;AI is very good at boilerplate, translating between frameworks, explaining unfamiliar code, drafting tests and remembering the name of a thing you forgot. Use it for all of that without guilt.&lt;/p&gt;

&lt;p&gt;It fails in a handful of repeatable ways, and the failures look like success because the code runs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It invents functions and options that do not exist, with total confidence.&lt;/li&gt;
&lt;li&gt;It handles the happy path. Empty input, a second click, a slow network and a missing record are somebody else’s problem.&lt;/li&gt;
&lt;li&gt;It fixes the symptom. If an error is annoying, it will catch and hide the error instead of finding the cause.&lt;/li&gt;
&lt;li&gt;It repeats logic in three places. Change one, and the other two quietly rot.&lt;/li&gt;
&lt;li&gt;It leaves security gaps: no ownership checks, secrets in the code, SQL built from strings.&lt;/li&gt;
&lt;li&gt;It has no feel for load. A query inside a loop, a list with no limit, a whole file read into memory.&lt;/li&gt;
&lt;li&gt;It forgets earlier decisions as a project grows, and contradicts them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is what that looks like in practice. Ask for “an endpoint to list posts and one to edit a post” and you can easily get something like this:&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;app&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/posts&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;posts&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;select * from posts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&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;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/posts/:id&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`update posts set body = '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' where id = &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&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="nx"&gt;id&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It runs, and the demo works. It also has three serious problems. The update builds SQL out of user input, so a crafted body can run any command against your database. Nobody is checked, so any visitor can edit any post by changing the id. And the list returns every row, which is fine with 10 posts and a slow disaster with 10 million.&lt;/p&gt;

&lt;p&gt;Here is the same thing after someone who knows what to look for has read it:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&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;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;EditPost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&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;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;app&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/posts&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;20&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;rows&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;select id, body, created_at from posts order by id desc limit $1&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="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&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;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/posts/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireLogin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;EditPost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;rowCount&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;update posts set body = $1 where id = $2 and user_id = $3&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="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&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="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&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;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;rowCount&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;204&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;Look at what changed. Values go in as parameters, never glued into the query. The input is validated before it touches anything. The update only matches a row that belongs to the logged in user, so editing someone else’s post simply finds nothing. The list has a hard upper limit. None of this is clever. All of it is the kind of thing you only add when you have read the first version and asked what could go wrong.&lt;/p&gt;

&lt;p&gt;You can ask the AI to make these fixes, and it will, once you know to ask. That is the whole point. The skill is in the asking.&lt;/p&gt;

&lt;h2&gt;
  
  
  The skills that pay now
&lt;/h2&gt;

&lt;p&gt;If I had to start again today, this is the order I would build skills in.&lt;/p&gt;

&lt;p&gt;Reading code comes first. You will read far more code than you write, because most of it will be generated or inherited. For any change, ask three things. What does this do? What input would break it? What else does it touch? A reviewer who asks those three questions catches most of what matters.&lt;/p&gt;

&lt;p&gt;Debugging comes second, and it is a method, not a talent. Reproduce the problem. Shrink it to the smallest case that still fails. Read the error from top to bottom, including the part you want to skip. Form one guess and test it. Change one thing at a time. When you do ask an AI for help, give it the smallest failing case, the exact error text and what you have already tried. “It is not working” gets you a guess. A precise failing case gets you an answer.&lt;/p&gt;

&lt;p&gt;Systems thinking is the skill that separates people most. For every feature you build, answer these in writing before you code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where does this data live, and who owns it?&lt;/li&gt;
&lt;li&gt;Who is allowed to see it, and who is allowed to change it?&lt;/li&gt;
&lt;li&gt;What happens if this fails halfway through?&lt;/li&gt;
&lt;li&gt;What happens if two people do it at the same moment?&lt;/li&gt;
&lt;li&gt;What happens with 100 times the data or traffic?&lt;/li&gt;
&lt;li&gt;How will I find out that it broke?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those six questions are most of what people mean by “scale” and “production ready”. They are cheap to ask and very expensive to skip. A model will answer them well if you put them in the prompt, and will not raise them on its own.&lt;/p&gt;

&lt;p&gt;Testing is how you tell the AI what you mean. A test is a precise statement of what correct looks like. Write the expectations yourself, let the AI make them pass, then read what it wrote. Be careful with tests the AI writes for its own code, because they often confirm its own mistakes. If the expectation came from you, the test means something.&lt;/p&gt;

&lt;p&gt;Fundamentals sit underneath all of it. Learn one language properly, whether that is JavaScript, TypeScript or Python. Learn how HTTP works, what a database index does, how Git tracks change, and how a browser, a server and a database talk to each other. Learn the data structures you will actually meet, such as maps, lists and queues. None of this is for the sake of tradition. It is what lets you look at generated code and say that it is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to use AI while you are still learning
&lt;/h2&gt;

&lt;p&gt;The danger when you are new is not that AI is bad. It is that it is good enough to let you skip the struggle that builds understanding. These habits keep the benefit and remove the trap.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Try first. Give a problem twenty honest minutes before you ask. The failed attempt is what makes the answer stick.&lt;/li&gt;
&lt;li&gt;Ask for the explanation, then explain it back in your own words. If you cannot, you have not learned it yet.&lt;/li&gt;
&lt;li&gt;Never keep a line you cannot explain. Not for a deadline and not for a demo.&lt;/li&gt;
&lt;li&gt;Ask for options and tradeoffs instead of an answer. You are learning to choose.&lt;/li&gt;
&lt;li&gt;Ask what could go wrong. It is the cheapest code review there is.&lt;/li&gt;
&lt;li&gt;Let it review your code after you write it. This direction teaches more than the reverse.&lt;/li&gt;
&lt;li&gt;Turn it off while you practise fundamentals. You do not learn to lift by watching someone else do it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Prompts make a difference. Compare these two.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Weak:
Make a login system.

Stronger:
I am building a login for a Node and Postgres app. I have never done this.
Explain the main design choices (sessions or tokens, how to store passwords)
and the tradeoffs of each. Then show me the smallest safe version, and list
five ways it could be attacked or break in production.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second one teaches you something even if you throw the code away.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 90 day plan
&lt;/h2&gt;

&lt;p&gt;This is a plan for someone who can give a couple of focused hours a day. Stretch it if you have less time, but keep the order.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Days 1 to 30, by hand. Pick one language and write small things yourself: a command line todo list, a page that fetches data from an API and shows it, a script that reads a CSV file. Learn basic SQL. Use Git every day. AI is allowed only to explain things.&lt;/li&gt;
&lt;li&gt;Days 31 to 60, one real app. Build a full stack project with login, a database, create, read, update and delete, and a live deployment. Now use AI as a pair, following the habits above. Keep a short decision log: what you chose and why.&lt;/li&gt;
&lt;li&gt;Days 61 to 90, make it survive. Add tests for the core paths. Validate every input. Check ownership on every change. Add pagination and sensible limits. Add logging and proper error handling. Load it with 100,000 rows and fix whatever gets slow. Write down what broke and how you fixed it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last thirty days are where most of the learning is, and where most people stop. A project that has survived your own attempts to break it is worth more than three that only work in a demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  What your portfolio should show
&lt;/h2&gt;

&lt;p&gt;Anyone can generate a good looking app now, so a good looking app proves little. What stands out is evidence of judgment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A live link that works, so people can try it in thirty seconds.&lt;/li&gt;
&lt;li&gt;Tests that run, and a way to run them with one command.&lt;/li&gt;
&lt;li&gt;A README that explains the decisions and, more importantly, what went wrong and how you fixed it.&lt;/li&gt;
&lt;li&gt;A commit history that shows the project growing, not one enormous commit.&lt;/li&gt;
&lt;li&gt;One part that handles something serious, even in a small way: permissions, money, a background job or a rate limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In an interview, expect to be asked to open a random file and explain it. Pick the dullest file in your project and make sure you could do that. If you cannot say what you would change at 100 times the traffic, you have found your next thing to learn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Habits that keep AI code safe
&lt;/h2&gt;

&lt;p&gt;These apply whether you are a beginner or years in.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep changes small, so every diff is reviewable. Large generated changes are where bugs hide.&lt;/li&gt;
&lt;li&gt;Read every diff before you accept it. Decide, do not skim.&lt;/li&gt;
&lt;li&gt;Own the architecture. Let AI fill in the parts, but you choose how the parts fit.&lt;/li&gt;
&lt;li&gt;Never let it run migrations, deletes or anything involving secrets without you looking first.&lt;/li&gt;
&lt;li&gt;Write down decisions. A model has no memory of why you chose something last month, and neither will you.&lt;/li&gt;
&lt;li&gt;Run the tests and the linter every time, and trust them over the explanation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Will AI replace junior developers?
&lt;/h2&gt;

&lt;p&gt;Nobody can promise you an answer, and you should be suspicious of anyone who does. What can be said honestly is this. The tasks that are easiest to hand to a model are the routine ones: simple endpoints, boilerplate, small bug fixes, first drafts of tests. Those used to be the work that justified hiring a junior, and many teams are asking for more than that now.&lt;/p&gt;

&lt;p&gt;That sounds bleak, and it is actually a clear instruction. If routine coding is the part that is being absorbed, then do not build your whole identity on routine coding. Build the things that make a model’s output trustworthy: reading, debugging, testing, security thinking and systems thinking. A fresher who can ship a small project and explain every decision in it is much more useful to a team than one who can only prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do tomorrow, depending on who you are
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If you vibe code: take the app you already built and read it file by file. List every line you cannot explain. That list is your syllabus.&lt;/li&gt;
&lt;li&gt;If you know syntax but not systems: pick any feature and answer the six questions in writing before you code. Then write one test that should fail, and make it pass.&lt;/li&gt;
&lt;li&gt;If you think there is no point learning: build one small feature twice in a day, once with AI and once by hand. Notice what you understood after each.&lt;/li&gt;
&lt;li&gt;If you think AI cannot scale: put 100,000 rows in your database and see which page gets slow. Fix it with an index or pagination. You will have learned more about scale than any article can teach.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI did not remove the need to understand software. It moved the work from typing to judging, and judging is learnable. If you want to see what thinking about failure looks like in real systems, the posts on &lt;a href="https://ahmadhsn1.github.io/blog/llm-rate-limits-bullmq-reschedule/" rel="noopener noreferrer"&gt;rate limits&lt;/a&gt; and &lt;a href="https://ahmadhsn1.github.io/blog/rag-tenant-isolation-vector-index/" rel="noopener noreferrer"&gt;tenant isolation&lt;/a&gt; are written from exactly that angle.&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://ahmadhsn1.github.io/blog/vibe-coding-vs-learning-to-code-freshers/" rel="noopener noreferrer"&gt;my portfolio blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>beginners</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
