<?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: Paul Salmon</title>
    <description>The latest articles on DEV Community by Paul Salmon (@frenchsalmon).</description>
    <link>https://dev.to/frenchsalmon</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3702191%2F0b9c44d4-df9d-4d1e-a0e0-d7cf34e034a0.png</url>
      <title>DEV Community: Paul Salmon</title>
      <link>https://dev.to/frenchsalmon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/frenchsalmon"/>
    <language>en</language>
    <item>
      <title>How to Keep AI-Generated Code Modular</title>
      <dc:creator>Paul Salmon</dc:creator>
      <pubDate>Fri, 16 Jan 2026 09:34:50 +0000</pubDate>
      <link>https://dev.to/frenchsalmon/keep-modular-code-alive-1bfa</link>
      <guid>https://dev.to/frenchsalmon/keep-modular-code-alive-1bfa</guid>
      <description>&lt;p&gt;Hi, my name is Paul, and I am a senior software engineer exclusively working for start-ups.&lt;/p&gt;

&lt;p&gt;I treat programming languages like an all you can eat buffet, and no matter the language, the one thing I value is modularization. For this, our Openai and Claude assistants aren't very helpful. They are trained on a ton of old-school projects where everything's crammed into giant &lt;code&gt;index.js&lt;/code&gt; files. Unsupervised vibe coding will often get you +1000 lines of code per file, and a lot of repetition.&lt;/p&gt;

&lt;p&gt;Why is it a problem? Well, in start-ups, code is short lived and when vibe coding is used intensively, the AI will always find a way and the burden of refactoring will never be needed. But that is not quite true in reality. Things change quickly, and implementing new components or behavior must be quick and risk free. The AI is very bad at evaluating the impact of one change. If I add a field to this model, what consequences will it have over the whole system? Am I introducing a security risk? Am I breaking something? There come the need for unit tests, of course, but the bigger need, imho, is for modularization. Keep the code isolated, unaware of the exterior, specific to one usage per function, tested and in a single file, that behavior saved me a lot of troubles and a lot of refactoring. While it makes the development a little more verbose, a little more brain teaser, it does make it more robust, and a lot more fun.&lt;/p&gt;

&lt;p&gt;And this is what this article is about: how to force your AI tools to generate modular code. For this we need to severely jail the models, prune all liberties and kill every bit of creativity. But I work at Vybe.build, and keeping AI on a short, well-designed leash is kind of our thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Stack
&lt;/h2&gt;

&lt;p&gt;For the sake of this article, I will use a stack I know very well and that I reach for almost every time I build a small SaaS or a "SaaS-replacement" project.&lt;/p&gt;

&lt;p&gt;It's not exotic but it is extremely compatible with modular thinking and with AI-assisted development.&lt;/p&gt;

