Cristhian Villegas
Backend13 min read3 views

Cómo Crear un CRUD en Python con FastAPI Usando Claude Code

¿Por qué Claude Code + FastAPI?

Construir una API REST desde cero puede ser tedioso: configurar el proyecto, definir modelos, crear schemas, escribir endpoints, manejar errores, validar datos... Ahora imagina que tienes un asistente de IA en tu terminal que hace todo eso por ti mientras tú diriges.

Eso es Claude Code: el CLI de Anthropic que lee tu código, ejecuta comandos, crea archivos y modifica tu proyecto — todo desde la terminal, sin salir de tu flujo de trabajo.

En este artículo vamos a construir un CRUD completo de usuarios con FastAPI, SQLAlchemy y Pydantic, usando Claude Code como copiloto en cada paso.

Código en pantalla — entorno de desarrollo

Fuente: Florian Olivo — Unsplash

Lo que vamos a construir

  • POST /users — Crear usuario
  • GET /users — Listar todos los usuarios
  • GET /users/{id} — Obtener usuario por ID
  • PUT /users/{id} — Actualizar usuario
  • DELETE /users/{id} — Eliminar usuario

Stack

TecnologíaVersiónRol
Python3.12+Lenguaje
FastAPI0.115+Framework web
SQLAlchemy2.0+ORM
Pydantic2.xValidación de datos
SQLiteBase de datos (desarrollo)
Claude CodeÚltimaAsistente IA en terminal

Paso 1: Instalar Claude Code

Si aún no tienes Claude Code, la instalación es un solo comando:

bash
1# Instalar Claude Code globalmente
2npm install -g @anthropic-ai/claude-code
3
4# Verificar la instalación
5claude --version
6
7# Iniciar Claude Code en tu directorio de trabajo
8claude

La primera vez te pedirá autenticarte con tu cuenta de Anthropic. Una vez dentro, verás el prompt interactivo donde puedes hablar con Claude directamente.

📌 Requisito: Necesitas una cuenta de Anthropic con un plan activo (Max, Team o Enterprise). Claude Code usa los modelos Claude directamente.

Paso 2: Crear la Estructura del Proyecto

Abrimos la terminal en un directorio vacío y lanzamos Claude Code. Le pedimos que cree toda la estructura:

bash
1# En tu terminal
2mkdir users-api && cd users-api
3claude

Dentro de Claude Code, escribimos nuestro primer prompt:

bash
1> Crea la estructura de un proyecto FastAPI con SQLAlchemy para un CRUD de usuarios.
2> Usa esta estructura de carpetas:
3> - app/
4>   - models/     (modelos SQLAlchemy)
5>   - schemas/    (schemas Pydantic)
6>   - routes/     (endpoints)
7>   - database.py (configuración de DB)
8>   - main.py     (entry point)
9> - requirements.txt
10> Usa SQLite para desarrollo y SQLAlchemy 2.0 con mapped_column.

Claude Code leerá tu instrucción, creará todos los archivos y te mostrará exactamente qué hizo. El resultado será algo así:

python
1# app/database.py
2from sqlalchemy import create_engine
3from sqlalchemy.orm import DeclarativeBase, sessionmaker
4
5SQLALCHEMY_DATABASE_URL = "sqlite:///./users.db"
6
7engine = create_engine(
8    SQLALCHEMY_DATABASE_URL,
9    connect_args={"check_same_thread": False}  # Solo para SQLite
10)
11
12SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
13
14
15class Base(DeclarativeBase):
16    pass
17
18
19def get_db():
20    db = SessionLocal()
21    try:
22        yield db
23    finally:
24        db.close()

Paso 3: Definir el Modelo de Usuario

Ahora le pedimos a Claude Code que cree el modelo SQLAlchemy:

bash
1> Crea el modelo User en app/models/user.py con estos campos:
2> - id: entero autoincremental (PK)
3> - name: string, requerido, máximo 100 chars
4> - email: string, único, requerido
5> - is_active: boolean, default True
6> - created_at: datetime, auto-generado
7> Usa SQLAlchemy 2.0 con Mapped y mapped_column.

