Flamehaven LogoFlamehaven.space
back to writing
Pretty, but Wrong — How I Stop AI from Shipping Beautiful Garbage

Pretty, but Wrong — How I Stop AI from Shipping Beautiful Garbage

AI can generate clean-looking code fast—but without discipline it becomes “beautiful garbage” that silently rots your codebase. Learn six rules to prevent it.

notion image
The first time I watched AI pour out code, I felt that tiny hit of magic.
It ran. Tests passed. My brain whispered: ship it.
Two weeks later I changed one line and the whole thing cracked like sugar glass.
That’s when I named it: beautiful garbage.
It looks clean.
It “works.”
It rots your codebase.
This isn’t a style guide. It’s what I tell myself when autocomplete gets cocky and I’m tempted to merge without thinking.

1) Respect the pattern — or the team pays later

AI is great at a solution, not our solution.
Ask for “a new function,” and it happily ignores how we actually build things — folder conventions, controller/service/repo, that weird-but-intentional CRUD rhythm future-me depends on.
My pre-flight (out loud):
“what does CRUD look like here?” · “which layer touches I/O?” · “show me 3 similar files and copy their shape.”
Prompt I paste
Match the existing architecture exactly. Same folders, same layer boundaries, same naming, same CRUD flow. Do not invent directories.
Tiny snippet
Smell test: if the new file wouldn’t pass a review from your past self, don’t let your present self merge it.

2) One source of truth — or a hundred places to be wrong

Duplication “works,” which is why AI does it. Types, constants, flags — sprinkled like confetti. Festive. Also maintenance hell. I once chased a single product type through 47 files because I let it slide once.
Ritual
Search before naming. If canonical exists, reference it. If not, create it once in the right module — then import it everywhere.
Prompt I paste
Reuse existing types/constants if present. If new, define once in core/domain and import; do not redefine locally.
Tiny snippet
One truth. One file. One fix when the world changes Thursday at 3:17 p.m.

3) Hardcoded values are landmines with pretty labels

“Cancel.”
“Complete.”
“Retry-After.”
“5000.”
Hardcoding works — until Cancel becomes CANCEL in one place, cancelled in another, or someone types Cnacel and nobody notices.
Rule
Any value a human sees or a system depends on lives in config/constants.
Prompt I paste
Extract all user-visible statuses, numeric thresholds, and feature flags into @/core/config. Replace inline literals with imports.
Micro-diff
Future-you will buy future-you a coffee.

4) The happy path is a fairy tale; ship for the weird path

Users double-click.
Tokens expire.
Networks blink.
Someone pastes a 3MB base64 into a text input because of course they do. AI writes the brochure version: success → success → success.
The rest? console.log(err) and a shrug. Worse, TypeScript sins like as any to silence red squiggles. That’s not engineering. That’s hiding.
Minimums I enforce
  • Every I/O: timeoutretry/backoffuser-facing failure state
  • Every form: server boundary validation + UI validation
  • Every async: disable/enableidempotence, dead-button defense
  • Every error: an actual recovery plan (“Try again,” not “Unhandled promise”)
Prompt I paste
List likely failure modes. For each, implement behavior, user message, and state transitions. No any. No silent catch. Add tests for error paths, not just the happy one.
Sketch
If you haven’t argued with your error states, you’re not done.

5) If a function does two things, it does neither well

Left alone, AI will pack API calls, caching, date formatting, and UI rendering into a 400-line blob. Not evil — just literal.
Knife I use
UI renders · Services orchestrate · Repos talk to storage · Utils are pure and boring.
Prompt I paste
Decompose: page (render), service (business flow), repo (IO), utils (pure). Propose which functions belong in Shared/ and extract them.
Before/After
Smaller pieces invite better tests. Better tests invite sleep.

6) The Shared folder is a garden, not a junk drawer

AI won’t curate. It won’t notice that formatMoneyformatCurrency, and moneyToText are the same idea with three names and four bugs.
Humans curate. We promote, rename without shame, and delete with joy.
My rule after AI-assisted changes
  • Did I duplicate something generic? Promote it to Shared/.
  • Add the tiniest docstring and one test.
  • Replace local copies with imports; tell the team where it lives now.
Prompt I paste
Identify generic utilities/components. Move them to Shared/. Replace local copies with imports. If behaviors differ, propose a superset API with tests.
Tiny doc habit
Shared code raises the project’s IQ — slowly, then suddenly.

Unglamorous habits that save me

  • Diff with intent. Not “what changed?” but “what doesn’t belong here?”
  • Search before you name. Nothing new until you’ve looked.
  • Write the failure path first. If it’s awkward, the design is wrong.
  • Keep a tiny “AI debt” list. If you accept a compromise, log it and clear it within the week.
And yes, I still let AI help. I just refuse to let it decide.
AI is not a junior dev. It’s a fast typist with no memory of your last incident.

TL;DR (tape this above your monitor)

  • Match the pattern you already use.
  • One source of truth — imports, not clones.
  • Rip out magic strings/numbers. Name them. Centralize them.
  • Ship the error path like it’s the only path.
  • One job per unit — cut earlier than feels comfortable.
  • Curate Shared/: promote, rename, delete. Repeat.
If this reads uneven — good. Real projects are. Real codebases are specific. “General best practices” are where beautiful garbage grows.
What did I miss? What’s your ugliest AI-generated bug that looked fine until it didn’t?
 

Share

Related Reading