&lt;p&gt;I am using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Next.js with the App Router&lt;/li&gt;
&lt;li&gt;TypeScript, everywhere&lt;/li&gt;
&lt;li&gt;Vercel for deployment (serverless, zero friction)&lt;/li&gt;
&lt;li&gt;Cursor as my AI-powered editor (the same ideas apply to Claude Code, Copilot, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This stack matters because it forces you to think in terms of boundaries: server vs client, routes vs logic, data vs UI. And boundaries are exactly what AI tends to ignore unless you make them impossible to cross.&lt;/p&gt;

&lt;p&gt;You can find a &lt;a href="https://github.com/p-salmon/nextjs-starter" rel="noopener noreferrer"&gt;starter project here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A specific file organization
&lt;/h2&gt;

&lt;p&gt;This is the structure I use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./
├── app/                    # Next.js App Router pages and routes
├── src/
│   ├── __template__/      # Template used to generate new modules
│   │   ├── api/           # Server-side actions (DB writes, heavy logic, etc.)
│   │   ├── components/    # React components specific to the module
│   │   ├── hooks/         # React hooks
│   │   └── types/         # TypeScript types (shared client/server)
│   ├── auth/              # Authentication module
│   ├── db/                # Database module
│   └── ui/                # Shared UI library
│       ├── components/
│       ├── hooks/
│       └── globals.css
├── prisma/                # Database schema and migrations
└── public/                # Static assets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each folder under &lt;code&gt;src&lt;/code&gt; is a module. A module owns its logic, its hooks, its components, and its types. No reaching into other modules "just this once". If something is shared, it goes into &lt;code&gt;ui&lt;/code&gt; or a dedicated shared module.&lt;/p&gt;

&lt;p&gt;The template folder is important. It is the blueprint for every new module. When a new feature appears, I don't start by writing code, I start by generating a module from that template. This removes a huge amount of decision-making, both for me and for the AI.&lt;/p&gt;

&lt;p&gt;I like the &lt;code&gt;api&lt;/code&gt; / &lt;code&gt;components&lt;/code&gt; / &lt;code&gt;hooks&lt;/code&gt; / &lt;code&gt;types&lt;/code&gt; split because it works well with Next.js dual environment. Server code and client code are clearly separated, and the AI has fewer excuses to mix everything together, but ultimately, what your modules are made of is up to you.&lt;/p&gt;

&lt;h3&gt;
  
  
  NextJS structure
&lt;/h3&gt;

&lt;p&gt;One rule I am very strict about: the app/ directory contains only very specific logic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prop injections, server-side data fetching for pages&lt;/li&gt;
&lt;li&gt;Access protection, authentication, return value for routes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next.js has very strong opinions about routing and execution contexts, and that's fine. The app router defines pages and API routes. That's it.&lt;/p&gt;

&lt;p&gt;Anything related to business logic, data access, transformations, or even slightly reusable behavior lives in src, inside a module.&lt;/p&gt;

&lt;p&gt;Here is an example of a page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;getUsers&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;@/src/user/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;User&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;@/src/user/types&lt;/span&gt;&lt;span class="dl"&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UserPage&lt;/span&gt;&lt;span class="p"&gt;()&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;users&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="o"&gt;=&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;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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="nx"&gt;users&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;getUsers&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="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&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="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;An error occurred&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="k"&gt;if &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="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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;:&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserPage&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&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;Fetching data and handling the route-level error belongs to Next.js. Everything else belongs to the user module.&lt;/p&gt;

&lt;p&gt;Same idea for API routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;NextResponse&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;next/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;getUsers&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;@/src/user/api/getUsers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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;auth&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;@/src/auth&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withAuth&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;request&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="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;session&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;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSession&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&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="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="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&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;getUsers&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;NextResponse&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;users&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;Authentication checks and HTTP concerns stay in the route. The actual logic lives in the module.&lt;/p&gt;

&lt;p&gt;This separation is boring, repetitive, and extremely effective. It also gives the AI a very clear signal: routes glue things together, modules do the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now with AI
&lt;/h2&gt;

&lt;p&gt;Once this structure is in place, AI becomes both incredibly powerful and incredibly dangerous.&lt;/p&gt;

&lt;p&gt;Left alone, it will always try to be helpful in the laziest possible way: it will code everything in the page, and you will end up doing multiple API calls for nothing, rewrite logic everywhere, etc.&lt;/p&gt;

&lt;p&gt;The goal is simple: remove choices. If there is only one valid way to do things, the AI will eventually follow it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using EJS templates to generate modules
&lt;/h3&gt;

&lt;p&gt;The first step is to make creating a new module trivial and mechanical.&lt;/p&gt;

&lt;p&gt;I use a small script (Node + EJS) that generates a module from the template folder. From the outside, it looks like this:&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="o"&gt;&amp;gt;&lt;/span&gt; npm run create-module user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it generates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/user/
├── api/
│   ├── getUser.ts
│   ├── getUsers.ts
│   ├── createUser.ts
│   └── updateUser.ts
├── components/
│   └── UserCard.tsx
├── hooks/
│   ├── useUser.ts
│   └── useUsers.ts
└── types/
    └── User.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing magical here. The API files are thin, explicit functions. The hooks are small wrappers, usually around React Query. Components are dumb by default. Types are shared between server and client.&lt;/p&gt;

&lt;p&gt;This does two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It removes friction for humans.&lt;/li&gt;
&lt;li&gt;It gives the AI a very strong prior.&lt;/li&gt;
&lt;li&gt;No giant service files.&lt;/li&gt;
&lt;li&gt;No "utils.ts" dumping ground.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I ask the AI to "add a user feature", it doesn't invent a structure. It follows the one that already exists. And if it doesn't, I regenerate or fix the module instead of letting entropy creep in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom ESLint rules as guard rails
&lt;/h3&gt;

&lt;p&gt;Templates handle the happy path. Linters handle the cheating.&lt;/p&gt;

&lt;p&gt;A concrete example: API routes.&lt;/p&gt;

&lt;p&gt;In Next.js, it is very easy to write this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&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;Request&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="c1"&gt;// do stuff&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But in my apps, every route must go through a wrapper that injects context (session, user, permissions, etc.). Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withAuth&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;user&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// do stuff&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I add a custom ESLint rule that enforces this. If a route exports POST, GET, PUT, whatever, and it is not wrapped correctly, linting fails.&lt;/p&gt;

&lt;p&gt;What is interesting is how AI reacts to this.&lt;/p&gt;

&lt;p&gt;When the AI generates a route and forgets the wrapper, the linter error shows up immediately. The AI sees the error, understands the pattern, and fixes the code by adding the missing wrapper. You didn't explain security again. You didn't argue. You let the toolchain do the teaching.&lt;/p&gt;

&lt;p&gt;This works for many things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enforcing file boundaries&lt;/li&gt;
&lt;li&gt;Preventing cross-module imports&lt;/li&gt;
&lt;li&gt;Forcing naming conventions&lt;/li&gt;
&lt;li&gt;Blocking "clever" shortcuts&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Clear instructions
&lt;/h3&gt;

&lt;p&gt;This seems obvious but it is good to bring up again. Whether you are using Cursor rules, Claude Code instructions, or a system prompt somewhere else, the idea is the same: write down your constraints early, and evolve them over time.&lt;/p&gt;

&lt;p&gt;I don't try to be exhaustive. I start with a short list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where code is allowed to live&lt;/li&gt;
&lt;li&gt;How modules are structured&lt;/li&gt;
&lt;li&gt;What is forbidden (monolithic files, cross-imports, business logic in routes, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every time the AI surprises me in a bad way, I don't fix it once. I add a rule.&lt;/p&gt;

&lt;p&gt;Here is where I usually put a big instruction block explaining the module structure, the rules around app/, and how to extend existing modules instead of creating new patterns. I won't paste it here, but the exact content matters less than the habit: treat AI instructions like code. Version them. Improve them. Assume they will rot if you don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;Those tools have been around for a long time, and I mostly ignored them until recently. It’s only now, with AI doing a large part of the typing, that their impact on productivity really clicked for me.&lt;/p&gt;

&lt;p&gt;I’m not advocating for modular code, or vibe coding either. You should do whatever works for you. I’m just sharing what I’ve been doing so far, and in my case, it works pretty well.&lt;/p&gt;

&lt;p&gt;Hopefully it will give you some ideas.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