Claude generará:

python
1# app/models/user.py
2from datetime import datetime
3
4from sqlalchemy import String
5from sqlalchemy.orm import Mapped, mapped_column
6from sqlalchemy.sql import func
7
8from app.database import Base
9
10
11class User(Base):
12    __tablename__ = "users"
13
14    id: Mapped[int] = mapped_column(primary_key=True, index=True)
15    name: Mapped[str] = mapped_column(String(100))
16    email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
17    is_active: Mapped[bool] = mapped_column(default=True)
18    created_at: Mapped[datetime] = mapped_column(server_default=func.now())
💡 Tip: Fíjate que usamos Mapped[tipo] y mapped_column() — esta es la sintaxis moderna de SQLAlchemy 2.0 que te da tipado completo y autocompletado en tu IDE.

Paso 4: Crear los Schemas de Pydantic

Los schemas de Pydantic separan la validación de entrada/salida de la lógica de base de datos. Le pedimos a Claude:

bash
1> Crea los schemas Pydantic en app/schemas/user.py:
2> - UserCreate: para crear usuario (name, email)
3> - UserUpdate: para actualizar (name opcional, email opcional, is_active opcional)
4> - UserResponse: para respuestas (incluye id, created_at)
5> Usa Pydantic v2 con model_config ConfigDict.
python
1# app/schemas/user.py
2from datetime import datetime
3
4from pydantic import BaseModel, ConfigDict, EmailStr
5
6
7class UserCreate(BaseModel):
8    name: str
9    email: EmailStr
10
11
12class UserUpdate(BaseModel):
13    name: str | None = None
14    email: EmailStr | None = None
15    is_active: bool | None = None
16
17
18class UserResponse(BaseModel):
19    model_config = ConfigDict(from_attributes=True)
20
21    id: int
22    name: str
23    email: str
24    is_active: bool
25    created_at: datetime

Nota cómo UserUpdate tiene todos los campos opcionales — esto permite actualizaciones parciales (PATCH semántico). Y UserResponse usa from_attributes=True para convertir automáticamente el modelo SQLAlchemy a la respuesta JSON.

Paso 5: Implementar los Endpoints CRUD

Ahora viene la parte jugosa. Le pedimos a Claude que implemente todos los endpoints:

bash
1> Implementa el CRUD completo en app/routes/user.py:
2> - POST /users — crear usuario, validar email duplicado (409)
3> - GET /users — listar con paginación (skip, limit)
4> - GET /users/{id} — obtener por ID o 404
5> - PUT /users/{id} — actualizar parcialmente, validar email duplicado
6> - DELETE /users/{id} — eliminar o 404
7> Usa Depends para la sesión de DB. Maneja errores con HTTPException.
python
1# app/routes/user.py
2from fastapi import APIRouter, Depends, HTTPException, Query
3from sqlalchemy import select
4from sqlalchemy.exc import IntegrityError
5from sqlalchemy.orm import Session
6
7from app.database import get_db
8from app.models.user import User
9from app.schemas.user import UserCreate, UserResponse, UserUpdate
10
11router = APIRouter(prefix="/users", tags=["Users"])
12
13
14@router.post("/", response_model=UserResponse, status_code=201)
15def create_user(payload: UserCreate, db: Session = Depends(get_db)):
16    user = User(**payload.model_dump())
17    db.add(user)
18    try:
19        db.commit()
20        db.refresh(user)
21    except IntegrityError:
22        db.rollback()
23        raise HTTPException(status_code=409, detail="Email already registered")
24    return user
25
26
27@router.get("/", response_model=list[UserResponse])
28def list_users(
29    skip: int = Query(0, ge=0),
30    limit: int = Query(20, ge=1, le=100),
31    db: Session = Depends(get_db),
32):
33    stmt = select(User).offset(skip).limit(limit)
34    return db.scalars(stmt).all()
35
36
37@router.get("/{user_id}", response_model=UserResponse)
38def get_user(user_id: int, db: Session = Depends(get_db)):
39    user = db.get(User, user_id)
40    if not user:
41        raise HTTPException(status_code=404, detail="User not found")
42    return user
43
44
45@router.put("/{user_id}", response_model=UserResponse)
46def update_user(user_id: int, payload: UserUpdate, db: Session = Depends(get_db)):
47    user = db.get(User, user_id)
48    if not user:
49        raise HTTPException(status_code=404, detail="User not found")
50
51    update_data = payload.model_dump(exclude_unset=True)
52    for field, value in update_data.items():
53        setattr(user, field, value)
54
55    try:
56        db.commit()
57        db.refresh(user)
58    except IntegrityError:
59        db.rollback()
60        raise HTTPException(status_code=409, detail="Email already registered")
61    return user
62
63
64@router.delete("/{user_id}", status_code=204)
65def delete_user(user_id: int, db: Session = Depends(get_db)):
66    user = db.get(User, user_id)
67    if not user:
68        raise HTTPException(status_code=404, detail="User not found")
69    db.delete(user)
70    db.commit()
⚠️ Nota: Este ejemplo usa SQLite para simplificar. En producción, cambia a PostgreSQL y usa asyncpg con async_sessionmaker para aprovechar el modelo async de FastAPI.

