AI-generated code has a particular way of failing. It does not look broken. It is neatly formatted, well named, commented, and the happy path works on the first run.

That is exactly the problem. When code looks this confident, we review it less carefully. The bugs that get through are not syntax errors. They are plausible mistakes: code that reads as correct and is wrong in a way you only see if you ask the right question.

Here are the six questions I ask before merging anything an AI tool wrote. They also work on code from a tired human at 6 p.m.

1. Does Every Import and Method Actually Exist?

Models sometimes produce a package name or a method that sounds right and is not real. Sometimes it existed in an older version and was removed.

import { parseDuration } from 'time-utils-pro'; // does this package exist? const result = Array.prototype.groupBy(items, 'type'); // is this a real method in your runtime? 

Checks that take a minute:

  • Open the package on the registry. Look at who publishes it and when it was last updated.
  • Compare the API against the docs for the version in your package.json.
  • If the install fails or the type checker complains, believe them, not the code.

There is a security angle too. If a model suggests a package name that does not exist, anyone can register that name later and put something harmful in it. Never install a dependency just because generated code imported it.

2. Does It Check Who Owns the Data, Not Just Who Is Logged In?

This is the most common serious bug I see in generated API routes. The code checks that a user is logged in and then loads a record by ID:

export async function DELETE(req: Request, { params }: { params: Promise<{ id: string }> }) { const session = await getSession(); if (!session) return new Response('Unauthorized', { status: 401 }); const { id } = await params; await Invoice.findByIdAndDelete(id); // any logged-in user can delete any invoice return new Response(null, { status: 204 }); } 

Being logged in is authentication. Being allowed to touch this specific record is authorization, and generated code often skips it. The fix puts ownership into the query itself:

const deleted = await Invoice.findOneAndDelete({ _id: id, userId: session.userId }); if (!deleted) return new Response('Not found', { status: 404 }); 

For every route that takes an ID, ask: "What stops user A from sending user B's ID?"

3. What Happens When Something Fails?

Generated code loves a try/catch that makes errors disappear:

async function saveOrder(order: Order) { try { await db.orders.insertOne(order); return { success: true }; } catch (err) { console.log(err); return { success: true }; // the order was not saved } } 

That example is exaggerated, but softer versions are everywhere: a catch that logs and continues, a default value that hides a failed request, a function that returns null for both "not found" and "database down."

Ask of every catch: who finds out? If the answer is "nobody except whoever reads the logs later," the code is hiding a failure from the user and from you.

4. Is It Written for the Version You Actually Run?

Models learn from code written over many years, so they may produce patterns from older versions of a framework. Examples that bite in practice:

  • In Next.js 15, params and searchParams in pages and route handlers are Promises and need to be awaited. Older examples read them directly.
  • Old React patterns such as data fetching in useEffect when your framework has a better, server-side option.
  • Deprecated options in config files that are now ignored or rejected.

Habit: when generated code uses a framework feature, find the same feature in the official docs for your version and compare the shape. If you tell the model your exact versions in the prompt, you get fewer of these, but you still verify.

5. What Does It Do With Input It Did Not Expect?

Generated code is usually written for the example in the prompt. Try the inputs the prompt did not mention:

  • An empty string, an empty array, null
  • A very long value
  • The same request twice, or two at once
  • A value of the wrong type

A classic one in Node and MongoDB:

const user = await User.findOne({ email: body.email, password: body.password }); 

If body comes straight from JSON and nobody validated it, an attacker can send { "email": "a@b.com", "password": { "$ne": null } }. That is a query operator where you expected a string, and it can match without knowing the password. Validate input shape with a schema first (Zod works well), and never compare plain-text passwords in a query at all. Hash them and compare the hash.

The general rule: types in TypeScript do not exist at runtime. Anything from outside your code needs a runtime check.

6. Did It Add More Than I Asked For?

I asked for a function that formats a date. I got a function, a new dependency, a config file and a utility class. Generated code tends to be generous, and every extra piece is something you now maintain and might have to secure.

Before merging, look at the diff stats, not just the code:

  • New dependencies in package.json? Do you need each one?
  • New files you did not request?
  • Changes to files unrelated to the task?
  • A 120-line solution to something a standard library function does in two?

Smaller diffs are easier to review, and review is where bugs get caught.

The Tests Trap

One more thing, because it connects all of the above. If you ask the same tool to write the tests, it will write tests that agree with its own code, including its misunderstandings. Green tests then prove only that the code matches itself.

Write at least the important tests from the requirement, not from the implementation. For the invoice route above, the test that matters is: "user B cannot delete user A's invoice." A generated test suite will often not include it unless you ask.

The Checklist

Copy this into your PR template:

  • [ ] Every package and method exists, for my version
  • [ ] Every route that takes an ID checks ownership
  • [ ] Every catch reports or rethrows, nothing disappears
  • [ ] Framework usage matches the docs for my version
  • [ ] Outside input is validated at runtime, edge cases tried
  • [ ] The diff contains only what I asked for

AI tools are a real speed boost, and I use the output as a fast first draft. The review is where the engineering happens, and a short, consistent checklist makes it a habit instead of a mood.

I build SaaS products and client dashboards with Next.js, and ownership checks and input validation sit in the server code of everything I ship. If you want a typed starting point, you can browse my templates on pixelanas.com.

Get the templates: https://pixelanas.gumroad.com

Which mistake does your AI tool repeat most often? Tell me in the comments, and I will add the best ones to the checklist.

Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751