Andrea Possidente
iten

AI Published 10 min read

Vibe coding: why knowing the code matters more than ever

Describing an app in one sentence and watching it run a few minutes later is real. But who sets the direction, and who checks the result is right, if nobody can read the code?

In this post

One sentence is now enough to get an application that runs. Type “build me a dashboard with login, charts and a filterable table” and within minutes you have a project that compiles, starts and even looks decent. A few years ago that would have taken a week. It’s a huge shift, and there’s no point pretending otherwise: AI coding assistants are among the most powerful tools developers have ever had.

That is exactly why an uncomfortable question is worth asking. If generating code has become easy, what has become hard? My answer is that the difficulty hasn’t gone away. It has moved: from writing code to deciding what to write and understanding whether what was written is right. And for those two things, knowing the code isn’t less important than it used to be. It matters more.

What we mean by vibe coding#

The phrase was popularised by Andrej Karpathy in early 2025, describing a way of programming where you give in to the flow: you ask, you accept, you paste the error back when something breaks, and you keep going without really reading the code. He framed it as a good fit for weekend projects, not as a universal way of working.

Within those limits it works remarkably well. A prototype to show tomorrow, a script you’ll run once, an experiment to see whether an idea holds up: in all of these, speed matters more than robustness and a mistake costs very little. It can be valuable for learning too, as long as you use it to explore rather than to skip steps.

The trouble starts when the same approach is applied to things that have to last: a product with real users, real data, real money. There, “it works on my machine” guarantees nothing.

Plausible code is the most dangerous kind#

A language model is very good at producing plausible code: well formatted, sensibly named, structured the way a competent person would write it. Most of the time it’s also correct. But when it isn’t, the mistake looks exactly like the right answer. That’s what makes it so hard to spot if you don’t know what to look for.

Take a React component that shows search results as the user types. It’s the kind of thing an assistant produces in seconds:

function ProductSearch() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState<Product[]>([]);

  useEffect(() => {
    fetch(`/api/products?q=${query}`)
      .then((res) => res.json())
      .then(setResults);
  }, [query]);

  return (/* input and results list */);
}

You try it, you type “shoes”, shoes appear. It works. Yet it has at least four problems, none of which shows up in a quick test:

  • Responses can arrive out of order. If the user types “sh” and then “shoes”, the request for “sh” may come back last and overwrite the right results with the wrong ones. On a slow connection this happens a lot.
  • It fires a request on every keystroke, even for one-letter queries, and even when the field is empty.
  • The text isn’t encoded: a search containing & or # produces a different URL than intended.
  • Errors aren’t handled: if the server fails, the interface simply goes quiet.

A sturdier version isn’t much longer, but it requires knowing those problems exist:

useEffect(() => {
  if (!query.trim()) {
    setResults([]);
    return;
  }
  const controller = new AbortController();
  const timer = setTimeout(async () => {
    try {
      const res = await fetch(`/api/products?q=${encodeURIComponent(query)}`, {
        signal: controller.signal,
      });
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      setResults(await res.json());
    } catch (error) {
      if (!controller.signal.aborted) setError(error);
    }
  }, 250);
  return () => {
    clearTimeout(timer);
    controller.abort();
  };
}, [query]);

The assistant can write this version too, and often will if you ask. That’s the whole point: if you ask. To ask, you need to know the first version is fragile.

Who sets the direction#

A prompt is a specification. A vague specification gets its gaps filled with the most likely choices, not the right ones for your case.

“Build me a product list” is one request. “Build me a product list with server-side pagination, filters in the URL so they can be shared, a loading state that doesn’t make the layout jump, and full keyboard support” is a different request, and it produces different software. The gap between them isn’t prompt-writing skill. It’s knowledge. To write the second one you need to know what server-side pagination is, why filters belong in the URL, what layout shift is, what keyboard accessibility means.

The same goes for the decisions that really matter, the architectural ones. Server or client rendering? Where does state live? What happens when there are ten thousand records instead of ten? Which library should you add, and more importantly, which shouldn’t you? An assistant will offer a reasonable answer to each of these. But “reasonable on average” isn’t “right for your project”, and choosing between options means understanding them.

In my frontend work on online gaming platforms, and before that on the e-commerce stores I built and ran, the decisions that weighed most were never about how to write a function. They were about what to build, in what order, with which trade-offs. That’s exactly the part no tool can do for you, because it depends on context only you have.

Verifying: the skill you can’t delegate#

If setting the direction is the first half of the job, verifying is the second. It’s also where vibe coding shows its limits most clearly.

Here’s an Express endpoint that returns orders filtered by status. Again, the kind of code you get in seconds and that, tried once, looks perfect:

app.get("/api/orders", async (req, res) => {
  const { status } = req.query;
  const result = await db.query(
    `SELECT * FROM orders WHERE status = '${status}'`,
  );
  res.json(result.rows);
});

