JP Voogt A Data Enthusiasts Ramblings

Building Complex Web Apps with Claude Code, React, and FastAPI

Transform Claude from a code assistant into your autonomous development team.


Gone are the days of copy-pasting code snippets from AI chatbots. Claude Code represents a paradigm shift: an autonomous coding agent that reads your codebase, runs commands, manages git, and writes production-ready code—all while respecting the guardrails you define.

But here’s the thing: Claude Code out-of-the-box is powerful. Claude Code with proper setup? That’s a different beast entirely.

This guide walks you through building a production-grade full-stack application (React + FastAPI + MySQL) with a comprehensive Claude Code configuration. You’ll learn to implement:

  • MCP servers for real-time type checking and documentation lookup
  • Hooks that catch bugs before they’re committed
  • Skills that encode your team’s patterns and standards
  • Multi-agent architectures that optimize for both cost and quality

Let’s build something that actually works.


The Stack

Before we dive in, here’s what we’re working with:

Layer Technology
Backend Python 3.11+, FastAPI (async), SQLAlchemy 2.0
Frontend React 18, TypeScript (strict), Vite, Tailwind CSS
Database MySQL 8.0
Infrastructure Docker Compose, Nginx, Celery + Redis
Testing pytest (backend), Playwright (E2E)

1. Project Foundation

Docker Compose: Your Development Stack

Everything runs in containers. This gives Claude Code a predictable environment to work with.

graph TB
    subgraph "Development Stack"
        nginx[Nginx :80]
        frontend[React Frontend :5173]
        backend[FastAPI Backend :8000]
        mysql[(MySQL :3306)]
        redis[(Redis :6379)]
        celery[Celery Worker]
        beat[Celery Beat]
    end

    nginx --> frontend
    nginx --> backend
    backend --> mysql
    backend --> redis
    celery --> redis
    celery --> mysql
    beat --> celery

A minimal docker-compose.yml structure:

services:
  mysql:
    image: mysql:8.0
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  backend:
    build: ./backend
    volumes:
      - ./backend:/app
    depends_on:
      mysql:
        condition: service_healthy

  frontend:
    build: ./frontend
    volumes:
      - ./frontend:/app
      - /app/node_modules  # Exclude node_modules from mount
    command: npm run dev -- --host

Key insight: Health checks matter. Claude Code will attempt to run tests and queries—if services aren’t ready, you’ll get confusing failures.

CLAUDE.md: Your Project’s Constitution

The CLAUDE.md file is auto-loaded into every Claude Code session. Think of it as onboarding documentation for your AI teammate.

# Project Name - AI Assistant Guide

## Critical Rules
1. **Read before edit** - Always use Read tool first
2. **Models require:** `__table_args__ = {'extend_existing': True}`
3. **DB naming:** Tables=PascalCase, Columns=camelCase
4. **Money:** Use `Decimal`, never `float`
5. **Auth:** HttpOnly cookies (not localStorage)
6. **Quality:** Run `make quality` after every change

## Essential Commands
| Command | Purpose |
|---------|---------|
| `make up` | Start all services |
| `make quality` | Lint + typecheck + tests |
| `make test-backend` | Run pytest |
| `make test-e2e` | Run Playwright tests |

## MCP Tools (Use These)
| Tool | When to Use |
|------|-------------|
| `mcp__language-server-python__diagnostics` | Before/after Python edits |
| `mcp__language-server-typescript__diagnostics` | Before/after TypeScript edits |
| `mcp__context7__*` | Library documentation lookup |

Keep it concise. As Anthropic’s best practices note: “A single file that’s hundreds of lines long should trigger a review; Claude can always look at additional details on demand.”


2. MCP Server Integration

MCP (Model Context Protocol) servers extend Claude’s capabilities with real-time tools. Here’s the configuration that makes everything work.

The .mcp.json Configuration

