Skip to content
What Is Vibe Coding? Where It Works and Where It Breaks
Milos KnezevicMilos Knezevic02 Nov 2026 · 22 min read

What Is Vibe Coding? Where It Works and Where It Breaks

Vibe coding means letting an AI write the code while you steer by feel. What it is, where it works, where it breaks, and how to use it without getting burned.

Here's the short answer first. Vibe coding is building software by describing what you want to an AI in plain language and accepting the code it writes without reading it closely. It works for prototypes, throwaway tools and learning by seeing. It breaks the moment other people, real money or real data depend on the result, because nobody checked the parts that matter.

That's the whole article in four sentences. The rest is the part that matters: what "the parts that matter" are, why the AI skips exactly those, and what you do about it. If you're a beginner, read it top to bottom; I define every term the first time it shows up. If you already ship code with an agent every day, jump to the orders example and the live demo, because those two show the exact mistake I see most often in AI-written code. The section near the end about checking AI output is for anyone who reviews other people's pull requests.

What is vibe coding, in plain words

The term is young. Andrej Karpathy, one of the founders of OpenAI, used it in a post on X in February 2025 to describe a way of working where you "forget that the code even exists". You talk to the AI, it writes, you run the result, and when something breaks you paste the error back and let it fix itself. Less than a year later Collins Dictionary made it their Word of the Year 2025. That tells you how fast this spread.

Let me try a comparison from outside tech. Imagine you hire a builder who works unbelievably fast, speaks every language, never gets tired and never argues. You say "I want a kitchen with an island" and by the evening there's a kitchen with an island. The catch: now and then he fits a door that opens the wrong way, or leaves a gas pipe loose, and he doesn't notice, because he was busy doing what you asked. If you walk through the house before guests arrive, you catch it. If you don't, the guests catch it. Vibe coding is hiring that builder and skipping the walk-through.

A few terms before we go on, because this topic is full of them. A model, or a large language model, is the AI itself: a program that was trained on an enormous amount of text and code and predicts what should come next. A prompt is what you type to it. An AI coding agent is a program built around a model that can read and write the files in your project, run commands and read the errors that come back, so it can keep going without you pasting things by hand. A pull request is a bundle of changes you ask someone to review before it goes into the real code. That's all you need for this article.

Here's the thing most explanations miss. Vibe coding is not a tool. It's a relationship with the output. Two people can use the exact same agent on the exact same task. One reads what came back, asks why a line is there, runs the tests and changes two things. The other says "looks good" and moves on. Only the second one is vibe coding. The word describes what you do with the code, not what wrote it.

Why does vibe coding exist now

Why did this appear in 2025 and not in 2015? Good question. Two things happened at once. The models got good enough at code that the loop actually works: describe, run, paste the error, run again, and most of the time you end up with something that runs. And agents arrived, so the AI could run the commands itself instead of waiting for you to copy and paste. Put those together and the time from "I have an idea" to "it runs on my laptop" collapsed from weeks to an afternoon.

And then the marketing arrived. I'll be honest: the vibe coding story annoys me. Not because people are building things, that part is great. It annoys me because it tells people they can build anything with AI without understanding what sits behind the code, or how to fix it when it breaks. And it will break. That's not pessimism, that's every piece of software ever written.

What the story leaves out is which part got easy. Making something work on your own laptop was never the hard part of programming. It was the part courses taught you, and the part you could always muddle through. The hard part was always what happens after: when a stranger types something you didn't expect, when someone tries to read data that isn't theirs, when the database goes down at two in the morning, when a thousand people have accounts and one of them forgot a password. AI made the easy part almost free. The hard part is exactly where it was.

I know this because I did it myself, before any AI was involved. For a long time I built things from my head until they looked good. No plan, no sketch, no thought about security. I never once stopped to think about what happens with a thousand users with profiles: protecting their accounts from someone trying to break in, resetting passwords, helping them when my own bug hurt them. The thing worked, so I was done. Vibe coding is that same mistake with a much faster engine under it.

Where vibe coding works

Let's be fair to it, because the hype has a true core. There are plenty of situations where reading every line is overkill, and telling a beginner to review a hundred lines of a throwaway script is how you make them quit.

  • A prototype to show someone an idea. You want to see whether the screen makes sense, not whether it survives a hacker. It will be thrown away or rebuilt.
  • A one-off script. Rename four hundred files, convert a spreadsheet, pull some numbers out of a log. You run it once, you look at the result, done.
  • A tool only you use. A little dashboard for your own notes, on your own machine. If it breaks, you lose an hour, not a customer.
  • Trying a layout or a library. "Show me what this looks like with cards instead of a table." You're exploring, not shipping.
  • Learning by seeing, with a catch I'll come back to. Watching an agent build something and then asking it to explain every line is a very good way to learn. Watching it build and asking nothing is not.

