DEV Community

Cover image for Vibe-Coding vs Stress-Coding: How I Actually Use AI on Projects That Matter
Juan Torchia
Juan Torchia

Posted on • Originally published at juanchi.dev

Vibe-Coding vs Stress-Coding: How I Actually Use AI on Projects That Matter

87% of the bugs I found in AI-generated code showed up in edge cases the prompt never mentioned. Not in the main functionality. In the edges.

When I saw that number in my own PR history, I had to read it twice. Because I'd spent weeks talking about how much Cursor and Claude were helping me — and it turned out 87% of my fixes were landing exactly where business context matters more than syntax.

That made me think differently about vibe-coding. And today I want to say it straight.

Vibe-coding, real productivity with AI — the difference nobody explains

There's a movement (legitimate, interesting) that says the future of development is vibe-coding: you describe what you want, the AI generates it, you guide it with prompts, and the code basically writes itself. I saw the post that was circulating on Dev.to a while back about "stress-coding" — the anxious flip side — and even though the post itself was pretty shallow, the concept clicked something that had been rattling around in my head.

Vibe-coding isn't bad. It's contextual.

I vibe-code. Every single day. But not the same way across every project. The distinction that took me a while to put into words is this:

  • When I'm experimenting: the AI is copilot with the reins loose
  • When there are real users: the AI is a powerful tool that I audit with my own judgment

Sounds obvious. It's not — not when you're in the flow and everything seems to be working.