{
  "mcpServers": {
    "context7": {
      "type": "http",
      "url": "https://mcp.context7.com/mcp"
    },
    "language-server-typescript": {
      "type": "stdio",
      "command": "/path/to/mcp-language-server",
      "args": [
        "--workspace", "/path/to/project/",
        "--lsp", "/usr/local/bin/typescript-language-server",
        "--", "--stdio"
      ]
    },
    "language-server-python": {
      "type": "stdio",
      "command": "/path/to/mcp-language-server",
      "args": [
        "--workspace", "/path/to/project/",
        "--lsp", "/usr/local/bin/pyright-langserver",
        "--", "--stdio"
      ]
    },
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest", "--browser", "chromium"]
    },
    "mysql-db": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@f4ww4z/mcp-mysql-server",
               "mysql://user:pass@localhost:3306/database"]
    }
  }
}

Language Servers: Your Type Safety Net

These are game-changers. Instead of Claude guessing at types and hoping for the best, it can:

# Before ANY code change:
mcp__language-server-python__diagnostics -> Check existing errors

# After EVERY code change:
mcp__language-server-python__diagnostics -> Catch new errors immediately

This catches issues during the coding session, not after a failed build.

Context7: No More Hallucinated APIs

Claude’s training data has a cutoff. Context7 solves this by fetching live documentation:

# Step 1: Find the library
mcp__context7__resolve-library-id("fastapi")

# Step 2: Get current docs
mcp__context7__get-library-docs(library_id, topic="dependency injection")

This is crucial for frameworks that evolve quickly. No more from fastapi import Depends when the API changed six months ago.

Database MCP: Schema at Your Fingertips

Instead of Claude reading model files and inferring relationships:

# Direct schema inspection
mcp__mysql-db__describe_table("User")
mcp__mysql-db__list_tables()

# Verify data during debugging
mcp__mysql-db__query("SELECT COUNT(*) FROM Reservation WHERE status = 'pending'")

This is faster and more accurate than parsing Python models.


3. Hooks: Your Automated Guardrails

Hooks intercept Claude’s actions and enforce quality gates. They’re the difference between “it works on my machine” and “it works.”

graph LR
    A[Claude Edits File] --> B{Post-Edit Hook}
    B -->|Lint Check| C{Pass?}
    C -->|Yes| D[Continue]
    C -->|No| E[Feedback to Claude]
    E --> F[Claude Fixes]
    F --> A

    G[Claude Commits] --> H{Pre-Commit Hook}
    H -->|.env check| I{Pass?}
    I -->|Yes| J[Commit Allowed]
    I -->|No| K[Block + Feedback]

Post-Edit Hook: Instant Quality Feedback

This hook runs after every file edit, providing immediate feedback:

#!/bin/bash
# .claude/scripts/post-edit-hook.sh

HOOK_DATA=$(cat)
FILE_PATH=$(echo "$HOOK_DATA" | jq -r '.tool_input.file_path')

ERRORS=()
WARNINGS=()

# Python files: Run ruff + black
if [[ "$FILE_PATH" =~ \.py$ ]]; then
  if ! docker exec backend poetry run ruff check "$REL_PATH" --quiet; then
    ERRORS+=("Ruff lint errors detected")
  fi

  if ! docker exec backend poetry run black "$REL_PATH" --check --quiet; then
    WARNINGS+=("Formatting needed - run: poetry run black $REL_PATH")
  fi
fi

# TypeScript files: Run tsc + eslint
if [[ "$FILE_PATH" =~ \.(ts|tsx)$ ]]; then
  TSC_OUTPUT=$(docker exec frontend npx tsc --noEmit 2>&1 || true)
  if echo "$TSC_OUTPUT" | grep -q "error TS"; then
    ERRORS+=("TypeScript errors detected")
  fi
fi

