When Andrej Karpathy coined the term “vibe coding,” he described a workflow where he would “fully give in to the vibes, embrace exponentials, and forget that the code even exists.” By 2025 and into 2026, the practice became far more disciplined — and this guide walks through the structured, four-stage workflow that makes AI-first development repeatable and production-ready.
Table of Contents
What Vibe Coding Actually Means in 2026
From Andrej Karpathy’s Tweet to Engineering Methodology
When Andrej Karpathy coined the term “vibe coding” in a February 2023 post on X, he described a workflow where he would “fully give in to the vibes, embrace exponentials, and forget that the code even exists.” That framing captured something real about the emerging relationship between developers and large language models, but it also invited dismissal. Through 2024, vibe coding largely meant experimentation: developers testing what AI tools could generate, often with mixed results and little repeatable structure. By 2025 and into 2026, in the author’s assessment, the practice became far more disciplined.
Vibe coding in 2026 is best understood as structured AI-first development where the developer architects, reviews, and refines rather than writing line-by-line. The developer defines intent, evaluates output, and iterates toward production quality. This is distinct from no-code platforms, which typically abstract away the codebase entirely, and from traditional development, where every line originates from human keystrokes. Vibe coding is a third path: the developer remains deeply technical but offloads initial generation to AI, concentrating human effort on the decisions that require judgment, domain knowledge, and taste.
This guide targets intermediate developers already proficient in JavaScript, React, and Node.js who want to integrate AI tools into a disciplined, repeatable workflow rather than treating them as novelty autocomplete.
The Core Workflow: Prompt, Generate, Review, Refine
Understanding the Four-Stage Loop
The vibe coding workflow operates as an iterative four-stage cycle: Prompt, Generate, Review, Refine. Each stage demands a distinct skill set and carries its own failure modes.
Prompting compresses architectural decisions into natural language. Generation is the AI producing a first draft. Review is where engineering discipline lives, applying human judgment to AI output against production standards. Refinement closes the loop, feeding targeted corrections back to the AI to produce improved output without regenerating from scratch.
This is emphatically not “type a prompt and ship.” The most common failure pattern in vibe coding comes from developers who treat the Generate stage as the finish line.
This is emphatically not “type a prompt and ship.” The most common failure pattern in vibe coding comes from developers who treat the Generate stage as the finish line. In practice, Review and Refine consume a significant share of the total effort in the author’s experience and represent the highest-value human contribution. Prompt engineering in this context functions as software architecture: the quality of the initial specification directly determines the quality of every subsequent stage.
Choosing Your AI-First Toolchain
The 2026 set of AI-first development tools includes Cursor, GitHub Copilot Workspace, Claude Code, Windsurf, and Bolt, among others. Each offers different integration depths, context window handling, and iteration workflows.
This tutorial uses Cursor paired with Claude as the primary toolchain, though the principles translate across any capable AI coding environment. The runtime and framework baseline for all examples is Node.js 22+ and React 19.
To follow along, you will need Node.js 22.x or later, a Vite 5+ or Next.js 14+ project initialized with React 19, Express 4.x, and "type": "module" in package.json. A minimal package.json should include:
{
"type": "module",
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"express": "^4.21.0"
}
}
Developers using alternative toolchains should adapt the interaction patterns while following the same four-stage structure.
Stage 1: Prompt (Designing Your Intent)
Writing Structured Prompts That Produce Shippable Code
An effective development prompt has four components: context, constraints, examples, and output format. Context tells the AI what the project is, what exists already, and what the feature should accomplish. Constraints specify versions, libraries, coding standards, and architectural boundaries, while examples provide reference patterns the AI should follow. Output format instructions dictate file structure, naming conventions, and how the code should be organized.
Vague prompts produce vague code. The “garbage in, garbage out” principle has not been repealed by advances in language models. A prompt that says “make a dashboard component” will produce generic boilerplate. A prompt that specifies data shape, error states, accessibility requirements, and API contract will produce something approaching shippable output.
Prompt Template for a React + Node.js Feature
The following structured prompt template demonstrates how to request a full-stack feature with enough specificity to generate useful output on the first pass:
## Context
I'm building a project analytics dashboard in React 19 with a Node.js 22 Express backend.
The app uses ES modules throughout. The frontend fetches data from a REST API.
No TypeScript — plain JavaScript with JSDoc annotations for type hints.
## Feature Request
Create a "ProjectStatsCard" React component that displays a project's name,
total tasks, completed tasks, and completion percentage. The component fetches
its data from a new API endpoint.
Create the corresponding GET /api/projects/:id/stats Express route that returns
the stats payload.
## Constraints
- React 19 functional component using the `use()` hook for data fetching
- Express 4.x route handler (Note: Express 5 handles async errors differently —
remove the try-catch and rely on Express 5's automatic async error propagation
if using that version.)
- Error handling: the component must handle loading, error, and empty states
- Input validation: the endpoint must validate that `:id` is a positive integer
- No external UI libraries — use plain HTML elements with CSS class names
- File structure: `src/components/ProjectStatsCard.jsx` and `src/routes/projectStats.js`
- Naming: camelCase for variables/functions, PascalCase for components
## Acceptance Criteria
- The component renders a card with project name, "X / Y tasks completed", and a
percentage bar
- The API returns JSON: { projectName, totalTasks, completedTasks, completionPercent }
- Invalid IDs return 400 with an error message
- Server errors return 500 with a generic error message
## Output Format
Provide two complete files with full import/export statements. Add JSDoc annotations
for function parameters and return types. Include inline comments only where logic
is non-obvious.
Each section of this template serves a specific function. The context block prevents the AI from making incorrect assumptions about the tech stack. The constraints block eliminates ambiguity about versions, patterns, and file organization. The acceptance criteria provide a testable definition of done. The output format instructions ensure the generated code integrates into the existing project without restructuring.
Stage 2: Generate (AI Produces the First Draft)
What to Expect from AI-Generated Output
AI-generated code in 2026, when responding to well-structured prompts, tends to be structurally sound more often than not, though the rate varies widely by prompt quality. In my last three projects, about 18 of 25 detailed prompts produced code that passed initial review without major rework. That ratio drops sharply with vague prompts and rises with detailed specifications. Common generation patterns worth anticipating include over-abstraction (the AI adds unnecessary layers of indirection), hallucinated imports (referencing packages or APIs that do not exist), outdated API usage (defaulting to older patterns from the training data), and missing edge cases (particularly around error handling and input validation).
Example: Generated React Component and Node.js Endpoint
The following represents realistic AI-generated output from the prompt above. It is presented as raw AI output, and it contains subtle issues that will be identified in the Review stage:
import { use, Suspense } from 'react';
async function fetchProjectStats(projectId) {
const response = await fetch(`/api/projects/${projectId}/stats`);
const data = await response.json();
return data;
}
export default function ProjectStatsCard({ projectId }) {
const statsPromise = fetchProjectStats(projectId);
const stats = use(statsPromise);
return (
<div className="project-stats-card">
<h3 className="project-stats-card__title">{stats.projectName}h3>
<p className="project-stats-card__tasks">
{stats.completedTasks} / {stats.totalTasks} tasks completed
p>
<div className="project-stats-card__bar-container">
<div
className="project-stats-card__bar-fill"
style={{ width: `${stats.completionPercent}%` }}
/>
div>
<span className="project-stats-card__percent">
{stats.completionPercent}%
span>
div>
);
}
import express from 'express';
const router = express.Router();
const projects = {
1: { projectName: 'Website Redesign', totalTasks: 24, completedTasks: 18 },
2: { projectName: 'API Migration', totalTasks: 12, completedTasks: 12 },
};
router.get('/:id/stats', (req, res) => {
const id = req.params.id;
const project = projects[id];
if (!project) {
return res.status(404).json({ error: 'Project not found' });
}
const completionPercent = Math.round(
(project.completedTasks / project.totalTasks) * 100
);
res.json({
projectName: project.projectName,
totalTasks: project.totalTasks,
completedTasks: project.completedTasks,
completionPercent,
});
});
export default router;
This output is structurally coherent and follows most of the prompt’s instructions. However, three issues are present that would fail production standards, which the next stage will surface.
Stage 3: Review (The Developer’s Critical Eye)
The Review Checklist for AI-Generated Code
A structured review process evaluates AI-generated code across five dimensions: security, performance, correctness, maintainability, and accessibility. (Authentication is omitted from the example endpoint; apply per your application’s auth strategy.) This stage is the highest-value human contribution in the vibe coding loop because it requires the judgment, context, and domain knowledge that AI models lack. Automating syntax correctness is trivial. Knowing whether code is safe, performant, and correct within a specific production context is not.
Applying the Checklist to Our Generated Code
Start by reading the generated output line by line against the prompt’s acceptance criteria. Three issues surface that pass syntax checks but fail production standards:
router.get('/:id/stats', (req, res) => {
const id = req.params.id;
const project = projects[id];
});
async function fetchProjectStats(projectId) {
const response = await fetch(`/api/projects/${projectId}/stats`);
const data = await response.json();
return data;
}
const completionPercent = Math.round(
(project.completedTasks / project.totalTasks) * 100
);
These issues are characteristic of AI-generated code: the happy path works, but edge cases, validation, and defensive programming are absent or incomplete.
These issues are characteristic of AI-generated code: the happy path works, but edge cases, validation, and defensive programming are absent or incomplete. The AI followed the structural requirements while missing the behavioral requirements that make code production-safe.
Stage 4: Refine (Iterating with Precision)
Writing Refinement Prompts That Fix Without Breaking
Refinement prompts should be surgical. Rather than asking the AI to regenerate the entire feature, target specific functions, line numbers, and behavioral changes. This preserves the working portions of the generated code while addressing identified defects. A well-written refinement prompt references the exact function name, describes the current incorrect behavior, and specifies the desired correct behavior.
For this example, a refinement prompt might read: “In src/routes/projectStats.js, the route handler for GET /:id/stats needs three fixes: (1) validate that req.params.id is a positive integer and return 400 if not, (2) handle the case where totalTasks is 0 by returning completionPercent: 0 instead of NaN, (3) wrap the handler body in a try-catch that returns 500 on unexpected errors. In src/components/ProjectStatsCard.jsx, (4) add a response.ok check in fetchProjectStats that throws on non-2xx responses, and (5) wrap the component usage in an ErrorBoundary and Suspense for loading/error states.”
The Refined Output
After one refinement cycle, the corrected output addresses all identified issues. Note: React 19’s use() hook requires a stable Promise reference — if the Promise is created anew on every render, React will suspend, resolve, re-render, create another Promise, and loop indefinitely. The useMemo wrapper below ensures the Promise is only created when projectId changes.
import { use, Suspense, Component, useMemo } from 'react';
async function fetchProjectStats(projectId) {
const url = `/api/projects/${encodeURIComponent(projectId)}/stats`;
let response;
try {
response = await fetch(url, { signal: AbortSignal.timeout(5000) });
} catch (err) {
throw new Error('Unable to reach the server. Please try again.');
}
if (!response.ok) {
console.warn('[fetchProjectStats] non-OK response:', response.status);
throw new Error('Failed to load project stats. Please try again.');
}
return response.json();
}
class StatsErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
this.resetError = this.resetError.bind(this);
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, info) {
if (typeof this.props.onError === 'function') {
this.props.onError(error, info);
} else {
console.error('StatsErrorBoundary caught:', error, info);
}
}
resetError() {
this.setState({ hasError: false, error: null });
}
render() {
if (this.state.hasError) {
return (
<div className="project-stats-card project-stats-card--error">
<p>Failed to load project stats.p>
<button type="button" onClick={this.resetError}>
Retry
button>
div>
);
}
return this.props.children;
}
}
function ProjectStatsCardInner({ projectId }) {
const statsPromise = useMemo(() => fetchProjectStats(projectId), [projectId]);
const stats = use(statsPromise);
return (
<div className="project-stats-card">
<h3 className="project-stats-card__title">{stats.projectName}h3>
<p className="project-stats-card__tasks">
{stats.completedTasks} / {stats.totalTasks} tasks completed
p>
<div className="project-stats-card__bar-container">
<div
className="project-stats-card__bar-fill"
style={{ width: `${stats.completionPercent}%` }}
role="progressbar"
aria-valuenow={stats.completionPercent}
aria-valuemin={0}
aria-valuemax={100}
aria-label={`${String(stats.projectName).slice(0, 60)} completion`}
/>
div>
<span className="project-stats-card__percent">
{stats.completionPercent}%
span>
div>
);
}
export default function ProjectStatsCard({ projectId }) {
return (
<StatsErrorBoundary>
<Suspense
fallback={
<div className="project-stats-card project-stats-card--loading">
Loading stats…
div>
}
>
<ProjectStatsCardInner projectId={projectId} />
Suspense>
StatsErrorBoundary>
);
}
import express from 'express';
const router = express.Router();
const projects = {
1: { projectName: 'Website Redesign', totalTasks: 24, completedTasks: 18 },
2: { projectName: 'API Migration', totalTasks: 12, completedTasks: 12 },
3: { projectName: 'New Feature Spike', totalTasks: 0, completedTasks: 0 },
};
router.get('/:id/stats', function getProjectStats(req, res) {
try {
const raw = req.params.id;
if (!/^\d+$/.test(raw)) {
return res.status(400).json({ error: 'ID must be a positive integer' });
}
const id = parseInt(raw, 10);
if (id <= 0) {
return res.status(400).json({ error: 'ID must be a positive integer' });
}
const project = projects[id];
if (!project) {
return res.status(404).json({ error: 'Project not found' });
}
const completionPercent =
project.totalTasks === 0
? 0
: Math.min(
100,
Math.round((project.completedTasks / project.totalTasks) * 100)
);
res.json({
projectName: project.projectName,
totalTasks: project.totalTasks,
completedTasks: project.completedTasks,
completionPercent,
});
} catch (err) {
console.error('[projectStats] unhandled error:', err);
res.status(500).json({ error: 'Internal server error' });
}
});
export default router;
To run the backend, create a minimal server entry point:
import express from 'express';
import projectStatsRouter from './src/routes/projectStats.js';
const app = express();
app.use(express.json());
app.use('/api/projects', projectStatsRouter);
app.use((err, req, res, next) => {
console.error('[server] unhandled error:', err);
res.status(500).json({ error: 'Internal server error' });
});
const PORT = process.env.PORT ?? 3000;
app.listen(PORT, () =>
console.log(`Server running on http://localhost:${PORT}`)
);
If using Vite for the frontend dev server, add a proxy so that fetch('/api/...') reaches the Express server:
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
'/api': 'http://localhost:3000',
},
},
});
All three issues from the review stage are addressed. Additionally, use() requires a stable Promise reference; the useMemo wrapper ensures fetchProjectStats is only called when projectId changes, preventing an infinite re-fetch loop. Input validation rejects non-integer and non-positive IDs with a 400 response. The division-by-zero case returns 0 instead of NaN. The React component handles loading states via Suspense, error states via an ErrorBoundary with a retry button for recovery from transient failures, and non-2xx API responses by throwing in the fetch function. The fetch call includes a 5-second timeout to prevent the Suspense fallback from hanging indefinitely against a slow backend, and encodes the projectId to prevent URL injection. The progress bar also gains ARIA attributes for accessibility; in this session’s output, the AI added them unprompted during the refinement pass. The completionPercent is capped at 100 to prevent the progress bar from overflowing its container if the data contains inconsistencies.
Putting It All Together: A Complete Vibe Coding Session
End-to-End Walkthrough Recap
Across the four stages, the running example produced a full-stack feature. The React component handles data fetching, error states, loading states, and accessibility. The Express endpoint validates input and includes defensive error handling. The developer wrote zero boilerplate but made every architectural and quality decision: what the feature should do, which constraints to enforce, which issues to flag, and how to direct the AI’s corrections.
For a single-developer session building a feature of this scope (one component, one endpoint, validation and error handling), structured vibe coding in the author’s experience takes about half the time of writing the same feature from scratch. The gain comes not from skipping engineering judgment but from offloading the work of translating decisions into syntax.
When Vibe Coding Breaks Down
Vibe coding has clear limitations. Complex state machines with intricate transition logic tend to produce output that is subtly incorrect and difficult to verify. Performance-critical algorithms, where the difference between O(n) and O(n log n) matters, require deliberate optimization that AI models handle inconsistently; the model may select a correct algorithm but miss a cache-friendly data layout or an opportunity to avoid repeated allocations. Novel or undocumented APIs, particularly internal ones without public training data, leave the AI guessing.
The practical response is a hybrid approach: vibe code the scaffolding, boilerplate, and patterns with extensive documentation and public examples (CRUD endpoints, form validation, data-fetching components), then hand-write the critical paths where correctness and performance demand direct human control.
The Vibe Coding Implementation Checklist
Your Complete Vibe Coding Workflow Checklist
Save this checklist and use it for every vibe coding session.
Pre-Session Setup
- AI toolchain installed and configured (Cursor, Copilot Workspace, or equivalent)
- Project context files prepared (README, architecture docs, existing patterns)
- Coding standards documented in a format the AI can consume (style guide, linting config)
- Dependencies pinned in lockfile; use
npm ciorpnpm install --frozen-lockfileto enforce lockfile fidelity in all team and CI environments - Version control on a clean branch
Prompt Stage
- Context block specifies project, tech stack, and existing architecture
- Constraints include versions, libraries, and patterns to follow or avoid
- Output format defines file structure, naming conventions, and annotation style
- Acceptance criteria are testable and specific
- Example patterns referenced where applicable
Generate Stage
- AI output captured in version control before manual edits
- Diff reviewed to understand what was generated
- All imports and dependencies verified against the project lockfile
- No hallucinated packages or nonexistent APIs
Review Stage
The review stage catches the errors AI generation reliably misses: validation gaps, missing edge cases, and security oversights. Check each of the following:
- Security: input validation, authentication checks, no exposed secrets
- Correctness: edge cases handled (null, zero, empty, malformed input)
- Error handling: all failure modes produce meaningful responses
- Accessibility: ARIA attributes, semantic HTML, keyboard navigation
- Performance: no unnecessary re-renders, efficient queries, no N+1 patterns
- Test coverage: gaps identified and tests added for critical paths
Refine Stage
Refinement prompts succeed when they are specific. Reference exact function names, describe the wrong behavior, and specify the correct behavior:
- Refinement prompts target specific functions and behaviors
- Changes are isolated and do not introduce regressions
- Regression check performed (existing tests pass)
- Final human sign-off before merge
What’s Next for AI-First Development
The developer’s role continues shifting toward architecture, quality judgment, and the kind of contextual taste that determines whether software is merely functional or genuinely well-built.
The Prompt, Generate, Review, Refine loop is the durable skeleton of AI-first development. As models improve in accuracy and context handling, developers will intervene less during Review and Refine for patterns with strong documentation coverage, but those stages will not disappear. The developer’s role continues shifting toward architecture, quality judgment, and the kind of contextual taste that determines whether software is merely functional or genuinely well-built. The checklist above is the starting point. Pick one feature this week, run it through all four stages, and measure the result against building it from scratch.