How I use AI in experiment mode (and why that's fine)

When I built juanchi.dev, the process was almost pure vibe-coding in the first few weeks. Next.js 16, React 19, Tailwind v4 — all bleeding edge, all with sparse documentation, all with AI as my first line of reference.

In that context, the flow looked like this:

// Typical prompt in experiment mode:
// "I need a component that animates card entrances
// using Framer Motion with the new useAnimate hook"

// The AI generates this, I accept it and test:
import { useAnimate, stagger } from 'framer-motion'

export function AnimatedGrid({ items }: { items: PostCard[] }) {
  const [scope, animate] = useAnimate()

  // AI suggested this approach — I tested it, it worked, I moved on
  useEffect(() => {
    animate(
      '.card',
      { opacity: [0, 1], y: [20, 0] },
      { delay: stagger(0.1) }
    )
  }, [])

  return (
    <div ref={scope} className="grid gap-6">
      {items.map(item => (
        <div key={item.slug} className="card">
          <PostCard {...item} />
        </div>
      ))}
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

In experiment mode, if this blows up on an edge case, the cost is: I notice, I fix it, I keep going. No user is waiting. No SLA. Vibe-coding here genuinely multiplies my exploration speed.

The problem starts when that mental mode doesn't shift when you move to production.

How I use AI when production is on the line (stress-coding, the good kind)

I have a client project — e-commerce, real traffic, real orders. When I worked on the performance optimization for that app, the flow with AI was completely different.

What changed:

1. The prompt always includes business context

// Prompt in production mode:
// "I have a query that pulls orders from the last 30 days
// with a JOIN to users and products. It runs every time
// someone opens the admin dashboard. Average 2.3 seconds.
// The orders table has 180k rows. How do I optimize this?
// I do NOT want solutions that break the existing pagination."

// What the AI generates — I do NOT accept without reviewing:
const getRecentOrders = async (page: number, limit: number) => {
  // AI suggested this composite index — I evaluated it on staging first
  // CREATE INDEX idx_orders_created_user 
  // ON orders(created_at DESC, user_id) 
  // WHERE created_at > NOW() - INTERVAL '30 days';

  return await db
    .select({
      id: orders.id,
      total: orders.total,
      // Only the fields I actually need — AI wanted to pull everything
      userName: users.name,
      // Removed the JOIN to products because it's not shown in this view
    })
    .from(orders)
    .innerJoin(users, eq(orders.userId, users.id))
    .where(
      and(
        gte(orders.createdAt, sql`NOW() - INTERVAL '30 days'`),
        eq(orders.status, 'completed') // AI had no idea I only wanted completed orders
      )
    )
    .orderBy(desc(orders.createdAt))
    .limit(limit)
    .offset((page - 1) * limit)
}
Enter fullscreen mode Exit fullscreen mode

The AI didn't know I only wanted completed orders. That one filter dropped the query from 2.3 seconds to 400ms without a new index. The business context I brought to the table was worth more than the code it generated.

2. Nothing goes to production unless I understand every line

This sounds basic because it is. But in vibe-coding mode it's way too easy to hit Accept All and keep rolling. In production, if you can't explain what a function does in 30 seconds, you don't deploy it.

3. I think through the edge cases myself — I don't delegate that

Back to that 87% from the top: edge cases are exactly where business context matters most. What happens if the user cancels the order right while the payment is being processed? What happens if stock hits zero between addToCart and checkout? That's not in the prompt. It's never going to be in the prompt unless you put it there yourself.

The mistakes I made when I mixed the two modes

The real problem isn't vibe-coding or stress-coding. The problem is not knowing which mode you're in.

In my 30-year journey with technology, I took down a production server with rm -rf in my first week as a sysadmin. That was stress-coding without judgment: urgency, pressure, execute without thinking. The modern equivalent is vibe-coding in production — speed, flow, Accept All without auditing.

Two concrete mistakes I made:

Mistake 1: Trusting AI-generated TypeScript types without runtime validation

// AI generated this, I accepted it:
type OrderResponse = {
  id: string
  total: number
  items: OrderItem[]
}

// Problem: the real API sometimes returns total as a string
// TypeScript won't catch it at runtime — Zod will:
import { z } from 'zod'

// What I should have done from the start:
const OrderResponseSchema = z.object({
  id: z.string(),
  total: z.coerce.number(), // coerce handles string -> number
  items: z.array(OrderItemSchema)
})

// Now if the API breaks the contract, I find out at runtime
// and not when the user sees "NaN" in their order total
Enter fullscreen mode Exit fullscreen mode

That mistake cost me 2 hours of debugging in production. The tech stack I choose today includes Zod as non-negotiable, for exactly this reason.

Mistake 2: Letting the AI decide the architecture for new features

AI is excellent at implementing. It's mediocre at designing. When you ask "how should I structure the notifications module?", you'll get an answer that's technically correct and generically useless for your context.

The TypeScript patterns I actually use came from design decisions I made, not the AI. It implements the patterns. I decide when and why to apply them.

What concretely changed in my workflow

I have a simple internal rule now:

If a bug in production is going to wake me up at 2am, I don't vibe-code that part.

More specifically:

  • Authentication and authorization: every line audited
  • Payment handling: zero vibe-coding
  • Production database queries: AI generates, I review the execution plan
  • Error handling and edge cases: I think through them, AI implements them
  • UI components with no critical state: vibe-coding, no problem
  • Animations, styles, layout: vibe-coding with my eyes closed
  • Data migration scripts: audited line by line, every single time

The result is that I use AI for about 80% of my coding time — but in a differentiated way. It's not less AI. It's AI with judgment.

FAQ: Vibe-coding and real productivity with AI

Is vibe-coding only for personal projects, or does it work for client work?

It works for client work, but in specific layers. For initial exploration, prototypes, UI components with no critical logic — perfect. For core business logic, payment integrations, sensitive data handling — you need a more rigorous mode. The key is knowing which is which before you start typing.

How do you avoid accepting AI code that looks like it works but has hidden bugs?

Two concrete practices: first, always run your tests before committing (you have tests, right?). Second, if the code touches external APIs or a database, test it with real edge case data: empty strings, IDs that don't exist, responses with missing fields. AI generates the happy path beautifully. The edges you have to test yourself.

What AI tools are you using right now?

Cursor as my main editor with Claude Sonnet for day-to-day work. Claude Opus when I need to think through architecture or debug something complex I genuinely don't understand. ChatGPT I barely use for code anymore. GitHub Copilot I dropped — Cursor blows it away on whole-project context. Context is everything in this equation.

Doesn't vibe-coding make you lose technical depth over time?

It's the question I ask myself most. My honest answer: yes, if you're not careful. The way I fight it is by deliberately choosing to understand the hard code instead of just accepting it. When the AI generates something I don't fully understand, I ask it to explain. Not out of paranoia — to keep the muscle active. I've got 30 years of technical background that I really don't want to let atrophy.

Is there a type of project where you wouldn't use AI in the loop at all?

Honestly, no. But there are parts of projects where AI is in the loop differently. In security-critical code, I use it to review what I write, not to generate. "Here's my rate-limiting implementation — what attacks am I not covering?" That's AI as auditor, not generator. It's another mode, not the absence of AI.

How do you know when AI-generated code is good versus when it needs to be rewritten?

Rewrite signals: you can't explain what it does in 30 seconds, it has more than 3 levels of nesting for no obvious reason, variable names are generic (data, result, temp), or there's no error handling. Signs it's solid: you'd be proud to have it in a code review, the edge cases are covered, and if something fails, the error will be clear about what broke and why.

What I'd do differently if I were starting today

Vibe-coding is real, it's productive, and it's here to stay. But the "just describe it and the AI does it" narrative has a problem: it leaves out the fact that the value of a senior developer isn't in writing code. It's in knowing what code to write, when, and with what trade-offs.

I started taking AI seriously during the pandemic, when I made the pivot to software development. The first few months were rough — everything I knew from infra was barely applicable to React. I had to learn to think differently. And today, with AI, something similar is happening: the developer who doesn't learn when to trust and when to audit is going to have the same problem as the dev who copied Stack Overflow without understanding it. The bugs will appear at the worst moment, at the edges, exactly where the business hurts most.

AI genuinely multiplied my real productivity. But real productivity includes not waking up at 2am because something "seemed to work."

How do you split your AI usage between projects that matter and pure experimentation? Genuinely curious whether your criteria are different from mine.

Top comments (0)