# Return structured JSON feedback
cat <<EOF
{
  "decision": "approve",
  "feedback": {
    "errors": $(printf '%s\n' "${ERRORS[@]}" | jq -R -s 'split("\n")'),
    "warnings": $(printf '%s\n' "${WARNINGS[@]}" | jq -R -s 'split("\n")')
  }
}
EOF

Key pattern: Always return "decision": "approve" with feedback. Blocking edits is frustrating; providing feedback lets Claude self-correct.

Pre-Commit Hook: The Final Gatekeeper

This hook runs before git commit commands:

#!/bin/bash
# .claude/scripts/pre-commit-hook.sh

HOOK_DATA=$(cat)
COMMAND=$(echo "$HOOK_DATA" | jq -r '.tool_input.command')

# Only check actual commits
if ! echo "$COMMAND" | grep -q "git commit"; then
  echo '{"decision": "approve"}'
  exit 0
fi

# BLOCK: .env files
STAGED_ENV=$(git diff --cached --name-only | grep -E '^\.env')
if [[ -n "$STAGED_ENV" ]]; then
  cat <<EOF
{
  "decision": "block",
  "reason": "Cannot commit .env files",
  "feedback": {"suggestion": "Remove sensitive files from staging"}
}
EOF
  exit 0
fi

# WARN: Non-conventional commit messages
COMMIT_MSG=$(echo "$COMMAND" | grep -oP '(?<=-m ")[^"]+')
if ! echo "$COMMIT_MSG" | grep -qE '^(feat|fix|docs|refactor):'; then
  echo '{"decision": "approve", "feedback": {"warnings": ["Non-conventional commit message"]}}'
  exit 0
fi

echo '{"decision": "approve"}'

Registering Hooks

In .claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{
          "type": "command",
          "command": ".claude/scripts/post-edit-hook.sh",
          "timeout": 60000
        }]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{
          "type": "command",
          "command": ".claude/scripts/pre-commit-hook.sh",
          "timeout": 120000
        }]
      }
    ]
  }
}

4. Skills: Domain Knowledge Encapsulation

Skills transform Claude from a general-purpose assistant into a specialized developer who knows your codebase.

Skill Anatomy

skill-name/
├── SKILL.md           # Required: Instructions + metadata
├── references/        # Optional: Detailed documentation
│   └── patterns.md
└── scripts/           # Optional: Executable helpers
    └── generate.py

The SKILL.md file has two parts:

---
name: my-backend
description: |
  FastAPI/SQLAlchemy patterns for this project. Use when working on
  backend/ files: endpoints, services, models, schemas. Loads anti-pattern
  rules automatically.
---

# Backend Development

## Model Pattern
```python
class User(Base):
    __tablename__ = "User"
    __table_args__ = {'extend_existing': True}  # REQUIRED

    id: Mapped[str] = mapped_column(String(36), primary_key=True)

Anti-Pattern Checklist

  • No HTTPException in services - use DomainError
  • Money uses Decimal, never float
  • All DB operations are async ```

Critical: The description field in frontmatter is the trigger. Claude reads this to decide when to load the skill. Be explicit about when it should activate.

Progressive Disclosure

Skills use a three-level loading system to manage context:

graph TD
    A[Level 1: Metadata] -->|Always loaded| B[~100 words]
    C[Level 2: SKILL.md body] -->|When triggered| D[~5000 words max]
    E[Level 3: References] -->|As needed| F[Unlimited]

    B --> G[Claude decides to use skill]
    G --> D
    D --> H[Claude needs details]
    H --> F

This prevents context window bloat. Keep SKILL.md under 500 lines; move detailed patterns to references/.

Creating Skills with skill-creator

Use the /skill-creator skill to generate new skills:

/skill-creator Create a testing skill that covers pytest patterns,
Playwright E2E setup, and quality gates for our FastAPI + React project

The skill-creator will:

  1. Ask clarifying questions about your testing patterns
  2. Generate the skill structure
  3. Create SKILL.md with proper frontmatter
  4. Suggest reference files for detailed documentation