Paso 6: Configurar el Entry Point

Le pedimos a Claude que conecte todo en main.py:

bash
1> Configura app/main.py:
2> - Crea la app FastAPI con título y versión
3> - Crea las tablas automáticamente al iniciar
4> - Registra el router de usuarios
5> - Agrega un endpoint de health check en /
python
1# app/main.py
2from contextlib import asynccontextmanager
3
4from fastapi import FastAPI
5
6from app.database import Base, engine
7from app.routes.user import router as user_router
8
9
10@asynccontextmanager
11async def lifespan(app: FastAPI):
12    # Crear tablas al iniciar
13    Base.metadata.create_all(bind=engine)
14    yield
15
16
17app = FastAPI(
18    title="Users API",
19    version="1.0.0",
20    lifespan=lifespan,
21)
22
23app.include_router(user_router)
24
25
26@app.get("/")
27def health_check():
28    return {"status": "ok"}

Y el requirements.txt:

text
1fastapi>=0.115.0
2uvicorn[standard]>=0.34.0
3sqlalchemy>=2.0.0
4pydantic[email]>=2.0.0

Paso 7: Ejecutar y Probar

Aquí es donde Claude Code brilla. Desde el mismo prompt, le pedimos que instale dependencias y lance el servidor:

bash
1# Claude Code puede ejecutar comandos de terminal con !
2> !python -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt
3
4# Iniciar el servidor
5> !uvicorn app.main:app --reload

El servidor estará corriendo en http://localhost:8000. FastAPI genera automáticamente la documentación interactiva en:

  • Swagger UI: http://localhost:8000/docs
  • ReDoc: http://localhost:8000/redoc

Probando con curl

bash
1# Crear un usuario
2curl -X POST http://localhost:8000/users/ \
3  -H "Content-Type: application/json" \
4  -d '{"name": "Ana García", "email": "[email protected]"}'
5
6# Listar usuarios
7curl http://localhost:8000/users/
8
9# Obtener usuario por ID
10curl http://localhost:8000/users/1
11
12# Actualizar usuario
13curl -X PUT http://localhost:8000/users/1 \
14  -H "Content-Type: application/json" \
15  -d '{"name": "Ana García López"}'
16
17# Eliminar usuario
18curl -X DELETE http://localhost:8000/users/1
💡 Tip: Puedes pedirle a Claude Code que pruebe los endpoints por ti: Prueba todos los endpoints del CRUD con curl y verifica que funcionan correctamente. Él ejecutará los comandos y te mostrará las respuestas.

