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 });
});
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

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

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

Complete Example: From Vulnerable to Secure
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:
- SQL injection through string concatenation
- Password stored in plain text
- Password returned in response
- No input validation
- No error handling
- No rate limiting
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 auditwith 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:
- Plan before generating reduces iterations and improves output quality
- Generate with complete context produces code closer to production
- 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
- OWASP Top 10 - Most critical web vulnerabilities
- Claude Code Documentation - Official guide
- ESLint Plugin Security - Security rules
- Zod Documentation - TypeScript schema validation
Published on yoDEV.dev - The Latin American developers community