11. Segundo proyecto práctico (Python + FastAPI)

Objetivo del tema

Consolidar el flujo de trabajo con OpenCode en otro ecosistema: construir una pequeña API de tareas con Python y FastAPI, con validación de datos mediante Pydantic y pruebas automatizadas usando Pytest.

11.1 Plan del proyecto

A diferencia del servicio estático del tema anterior, esta API tendrá estado interno y dos operaciones: listar tareas y crear tareas con validación. El recorrido:

1. Preparar entorno

Crear entorno virtual, instalar FastAPI, Uvicorn, Pytest y Httpx; inicializar Git.

2. Inicializar agente

/init para generar el AGENTS.md con comandos Python del proyecto.

3. Planificar (Plan)

Diseño de la estructura, modelos Pydantic y casos de prueba.

4. Construir (Build)

Implementación de rutas, modelos y suite de pruebas.

5. Validar y confirmar

Ejecutar Pytest, revisar el diff y crear el commit.

11.2 Preparación del entorno

mkdir api-tareas && cd api-tareas
git init
python -m venv .venv
source .venv/bin/activate        # Windows PowerShell: .venv\Scripts\Activate.ps1
pip install fastapi uvicorn
pip install --dev pytest httpx

Crea un requirements.txt para dejar fijadas las dependencias (pip freeze > requirements.txt) y un pytest.ini mínimo:

[pytest]
testpaths = tests

11.3 Inicialización con OpenCode

Abre el agente y ejecuta /init. Al detectar el entorno Python, el AGENTS.md resultante documentará cómo activar el entorno virtual y correr los tests. Agrega tus convenciones si hace falta:

  • "Usa type hints en todas las funciones".
  • "Los errores de validación deben devolver 422 (comportamiento por defecto de FastAPI)".
  • "Ejecuta siempre python -m pytest antes de considerar terminada una tarea".

11.4 Planificar en modo Plan

Cambia a Plan con Tab y plantea el diseño:

Diseña una API de tareas en memoria (sin base de datos): GET /tareas devuelve la lista completa; POST /tareas crea una tarea validada con Pydantic (id autogenerado, titulo obligatorio de 1 a 80 caracteres, completada booleana con valor por defecto False). Organiza el código en main.py y models.py. Escribe tests/test_tareas.py con TestClient cubriendo: lista vacía inicial, creación exitosa, error 422 con titulo vacío o demasiado largo, y que el id incrementa entre creaciones. Ejecuta python -m pytest al final.

El plan devuelto debería incluir la separación de responsabilidades (rutas vs modelos), el uso de un contador o lista para los ids y el orden de implementación. Itera hasta que te convenza: por ejemplo "agrega también DELETE /tareas/{id} con respuesta 404 si no existe".

11.5 Construir en modo Build

Vuelve a Build, aprueba y deja que implemente. El resultado esperado:

# models.py
from pydantic import BaseModel, Field

class Tarea(BaseModel):
    id: int
    titulo: str = Field(min_length=1, max_length=80)
    completada: bool = False

class TareaEntrada(BaseModel):
    titulo: str = Field(min_length=1, max_length=80)
    completada: bool = False
# main.py
from fastapi import FastAPI, HTTPException
from models import Tarea, TareaEntrada

app = FastAPI(title="API de tareas")

_tareas: list[Tarea] = []
_siguiente_id = 1

@app.get("/tareas", response_model=list[Tarea])
def listar_tareas():
    return _tareas

@app.post("/tareas", response_model=Tarea, status_code=201)
def crear_tarea(entrada: TareaEntrada):
    global _siguiente_id
    tarea = Tarea(id=_siguiente_id, **entrada.model_dump())
    _siguiente_id += 1
    _tareas.append(tarea)
    return tarea
# tests/test_tareas.py
from fastapi.testclient import TestClient
from main import app

cliente = TestClient(app)

def test_lista_vacia_inicial():
    respuesta = cliente.get("/tareas")
    assert respuesta.status_code == 200
    assert respuesta.json() == []

def test_crear_tarea_exitosa():
    respuesta = cliente.post("/tareas", json={"titulo": "Estudiar OpenCode"})
    assert respuesta.status_code == 201
    cuerpo = respuesta.json()
    assert cuerpo["id"] == 1
    assert cuerpo["completada"] is False

def test_titulo_invalido_devuelve_422():
    respuesta = cliente.post("/tareas", json={"titulo": ""})
    assert respuesta.status_code == 422

11.6 Validación del resultado

Checklist final de la iteración
Verificación Cómo Esperado
Suite de pruebas python -m pytest -v Todos los tests en verde.
Prueba manual uvicorn main:app --reload y abrir /docs La documentación interactiva de FastAPI permite probar POST y GET.
Validación negativa POST con título vacío desde /docs o curl Respuesta 422 con detalle del campo inválido.
Revisión del diff git diff Sólo main.py, models.py y los tests.

11.7 Cierre: commit y siguientes pasos

git add -A
git commit -m "feat: API de tareas con validacion Pydantic y tests Pytest"

Ejercicios para seguir practicando con el mismo proyecto:

  • Agregar PATCH /tareas/{id} para marcar una tarea como completada (con 404 si no existe).
  • Mover el almacenamiento en memoria a una clase repositoria inyectable y testearla aparte.
  • Pedirle al agente en modo Plan una estrategia de migración a SQLite con SQLModel.
  • Generar el workflow de CI con opencode run para correr Pytest en cada push.

¿Notaste la diferencia con el tema 10? Aquí el prompt incluyó reglas de validación explícitas (límites del título, códigos 422/404). Cuando tu dominio tiene reglas, escribirlas en el prompt inicial es más barato que corregirlas después.

Conclusión: con este segundo proyecto el flujo ya es automático: preparar, inicializar, planificar, construir y validar. Lo has aplicado sobre dos ecosistemas distintos, lo que confirma que el método funciona igual sin importar el lenguaje. En el próximo tema pasaremos a las integraciones con editores e IDEs.