Paso 8: Agregar Tests con Pytest

Un CRUD sin tests no está completo. Le pedimos a Claude Code que genere la suite de tests:

bash
1> Crea tests para todos los endpoints del CRUD de usuarios usando pytest
2> y TestClient de FastAPI. Usa una base de datos SQLite en memoria para
3> que los tests sean rápidos e independientes. Cubre: happy path, email
4> duplicado (409), usuario no encontrado (404), y paginación.
python
1# tests/test_users.py
2import pytest
3from fastapi.testclient import TestClient
4from sqlalchemy import create_engine
5from sqlalchemy.orm import sessionmaker
6
7from app.database import Base, get_db
8from app.main import app
9
10# DB en memoria para tests
11engine = create_engine("sqlite://", connect_args={"check_same_thread": False})
12TestSession = sessionmaker(autocommit=False, autoflush=False, bind=engine)
13
14
15def override_get_db():
16    db = TestSession()
17    try:
18        yield db
19    finally:
20        db.close()
21
22
23app.dependency_overrides[get_db] = override_get_db
24client = TestClient(app)
25
26
27@pytest.fixture(autouse=True)
28def setup_db():
29    Base.metadata.create_all(bind=engine)
30    yield
31    Base.metadata.drop_all(bind=engine)
32
33
34class TestCreateUser:
35    def test_create_user_success(self):
36        response = client.post(
37            "/users/", json={"name": "Ana García", "email": "[email protected]"}
38        )
39        assert response.status_code == 201
40        data = response.json()
41        assert data["name"] == "Ana García"
42        assert data["email"] == "[email protected]"
43        assert data["is_active"] is True
44        assert "id" in data
45
46    def test_create_user_duplicate_email(self):
47        client.post("/users/", json={"name": "Ana", "email": "[email protected]"})
48        response = client.post(
49            "/users/", json={"name": "Otra Ana", "email": "[email protected]"}
50        )
51        assert response.status_code == 409
52
53    def test_create_user_invalid_email(self):
54        response = client.post(
55            "/users/", json={"name": "Test", "email": "not-an-email"}
56        )
57        assert response.status_code == 422
58
59
60class TestGetUsers:
61    def test_list_users_empty(self):
62        response = client.get("/users/")
63        assert response.status_code == 200
64        assert response.json() == []
65
66    def test_list_users_with_pagination(self):
67        for i in range(5):
68            client.post(
69                "/users/", json={"name": f"User {i}", "email": f"user{i}@test.com"}
70            )
71        response = client.get("/users/?skip=2&limit=2")
72        assert response.status_code == 200
73        assert len(response.json()) == 2
74
75    def test_get_user_by_id(self):
76        create = client.post(
77            "/users/", json={"name": "Ana", "email": "[email protected]"}
78        )
79        user_id = create.json()["id"]
80        response = client.get(f"/users/{user_id}")
81        assert response.status_code == 200
82        assert response.json()["name"] == "Ana"
83
84    def test_get_user_not_found(self):
85        response = client.get("/users/999")
86        assert response.status_code == 404
87
88
89class TestUpdateUser:
90    def test_update_user_partial(self):
91        create = client.post(
92            "/users/", json={"name": "Ana", "email": "[email protected]"}
93        )
94        user_id = create.json()["id"]
95        response = client.put(f"/users/{user_id}", json={"name": "Ana Updated"})
96        assert response.status_code == 200
97        assert response.json()["name"] == "Ana Updated"
98        assert response.json()["email"] == "[email protected]"
99
100    def test_update_user_not_found(self):
101        response = client.put("/users/999", json={"name": "Ghost"})
102        assert response.status_code == 404
103
104
105class TestDeleteUser:
106    def test_delete_user_success(self):
107        create = client.post(
108            "/users/", json={"name": "Ana", "email": "[email protected]"}
109        )
110        user_id = create.json()["id"]
111        response = client.delete(f"/users/{user_id}")
112        assert response.status_code == 204
113        assert client.get(f"/users/{user_id}").status_code == 404
114
115    def test_delete_user_not_found(self):
116        response = client.delete("/users/999")
117        assert response.status_code == 404
bash
1# Ejecutar los tests
2> !pip install pytest httpx && pytest tests/ -v