Notice what all of these have in common. If the code is wrong, the only person who pays is you. I'll give you one word for this: blast radius. It's the number of people who get hurt when the thing breaks. When the blast radius is you, vibe away. The moment the blast radius includes anybody else, their data, their money or their time, you need a different rule, and the rest of this article is that rule.

Where it breaks: the distance between your laptop and real users

Let me show you rather than tell you. Here's a route handler, which is a small piece of server code that answers one kind of request, in this case "give me the orders for a user". This is what you get when you ask an agent for exactly that sentence. I'm not quoting a particular session; this is the shape I see again and again, and I've type-checked it against Next.js 16 so you know it compiles.

app/api/orders/route.ts (what you get for "show the orders for a user")
import { NextResponse } from "next/server";
import { db } from "@/lib/db";

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const userId = searchParams.get("userId");

  if (!userId) {
    return NextResponse.json({ error: "userId is required" }, { status: 400 });
  }

  const orders = await db.order.findMany({ where: { userId } });
  return NextResponse.json(orders);
}

Line by line, because every piece matters here. The first two lines import NextResponse, a helper for sending JSON back, and db, which stands for your database client. The GET function runs whenever the browser asks for this address. Inside it, new URL(request.url) breaks the address into parts, and searchParams.get("userId") reads the userId from the end of the address, the part after the question mark. Then there's a check: if userId is missing, send back a 400 error, which means "your request was bad". Only then does it ask the database for every order whose userId matches, and send that list back as JSON.

Look at that code for a second. It validated the input. It returns a proper error code when something's missing. It's short, it's tidy, and if you open it in your browser with your own id on the end, it shows your orders. By every test the agent ran, it works.

Now I'll run it with a tiny fake database that holds one order for Emily and one for James.

three requests against the handler above
GET /api/orders                -> 400 {"error":"userId is required"}
GET /api/orders?userId=emily   -> 200 [{"id":"o1","userId":"emily","total":1200,...}]
GET /api/orders?userId=james   -> 200 [{"id":"o2","userId":"james","total":5900,...}]

Do you see it? Emily can type James's id into the address bar and read James's orders. The server never asked who is making the request. It only asked whose orders were requested, and it took the answer from the one place anyone can edit: the address.

This is the gap between two words that sound similar and aren't. Validation asks "is this a userId?" Authorization asks "is this your userId?" The agent did the first one, which is why the code looks careful. It skipped the second one, which is the one that matters when the blast radius includes other people.

This isn't a rare slip. It has a name, broken access control, and it sits at number one on the OWASP Top 10:2025, the most used list of web security risks. The OWASP page says that every application tested had some form of it. Every one. That was true before AI wrote code, and the agents learned from the same code we wrote.

Here's the same handler, fixed. Two changes and one line removed.

app/api/orders/route.ts (fixed)
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";

export async function GET() {
  const session = await auth();

  if (!session) {
    return NextResponse.json({ error: "Sign in first" }, { status: 401 });
  }

  const orders = await db.order.findMany({
    where: { userId: session.user.id },
    orderBy: { createdAt: "desc" },
  });
  return NextResponse.json(orders);
}

Walk through it. GET now takes no request at all, because nothing in the address is allowed to influence whose orders come back. The first line inside calls auth(), which reads the session cookie the browser sent and tells you who is signed in. If nobody is, we return 401, which means "you need to sign in", a different thing from 400. Then the database query uses session.user.id, a value the server got from its own session store, not from the address. Emily can type whatever she wants after the question mark; the handler doesn't even look. The orderBy line just sorts newest first, which is what a real orders page wants anyway.

That's the entire fix. If you remove the auth() call, you're back to the first version. If you take the id from the address again "just for admin users", you've reopened the hole with extra steps. The pattern of taking the user from the session and never from the request is the same one I use in the login form article, and if you want to see how the session itself is set up, the NextAuth v5 guide builds it from zero.

How often does AI-written code carry a problem like this? I'll give you one number with its source and its label. Veracode, a company that sells security scanning, so this is a vendor figure, tested more than 100 models and reported that AI-generated code introduced security flaws in 45% of their tests in their 2025 report. I can't check their method, so don't quote it as gospel. But I don't need the exact number. The orders handler above is enough to make the point: the code that "works" and the code that's safe are two different things, and the agent only optimised for the first.

See it happen: the price that came from the browser

The second mistake is the same mistake wearing different clothes, and it's the one that costs money. Here's a tiny shop. Pick a product, pick a quantity, place the order. Then press the red button, which does what an attacker would do: edit the request before it reaches the server. Both "servers" in this demo are just functions in the page, so this is a front-end demo of a server-side problem; in a real app the request goes over the network, but the logic is identical.