Anyone with a basic grasp of security spots it immediately: a value from the URL goes straight into the SQL query. It’s textbook SQL injection, and one crafted parameter is enough to read or change data that should never be reachable. It isn’t the only problem, either. Nothing checks who is asking, so anyone can see everyone’s orders; SELECT * returns columns that maybe shouldn’t leave the database; and there’s no limit on how many rows come back.

app.get("/api/orders", requireAuth, async (req, res) => {
  const status = String(req.query.status ?? "open");
  const { rows } = await db.query(
    `SELECT id, status, total, created_at
       FROM orders
      WHERE customer_id = $1 AND status = $2
      ORDER BY created_at DESC
      LIMIT 50`,
    [req.user.id, status],
  );
  res.json(rows);
});

You might say: just ask the AI to write the tests as well. It will, and often well. But tests check the assumptions of whoever writes them. If the model didn’t think about security in the code, it’s unlikely to think about it in the tests: it will check that the “open” status returns open orders, and the test will pass. A green suite tells you the code does what its author intended. It doesn’t tell you the intention was right.

So verification isn’t a mechanical step you bolt on at the end. It’s a way of reading code that keeps asking what happens when things go wrong: unexpected input, hostile users, slow networks, data that grows. Those questions come from experience and from knowing how things work below the surface.

The debt you don’t see#

Then there’s a cost that shows up later: maintenance.

Code spends far more time being read and changed than being written. A project built by accepting suggestions without understanding them works fine until it needs to change. Then comes the day there’s a bug in production, or a new feature touches three different places, and you’re staring at thousands of lines nobody has ever really read. You can ask the AI again, of course. But without understanding the system you end up layering fixes on top of fixes, and every change becomes a guess.

The same applies to silent problems. A div with a click handler instead of a real button works perfectly with a mouse and is invisible until someone tries the site with a keyboard or a screen reader. A huge image loaded without dimensions slows the page down and hurts search ranking, yet throws no error at all. These defects make no noise. You only notice them if you know they exist.

What “knowing the code” means today#

None of this means nothing has changed. Something really has, and it’s about which knowledge matters.

Remembering a function signature or the exact syntax of an API counts for less: you can look it up, and an assistant reminds you in a second. Everything else counts for much more:

  • Reading code carefully, including code you didn’t write, and understanding what it actually does as opposed to what it appears to do.
  • Solid mental models of how things work: the life of an HTTP request, how a page renders, application state, a database, an index, a transaction.
  • Security and accessibility fundamentals, because those are the areas where mistakes are invisible and expensive.
  • Debugging: forming a hypothesis, isolating the problem, testing it. This is the skill AI replaces worst, because it means reasoning about your specific system.
  • Taste: recognising a solution that’s too complex, a dependency you don’t need, an abstraction that came too early.

In other words, knowing the code today looks more like the work of someone who reviews and designs than someone who types line by line. And it’s work that requires having typed a great many lines first.

How I use AI without letting it use me#

Day to day I use assistants like Claude Code, and I see them as a huge multiplier. But I follow a few rules I’ve set myself:

  1. I set the direction before asking. I choose the architecture, constraints and trade-offs; I ask the AI to carry out a decision, not to make it for me.
  2. I work in small steps. A focused change can be read and understood; a whole feature generated in one go can’t.
  3. I read every line that goes into the project. If I can’t explain what a piece of code does, I don’t accept it.
  4. I ask for explanations and alternatives. “Why did you choose this approach?” and “What are the alternatives, and their downsides?” are some of the most useful questions you can ask.
  5. I write the tests that matter myself, at least the ones covering edge cases and behaviour that must never break.
  6. I use it to learn. When it suggests something I don’t know, that’s a chance to understand it, not to copy it.

The result is that I move faster than before, but I’m still the one responsible for what ships. That’s how it should be: code is signed by whoever puts it in production, not by the tool that suggested it.

For those just starting out#

The biggest risk, in my view, is for people learning right now. The temptation to skip the hard part is enormous, because AI hands you something that works straight away. But the hard part is precisely what builds the ability you later need to steer and check these tools.

When I started out, between courses, certifications and a bootcamp, what taught me most wasn’t the part where things worked first time. It was the part where they broke and I had to figure out why. If I were starting again today, I’d use AI as a patient tutor to ask for explanations, not as someone doing my homework. I’d write my first projects by hand, even if more slowly. And I’d make sure I really understood the basics: the language, the web, data. Those don’t age when the fashionable tool changes.

The autopilot#

Airliners have automated systems that can handle much of a flight. Pilots still train for a long time, and not out of nostalgia: their value shows precisely when automation isn’t enough, and in those moments they need to know exactly what’s going on.

Code is no different. AI tools have changed the way we work, and for the better. But they haven’t changed who is responsible for the result. The easier code becomes to produce, the more valuable it is to be able to tell whether it’s the right code. Knowing how to code isn’t a skill AI has made redundant. It’s the one that lets you actually use it.

Full Stack Web Developer. I write about what I learn building interfaces and web applications.

Comments No comments

No comments yet: you could be the first.

Leave a comment

Comments are published after moderation.