Skip to content
Lesson 9 of 22

Code Review: Reading What AI Writes

18 min read

Why Review Matters

AI-generated code usually works. It runs, it compiles, it does roughly what you asked. But "works" and "works well" are different things. Code can function correctly while being insecure, inefficient, hard to maintain, or full of subtle bugs waiting to surface in production.

Reviewing AI output isn't about distrust — it's about quality assurance. Professional development teams review every line of code before it ships, even code written by senior engineers. The same principle applies to AI-generated code, perhaps even more so because AI optimizes for "correct now" rather than "maintainable long-term."

There is a compounding benefit to reviewing code: you learn as you review. Every time you read AI-generated code and ask "why did it do it this way?", you deepen your understanding of how software works. Over months of vibe coding with active review, you'll develop real programming intuition — not because you studied it formally, but because you engaged with the code your AI partner wrote.

The goal isn't to understand every line. The goal is to develop a set of pattern-recognition skills that let you spot problems at a glance. Think of it like this: you don't need to be a chef to know that a dish is too salty. You don't need to be a programmer to notice that a 500-line file should probably be split into smaller pieces.

Reading Code at a High Level

When AI generates code, resist the urge to either blindly accept it or try to understand every character. Instead, read it at a high level, focusing on structure and intent.

What to Look For

File names and locations. Did the AI create files in the right directories? A component file in components/ makes sense. A component file in lib/ does not. If your project has a consistent structure, new files should follow it.

Function and variable names. Good code reads almost like English. A function called getUserProfile is self-explanatory. A function called processData is vague. Variables called user, email, and isAuthenticated tell a clear story. Variables called x, temp, and flag are red flags.

Comments. AI sometimes adds helpful comments that explain why code is structured a certain way. Read these — they're the AI's explanation of its own reasoning. If there are no comments on complex logic, that might be a place to ask for clarification.

Imports at the top of the file. The import section tells you what external tools and components this file depends on. If you see unfamiliar package names, ask what they are. If you see imports from packages you didn't install, the AI might be hallucinating a dependency.

The overall length. A 50-line component is manageable. A 500-line component is probably doing too much and should be broken into smaller pieces. File length is a quick proxy for complexity.

The Building Architecture Analogy

You can evaluate a building's architecture without knowing how to lay bricks. You notice if the hallways are too narrow, if the rooms flow logically, if there's enough natural light. Similarly, you can evaluate code architecture without understanding every syntax detail:

  • Are there too many files for a simple feature? (Over-engineering)
  • Is everything crammed into one file? (Under-engineering)
  • Do the file names clearly describe what they contain?
  • Is there a logical organization, or does it feel random?
  • Are there obvious repetitions — the same code appearing in multiple files?

Understanding Project Structure

If you're working with Next.js (which most of this course focuses on), here's what the key directories mean:

app/                    # Pages and routes
  page.tsx              # The homepage
  layout.tsx            # Shared layout (header, footer)
  about/
    page.tsx            # The /about page
  api/
    users/
      route.ts          # API endpoint: /api/users
  dashboard/
    page.tsx            # The /dashboard page
    loading.tsx         # Loading skeleton for /dashboard
    error.tsx           # Error boundary for /dashboard

components/             # Reusable UI pieces
  ui/                   # Generic components (Button, Modal, Card)
  features/             # Feature-specific components

lib/                    # Utility functions, database queries, helpers
  db.ts                 # Database connection
  auth.ts               # Authentication helpers
  utils.ts              # General utilities

public/                 # Static files (images, fonts, favicons)

types/                  # TypeScript type definitions

How Components Relate to Pages

Pages in app/ are what the user sees. They are assembled from components in components/. Think of pages as recipes and components as ingredients.

The /dashboard page might import and compose StatsCard, ActivityFeed, UsageChart, and Sidebar components. Each component handles its own rendering and styling. The page just arranges them on screen and passes data to them.

Data typically flows in one direction: the page fetches data from lib/ functions (which talk to the database or APIs) and passes it down to components as "props" (properties). Components receive data and render it — they don't usually fetch their own data.

When reviewing AI code, check that this flow makes sense. If a small UI component deep in the component tree is making database calls directly, that's a structural problem. Data fetching should happen at the page level and flow down.

Common Code Smells AI Generates

"Code smell" is a term for patterns that suggest something might be wrong, even if the code technically works. Here are the most common ones in AI-generated code.

Over-Engineering

AI loves abstractions. Ask for a simple contact form and you might get a generic FormBuilder class with a FieldFactory, a ValidationEngine, and a SubmissionHandler — all for three input fields and a button.

