Module 8: Capstone Project

Phase 1: Code Review — The TaskFlow API Codebase

Phase 1: Code Review — The TaskFlow API Codebase

Capsule overview

Here is the complete TaskFlow API codebase. These are the files you must review, with the exact code you'll examine during the project. The codebase contains between 15 and 20 problems distributed across the 5 categories you saw in the previous capsule: hallucinations, security holes, unhandled edge cases, incorrect logic, and runtime bugs.

Your job in this phase is to apply the module 4 code review checklist, compare each file against the functional requirements, and document each problem you find in the findings document format.

Don't fix anything yet. This phase is about observation and documentation. The corrections come in Capsule 04.


The Complete Codebase

File 1: requirements.txt

fastapi==0.115.0
uvicorn==0.30.0
pyjwt==2.9.0
bcrypt==4.2.0
pydantic==2.9.0
python-multipart==0.0.12

File 2: config.py

import os


class Settings:
    APP_NAME: str = "TaskFlow API"
    APP_VERSION: str = "1.0.0"
    DEBUG: bool = True

    DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./taskflow.db")
    DATABASE_PATH: str = "taskflow.db"

    JWT_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY", "super-secret-key-taskflow-2026")
    JWT_ALGORITHM: str = "HS256"
    JWT_EXPIRATION_MINUTES: int = 30

    BCRYPT_ROUNDS: int = 12

    DEFAULT_PAGE_SIZE: int = 10
    MAX_PAGE_SIZE: int = 100


settings = Settings()

File 3: database.py

import sqlite3
from contextlib import contextmanager

from config import settings


def init_db():
    conn = sqlite3.connect(settings.DATABASE_PATH)
    cursor = conn.cursor()

    cursor.execute("""
        CREATE TABLE IF NOT EXISTS users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            email TEXT UNIQUE NOT NULL,
            name TEXT NOT NULL,
            password TEXT NOT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
    """)

    cursor.execute("""
        CREATE TABLE IF NOT EXISTS tasks (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title TEXT NOT NULL,
            description TEXT DEFAULT '',
            priority TEXT DEFAULT 'medium',
            status TEXT DEFAULT 'pending',
            user_id INTEGER NOT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            FOREIGN KEY (user_id) REFERENCES users (id)
        )
    """)

    conn.commit()
    conn.close()


@contextmanager
def get_db():
    conn = sqlite3.connect(settings.DATABASE_PATH)
    conn.row_factory = sqlite3.Row
    try:
        yield conn
    finally:
        conn.close()


def execute_query(query: str, params: tuple = ()) -> list:
    with get_db() as conn:
        cursor = conn.cursor()
        cursor.execute(query, params)
        conn.commit()
        return cursor.fetchall()


def execute_insert(query: str, params: tuple = ()) -> int:
    with get_db() as conn:
        cursor = conn.cursor()
        cursor.execute(query, params)
        conn.commit()
        return cursor.lastrowid

File 4: models.py

from datetime import datetime
from typing import Optional

from pydantic import BaseModel, EmailStr, field_validator


class UserCreate(BaseModel):
    email: EmailStr
    name: str
    password: str

    @field_validator("password")
    @classmethod
    def validate_password(cls, v):
        if len(v) < 4:
            raise ValueError("Password must be at least 4 characters")
        return v


class UserResponse(BaseModel):
    id: int
    email: str
    name: str
    created_at: str


class LoginRequest(BaseModel):
    email: str
    password: str


class TokenResponse(BaseModel):
    access_token: str
    token_type: str = "bearer"


class TaskCreate(BaseModel):
    title: str
    description: Optional[str] = ""
    priority: str = "medium"
    status: str = "pending"

    @field_validator("title")
    @classmethod
    def validate_title(cls, v):
        if len(v) < 1:
            raise ValueError("Title cannot be empty")
        return v


class TaskUpdate(BaseModel):
    title: Optional[str] = None
    description: Optional[str] = None
    priority: Optional[str] = None
    status: Optional[str] = None


