The Hidden Cost of Code that "Works"

Systematic Code Review Workflow with AI: Plan, Generate, Validate

Practical guide to maintaining code quality at AI speed


Introduction: The Hidden Cost of Code That “Works”

The following code generates a user registration endpoint. It compiles correctly, passes basic tests, and works in staging:

app.post('/api/register', async (req, res) => {
  const { email, password, username } = req.body;
  
  const user = await db.query(
    `INSERT INTO users (email, password, username) 
     VALUES ('${email}', '${password}', '${username}') RETURNING *`
  );
  
  res.json({ success: true, user });
});

:warning: Critical issues:

  • SQL injection vulnerability
  • Password stored in plain text
  • No input validation
  • No error handling
  • No protection against duplicates

This is the typical result when generating code with AI without a validation process. AI optimizes for code that works, not code that is correct.

The solution is to implement a systematic workflow that separates generation from validation.


The Three-Phase Workflow

workflow-principal

Fundamental principle: Each phase has a specific purpose and uses different tools. Generative AI (Cursor, Copilot, Claude) optimizes for speed. Review AI (Claude Code, CodeRabbit) optimizes for security and quality.


Phase 1: Structured Planning

Clear planning produces better results from generative AI. Before writing code, complete the following template:

Planning Template

Feature: [Feature name]
Purpose: [One sentence describing the objective]

Inputs:
  - field: type, validation rules
  - field: type, validation rules

Outputs:
  Success:
    - HTTP code, response structure
  Error:
    - HTTP code, error structure

Edge Cases:
  - What happens if...?
  - What happens if...?

Security:
  - Consideration 1
  - Consideration 2

Dependencies:
  - Required library/service

Applied Example: User Registration

Feature: User Registration Endpoint
Purpose: Create new users with complete validation and secure storage

Inputs:
  - email: string, RFC 5322 format, unique in DB
  - password: string, minimum 8 characters, at least 1 number and 1 uppercase
  - username: string, 3-20 alphanumeric characters

Outputs:
  Success:
    - 201 Created, { user: { id, email, username, createdAt } }
  Error:
    - 400 Bad Request, { errors: [{ field, message }] }
    - 409 Conflict, { error: "Email already registered" }
    - 500 Internal Error, { error: "Internal server error" }

Edge Cases:
  - Email with valid format but non-existent domain
  - Password meeting minimum requirements but common (123456Aa)
  - Username with Unicode characters that appear alphanumeric
  - Simultaneous duplicate requests (race condition)

Security:
  - Password hash with bcrypt (cost factor 12)
  - Parameterized queries (prevent SQL injection)
  - Rate limiting on endpoint
  - Do not reveal if email exists in generic error message
  - Sanitize inputs before logging

Dependencies:
  - bcrypt for hashing
  - zod for validation
  - express-rate-limit for throttling

Phase 2: Generation with Context

With the complete template, a structured prompt is built that includes all requirements.

Generation Best Practices

Practice Description
One task per prompt Generate one endpoint, component, or function at a time
Clean context New chat for each task, avoid contaminated contexts
Explicit stack Specify versions, conventions, and project structure
Deliberate iteration 2-3 iterations refining, don’t expect perfection initially

Structured Prompt

Create an Express + TypeScript endpoint for user registration.

TECHNICAL REQUIREMENTS:
- POST /api/v1/auth/register
- Validation with Zod
- Hash with bcrypt (12 rounds)
- Parameterized queries with pg (node-postgres)
- TypeScript strict mode

VALIDATIONS:
- email: RFC 5322 format
- password: minimum 8 chars, 1 number, 1 uppercase
- username: 3-20 alphanumeric chars

RESPONSES:
- 201: { user: { id, email, username, createdAt } }
- 400: { errors: [{ field: string, message: string }] }
- 409: { error: string } for duplicate email
- 500: { error: string } generic

