Coding with AI Assistants: The Workflow That Ships Fast and the Review Discipline That Keeps the Code Safe
Coding with AI Assistants
Coding assistants generate code faster than any developer types it. This is a fact. They also generate code that looks correct, passes a visual review, compiles without errors, and contains a security vulnerability that would take a human reviewer ten seconds to identify if they were looking for it. This is also a fact. The speed and the risk come from the same source: the model produces plausible code, and plausible code is not the same as correct code.
This chapter establishes the workflow used throughout the rest of the book. Every subsequent chapter that uses a coding assistant to generate a non-trivial block of code references this workflow. The workflow has three parts: a prompt template that constrains the output, a review checklist that catches the common failure modes, and a concrete example of what happens when the review is skipped.
The Feature
A developer can use a coding assistant to generate FastAPI endpoints, React components, SQLAlchemy models, and Stripe webhook handlers with confidence that the generated code meets the project’s security, validation, and error handling requirements.
The Decision
Coding assistants are used throughout Marketflow’s development. The alternative, writing every line by hand, is slower and does not produce better code if the review discipline is in place. The risk of AI-generated code is not that it exists but that it is trusted without verification. The review checklist is the mechanism that captures the speed benefit while blocking the failure mode.
The Implementation
The Prompt Template
When asking a coding assistant to generate a FastAPI endpoint, the prompt includes constraints that prevent the most common failure modes:
Generate a FastAPI endpoint with these requirements:
ENDPOINT: [method] [path]
PURPOSE: [what it does]
AUTH: [required role or public]
INPUT: [request body or query parameters]
OUTPUT: [response schema]
CONSTRAINTS:
- Use async def with AsyncSession from SQLAlchemy 2.0
- Validate all input with Pydantic v2 models (no extra='allow')
- Use parameterized queries only, never string concatenation in SQL
- Include proper HTTP status codes for all error cases
- Add type hints to every function parameter and return value
- Use FastAPI's Depends for database session and auth
- Handle the case where the requested resource does not exist (404)
- Handle the case where the user does not have permission (403)
- Do not catch generic Exception unless re-raising
CONTEXT:
- Database models use SQLAlchemy 2.0 declarative base
- Auth is via Supabase JWT, validated in a dependency
- The current user is available as a dependency injection
This prompt produces substantially better output than “write an endpoint that lists vendor applications.” The constraints eliminate the three most common AI code generation failures: missing input validation, missing auth checks, and SQL injection via string formatting.
The Review Checklist
Every AI-generated code block passes through this checklist before entering the codebase. The checklist is mechanical. It does not require understanding the code’s business logic. It checks for structural issues that coding assistants consistently miss.
1. Input Validation
- Does every endpoint that accepts user input validate it with a Pydantic model?
- Are string fields constrained with
max_length? - Are numeric fields constrained with
ge,le,gt,ltwhere appropriate? - Is
model_config = ConfigDict(extra='forbid')or at minimum notextra='allow'?
2. Authentication and Authorization
- Does every non-public endpoint have an auth dependency?
- Does the endpoint verify that the current user has permission to access the requested resource?
- Is there a tenant isolation check (user can only access their own market’s data)?
3. SQL Safety
- Are all queries using SQLAlchemy’s query builder or parameterized text()?
- Is there any string concatenation or f-string formatting in SQL queries?
- Are any raw SQL strings constructed from user input?
4. Error Handling
- Does the endpoint handle the “not found” case?
- Does the endpoint handle the “not authorized” case?
- Are database errors handled (IntegrityError for duplicate entries)?
- Is there a generic exception handler that does not swallow errors silently?
5. Response Shape
- Does the endpoint return a Pydantic model, not a raw dict?
- Does the response exclude sensitive fields (passwords, internal IDs, service keys)?
- Is the HTTP status code correct for each response path (201 for creation, 204 for deletion)?
6. Concurrency
- Does the endpoint modify state that could be affected by concurrent requests?
- If yes, does it use database-level locking or optimistic concurrency?
- Does it commit the database session at the right point?
The Example: AI-Generated Code That Looks Correct
A developer asks a coding assistant to generate an endpoint that lets a market organizer update a vendor’s stall assignment. The AI produces:
# AI-generated code — looks correct at first glance
@router.put("/markets/{market_id}/stalls/{stall_id}/assign")
async def assign_vendor_to_stall(
market_id: uuid.UUID,
stall_id: uuid.UUID,
vendor_id: uuid.UUID,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
# Check stall exists
stall = await db.get(Stall, stall_id)
if not stall:
raise HTTPException(status_code=404, detail="Stall not found")
# Assign vendor
stall.assigned_vendor_id = vendor_id
await db.commit()
return {"status": "assigned", "stall_id": str(stall_id)}
This code compiles. It runs. It passes a superficial review. It has four problems:
# TRAP 1: No tenant isolation check
# Any authenticated user can assign vendors to ANY market's stalls
# Missing: verify current_user is the organizer of this market
# TRAP 2: No check that vendor_id belongs to an accepted application
# A random UUID could be assigned to a stall
# Missing: verify vendor has an accepted application for this market
# TRAP 3: No check that the stall belongs to the specified market
# stall_id is looked up without filtering by market_id
# Missing: where clause linking stall to market
# TRAP 4: No concurrency protection
# Two organizers assigning different vendors to the same stall
# simultaneously will both succeed, last write wins
# Missing: database-level check or optimistic locking
The corrected version:
# SAFE: All four issues addressed
@router.put("/markets/{market_id}/stalls/{stall_id}/assign")
async def assign_vendor_to_stall(
market_id: uuid.UUID,
stall_id: uuid.UUID,
body: AssignVendorRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> StallAssignmentResponse:
# Verify current user is the organizer of this market
market = await db.get(Market, market_id)
if not market:
raise HTTPException(status_code=404, detail="Market not found")
if market.organizer_id != current_user.id:
raise HTTPException(status_code=403, detail="Not your market")
# Verify stall belongs to this market
result = await db.execute(
select(Stall)
.where(Stall.id == stall_id, Stall.market_id == market_id)
.with_for_update() # Lock the row to prevent concurrent assignment
)
stall = result.scalar_one_or_none()
if not stall:
raise HTTPException(status_code=404, detail="Stall not found in this market")
# Verify vendor has an accepted application for this market
result = await db.execute(
select(Application)
.where(
Application.vendor_id == body.vendor_id,
Application.market_id == market_id,
Application.status == ApplicationStatus.ACCEPTED,
)
)
application = result.scalar_one_or_none()
if not application:
raise HTTPException(
status_code=400,
detail="Vendor does not have an accepted application for this market",
)
stall.assigned_vendor_id = body.vendor_id
await db.commit()
await db.refresh(stall)
return StallAssignmentResponse(
stall_id=stall.id,
vendor_id=stall.assigned_vendor_id,
stall_label=stall.label,
)
The AI-generated version had 8 lines of logic. The correct version has 30. The additional 22 lines are not over-engineering. They are the authorization check, the tenant isolation, the data integrity verification, and the concurrency protection that separate a working endpoint from a vulnerable one.
The Trap
The meta-trap of this chapter is trusting the checklist to catch everything. The checklist catches structural issues: missing auth, missing validation, SQL injection. It does not catch business logic errors. If the endpoint’s business logic is wrong (assigning a vendor to a stall they have not been approved for), the checklist does not find that. Business logic correctness comes from understanding the domain, which is the developer’s job, not the AI’s.
The Cost
| Tool | Monthly Cost |
|---|---|
| GitHub Copilot | $10/month (Individual) |
| Claude Pro | $20/month |
| Cursor | $20/month |
Pick one. The return on investment is measurable: faster boilerplate generation, faster test writing, faster documentation. The cost of not reviewing the output is also measurable: security vulnerabilities, data integrity bugs, and the debugging time to find them after they reach production.