class TaskResponse(BaseModel):
    id: int
    title: str
    description: str
    priority: str
    status: str
    user_id: int
    created_at: str
    updated_at: str


class TaskListResponse(BaseModel):
    tasks: list[TaskResponse]
    total: int
    page: int
    size: int


class StatsResponse(BaseModel):
    total_tasks: int
    by_status: dict
    by_priority: dict
    completion_percentage: float

File 5: services/auth_service.py

import jwt
from datetime import datetime, timedelta
from typing import Optional

from fastapi import Header, HTTPException

from bcrypt import hashpw, gensalt, checkpw, verify_hash

from config import settings


def hash_password(password: str) -> str:
    salt = gensalt(rounds=settings.BCRYPT_ROUNDS)
    return hashpw(password.encode("utf-8"), salt).decode("utf-8")


def verify_password(plain_password: str, hashed_password: str) -> bool:
    return checkpw(
        plain_password.encode("utf-8"),
        hashed_password.encode("utf-8")
    )


def create_access_token(user_id: int, email: str) -> str:
    expire = datetime.utcnow() + timedelta(
        minutes=settings.JWT_EXPIRATION_MINUTES
    )
    payload = {
        "sub": str(user_id),
        "email": email,
        "exp": expire,
        "iat": datetime.utcnow()
    }
    return jwt.encode(
        payload,
        settings.JWT_SECRET_KEY,
        algorithm=settings.JWT_ALGORITHM
    )


def decode_token(token: str) -> dict:
    try:
        payload = jwt.decode(
            token,
            settings.JWT_SECRET_KEY,
            algorithms=[settings.JWT_ALGORITHM]
        )
        return payload
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail="Token has expired")
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Invalid token")


async def get_current_user(authorization: str = Header(...)) -> dict:
    if not authorization.startswith("Bearer "):
        raise HTTPException(
            status_code=401,
            detail="Invalid authorization header"
        )
    token = authorization.split(" ")[1]
    payload = decode_token(token)
    return {
        "user_id": int(payload["sub"]),
        "email": payload["email"]
    }

File 6: services/task_service.py

from datetime import datetime
from typing import Optional

from database import execute_query, execute_insert, get_db
from models import TaskCreate, TaskUpdate


def create_task(task: TaskCreate, user_id: int) -> dict:
    query = """
        INSERT INTO tasks (title, description, priority, status, user_id)
        VALUES (?, ?, ?, ?, ?)
    """
    task_id = execute_insert(
        query,
        (task.title, task.description, task.priority, task.status, user_id)
    )
    return get_task_by_id(task_id, user_id)


def get_task_by_id(task_id: int, user_id: int) -> Optional[dict]:
    query = """
        SELECT id, title, description, priority, status, 
               user_id, created_at, updated_at
        FROM tasks WHERE id = ?
    """
    rows = execute_query(query, (task_id,))
    if not rows:
        return None
    task = dict(rows[0])
    return task


def get_tasks(
    user_id: int,
    status: Optional[str] = None,
    priority: Optional[str] = None,
    page: int = 1,
    size: int = 10,
) -> dict:
    base_query = "SELECT * FROM tasks WHERE user_id = ?"
    count_query = "SELECT COUNT(*) as total FROM tasks WHERE user_id = ?"
    params = [user_id]

    if status:
        base_query += f" AND status = '{status}'"
        count_query += f" AND status = '{status}'"

    if priority:
        base_query += f" AND priority = '{priority}'"
        count_query += f" AND priority = '{priority}'"

    offset = page * size
    base_query += f" ORDER BY created_at DESC LIMIT {size} OFFSET {offset}"

    with get_db() as conn:
        cursor = conn.cursor()

        cursor.execute(count_query, tuple(params))
        total = cursor.fetchone()["total"]

        cursor.execute(base_query, tuple(params))
        rows = cursor.fetchall(as_dict=True)

    tasks = [dict(row) for row in rows]
    return {
        "tasks": tasks,
        "total": total,
        "page": page,
        "size": size,
    }