Essential Skills for Full-Stack Apps

Skill Purpose Triggers On
my-backend FastAPI patterns, models, services backend/*.py files
my-frontend React patterns, hooks, i18n frontend/src/*.tsx files
my-testing pytest, Playwright, quality gates Test-related requests
my-deploy SSH, migrations, rollback Deployment requests
my-anti-pattern Code quality rules Always loaded via other skills

5. Multi-Agent Architecture

Here’s where it gets powerful. Instead of one Claude doing everything, you orchestrate multiple agents with different strengths.

Model Strategy

Model Use For Why
Opus 4.5 Orchestration, review, complex reasoning Highest quality, best at catching subtle issues
Sonnet 4.5 Bulk coding, routine implementations Cost-effective, still excellent for defined tasks
graph TB
    User[User Request] --> Opus[Opus 4.5 Orchestrator]

    Opus --> |"Task(model=sonnet)"| S1[Sonnet: Backend]
    Opus --> |"Task(model=sonnet)"| S2[Sonnet: Frontend]
    Opus --> |"Task(model=sonnet)"| S3[Sonnet: Testing]

    S1 --> |Report| Opus
    S2 --> |Report| Opus
    S3 --> |Report| Opus

    Opus --> Review[Opus Reviews & Integrates]
    Review --> Response[Final Response]

The Invoke Pattern

Create skills that dispatch subagents with proper context:

---
name: invoke-backend
description: |
  INVOKE COMMAND: Use `/invoke-backend <task>` to spawn a Sonnet 4.5
  subagent for backend tasks. Automatically loads backend + anti-pattern
  skills. Cost-effective for routine tasks.
---

# Invoke Backend Subagent

## Dispatch Template

When invoked, dispatch:

Task( subagent_type=”general-purpose”, model=”sonnet”, prompt=””” You are a backend developer. Complete this task following all patterns.

Task

{user_task}

Required Skills (LOAD FIRST)

  1. Read .claude/skills/my-backend/SKILL.md
  2. Read .claude/skills/my-anti-pattern/SKILL.md

Mandatory MCP Tool Usage

  • Before edits: mcp__language-server-python__diagnostics
  • After edits: mcp__language-server-python__diagnostics
  • Fix ALL type errors immediately

Anti-Pattern Checklist

  • No HTTPException in services
  • Models have extend_existing = True
  • Money uses Decimal

Return Format

| File | Changes | |——|———| | path | description |

Quality: make quality PASS/FAIL “”” )


### Background Agents for Parallel Work

When tasks are independent, run them in parallel:

```python
# Main agent dispatches multiple subagents
Task(
  subagent_type="general-purpose",
  model="sonnet",
  run_in_background=True,  # Don't wait
  prompt="Implement the user service..."
)

Task(
  subagent_type="general-purpose",
  model="sonnet",
  run_in_background=True,
  prompt="Implement the user API endpoints..."
)

# Later, collect results
TaskOutput(task_id="agent-1", block=True)
TaskOutput(task_id="agent-2", block=True)

When to use:

  • Independent features that don’t share files
  • Testing while implementation continues
  • Code review alongside bug fixes

6. Quality Automation Pipeline

Tie everything together with a Makefile:

.PHONY: quality lint typecheck test-backend test-e2e

quality: lint typecheck test-backend
	@echo "All quality checks passed"

lint:
	docker exec backend poetry run ruff check app/ --fix
	docker exec backend poetry run black app/
	docker exec frontend npm run lint

typecheck:
	docker exec backend poetry run mypy app/
	docker exec frontend npx tsc --noEmit

test-backend:
	docker exec backend poetry run pytest -v

test-e2e:
	npx playwright test

Playwright Configuration for Reliable E2E Tests

// playwright.config.ts
export default defineConfig({
  testDir: './e2e/tests',
  fullyParallel: false,  // Sequential for state-dependent tests
  workers: 1,
  retries: process.env.CI ? 2 : 1,

  use: {
    baseURL: 'http://localhost',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    trace: 'on-first-retry',
  },

  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    {
      name: 'booking-flow',
      use: { ...devices['Desktop Firefox'] },
      retries: 0,  // No retries for deterministic flows
    },
  ],
});

7. Common Pitfalls & Anti-Patterns

Context Window Abuse

Problem: Loading everything into skills, never clearing context.

Solution:

  • Use /clear between unrelated tasks
  • Keep SKILL.md under 500 lines
  • Use progressive disclosure—references load on demand

As Anthropic notes: “Every time you start something new, clear the chat. You don’t need all that history eating your tokens.”

Agent Misuse

Problem: Using Opus for routine tasks; not enforcing quality in subagents.

Solution:

  • Opus orchestrates, Sonnet implements
  • Every subagent prompt includes MCP tool requirements
  • Subagents must report make quality results

Guardrail Gaps

Problem: Hooks that only warn, never block; missing secret detection.

Solution:

  • Block .env files unconditionally
  • Block commits with failing type checks
  • Log everything to .claude/logs/ for debugging

Skill Design Mistakes

Problem: README files in skills; duplicated information; deep nesting.

Solution:

  • Skills are for Claude, not humans—no README needed
  • Information lives in SKILL.md OR references, not both
  • Keep references one level deep from SKILL.md

8. Advanced Patterns We Haven’t Implemented (Yet)

Git Worktrees for Parallel Development

Run multiple Claude instances on isolated branches:

git worktree add ../project-feature-a feature-a
git worktree add ../project-feature-b feature-b

# Terminal 1: Claude works on feature-a
# Terminal 2: Claude works on feature-b

This enables true parallel development without merge conflicts.

Headless Mode for CI/CD

Integrate Claude Code into your pipeline:

claude -p "Run all tests and fix any failures" \
  --output-format stream-json \
  --dangerously-skip-permissions

Warning: Only use --dangerously-skip-permissions in isolated containers.

Visual Iteration with Screenshots

Paste UI screenshots directly:

  1. Take screenshot (Cmd+Ctrl+Shift+4 on macOS)
  2. Paste into Claude Code (Ctrl+V)
  3. “Make this match the design”
  4. Screenshot result, iterate

Results improve significantly after 2-3 cycles.

Memory Snapshots for Session Continuity

Save context before complex sessions:

mkdir -p .claude/memory/snapshots
# Claude can write session state here
# Load on next session to continue work

Extended Thinking for Complex Decisions

Use specific phrases to trigger deeper reasoning:

Phrase Thinking Budget
“think” Standard
“think hard” Extended
“think harder” Maximum
“ultrathink” Unlimited

Example: “Think hard about the best way to implement this payment flow considering our existing architecture.”


Conclusion

You now have a blueprint for turning Claude Code into an autonomous development team:

  1. Foundation: Docker Compose + CLAUDE.md give Claude a predictable environment
  2. MCP Servers: Real-time type checking and documentation eliminate hallucinations
  3. Hooks: Automated guardrails catch issues before they’re committed
  4. Skills: Domain knowledge makes Claude an expert in your codebase
  5. Multi-Agent: Opus orchestrates, Sonnet implements—optimized for cost and quality

The key insight? Claude Code isn’t just an assistant—it’s infrastructure. Treat it like you’d treat any other part of your development stack: configure it properly, enforce standards automatically, and iterate on what works.

Start with skills (they’re the highest ROI), add hooks (they prevent regressions), then scale with agents (for parallel work). Your future self will thank you.


Resources


Happy building.

Disclaimer: Content is accurate at the time of publication, however updates and new additions happen frequently which could change the accuracy or relevance. Please keep this in mind when using my content as guidelines. Please always test in a testing or development environment, I do not accept any liability for damages caused by this content.

If you liked this post, you can share it with your followers or follow me on Twitter!