SECURITY:
- Do not include password in any response
- Generic message for duplicate email
- Logging without sensitive data

PROJECT STRUCTURE:
src/
  controllers/
  middleware/
  validators/
  types/

Phase 3: Automated Validation

Generated code requires validation with a second perspective specialized in security.

Validation Architecture

arquitectura-validacion

Option 1: Claude Code as Security Reviewer

Claude Code allows running security analysis directly from the terminal. Recommended configuration:

Installation and Setup

# Install Claude Code
npm install -g @anthropic-ai/claude-code

# Install analysis tools
npm install -D eslint @eslint/js eslint-plugin-security
npm install -D @typescript-eslint/parser @typescript-eslint/eslint-plugin

ESLint Configuration for Security

// eslint.config.js
import security from 'eslint-plugin-security';
import tseslint from '@typescript-eslint/eslint-plugin';

export default [
  {
    plugins: {
      security,
      '@typescript-eslint': tseslint
    },
    rules: {
      'security/detect-object-injection': 'error',
      'security/detect-non-literal-regexp': 'error',
      'security/detect-unsafe-regex': 'error',
      'security/detect-buffer-noassert': 'error',
      'security/detect-child-process': 'warn',
      'security/detect-disable-mustache-escape': 'error',
      'security/detect-eval-with-expression': 'error',
      'security/detect-no-csrf-before-method-override': 'error',
      'security/detect-non-literal-fs-filename': 'warn',
      'security/detect-non-literal-require': 'warn',
      'security/detect-possible-timing-attacks': 'error',
      'security/detect-pseudoRandomBytes': 'error',
      'security/detect-sql-injection': 'error'
    }
  }
];

Review Prompt for Claude Code

Create a CLAUDE.md file in the project root with review instructions:

# Security Review Instructions

When reviewing code, systematically analyze:

## 1. Injection
- [ ] SQL Injection: Are parameterized queries used?
- [ ] NoSQL Injection: Are MongoDB operators validated?
- [ ] Command Injection: Are inputs sanitized for exec/spawn?
- [ ] XSS: Are outputs escaped in templates?

## 2. Authentication
- [ ] Are passwords hashed with bcrypt/argon2 (cost >= 10)?
- [ ] Do tokens have appropriate expiration?
- [ ] Is rate limiting implemented on auth endpoints?

## 3. Authorization
- [ ] Is ownership verification present on resources?
- [ ] Is RBAC/ABAC correctly implemented?

## 4. Sensitive Data
- [ ] Are secrets in environment variables (not hardcoded)?
- [ ] Is logging done without sensitive data?
- [ ] Are responses free of internal information?

## 5. Dependencies
- [ ] Run: npm audit
- [ ] Verify: no deprecated dependencies

## Analysis Commands
npx eslint --ext .ts,.js src/
npm audit --audit-level=moderate

Run Review with Claude Code

# Navigate to project
cd my-project

# Start security review
claude-code

# Within Claude Code, execute:
> Review the file src/controllers/auth.controller.ts 
> following the instructions in CLAUDE.md. 
> Run the analysis commands and report vulnerabilities.
```### Option 2: CodeRabbit (CI/CD Integration)

CodeRabbit integrates directly with GitHub/GitLab for automatic review on every PR.

```yaml
# .github/workflows/code-review.yml
name: Automated Code Review

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  security-review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          
      - name: Install Dependencies
        run: npm ci
        
      - name: Security Audit
        run: npm audit --audit-level=moderate
        
      - name: ESLint Security
        run: npx eslint --ext .ts,.js src/
        
      # CodeRabbit activates automatically on PRs
      # after installing it in the repository

Security Decision Flow

checklist-seguridad


Complete Example: From Vulnerable to Secure

:cross_mark: Vulnerable Code (Generation without validation)

// ⚠️ DO NOT USE IN PRODUCTION - For demonstration only
import express from 'express';
import { Pool } from 'pg';

