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.
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ía | Versión | Rol |
|---|---|---|
| Python | 3.12+ | Lenguaje |
| FastAPI | 0.115+ | Framework web |
| SQLAlchemy | 2.0+ | ORM |
| Pydantic | 2.x | Validación de datos |
| SQLite | — | Base de datos (desarrollo) |
| Claude Code | Última | Asistente IA en terminal |
Paso 1: Instalar Claude Code
Si aún no tienes Claude Code, la instalación es un solo comando:
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.
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:
1# En tu terminal
2mkdir users-api && cd users-api
3claude
Dentro de Claude Code, escribimos nuestro primer prompt:
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í:
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:
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á:
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())
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:
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.
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:
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.
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()
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:
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 /
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:
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:
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
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
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:
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.
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
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
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
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")
Estructura Final del Proyecto
Al terminar, tu proyecto debería verse así:
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-joseypasslib - 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
Comments
Sign in to leave a comment
No comments yet. Be the first!