Trucos de Claude Code para Python

Ahora que ya tienes el CRUD funcionando, estos son los comandos y trucos de Claude Code que más vas a usar en proyectos Python:

Comandos esenciales

bash
1# Inicializar CLAUDE.md con las convenciones del proyecto
2/init
3
4# Referenciar archivos directamente en tu prompt
5> @app/models/user.py agrega un campo "role" con enum ADMIN/USER
6
7# Deshacer el último cambio si no te gusta
8/undo
9
10# Rehacer
11/redo
12
13# Ejecutar comandos de terminal sin salir
14> !pytest tests/ -v --tb=short
15
16# Pedir que analice sin tocar nada (modo plan)
17# Presiona Tab para cambiar a modo Plan
18> Analiza la arquitectura del proyecto y sugiere mejoras

Prompts efectivos para Python

bash
1# Generar un módulo completo
2> Crea un sistema de autenticación JWT con login, registro y refresh token
3
4# Refactorizar a async
5> @app/routes/user.py refactoriza todos los endpoints a async
6> usando AsyncSession de SQLAlchemy
7
8# Agregar middleware
9> Agrega un middleware de logging que registre method, path,
10> status code y tiempo de respuesta de cada request
11
12# Generar Dockerfile
13> Crea un Dockerfile multi-stage optimizado para esta API FastAPI
14> con un usuario no-root y health check
15
16# Pedir explicación
17> @app/database.py explícame qué hace el lifespan y por qué se usa
18> en vez de @app.on_event("startup")
💡 Tip: Cuanto más contexto le des a Claude Code, mejores resultados obtienes. En vez de decir "agrega validación", dile "agrega validación al campo email para que rechace dominios temporales como mailinator.com y guerrillamail.com".

Estructura Final del Proyecto

Al terminar, tu proyecto debería verse así:

bash
1users-api/
2├── app/
3│   ├── __init__.py
4│   ├── main.py              # Entry point + lifespan
5│   ├── database.py           # Engine, session, Base
6│   ├── models/
7│   │   ├── __init__.py
8│   │   └── user.py           # Modelo SQLAlchemy
9│   ├── schemas/
10│   │   ├── __init__.py
11│   │   └── user.py           # Schemas Pydantic
12│   └── routes/
13│       ├── __init__.py
14│       └── user.py           # Endpoints CRUD
15├── tests/
16│   ├── __init__.py
17│   └── test_users.py         # Tests con pytest
18├── requirements.txt
19├── CLAUDE.md                 # Instrucciones para Claude Code
20└── users.db                  # SQLite (auto-generado)

Conclusión

En menos de 30 minutos, construimos una API REST completa con CRUD de usuarios, validación de datos, manejo de errores y tests — todo desde la terminal usando Claude Code como copiloto.

Lo que hace poderosa esta combinación no es que Claude Code escriba código por ti, sino que acelera las partes repetitivas mientras tú te enfocas en las decisiones de diseño. Tú decides la arquitectura, los patrones y las reglas de negocio; Claude Code implementa.

Próximos pasos que puedes pedirle a Claude Code:

  • Agregar autenticación JWT con python-jose y passlib
  • Migrar de SQLite a PostgreSQL con Alembic para migraciones
  • Agregar un Dockerfile multi-stage para producción
  • Implementar rate limiting y CORS
  • Crear un pipeline de CI/CD con GitHub Actions
Share:
CV

Cristhian Villegas

Software Engineer specializing in Java, Spring Boot, Angular & AWS. Building scalable distributed systems with clean architecture.

Comments

Sign in to leave a comment

No comments yet. Be the first!

Related Articles