ai newsMi., 4. Nov.

The Tests Pass and the Code Looks Great: 6 Things I Check in AI-Generated Code Before Merging

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 er

dev.toMittwoch 08:36 · 04.11.5 Min. Lesezeit
Bild: dev.to

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.

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? Enter fullscreen mode Exit fullscreen mode Checks that take a minute:

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.

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 }); } Enter fullscreen mode Exit fullscreen mode 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 }); Enter fullscreen mode Exit fullscreen mode For every route that takes an ID, ask: "What stops user A from sending user B's ID?"

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 } } Enter fullscreen mode Exit fullscreen mode 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.

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

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.

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

const user = await User. findOne ({ email: body. email, password: body. password }); Enter fullscreen mode Exit fullscreen mode 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.

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:

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

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.

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

Quelle: dev.to (dev.to). Original öffnen