← All articles

How to vibe code without shipping a pile of spaghetti

If you've built a couple of small apps by vibe coding, you've probably already noticed the problem. The first version works great. The second version works, but the code is starting to look a little weird. By the fifth or sixth round of changes, you're staring at files that have the same logic written three different ways, config values hardcoded in six places, and a 900-line file that does everything from database calls to HTML rendering. The app runs. The code is a mess.

That's the default output when you let an AI run unsupervised. It's not that the AI can't write clean code — it absolutely can — it's that it won't unless you tell it what clean looks like. You can fix most of this before you ever ask for your first feature, by setting the ground rules up front and then holding the AI to them as you go.

Set the rules before you build anything

Before I ask Claude to build a single feature on a new app, I give it a project instruction file that spells out how I want the code organized. Not vague stuff like "write clean code" — specific stuff it can actually follow. Here's the kind of thing that goes in there:

  • Folder structure. "Controllers go in /app/controllers. Database access goes in /app/models. Views go in /app/views. Shared helpers go in /app/lib." Pick a structure and name it. The AI will follow it if you tell it to.
  • Database access layer. "All database queries go through the model classes. No raw SQL in controllers, no raw SQL in views, ever." This one rule alone prevents half the spaghetti.
  • Secrets and config. "API keys, database passwords, and anything environment-specific lives in a .env file loaded at startup. Never hardcode a secret in source. Never commit .env to the repo."
  • Styling. "No inline styles. All CSS lives in /public/css. Use existing utility classes before adding new ones."
  • Error handling. "Every database call and every external API call is wrapped in try/catch. Errors are logged, not swallowed, and the user sees a friendly message."
  • Naming. "Functions are verbs, variables are nouns, constants are UPPER_SNAKE_CASE." Pick a convention and the AI will stick to it.

None of this is exotic. It's the same stuff a senior developer would set up on day one of a new project. The difference is you have to say it out loud because the AI defaults to "whatever gets this one feature working right now," and that default is what builds the pile.

Check its work as you go

The project instructions get you a clean start. Keeping it clean through 50 iterations is a different habit, and it's the one most people skip.

A few things I do during a session that have saved me cleanup time later:

Ask it to explain its architecture choices. After it adds a nontrivial feature, I'll ask "walk me through how this fits with the existing code — where did you put the new logic, and why there?" Half the time the answer is reasonable. The other half, the AI admits it tucked something in a weird place because it was easier than refactoring, and now you know to push back.

Catch duplication the moment it starts. If I notice the AI writing a function that looks a lot like something it wrote last week, I stop and say "this is doing the same job as X — pull the shared logic into a helper and have both call it." Duplication is cheap to fix in the moment and expensive to untangle a month later, when there are four slightly different versions and none of them quite match.

Write the test before the fix. When something's broken, resist the urge to just say "fix it." Instead: "write a test that fails because of this bug, then fix the bug so the test passes." You end up with a test suite that covers the stuff that's broken in real life, which is way more useful than tests the AI invents on its own.

Run a periodic cleanup pass. Every so often — maybe every ten features or so — ask the AI to review the whole codebase against the project instructions and flag anything that drifted. Dead code, duplicated logic, hardcoded values that should be in config, files that have grown too big. Then have it fix what it found.

The tradeoff

All of this slows the vibe down a little. You're not just saying "build me a thing" and watching it appear — you're setting rules, asking questions, pushing back on shortcuts, and occasionally stopping to clean up. The first feature takes a bit longer than it would have otherwise but the end result is so much better. You can add a feature without breaking two others. You can hand it to another developer and they can find things. You can read your own code six months from now and understand what it's doing. The alternative is the version where every new change takes longer than the last one, until eventually you give up and start over, which is where a lot of vibe-coded projects actually end up.

Leave a comment

Comments are moderated — I read every one. Your email is only used if I need to follow up; it isn't published. Links aren't allowed.

← All articles