AI Writes Code Faster, but Developers Still Have to Understand What Ships to Production
AI can speed up writing code, but developers still need to review, test, and understand every change before it reaches production.
The prompt is short: “build an endpoint to create an order, including validation.” A few seconds later there is code that looks reasonable — a route, a model call, a try/catch. Run it locally with sample data and it returns 201 Created. It even passes the formatter and the linter.
The unanswered questions: does that endpoint actually validate its input, check permissions, handle a failure halfway through, and follow the way this project usually writes code? A linter does not answer any of that. Only someone who understands the requirement can.
The hard part of this work was never typing code faster. The hard part is making sure the code — written by anyone, including an AI — is actually fit to run in production.
What AI really speeds up
An AI coding assistant compresses the distance between intent and text. You have a rough idea, and seconds later there is something to read, criticise, and change. That is a real benefit, especially for work that does not require a new design decision.
Things that genuinely go faster with assistance:
- Scaffolding: routes, empty handlers, DTOs, configuration, and files whose structure is already obvious.
- Repetitive code that follows an existing pattern, such as the fifteenth CRUD endpoint.
- A first draft of unit tests for a function that already exists, which you then review and extend.
- Explaining old code: “what does this function actually do, and who calls it?”
- Turning an error message or stack trace into a hypothesis about the cause.
- Small refactors with clear boundaries, like splitting one long function.
What does not get shorter is the rest of the chain: understanding the requirement, deciding business rules, checking assumptions, testing behaviour, reviewing impact, and owning the result. Writing code is one step in that chain. If one step becomes very fast while the others stay the same, the whole process does not automatically become faster — only where the time is spent changes.
An example: the orders endpoint
Assume the project has users, products, and orders tables. The requested endpoint is POST /api/orders. The project already has a service layer, a validation helper, and a convention for error responses.
Here is an illustration of what a prompt like that might produce. This is for reading, not production code:
// Illustration. Not code ready to use.
$router->post('/api/orders', function () {
$body = json_decode(file_get_contents('php://input'), true);
$order = Order::create([
'user_id' => $body['user_id'],
'product_id' => $body['product_id'],
'quantity' => $body['quantity'],
'note' => $body['note'] ?? null,
]);
return json_encode(['id' => $order->id]);
});
Nothing is syntactically wrong. The problem is that the code skips the questions that decide whether it belongs in production.
Input validation. Is quantity really a positive integer? What happens when product_id does not exist? Is there a length limit on note? Are unknown fields rejected or silently ignored? Does the same request sent twice — because the user double-clicked — create two orders?
Authorisation. user_id comes from the request body. Anyone with a valid token can create an order on behalf of another user. That value should come from a verified session or token, not from client-supplied input. And is this user even allowed to buy that product — for example, a product restricted to a tenant, a plan, or a region?
Security. The loose Order::create($body) shape becomes mass assignment the moment a field like status or price is included. Errors returned straight to the client can leak internals: table names, query fragments, configuration. And if a later query is assembled by string concatenation, injection is back on the table.
Business logic. Is stock checked? Does the price come from the database or from the body? Are there minimum quantities, per-user limits, or discount rules? Edge conditions like these rarely appear in the prompt, and rarely appear in the generated code either.
Database. Creating an order usually writes to more than one table: orders, possibly order_items, and a stock decrement. Those need a transaction, so a failure on the second step does not leave a half-finished order behind. Foreign keys, column types, and relations must match the schema that actually exists — not the one the AI assumed.
Error handling. Is the response for invalid input consistent with the other endpoints? Does a database failure return a 500 in the same shape, without exposing details? Is there a timeout when calling the payment provider?
Project fit. This code puts logic directly in the handler, while the project uses a service layer. It repeats manual validation while a helper already exists. None of that crashes the app, but it makes the codebase harder to maintain — and that is exactly what a quick review tends to miss.
After review, a more sensible version is not merely patched but aligned with the project’s patterns:
// Illustration of the reviewed shape. Still not ready-to-ship code.
$router->post('/api/orders', function (Request $request, Response $response) {
$user = $request->getAttribute('user'); // verified by middleware
if ($user === null) {
return $response->withStatus(401)->withJson(['error' => 'unauthenticated']);
}
$input = $request->getParsedBody();
$errors = $this->validator->validate($input, [
'product_id' => 'required|integer|exists:products,id',
'quantity' => 'required|integer|min:1|max:100',
'note' => 'max:500',
]);
if ($errors) {
return $response->withStatus(422)->withJson(['errors' => $errors]);
}
$order = $this->orders->place($user->id, $input); // transaction + stock check
return $response->withStatus(201)->withJson(['id' => $order->id]);
});
Notice that the most important change is not added code. It is moving decisions to the right place: identity from middleware, validation into the project helper, the transaction into the service layer.
How to review AI-generated code
There is no mandatory order, but this works as a frame. Scale it to the risk of the change instead of performing it as a ritual.
- Read the diff. Not only the files that changed, but the ones that were touched along the way. AI sometimes edits configuration, adds dependencies, or removes code it should not.
- Match it against the requirement. Reopen the ticket. Every change should have an explainable link to the requirement.
- Check validation, authorisation, error handling, and data exposure. These are the areas where mistakes are most expensive and least visible on the happy path.
- Run the tools the project already has. Formatter, linter, type checks, static analysis. If they report new errors, fix the code rather than disabling the check.
- Review the tests and add what is missing. Generated tests lean towards the success path. Failure cases, empty input, boundaries, and duplicates are usually on you.
- Run the tests and read the output. Do not rely on an AI summary saying “all tests pass.” Look at the result, including warnings that do not fail the build.
- Think beyond the code. Schema changes, API contracts, query performance, log format, and compatibility with existing clients.
- Review before merging. For higher-risk changes — authentication, payments, migrations — get a second reviewer. AI does not remove the need for a second pair of eyes.
A safer workflow
The flow for AI-assisted work is not very different from the normal one:
Clear requirement → AI helps make the change → developer reads the diff → tests and automatic checks run → review → merge → deploy with monitoring and a rollback plan.
What makes this work is the context given up front. AI does much better when it knows the project structure, naming conventions, layers that must not be bypassed, dependencies that must not be added, and acceptance criteria. Many tools let you supply that through a project instruction file, a link to the database schema, or an existing endpoint used as an example.
Two things not to do: paste secrets (tokens, credentials, connection strings) or personal user data into a tool that has not been approved to receive them. The rule is the same one that applies to any third-party service.
The simplest way to keep the right attitude: treat AI output like a pull request from a contributor you have never met. It may be good, it may be exactly right, but it still has to be read, tested, and owned before it reaches the main branch.
Where AI helps, and where to be more careful
Uses that fit reasonably well:
- Boilerplate and repetitive code with an established pattern.
- A first draft of unit tests, then reviewed and extended.
- Explaining legacy code and tracing call paths.
- Small refactors with clear boundaries that are easy to verify.
- Documentation written from code that actually exists, then checked against real behaviour.
Uses that need close supervision:
- Authentication and authorisation.
- Payments and financial transactions.
- Database schema changes and data migrations.
- Handling personal data and privacy obligations.
- Infrastructure, permissions, secrets, and production configuration.
- Large changes spanning many modules or services.
One more note: a task that looks simple is not automatically safe. Adding one column to a table seems trivial until the migration locks a large table during peak hours.
Closing
AI is useful precisely because it produces something quickly. That something is material to work with: you can criticise, compare, and improve far faster than writing from scratch. But the speed of producing code is not the measure of success. The real measure is whether the change meets the requirement, can be tested, is safe, stays maintainable, and can be defended by the person who shipped it.
AI can help us write code faster. Making sure that code is fit to ship remains engineering work.
Note: this article contains no statistics or benchmark results, because no measurements were taken to support such claims. The code above is illustrative, not a snippet that was executed.
Further reading: OWASP Top Ten and the OWASP Application Security Verification Standard — general references for checking input validation, access control, and error handling.