What to look for: More files than seem necessary for the feature's complexity. Abstract base classes when there's only one implementation. Configuration objects for behavior that's straightforward enough to hard-code.

What to say: "This is over-engineered for our needs. Simplify it to a single component with the form fields directly in the JSX. No abstractions needed for a simple contact form."

Unused Imports and Dead Code

AI sometimes leaves behind imports it generated during an earlier iteration but no longer uses, or functions it wrote but never calls.

What to look for: Grayed-out imports in your editor (most editors dim unused imports). Functions or variables defined but never referenced. Code blocks inside if (false) or commented-out sections.

What to say: "Remove unused imports and dead code from this file."

The any Type in TypeScript

In TypeScript, any is the escape hatch — it tells the compiler "don't check this, I give up." AI sometimes uses any when it can't figure out the correct type, and it silently disables the safety net TypeScript provides.

What to look for: The word any in TypeScript files. Especially concerning in function parameters, return types, and state definitions.

// Bad: any disables type checking
const handleSubmit = (data: any) => { /* ... */ }

// Good: specific type provides safety
const handleSubmit = (data: ContactFormData) => { /* ... */ }

What to say: "Replace all any types with proper TypeScript types. Define interfaces for the data structures."

Hardcoded Values

Values that should be configurable but are written directly in the code.

// Bad: hardcoded values
const API_URL = "https://api.mysite.com/v2";
const MAX_RETRIES = 3;
const ITEMS_PER_PAGE = 25;

// Better: environment variable for the URL, constants file for the rest
const API_URL = process.env.NEXT_PUBLIC_API_URL;

What to look for: URLs, API endpoints, magic numbers, email addresses, and configuration values embedded directly in component or page files.

What to say: "Move the API URL to an environment variable. Move the constants (MAX_RETRIES, ITEMS_PER_PAGE) to a constants.ts file."

Duplicated Logic

AI doesn't always remember what it's already written in other files. You might end up with the same formatting function, the same validation logic, or the same API call in three different places.

What to look for: Copy-pasted code across files. Multiple functions that do almost the same thing. The same API endpoint being called in different components.

What to say: "The date formatting logic is duplicated in UserProfile, ActivityFeed, and CommentList. Extract it into a shared utility function in lib/utils.ts and import it in all three."

Missing Error Handling

AI-generated code often follows the "happy path" — it handles the case where everything works but doesn't account for failures.

// No error handling: will crash if the API is down
const response = await fetch("/api/users");
const users = await response.json();

// With error handling: graceful failure
const response = await fetch("/api/users");
if (!response.ok) {
  throw new Error(`Failed to fetch users: ${response.status}`);
}
const users = await response.json();

What to look for: fetch calls without checking response.ok. Database queries without try/catch. User-facing operations without loading and error states.

What to say: "Add error handling to all API calls and database queries. Show the user a meaningful error message if something fails."

Overly Complex Conditional Chains

// Hard to follow
if (user && user.subscription && user.subscription.plan !== "free"
    && user.subscription.status === "active"
    && !user.subscription.cancelledAt) {
  // show premium content
}

// Clearer: extract into a named function
const hasActiveSubscription = (user: User): boolean => {
  if (!user?.subscription) return false;
  const { plan, status, cancelledAt } = user.subscription;
  return plan !== "free" && status === "active" && !cancelledAt;
};

What to look for: Deeply nested if/else blocks, long conditional expressions, ternary operators chained together.

What to say: "The conditional logic in the subscription check is hard to follow. Extract it into a named function with a descriptive name."

Security Basics Every Vibe Coder Should Know

Security issues in code are invisible until someone exploits them. You don't need to be a security expert, but knowing these fundamentals will help you catch the most common problems.

Never Expose Secrets in Client Code

API keys, database passwords, and secret tokens must never appear in code that runs in the browser. In Next.js, only environment variables prefixed with NEXT_PUBLIC_ are sent to the browser. Everything else stays on the server.

What to look for: Environment variables without the NEXT_PUBLIC_ prefix being used in client components (files with "use client"). Any string that looks like an API key hard-coded in the source.

// DANGEROUS: this API key will be visible in the browser
const stripe = new Stripe("sk_live_abc123...");

// SAFE: server-side only, never sent to the browser
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

SQL Injection

SQL injection happens when user input is inserted directly into a database query, allowing an attacker to modify the query's meaning.

// VULNERABLE: user input directly in the query string
const query = `SELECT * FROM users WHERE email = '${userEmail}'`;