const app = express();
const pool = new Pool();

app.post('/api/register', async (req, res) => {
  const { email, password, username } = req.body;
  
  // 🔴 SQL Injection
  const result = await pool.query(
    `INSERT INTO users (email, password, username) 
     VALUES ('${email}', '${password}', '${username}') 
     RETURNING *`
  );
  
  // 🔴 Password in response
  res.json({ success: true, user: result.rows[0] });
});

Vulnerabilities detected:

  1. SQL injection through string concatenation
  2. Password stored in plain text
  3. Password returned in response
  4. No input validation
  5. No error handling
  6. No rate limiting

:white_check_mark: Secure Code (Post-validation)

// src/controllers/auth.controller.ts
import { Request, Response, NextFunction } from 'express';
import { z } from 'zod';
import bcrypt from 'bcrypt';
import { pool } from '../config/database';
import { AppError } from '../middleware/errorHandler';

// Validation schema
const registerSchema = z.object({
  email: z
    .string()
    .email('Invalid email format')
    .max(255, 'Email too long'),
  password: z
    .string()
    .min(8, 'Minimum 8 characters')
    .regex(/[0-9]/, 'Must contain at least one number')
    .regex(/[A-Z]/, 'Must contain at least one uppercase letter'),
  username: z
    .string()
    .min(3, 'Minimum 3 characters')
    .max(20, 'Maximum 20 characters')
    .regex(/^[a-zA-Z0-9]+$/, 'Only alphanumeric characters')
});

// Types
interface UserResponse {
  id: string;
  email: string;
  username: string;
  createdAt: Date;
}

interface RegisterBody {
  email: string;
  password: string;
  username: string;
}

export const register = async (
  req: Request<{}, {}, RegisterBody>,
  res: Response,
  next: NextFunction
): Promise<void> => {
  try {
    // 1. Validate input
    const validation = registerSchema.safeParse(req.body);
    
    if (!validation.success) {
      const errors = validation.error.errors.map(err => ({
        field: err.path.join('.'),
        message: err.message
      }));
      res.status(400).json({ errors });
      return;
    }
    
    const { email, password, username } = validation.data;
    
    // 2. Hash password (cost factor 12)
    const hashedPassword = await bcrypt.hash(password, 12);
    
    // 3. Parameterized query (prevents SQL injection)
    const query = `
      INSERT INTO users (email, password_hash, username, created_at)
      VALUES ($1, $2, $3, NOW())
      RETURNING id, email, username, created_at as "createdAt"
    `;
    
    const result = await pool.query<UserResponse>(query, [
      email.toLowerCase(),
      hashedPassword,
      username
    ]);
    
    // 4. Response without password
    res.status(201).json({
      user: result.rows[0]
    });
    
  } catch (error: unknown) {
    // 5. Specific error handling
    if (error instanceof Error && 'code' in error) {
      const pgError = error as { code: string };
      
      // Unique constraint violation (duplicate email)
      if (pgError.code === '23505') {
        // Generic message (do not reveal if email exists)
        res.status(409).json({
          error: 'Could not complete registration'
        });
        return;
      }
    }
    
    // Log without sensitive data
    console.error('Registration error:', {
      timestamp: new Date().toISOString(),
      path: req.path,
      // DO NOT include: email, password, username
    });
    
    next(new AppError('Internal server error', 500));
  }
};
// src/routes/auth.routes.ts
import { Router } from 'express';
import rateLimit from 'express-rate-limit';
import { register } from '../controllers/auth.controller';

const router = Router();

// Rate limiting: 5 attempts per IP every 15 minutes
const registerLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: { error: 'Too many attempts, please try again later' },
  standardHeaders: true,
  legacyHeaders: false
});

router.post('/register', registerLimiter, register);

export default router;

Implementation Checklist

Use this checklist before each deployment:

Pre-Generation

  • Planning template completed
  • Edge cases identified
  • Security requirements documented

