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