def update_task(task_id: int, task_update: TaskUpdate, user_id: int) -> Optional[dict]:
    existing = get_task_by_id(task_id, user_id)
    if not existing:
        return None

    fields = []
    values = []

    if task_update.title is not None:
        fields.append("title = ?")
        values.append(task_update.title)
    if task_update.description is not None:
        fields.append("description = ?")
        values.append(task_update.description)
    if task_update.priority is not None:
        fields.append("priority = ?")
        values.append(task_update.priority)
    if task_update.status is not None:
        fields.append("status = ?")
        values.append(task_update.status)

    if not fields:
        return existing

    fields.append("updated_at = ?")
    values.append(datetime.utcnow().isoformat())
    values.append(task_id)

    query = f"UPDATE tasks SET {', '.join(fields)} WHERE id = ?"
    execute_query(query, tuple(values))

    return get_task_by_id(task_id, user_id)


def delete_task(task_id: int, user_id: int) -> bool:
    existing = get_task_by_id(task_id, user_id)
    if not existing:
        return False

    query = "DELETE FROM tasks WHERE id = ?"
    execute_query(query, (task_id,))
    return True


def get_user_stats(user_id: int) -> dict:
    query = "SELECT status, priority FROM tasks WHERE user_id = ?"
    rows = execute_query(query, (user_id,))

    total = len(rows)
    by_status = {}
    by_priority = {}

    for row in rows:
        row_dict = dict(row)
        status = row_dict["status"]
        priority = row_dict["priority"]

        by_status[status] = by_status.get(status, 0) + 1
        by_priority[priority] = by_priority.get(priority, 0) + 1

    completion_percentage = (
        by_status.get("completed", 0) / total * 100
    )

    return {
        "total_tasks": total,
        "by_status": by_status,
        "by_priority": by_priority,
        "completion_percentage": round(completion_percentage, 2),
    }

File 7: routes/auth.py

from fastapi import APIRouter, HTTPException

from models import UserCreate, UserResponse, LoginRequest, TokenResponse
from services.auth_service import hash_password, verify_password, create_access_token
from database import execute_query, execute_insert

router = APIRouter(prefix="/auth", tags=["Authentication"])


@router.post("/register", response_model=UserResponse)
async def register(user: UserCreate):
    existing = execute_query(
        "SELECT id FROM users WHERE email = ?", (user.email,)
    )
    if existing:
        raise HTTPException(status_code=400, detail="Email already registered")

    user_id = execute_insert(
        "INSERT INTO users (email, name, password) VALUES (?, ?, ?)",
        (user.email, user.name, user.password),
    )

    created_user = execute_query(
        "SELECT id, email, name, created_at FROM users WHERE id = ?",
        (user_id,),
    )

    row = dict(created_user[0])
    return UserResponse(**row)


@router.post("/login", response_model=TokenResponse)
async def login(credentials: LoginRequest):
    users = execute_query(
        "SELECT id, email, password FROM users WHERE email = ?",
        (credentials.email,),
    )

    if not users:
        raise HTTPException(status_code=401, detail="Invalid credentials")

    user = dict(users[0])

    if not verify_password(credentials.password, user["password"]):
        raise HTTPException(status_code=401, detail="Invalid credentials")

    token = create_access_token(user["id"], user["email"])
    return TokenResponse(access_token=token)

File 8: routes/tasks.py

from typing import Optional

from fastapi import APIRouter, HTTPException, Depends, Query

from models import TaskCreate, TaskUpdate, TaskResponse, TaskListResponse
from services.auth_service import get_current_user
from services.task_service import (
    create_task,
    get_task_by_id,
    get_tasks,
    update_task,
    delete_task,
)

router = APIRouter(prefix="/tasks", tags=["Tasks"])