Post-Generation

  • Manual structure review
  • Project conventions respected
  • TypeScript types correct

Automated Validation

  • npm audit with no critical vulnerabilities
  • ESLint security with no errors
  • Claude Code / CodeRabbit with no security issues

Specific Security

  • Parameterized queries (no concatenation)
  • Passwords hashed (bcrypt >= 10 rounds)
  • Inputs validated with schema (Zod, Joi)
  • Errors without sensitive information
  • Rate limiting on critical endpoints
  • CORS configured correctly

Conclusion

The three-phase workflow transforms AI code generation from a potential risk into a competitive advantage:

  1. Plan before generating reduces iterations and improves output quality
  2. Generate with complete context produces code closer to production
  3. Validate with specialized tools captures what generation misses

The key is recognizing that generation AI and validation AI have different and complementary objectives. Integrating both into a systematic workflow allows maintaining development speed without sacrificing security.


Additional Resources


Published on yoDEV.dev - The Latin American developers community

Very good article. The three-phase workflow covers classical security well, but there’s a type of error that slips past even ESLint and CodeRabbit: when AI invents a function that doesn’t exist in the library (for example, requests.get_json() instead of response.json()). That code compiles, passes tests, and SAST doesn’t flag it because the method isn’t on the list of known vulnerabilities. I’ve been building a tool that specifically tackles that problem: a registry of valid APIs to compare function calls against. Have you seen this type of hallucination in your code?

Thanks, Carlos! And very good point.

To be honest, I’ve been pretty lucky: I’ve hardly run into hallucinations of non-existent APIs, and the few times it happened, the error surfaced quickly. But I completely understand why it’s a real risk, and you’re right that it falls outside what the article’s workflow covers, which is designed for known vulnerabilities.

I find your approach interesting because it attacks exactly that gap. In typed stacks (TypeScript in strict mode, or Python with mypy/pyright), type checking catches some of these cases. But in dynamic code, or in code paths that tests don’t exercise, an invented method can slip right into production and fail only when someone actually hits it. A registry of valid APIs covers something that neither the linter nor SAST are designed to catch. Plus it has a close and pretty dangerous cousin: when the AI invents the name of an entire package and someone registers it on npm or PyPI with malicious code (the famous slopsquatting).

If you’re interested, it would be great if you wrote a post for the community about your tool: what problem it solves, how you built the registry, and some real examples of hallucinations you’ve detected. I think it would be useful to a lot of people here.

Thanks so much for taking the time to read and comment, Greco! I completely agree with everything you’re bringing up.

I especially agree on typed stacks: TypeScript in strict mode or mypy/pyright catch a significant portion of these cases. But as you say, the gap remains in dynamic code and in the paths that tests don’t exercise. And I’d add another gap: even in typed code, if the AI invents a method that does exist in the signature but not at runtime (for example, a method that exists in the types but not in the actual implementation of the library), the type checker doesn’t catch it. That’s why the API registry seems complementary to me, not a substitute for typing.

Slopsquatting strikes me as the dangerous cousin, yes. In my case I cover it with two separate checks: a PyPI query to see if the package exists, and another typosquatting check using Levenshtein distance against popular names (reqeusts, numpi, pandaas…). The overlap between both is intentional: I’d rather have a suspicious import flagged twice than let it slip through.

About the post: it seems like an excellent idea and I’m getting on it. The plan is to cover (a) what problem it solves exactly and what it doesn’t, (b) how I built the API registry and why that part is the most handcrafted, and (c) real examples of hallucinations I’ve detected, including the false positives I’ve had to fix. If it works for you, I’ll let you know when it’s published so you can give me your take from your experience with the article’s workflow. Thanks again!

Hello again, Carlos.

A pleasure. It sounds like an interesting project. I’m sure our readers will love learning more about it.

I can’t wait to see it!

Best regards!
Greg