Vibe Coding vs. Engineering: When AI Code Is Enough and When It Isn’t
The Afternoon That Went Too Well
Picture this: a client needs an internal tool that captures requests, filters them and exports them as CSV. You open your AI assistant, describe what you want in three paragraphs, and two hours later you have an interface that looks like a team spent three weeks on it. The client is thrilled. So are you.
Six months later, a new field needs to be added. Suddenly it turns out that the validation lives in four places, the state sits half in the component and half in local storage, and nobody knows why a particular request goes out twice. The code works, but nobody has ever really read it. Including you.
This is a thought experiment, but I'm fairly sure it's playing out exactly like this in a lot of projects right now. And that's what this post is about. Vibe coding is neither nonsense nor the end of the world. It's a tool with a clear use case. Trouble only starts when we forget how it differs from engineering.
What Vibe Coding Actually Is
The term goes back to Andrej Karpathy, who coined it in early 2025. It describes a way of working: you tell the AI what you want, accept the result without checking it in detail, and steer only by looking at the outcome. Does it run? Good. Error message? Copy, paste, move on. You follow the vibes and let the code take shape in the background.
The distinction matters. If you read, understand and deliberately accept AI suggestions, you're not vibe coding. You're working with a very fast pair-programming partner. Vibe coding in the strict sense starts where you consciously give up understanding the code.
Why is it so tempting? For three reasons:
- Speed: From idea to a working interface in minutes.
- Low barrier: Even people who can barely program get something that works.
- Flow: No more jumping between docs, search engine and editor.
Honestly, it feels great. That's exactly why it's worth taking a sober look at what's going on. Vibe coding shifts the work, it doesn't make it disappear. Writing code becomes cheap. Understanding it, checking it and taking responsibility for it remain, and that work comes due later, usually at a worse moment.
Where Vibe Coding Shines
I don't want to give the impression that I'm against it. There are situations where it's exactly the right choice.
Prototypes and idea validation. When the question is whether anyone would use this at all, code quality is secondary. A clickable prototype built in an afternoon saves weeks of discussion. What matters is that the prototype is treated as a prototype and thrown away afterwards.
Throwaway scripts. A one-off data migration, a script that renames 300 files, a report for this afternoon. Once it has run correctly, the job is done. Here it's enough to check the result, not the code.
Internal tools for a small group. A dashboard for three colleagues that doesn't touch sensitive data and can't disrupt anything in production. The risk is limited, the benefit is immediate.
Learning and exploring. Trying out an unfamiliar library, testing two approaches against each other, seeing a concept in code before building it properly. AI is an excellent sparring partner for this, as long as you don't just accept everything unseen afterwards.
What these cases have in common: the damage from mistakes is small, the lifespan is short and the circle of people affected is small. As soon as one of these three criteria tips over, things get risky.
Where It Breaks Down
It becomes a problem when the prototype quietly turns into production code. That happens more often than you'd think, because it already works and the client is asking for the next feature.
Security. AI code looks plausible even when it isn't. Typical suspects are missing input validation, overly permissive CORS settings, secrets in the client, or database queries assembled from strings. If you never read the code, you won't notice. Attackers certainly will read it.
Maintainability. The model optimizes for "works now", not for "can still be changed a year from now". What comes out is often duplication, inconsistent patterns and abstractions that only make sense in the context of the current prompt.
Debugging. This is where you pay the price most clearly. If you don't understand the code, you can't narrow down a bug. You end up in a loop: paste the error, get a fix, new error. Sometimes the AI actually solves the problem. Sometimes it builds a workaround around the symptom, and the root cause moves one level deeper.
Responsibility. When a client loses data, they don't ask which model wrote the code. They ask the person they hired. Especially as a freelancer, that's something I can't hand off, and don't want to.
Technical Debt Nobody Notices
Classic technical debt is familiar: you know you're taking a shortcut, and you take it deliberately. Vibe coding creates debt you don't even know about, because you never read the code. In practice, that difference really hurts.
An example from everyday frontend work. This is the kind of thing an assistant hands you on request:
async function loadUser(id: string) {
const res = await fetch(`/api/users/${id}`)
const data = await res.json()
return data as User
}
The code is short, easy to read and does exactly the right thing in testing. Still, there's a lot hiding in it:
- There's no check on
res.ok. A 404 or 500 ends up in your code as a supposed user. - The
as Userisn't a check, it's a promise to the compiler. If the server returns something else, you only notice much later, somewhere else entirely, with a cryptic error message. - The
idis inserted into the URL unchecked. - With fast typing or navigation, requests can overtake each other, because nothing gets aborted.
A version I'd add to a project without a second thought:
import { z } from 'zod'
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.email()
})
type User = z.infer<typeof UserSchema>
async function loadUser(id: string, signal?: AbortSignal): Promise<User> {
const res = await fetch(`/api/users/${encodeURIComponent(id)}`, { signal })
if (!res.ok) {
throw new Error(`Failed to load user (${res.status})`)
}
return UserSchema.parse(await res.json())
}
It's not more complicated, but it's more honest. Errors become visible, and the data is validated at runtime instead of merely asserted in the type system. The point isn't that the AI couldn't write this. It can, if you ask for it. But if you don't know what to ask for, you get the first version. And that knowledge is exactly what engineering is.
What Engineering Still Means
There's a notion that software development is mostly typing. If you think that way, you'll inevitably see AI as a replacement. In practice, writing code is only part of the job, and rarely the hardest part.
To me, engineering is mainly about decisions:
- Architecture: Where does each responsibility live? What should be easy to change later, and what shouldn't? A model sees the prompt, but not the company, the roadmap or the client's quirks.
- Tests: They're not a chore, they're the answer to the question of how I know it still works after I've changed something. Especially with generated code, they're the most important safety net.
- Review: Someone has to understand the code before it goes live. That applies to code from colleagues just as much as to code from an AI. And the faster code is produced, the more review becomes the bottleneck.
- Operations: Logging, error handling, updates, dependencies. Everything that comes after the first deployment and never shows up in the demo video.
What I particularly like: good engineering practice and AI use don't exclude each other, they reinforce each other. Strict TypeScript, a well-configured linter and decent test coverage are guardrails that a model bounces off too when it builds nonsense. With guardrails like that, you can trust the AI with a lot more. Without them, you're driving blind at high speed.
So the roles are shifting: less typing, more reading, assessing and deciding. That doesn't devalue the profession. If anything, quite the opposite.
A Rule of Thumb for Everyday Work
The question isn't whether you use AI, but how much control you give up. Before every task, I ask myself three questions:
- What happens if it's wrong? Will one person be briefly annoyed at worst, or will data, money or trust be lost?
- How long will the code live? Does it run once today and get forgotten tomorrow, or will it still be extended a year from now?
- Who will need to understand it later? Just me, a team, or a client who takes it over at some point?
If the answers are "not much", "briefly" and "just me", the AI can run: prompt in, check the result, done. If even one answer is clearly different, I take back control. In concrete terms:
- Work in small steps. One feature per request, not the whole application at once. Small diffs are something you can actually read.
- Set the frame. Which libraries, which patterns, which error handling. The clearer the guidelines, the fewer the surprises.
- Read every line I accept. If I don't understand something, I ask or rewrite it until I do.
- Tests first or right after. The tests can come from the AI too, but I have to check that they test the right thing and not just what the code currently does.
- Turn up types and linting. The more the compiler catches, the less I have to find myself.
That sounds like a brake, but it isn't. The time you invest in reading pays for itself several times over in debugging.
Conclusion: Vibes Are Not a Foundation
Vibe coding is a great way to get from an idea to something tangible quickly. For prototypes, scripts and small internal helpers, I use it happily and with a clear conscience. But as soon as real users, real data or real money are involved, vibes are no longer enough.
So my position is pretty clear: for production code, pure vibe coding is not a good idea. Not because the AI is bad, but because responsibility can't be delegated. Whoever ships the code has to understand it, or at least be willing to understand it when it counts.
The exciting part is that AI doesn't make the profession obsolete, it shifts its focus. Generating code becomes cheap. Judging whether it's good stays valuable. If you can do that, these tools give you enormous leverage. If you can't, you'll create a lot of trouble for yourself very quickly.
My takeaway: use AI the way you'd use a talented but inexperienced colleague. Give clear tasks, read what comes back, and don't accept anything you can't explain. Then you get the speed of vibe coding and the reliability of engineering.