@router.post("/", response_model=TaskResponse)
async def create_new_task(
    task: TaskCreate,
    current_user: dict = Depends(get_current_user),
):
    created = create_task(task, current_user["user_id"])
    return TaskResponse(**created)


@router.get("/", response_model=TaskListResponse)
async def list_tasks(
    status: Optional[str] = Query(default=None),
    priority: Optional[str] = Query(default=None),
    page: int = Query(default=1, ge=0),
    size: int = Query(default=10, ge=1, le=100),
    current_user: dict = Depends(get_current_user),
):
    result = get_tasks(
        user_id=current_user["user_id"],
        status=status,
        priority=priority,
        page=page,
        size=size,
    )
    return TaskListResponse(**result)


@router.get("/{task_id}", response_model=TaskResponse)
async def get_task(
    task_id: int,
    current_user: dict = Depends(get_current_user),
):
    task = get_task_by_id(task_id, current_user["user_id"])
    if not task:
        raise HTTPException(status_code=404, detail="Task not found")
    return TaskResponse(**task)


@router.put("/{task_id}", response_model=TaskResponse)
async def update_existing_task(
    task_id: int,
    task_update: TaskUpdate,
    current_user: dict = Depends(get_current_user),
):
    updated = update_task(task_id, task_update, current_user["user_id"])
    if not updated:
        raise HTTPException(status_code=404, detail="Task not found")
    return TaskResponse(**updated)


@router.delete("/{task_id}")
async def delete_existing_task(
    task_id: int,
    current_user: dict = Depends(get_current_user),
):
    deleted = delete_task(task_id, current_user["user_id"])
    if not deleted:
        raise HTTPException(status_code=404, detail="Task not found")
    return {"message": "Task deleted successfully"}

File 9: routes/users.py

from fastapi import APIRouter, HTTPException, Depends

from models import StatsResponse
from services.auth_service import get_current_user
from services.task_service import get_user_stats
from database import execute_query

router = APIRouter(prefix="/users", tags=["Users"])


@router.get("/me")
async def get_profile(current_user: dict = Depends(get_current_user)):
    users = execute_query(
        "SELECT id, email, name, created_at FROM users WHERE id = ?",
        (current_user["user_id"],),
    )
    if not users:
        raise HTTPException(status_code=404, detail="User not found")
    return dict(users[0])


@router.get("/stats", response_model=StatsResponse)
async def get_stats(current_user: dict = Depends(get_current_user)):
    stats = get_user_stats(current_user["user_id"])
    return StatsResponse(**stats)


@router.get("/search")
async def search_users(query: str):
    sql = f"SELECT id, email, name FROM users WHERE name LIKE '%{query}%'"
    results = execute_query(sql)
    return [dict(row) for row in results]

File 10: main.py

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic_settings import BaseSettings

from config import settings
from database import init_db
from routes import auth, tasks, users

