AI assistants now write a large and growing share of new code, and the honest read on that is mostly positive: a working prototype in a weekend is a genuinely better starting point than a slide deck. The problem is what happens next. A prototype that demos well gets shown to a customer, the customer wants to use it, and code written to answer “does this idea work” is suddenly answering “can we run our business on this”.
We get called in at that exact moment more than any other. Here is what we consistently find, and what to do about it.
What is usually fine
Worth saying first, because the sneering take on AI-written code is as wrong as the hype. The structure is often reasonable. Naming is usually better than what we find in hand-written code of the same age. The framework conventions are followed. It is not gibberish and it does not need to be thrown away.
The problems are not sloppiness. They are a consistent set of blind spots, and they are consistent because they come from how the code was produced: file by file, prompt by prompt, with nobody holding the whole system in their head.
What is consistently missing
Authorization, as opposed to authentication. Login almost always works. What is usually absent is the check that this logged-in user is allowed to see this particular record. Endpoints take an ID and return the row. Nobody asked for the check, so it was never written, and it never shows up in testing because the person testing owns all the test data.
Anything that has to be true across files. A prompt sees the file it is editing. So the rate limiter exists on one route and not the other four, error handling is thorough in one service and absent in the next, and three different date-handling approaches coexist in one codebase. Every individual file is defensible. The system is not consistent.
Failure paths. Happy paths are well covered. What is missing is what happens when the payment provider times out, when the upload is 400MB, when two requests hit the same record at once, or when the third-party API returns a 200 with an error in the body. Nobody prompted for the bad day.
Real secret handling and real migrations. Keys in the repo or in a client bundle. Schemas that were changed by editing the model and letting the framework sync, which is fine until there is data in the table you cannot lose.
Tests that assert the code does what it does. Where tests exist, they were frequently generated from the implementation rather than the requirement. They pass. They will keep passing after the logic breaks, because they were written to describe the code rather than to constrain it.
The costly failure: N+1 everything
The performance profile deserves its own note, because it is the one that turns into an outage rather than a breach. Generated data access is typically written per record: fetch the list, then fetch each item’s relations in a loop. With 50 rows of seed data, it is instant. With 50,000 rows and real concurrency, it takes the database down.
This is the most common reason a prototype that “worked great” falls over the week it gets traction, and it is usually a two-day fix once someone actually profiles it. The trouble is that nobody profiles until it is on fire.
How to harden it without a rewrite
The instinct to start over is almost always wrong, for the same reason big-bang rewrites of legacy systems fail: you throw away working behavior along with the problems, and you spend months reaching parity with where you already are. A prototype that has real users is a specification that runs. Treat it as an asset.
The order that works:
- Audit before touching anything. Two to five days, mapping what exists, what the data actually looks like, and where the sharp edges are. Everything after this is cheaper for having done it.
- Close the security holes first. Authorization on every endpoint that returns data, secrets out of the repo and into a real store, credentials rotated. This is not negotiable and it is usually the fastest phase.
- Put a test harness around current behavior. Not comprehensive coverage. End-to-end tests over the handful of flows that make money, so the next steps are safe.
- Fix the queries. Profile against production-shaped data, not seed data. The N+1s and the missing indexes are usually most of the performance problem.
- Then, and only then, refactor for consistency. One approach to errors, one to dates, one to data access. With tests in place this becomes routine work instead of a gamble.
- Add the operational layer. Real migrations, monitoring, alerting, backups you have restored from at least once.
Most of the prototypes we take through this reach production in four to eight weeks, and they keep the thing that made them valuable: they already do what the business needs, because someone built it fast enough to find out.
The part that matters for next time
Keep using AI to write code. The speed is real and it is not going back. What changes in a production codebase is the standard the output has to clear: a review that asks about authorization and failure modes rather than whether it runs, tests written from the requirement instead of the implementation, and one person who holds the shape of the whole system in their head. AI is very good at writing the file in front of it. Nobody has automated caring about the system.
If you have a prototype with real users and a growing feeling that it is held together with tape, that is a normal place to be and a fixable one. See how our legacy and modernization work runs, or get an estimate: a short paid audit first, then a fixed price against what is actually there.