Who decides the price: the browser or the server?

Place an order with attacker mode off and both servers charge the same amount. Now turn attacker mode on and order again. Server A charges a few cents. Server B charges the real price. Same request, same quantity, same product.

Here's what each part of the code does. PRICES is the server's own price list, in cents, because storing money as decimals causes rounding bugs; in a real app this is a table in the database. buildRequest is the browser side: it packs up the product, the quantity and the price into one object, which is what a lot of shop pages genuinely do, because the price was right there on the screen. When attacker mode is on, it sets priceCents to 1 before sending, which is what anyone can do with the browser's developer tools or a one-line command. The browser is not yours. Whatever it sends, it sends because the person using it allowed it.

serverA is the trusting server. It multiplies the price it received by the quantity it received and calls it a day. serverB ignores the price in the request completely. It takes the product id, looks the price up in its own list, and if the product doesn't exist it returns null, which the page shows as a rejection. Then render puts all three results on the screen, and the submit handler calls preventDefault so the form doesn't reload the page, which is the one thing every form demo needs.

See the pattern? It's the orders handler again. The orders handler trusted the address for who you are. Server A trusts the request body for what things cost. The rule is the same both times: the server decides the things that matter, and the request only gets to say what it wants, never what it is entitled to. If you want the full version of this idea, with a schema that checks the shape of every request at the door, read Zod at the trust boundary. It's the article I'd hand to anyone who just shipped their first vibe-coded app.

Why the AI keeps making this kind of mistake

People read the two examples above and conclude the AI is bad at security. That's not quite it, and getting this right changes how you use the tool.

The agent is optimising for the sentence you gave it and the errors you pasted back. "Show the orders for a user" doesn't contain the word session. "Build a checkout" doesn't contain the sentence "the browser lies". Nothing in the loop ever rewarded safe over works, so works is what you got. And the training data doesn't help, because a huge share of the code on the internet is tutorial code, and tutorials skip exactly these parts to keep the lesson short. The agent learned from that too.

I check what the AI writes as far as I can, because it can hallucinate, which means confidently inventing a function or a package that doesn't exist, and because it assumes things it shouldn't. Mostly it assumes the happy path: the user is honest, the input is well formed, the network is fine. Those are the assumptions a beginner makes too, which is why beginner code and vibe-coded code fail in the same places.

And here's a realisation that took me a while. When the output isn't what I imagined, most of the time I could have asked better. The AI is a machine running maths and algorithms. It can't know what's in my head, what I saw on some other site last week, or that in my project the user id always comes from the session. If I didn't say it and the project didn't show it, it doesn't exist. That reframes most "the AI got it wrong" moments into "I left a gap and it filled it with the most common thing".

How I actually work with an AI agent

So you don't stop using the tool. You change your relationship with the output. This is my routine; it's not the only good one, but it's the one I can honestly say I use.

Before any code, I talk the project through with the agent. What are we building, who uses it, what must never happen. We make a plan together and I read the plan, because a wrong plan is cheap to fix and wrong code is not. Then we go step by step: I give the next step, the agent writes the code, I run it and read what changed. One step at a time, not "build the whole app". It saves a lot of time. It also makes mistakes without meaning to, so without some programming knowledge you'll hit a wall sooner or later; that's not a reason to avoid it, it's a reason to learn alongside it.

Compare two ways of asking for the orders route. The vague one is "add an API that returns a user's orders", and you've already seen what that gets you. The specific one says: add GET /api/orders that returns only the signed-in user's orders; take the user id from auth() in lib/auth.ts and never from the request; return 401 when there's no session; and add a test where a second user cannot see the first user's orders. Same agent, same model, completely different code, because the second prompt carried the two things the first one left in my head: who is asking, and what they must not be able to do.

For the review itself, I keep it to four questions, in this order, and I ask them of every change before I move on.

  • Who is asking, and where did the code learn that? If the answer is "from the request", stop.
  • What did the agent assume? I literally ask it: "List everything you assumed about the input, the user and the environment." It will tell you, and the list is usually where the bugs are.
  • What breaks if I delete this line? Asking that about every check is the fastest way to learn what each line is for.
  • Which paths move money, data or permissions, and do they have a test? Not everything needs a test. Those do.

Then, before anybody else touches it, I go through the same list of eight questions I wrote up in the production-grade code article. An agent can do the mechanical part of that list in minutes: finding every place input enters, listing every endpoint without a rate limit, grepping for secrets. The judgment part, deciding what's acceptable for this app and these users, is still yours, and I don't see that changing soon.