app = FastAPI(
    title=settings.APP_NAME,
    version=settings.APP_VERSION,
    debug=settings.DEBUG,
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.on_event("startup")
async def startup():
    init_db()


app.include_router(auth.router)
app.include_router(tasks.router)
app.include_router(users.router)


@app.get("/health")
async def health_check():
    return {
        "status": "healthy",
        "app": settings.APP_NAME,
        "version": settings.APP_VERSION,
    }

Code Review Instructions

Step 1: Initial reading (15 minutes)

Read all the files above in order. Don't look for bugs yet — your goal is to understand:

  1. How do the files connect to each other?
  2. What flow does a request follow from arrival to response?
  3. What external dependencies does the project use?
  4. What's the general data structure (tables, models, responses)?

Answer these questions mentally before continuing:

  • How is a user authenticated? What flow does a login follow?
  • How is a task created? What validations does it go through?
  • How are the endpoints protected? Are all of them protected?
  • Where are the passwords stored? How is a password verified?
  • How does pagination work? What happens on the first page?

Step 2: Review against functional requirements (30-45 minutes)

Open the functional requirements from Capsule 01 and compare each requirement against the implementation:

Requirements checklist

## Review against Requirements

### FR-01: User Management
- [ ] FR-01.1: Can you register with email, name, and password?
- [ ] FR-01.2: Is the password stored hashed with bcrypt?
- [ ] FR-01.3: Is the email unique?
- [ ] FR-01.4: Is email format and password >= 8 characters validated?

### FR-02: Authentication
- [ ] FR-02.1: Does login return a JWT token?
- [ ] FR-02.2: Does the JWT expire in 30 minutes?
- [ ] FR-02.3: Does the JWT secret come from an environment variable with no fallback?
- [ ] FR-02.4: Do all protected endpoints require a token?

### FR-03: Task Management
- [ ] FR-03.1: Can you create a task with the correct fields?
- [ ] FR-03.2: Does pagination work correctly (10 per page)?
- [ ] FR-03.3: Can you filter by status and priority?
- [ ] FR-03.4: Can you get the detail only if it belongs to the user?
- [ ] FR-03.5: Can only the owner update?
- [ ] FR-03.6: Is delete a soft delete (change status to "deleted")?
- [ ] FR-03.7: A user can't view/edit/delete another's tasks?

### FR-04: Statistics
- [ ] FR-04.1: Does it return total, by status, by priority?
- [ ] FR-04.2: Are they calculated only on active tasks (not deleted)?
- [ ] FR-04.3: Does it include the percentage of completed out of the active total?

### FR-05: Validations
- [ ] FR-05.1: Title between 3 and 100 characters?
- [ ] FR-05.2: Priority only low/medium/high?
- [ ] FR-05.3: Status only pending/in_progress/completed?
- [ ] FR-05.4: Page >= 1 and size between 1 and 100?
- [ ] FR-05.5: Does a nonexistent ID return 404?
- [ ] FR-05.6: Does another user's task return 403, not 404?

### FR-06: Non-Functional
- [ ] FR-06.1: Sensitive config from environment variables?
- [ ] FR-06.2: Do SQL queries use parameters, not concatenation?
- [ ] FR-06.3: Do internal errors avoid exposing stack traces?
- [ ] FR-06.4: Consistent responses in JSON format?

For each requirement that fails, document it immediately in your findings document.

Step 3: Module 4 security checklist (15-20 minutes)

Apply the first 5 items of the Module 4 security checklist to each file:

Security items to verify

Item 1: No hardcoded secrets

Look in each file for:

  • Strings that look like API keys, passwords, or tokens
  • os.getenv() with a fallback that contains real values
  • Connection strings with credentials

Item 2: No SQL injection

Look in each file that interacts with the database for:

  • f-strings inside SQL queries
  • String concatenation in queries
  • User variables inserted directly into the SQL

Item 3: Authentication on all sensitive endpoints

Review each endpoint:

  • Does it have Depends(get_current_user) or equivalent?
  • Are there endpoints that access data but don't verify identity?

Item 4: Passwords stored correctly

Verify the complete password flow:

  • Is it hashed before saving to the database?
  • Is it compared correctly during login?
  • At any point, is it stored or transmitted in plain text?

Item 5: Permissions verified correctly

For each operation on a resource:

  • Is it verified that the current user is the owner?
  • What happens if a user tries to access another's resource?

Step 4: Search for hallucinations (10-15 minutes)

Apply the Module 3 techniques:

For each import in the codebase:
1. Does the package exist in requirements.txt?
2. Does the imported module/class exist in that package?
3. Does the version in requirements.txt have that API?
For each external library function:
1. Are the parameters being passed correct?
2. Is the return type the expected one?
3. Does the function exist in the installed version?

Pay special attention to:

  • Imports that aren't used in the file
  • Imports of modules or functions that don't exist in the package
  • Use of APIs with incorrect signatures

Step 5: Search for error patterns (10-15 minutes)

From Module 5, actively look for:

Unhandled edge cases:

  • What happens when a list is empty?
  • What happens with None/null values where they aren't expected?
  • Are the pagination limits correct?

Incorrect logic:

  • Do the filters include/exclude the right thing?
  • Do the calculations produce correct results?
  • Are the validations sufficient?

Runtime bugs:

  • Are there operations that would fail with certain data types?
  • Are there divisions that could be by zero?
  • Are there dictionary accesses that could fail?

Template to Document Findings

Use this template to record each problem you find. Create a markdown file and add a row for each finding:

# TaskFlow API — Findings Document

## Reviewer Information
- **Name:** [Your name]
- **Date:** [Review date]
- **Time spent on code review:** [Time]

## Executive Summary

[2-3 sentences describing the overall state of the codebase.
Example: "The codebase implements the basic functionality but contains 
multiple critical security vulnerabilities and several business 
logic problems that don't meet the functional requirements."]

## Findings

| # | Severity | Category | File | Line(s) | Description | Requirement Violated | Impact |
|---|-----------|-----------|---------|----------|-------------|-------------------|---------|
| 1 |  |  |  |  |  |  |  |
| 2 |  |  |  |  |  |  |  |
| 3 |  |  |  |  |  |  |  |

## Findings Statistics

| Severity | Count |
|-----------|----------|
| Critical  |          |
| High      |          |
| Medium    |          |
| Low       |          |
| **Total** |          |

## Reviewer Notes

[General observations about the codebase, patterns you noticed,
areas that require special attention]

How to fill in each column

Severity:

  • Critical: Exploitable in production, can cause data loss or unauthorized access
  • High: Incorrect functionality that affects users
  • Medium: Edge case or robustness problem
  • Low: Quality or maintainability problem

Category:

  • Security — Security vulnerability
  • Hallucination — Import/API/function that doesn't exist
  • Logic — Incorrect business logic
  • Edge Case — Unhandled input that causes an error
  • Runtime — Error that only appears when running

File: Name of the file where you found the problem.

Line(s): Approximate line number(s) in the capsule's code.

Description: What the problem is in one clear sentence.

Requirement Violated: The ID of the functional requirement that isn't met (e.g., FR-01.2, FR-06.2).

Impact: What consequence the problem has in production.


Review Prioritization Guide

Don't review the files randomly. Follow this priority sequence based on the Module 4 pyramid:

Priority 1: Security (review first)

FileWhat to look for
config.pyHardcoded secrets, insecure configuration
services/auth_service.pyPassword handling, token generation
routes/auth.pyRegistration and login flow
database.pySQL injection in queries

Priority 2: Business logic

FileWhat to look for
services/task_service.pyCRUD logic, filters, statistics
routes/tasks.pyValidations, permissions, correct responses
routes/users.pyStatistics, search

Priority 3: Robustness and edge cases

FileWhat to look for
models.pySufficient validations, correct constraints
routes/tasks.pyPagination, filters with unexpected values
services/task_service.pyDivision by zero, empty lists

Priority 4: Quality and hallucinations

FileWhat to look for
main.pyCorrect imports, coherent configuration
All the filesImports that don't exist, APIs with incorrect parameters

Tips for the Code Review

Tip 1: Read the code as if it were production code

Don't read thinking "this is an exercise." Read thinking "this goes to production tomorrow." The problems you let slip through, your users will find.

Tip 2: Follow the data flow

For each endpoint, follow the complete path:

Request → Route → Service → Database → Response

The problems usually hide in the transitions between layers. A validation in the model is useless if the route doesn't use it. A correct query is useless if the service misinterprets the results.

Tip 3: Ask "What if...?"

For each function, ask yourself these questions:

  • What if the input is empty?
  • What if the input is None?
  • What if the user has no data?
  • What if the ID doesn't exist?
  • What if an attacker manipulates the input?

Tip 4: Don't stop at the first problem

It's tempting to find a problem and start fixing it. Resist. Complete the full review first. The corrections come after.

Tip 5: Use Claude Code strategically

You can use Claude Code during the review to:

Correct use:
"Does the BaseSettings class exist in pydantic_settings? 
Is it in the pydantic-settings package or in pydantic?"

"Are the jwt.encode parameters in PyJWT 2.9 correct?"

"Does sqlite3.Row support dict() directly?"
Incorrect use:
"Find all the bugs in this code"
"Does this code have security problems?"

The difference: specific and verifiable questions vs open questions that delegate your work.

Tip 6: Document while you review

Don't trust your memory. Every time you find something suspicious, add it immediately to the findings document even if you're not 100% sure. It's easier to discard a false finding than to remember one you didn't note down.


Common Mistakes in This Phase

Mistake 1: Only looking for one type of bug

Developers with a security background find all the security holes but ignore the incorrect logic. Those who are strong in business logic find the poorly implemented filters but overlook the SQL injection. Force a review for each category.

Mistake 2: Assuming the imports are fine

"If the code was written, the imports must exist." Not necessarily. AI generates imports of packages that exist but functions that don't. Or of packages that are named almost the same. Verify each external import.

Mistake 3: Not reading the requirements carefully

Some problems are only visible if you know the requirement. "The password must be at least 8 characters" — if the code validates 4, it's a bug you can only find by reading both documents.

Mistake 4: Confusing "works" with "correct"

An endpoint returning a 200 doesn't mean it's correct. Does it return the correct data? For all users? With all inputs? "Works" is a very low bar. "Correct according to the requirements" is what you're after.

Mistake 5: Not documenting in enough detail

"There's a problem in task_service.py" isn't a useful finding. "In task_service.py line 45, the get_tasks function uses f-strings to insert the status parameter into the SQL query, which is vulnerable to SQL injection (FR-06.2)" is.


Code Review Checkpoint

Before moving to Phase 2 (Debugging), verify:

  • ✅ You read all the files of the complete codebase
  • ✅ You compared each functional requirement against the implementation
  • ✅ You applied the security checklist to the relevant files
  • ✅ You looked for hallucinations in all the imports and external APIs
  • ✅ You looked for unhandled edge cases in each function
  • ✅ You have a findings document with at least 10 problems documented
  • ✅ Each finding has: severity, category, file, line, description, impact
  • ✅ You haven't started fixing anything yet

If you have fewer than 10 findings, do another pass. The codebase has between 15 and 20 problems. If you found 7, you're missing at least 8 more. Review the categories where you have fewer findings — you're probably missing something in that direction.


Verification Map by File

To help you be systematic, use this map that indicates what to verify in each file. It's not exhaustive — it's a starting point:

config.py

  • Do the sensitive values have a hardcoded fallback?
  • Is the debug configuration appropriate for production?

database.py

  • Do all the queries use parameters?
  • Are the connections closed correctly?

models.py

  • Do the validations match the requirements?
  • Are there missing validations?
  • Are the types correct?

services/auth_service.py

  • Is the password hashed before storing?
  • Is the password verification correct?
  • Is the token generated and decoded correctly?
  • Are the imports correct and do they exist?

services/task_service.py

  • Do the queries use parameters?
  • Do the filters work correctly?
  • Does the pagination calculate the offset correctly?
  • Do the statistics exclude deleted tasks?
  • Is division by zero possible?
  • Is task ownership verified?

routes/auth.py

  • Is the password hashed before inserting into the DB?
  • Is the password verification in login correct?

routes/tasks.py

  • Do all the endpoints verify authentication?
  • Are the pagination validations correct?
  • Is task ownership verified in all the endpoints?
  • Does the delete implement soft delete?

routes/users.py

  • Do all the endpoints require authentication?
  • Is there SQL injection in any query?

main.py

  • Are the imports correct and do they exist?
  • Is the CORS configuration appropriate?

Next capsule: Debugging Phase — How to set up and run the codebase locally, and debug the runtime bugs found during the review.


Debugging & Code Review with Claude Code — Module 8, Capsule 02 Claude Code Agentic Development Path — Guide #6 of 11