// SAFE: parameterized query (Drizzle ORM handles this automatically)
const user = await db.select().from(users).where(eq(users.email, userEmail));

If you're using an ORM like Drizzle or Prisma, SQL injection is largely handled for you. But if you see raw SQL strings with template literals (backtick strings with ${}), that's a red flag.

Cross-Site Scripting (XSS)

XSS happens when user-provided content is rendered as HTML without sanitization. An attacker could submit a comment containing script tags that steal other users' data.

What to look for: Any React property that renders raw HTML from user input. Any place where user content is rendered as raw HTML instead of text. React escapes text content by default, which is safe.

// DANGEROUS: renders user content as executable HTML
<div dangerouslySetInnerHTML={userContent} />

// SAFE: React automatically escapes text content
<div>{comment.body}</div>

If you must render HTML (for example, from a rich text editor), always sanitize it first with a library like DOMPurify.

Cross-Site Request Forgery (CSRF)

CSRF tricks a logged-in user into performing actions they didn't intend to. For example, clicking a link in an email that deletes their account because they happen to be logged in.

What to look for: State-changing operations (DELETE, POST, PUT) that don't verify a CSRF token. Most modern frameworks handle this automatically, but server actions and custom API routes might not.

Always Validate on the Server

Client-side validation is for user experience — it shows errors as the user types. Server-side validation is for security — it prevents malicious requests from bypassing the UI.

What to look for: API routes or server actions that trust incoming data without validation. If there's no Zod schema or validation check in the API route, any data could be submitted.

// No validation: trusts whatever the client sends
export async function POST(request: Request) {
  const data = await request.json();
  await db.insert(users).values(data); // dangerous!
}

// With validation: rejects invalid data
export async function POST(request: Request) {
  const body = await request.json();
  const data = createUserSchema.parse(body); // throws if invalid
  await db.insert(users).values(data);
}

HTTPS Everywhere

Any data sent over HTTP (without the S) can be intercepted. This includes passwords, API keys, and personal information. Vercel and most modern hosting platforms enforce HTTPS by default, but check that your API calls use https:// and not http://.

Performance Red Flags

Slow applications lose users. Here are performance problems to watch for in AI-generated code.

N+1 Database Queries

This happens when code fetches a list of items, then makes an additional database query for each item in the list.

// N+1: one query for posts, then one query per post for the author
const posts = await db.select().from(postsTable);
for (const post of posts) {
  const author = await db.select().from(usersTable)
    .where(eq(usersTable.id, post.authorId));
  post.author = author;
}

// Better: single query with a join
const postsWithAuthors = await db
  .select()
  .from(postsTable)
  .leftJoin(usersTable, eq(postsTable.authorId, usersTable.id));

What to look for: Database queries inside loops. If you see await inside a for loop or a .map(), check if it's hitting the database.

Unnecessary Re-Renders in React

React components re-render whenever their parent re-renders or their state changes. AI sometimes creates component structures that cause excessive re-rendering.