info

Tested with: Claude Code, October 2026. The two route handlers were type-checked with TypeScript 5.9 in strict mode against Next.js 16.4, and the vulnerable one was run under Node 24 to produce the three responses shown. The live demo was run in a browser. The routine above doesn't depend on the tool; it works the same with any agent that can edit files and run commands.

If you're just starting: does this mean you can skip learning to code

I get this question a lot, so here's a straight answer. No. But not for the reason you've been told.

AI will not replace programmers. It changes how they work. Less of the job is weeks of typing and debugging, more of it is directing: deciding what to build, in what order, and checking that what came back is right. The people who fall behind are the ones who refuse to use it, and they'll fall behind fast, because this moves fast. But directing needs understanding. You cannot check what you cannot read. You cannot tell the agent "take the user from the session" if you don't know what a session is. The vibe coder and the beginner hit the same wall, and the wall is concepts, not typing speed.

So what do you actually need to know first? Less than you fear. How a request travels from the browser to the server and back. What a database row is and what it means to look one up. What "who is asking" means, which is sessions and cookies. How to read an error message without panicking. That's the foundation, and I wrote the beginner's guide to web development to put those pieces in order, because when I started nobody connected them for me, and courses left me feeling like I knew nothing even after I'd finished them.

And here's the best news in this article. The agent is the most patient tutor you'll ever have. After it writes something, ask it to explain every line. Ask what would break if a line were removed. Ask it to write the same thing a second way and tell you the difference. That's learning by seeing done right, and it's the catch I promised earlier: watching it build teaches you nothing, interrogating what it built teaches you everything. One percent a day for a year beats a heroic weekend and then nothing. And if you don't know something yet, that's fine. Plenty of people working in companies right now don't know everything and ask for help; the difference is they know what to ask.

So that's all it is

Vibe coding is the same tool, with the reading taken out. That's the whole definition, and once you see it that way the fear and the hype both shrink. The AI didn't make programming disappear. It made the part that was always easy nearly free, and left the part that was always hard, judgment, exactly where it was: with you.

The one question that carries most of that judgment is this: who is asking, and what's the worst thing they could do with this? Ask it about every route, every form, every query. The orders handler fails that question. Server A fails it. Everything you'd be embarrassed to find in production fails it. That's not a security specialist's question. It's an older colleague's question, and after a few weeks you'll ask it without thinking.

What to be careful about, and the Plan B

A few pitfalls I see in vibe-coded projects more than anywhere else, and what to do about each.

  • Secrets in the code. An API key pasted straight into a file because the agent needed it to work, then committed. Keys go in environment variables, and if one was ever committed, rotate it, meaning generate a new one and delete the old one; removing it from the file isn't enough, it's still in the history.
  • Packages you never asked for. Agents add dependencies freely. Each one is code you didn't write and have to maintain. Read the package list and remove what you can't explain.
  • Fixes that delete the check. When a test fails, the quickest way to make it pass is to loosen the validation. Agents do this. If a "fix" removes an if statement, read it twice.
  • Tests that test nothing. A test file that exists, runs green, and checks that true is true. Open the tests and make sure each one would fail if the feature broke.
  • No limits on anything. No rate limit, no maximum input size, no timeout. The day you get traffic is the day you get abuse, and the limits are the subject of the rate limiting article linked below.
  • One enormous file. Works on day one, unreadable on day thirty, when you need to find the one place a thing happens. Ask the agent to split by responsibility before that day, not after.

Plan B, if you've already shipped something vibe-coded and real people are using it: don't panic and don't rewrite. Rewrites are how you get the same bugs in a new shape. Instead, make two lists: every place data enters your app, and every place data is read or changed. For each item ask who is asking and where the code learned that. Fix those first, rotate every key that ever touched the code, and add a rate limit on every public endpoint. Treat it as a prototype you're promoting to a product, one checked boundary at a time, not a product you're patching.

warning

The demo and the examples here are about the shape of the problem, not a complete security review. A real app has more trust boundaries than one route and one price: file uploads, webhooks, admin pages, background jobs. The method is the same for each, but you have to walk every one of them. Nothing in this article replaces that walk.

What to do next

Pick the thing you built most recently, with or without an AI, and run the one question over it: who is asking, and what's the worst thing they could do? Then read Zod at the trust boundary to learn how to check every request at the door, and the production-grade code article for the full list of what separates a demo from something you'd put your name on. If tests are new to you, the React testing strategy shows where tests pay off and where they're overkill.

Use the agent. Use it a lot. Just don't stop reading. You can do this, and honestly, you have to, because this is where the work is going. Don't give up at the first wall, and you'll have me on your side for the next one.