What to look for: State that changes frequently (like mouse position or scroll offset) stored high in the component tree, causing everything below it to re-render. Missing key props on list items (React can't optimize list rendering without them).

Fetching Too Much Data

Loading everything from the database when you only need a subset.

What to look for: API endpoints that return all records without pagination. Queries that select all columns when only a few are needed. Loading all historical data when only the current period is relevant.

What to say: "Add pagination to the /api/posts endpoint. Return 20 items per page with a cursor parameter for the next page."

Large Images Without Optimization

Unoptimized images are the most common cause of slow page loads.

What to look for: <img> tags instead of Next.js <Image> components. Missing width and height attributes. Images loaded at full resolution when displayed at a small size.

// Slow: no optimization
<img src="/hero.png" />

// Fast: Next.js handles optimization, lazy loading, responsive sizes
<Image src="/hero.png" width={1200} height={600} alt="Hero image" />

Blocking the Main Thread

Long-running JavaScript blocks the UI from responding. The page freezes, scrolling jitters, and buttons don't respond.

What to look for: Large data processing in component render functions. Synchronous operations that should be asynchronous. Heavy computations that could be moved to a Web Worker or the server.

AI Reviewing AI

One of the most powerful techniques in vibe coding is asking Claude Code to review its own work. After generating code, you can explicitly ask for a review:

Review the code you just generated for:
1. Security vulnerabilities
2. Performance issues
3. Code smells and maintainability concerns
4. Missing error handling
5. Accessibility problems

AI is surprisingly good at catching its own mistakes when explicitly asked to look for them. The generation mode and the review mode engage different reasoning patterns. During generation, AI focuses on making the code work. During review, it focuses on making the code right.

You can also ask targeted reviews:

Check the API route for SQL injection vulnerabilities
and any cases where user input is not validated.
Review this component for accessibility. Check for:
proper ARIA labels, keyboard navigation, focus management,
color contrast, and screen reader support.
Look at the data fetching pattern in this page. Are there
any unnecessary re-fetches, missing caching opportunities,
or potential race conditions?

Make AI review a regular part of your workflow, especially for security-sensitive code (authentication, payments, user data handling) and user-facing features.

When to Ask for a Rewrite vs. a Fix

Not all problems are created equal. Some deserve a targeted fix. Others need a fundamentally different approach.

Ask for a Fix When:

  • The overall approach is correct but a specific detail is wrong
  • It's a styling issue (wrong spacing, wrong color, wrong alignment)
  • A single function has a bug but the rest of the code works
  • An edge case isn't handled but the main flow is correct
  • An import is wrong or a variable name has a typo

Example fix prompt:

The date formatting in the ActivityFeed component shows
"2026-03-23T14:30:00Z" instead of "March 23, 2026 at 2:30 PM".
Update the formatDate function to use a human-readable format.

Ask for a Rewrite When:

  • The approach is fundamentally wrong (client-side when it should be server-side)
  • The code keeps getting more complex with each fix
  • You're on the third iteration of fixing the same component
  • The AI chose the wrong architecture pattern
  • Patches are accumulating on top of patches

Example rewrite prompt:

The current approach of managing cart state in localStorage and syncing
it with the server on every change is causing race conditions and data
loss. Let's take a completely different approach: manage the cart
entirely on the server using server actions. Remove the localStorage
logic entirely. The cart should be stored in the database, tied to
the user's session, and updated through server actions.

Signs the Approach Is Wrong

Watch for these signals that indicate a rewrite is needed rather than another fix:

  • Increasing complexity. Each fix makes the code longer and harder to follow. Simple features shouldn't require complex solutions.
  • Patches on patches. The code has comments like "workaround for..." or "temporary fix for..." Multiple workarounds mean the foundation is shaky.
  • AI suggests "workarounds." When AI can't solve a problem directly and proposes workarounds, the architecture might not support what you need.
  • The same bug in different disguises. You fix a data sync issue, then a similar sync issue appears elsewhere. The problem is systemic, not local.

Building Your Code Reading Skills Over Time

Code review is a skill that develops gradually. You don't need to understand everything on day one. Here's a realistic progression:

Week 1-2: File structure awareness. You can look at a project and understand which files are pages, which are components, and which are utilities. You notice when files are in the wrong place.

Month 1: Pattern recognition. You start recognizing common patterns: form handling, data fetching, conditional rendering. You can tell when something looks unusually complex for what it does.

Month 2-3: Problem detection. You spot missing error handling, unused code, and obvious security issues. You can read an error message and have a reasonable guess about what caused it.

Month 3-6: Architectural thinking. You develop opinions about how code should be organized. You notice when data flows in an unusual direction or when components have too many responsibilities. You start suggesting structural improvements.

Month 6+: Technical fluency. You can read most code and understand not just what it does but why it does it that way. You can evaluate tradeoffs between different approaches. You're no longer just reviewing — you're participating in architectural decisions.

This progression happens naturally if you actively engage with the code AI generates. Don't just accept it blindly — read it, ask questions about it, and gradually you'll develop the intuition that makes you a better builder.

Accelerating Your Learning

If you want to speed up this progression, here are some strategies:

Ask AI to explain its code. After generating a component, ask: "Walk me through this code and explain what each section does." The explanation will teach you patterns you'll recognize next time.

Read the diff before accepting. When AI modifies files, look at the changes. What was added? What was removed? Why? This habit builds pattern recognition faster than anything else.

Focus on one concept at a time. Don't try to understand everything. This week, focus on understanding how data flows from database to page. Next week, focus on how forms are handled. Build your knowledge incrementally.

Compare approaches. When AI rewrites code, compare the before and after. What improved? What tradeoffs were made? This comparison teaches you to evaluate quality.

What's Next

You now have the complete core skills toolkit for vibe coding: you can write precise prompts, configure CLAUDE.md for persistent context, use Git as a safety net, iterate and debug effectively, and review AI-generated code for quality.

In the next module, we enter full-stack territory. We'll start building complete applications — frontend layouts, backend APIs, databases, authentication, and more. The skills from this module become the foundation for everything ahead. You'll apply prompting, debugging, and reviewing in every lesson as we build real, deployable software.