Puedes crear una API con IA en una tarde: pide a un agente de programación como Claude Code o Codex “una API REST de tareas con usuarios” y tendrás rutas, una base de datos y un servidor funcionando. Otra cosa es que esa API sobreviva a sus primeros usuarios reales. Entradas sin validar, endpoints que reparten datos de otras personas y migraciones que nadie leyó son los problemas habituales. En este artículo verás cómo construyo un backend REST con agentes, desde la elección del stack hasta el despliegue, y dónde conviene ir más despacio.
Forma parte de la serie que empezó con crear un SaaS con IA. Aquel artículo trata del producto; este, del backend que hay debajo, al que llamarán una app móvil, un frontend u otros servicios. Si es tu primera vez trabajando así, cómo hacer vibe coding es un buen calentamiento.
1. Define la API antes de escribir código
Una API es un contrato. En cuanto una app móvil se publica contra ella, cambiar el nombre de un campo sale caro. Así que antes de que el agente escriba código, apunta:
- Recursos: los sustantivos. Por ejemplo
users,projects,tasks,comments. - Endpoints:
GET /v1/projects,POST /v1/projects,GET /v1/projects/:id/tasks,PATCH /v1/tasks/:id, etc. - Quién puede hacer qué: “Los miembros de un proyecto pueden leer sus tareas; solo el propietario puede borrar el proyecto”.
- Formato de error: una sola forma para todos los errores, con un código HTTP, un código legible por máquina y un mensaje.
- Paginación: por cursor o por página, con tamaño por defecto y máximo.
Limita la primera versión a los endpoints que necesita un cliente real. La API de administración, los webhooks y la importación masiva pueden esperar.
2. Pon las reglas en un archivo de especificación
Los agentes no recuerdan conversaciones anteriores. Claude Code lee CLAUDE.md al empezar cada sesión; Codex y muchos otros agentes leen AGENTS.md. Escribe ahí tus decisiones:
# Tasks API
## Stack
- Node.js LTS, TypeScript, Fastify
- PostgreSQL, Prisma for schema and migrations
- Zod for request validation; OpenAPI generated from the same schemas
- Vitest; tests use app.inject() against a test database
## Rules
- All routes under /v1. JSON only.
- Every request body, query and param is validated with a schema.
- Errors: { "error": { "code": "...", "message": "..." } }, never a stack trace.
- Every query on tasks and projects checks project membership.
- Never return password hashes or tokens; use explicit response schemas.
- Database changes only through migration files.
- List endpoints: cursor pagination, default 20, max 100.
- Secrets come from environment variables; never in code or logs.
- Ask before adding a dependency.
- After every task: pnpm lint && pnpm test
## Out of scope (for now)
Admin API, file uploads, webhooks, GraphQL.
Cada regla está ahí porque, sin ella, los agentes se equivocan. “Explicit response schemas” evita que el agente devuelva la fila entera del usuario, hash de la contraseña incluido. “Max 100” evita que alguien pida un millón de filas. Más sobre cómo escribir este archivo en CLAUDE.md y AGENTS.md.
3. Elige el stack: apuesta por lo aburrido
El agente escribe mejor código en un stack que ha visto miles de veces. Dos opciones seguras:
| TypeScript | Python | |
|---|---|---|
| Framework | Fastify (o Express, Hono) | FastAPI |
| Validación | Zod o JSON Schema | Pydantic |
| Acceso a datos | Prisma o Drizzle | SQLAlchemy |
| Migraciones | Prisma Migrate, Drizzle Kit | Alembic |
| OpenAPI | @fastify/swagger | Integrado |
| Tests | Vitest + app.inject() | pytest + TestClient |
Las dos funcionan. Elige el lenguaje que sepas leer, porque vas a leer muchos diffs. En los ejemplos uso TypeScript, Fastify, Prisma y PostgreSQL.
Ejecuta Postgres en local con Docker para que el agente nunca necesite una base de datos compartida:
docker run --name tasks-db -e POSTGRES_PASSWORD=dev -p 5432:5432 -d postgres:16
mkdir tasks-api && cd tasks-api && git init
4. Primero el plan, luego un recurso cada vez
Mi primer prompt pide un plan, no código:
Read CLAUDE.md. Don't write code yet.
Propose:
1. The Prisma schema (models, relations, indexes, unique constraints)
2. The full endpoint list with request and response shapes
3. How authorization works for each endpoint
4. 6–8 implementation steps, each ending with tests that pass
List open questions at the end.
Corrige el plan, responde a las preguntas y actualiza la especificación. Después, paso a paso:
Implement step 2: the projects resource (list, create, get, update,
delete). Validate input with Zod, use explicit response schemas,
check membership on every query. Write tests for the happy path,
invalid input, and a second user who is not a member. Run the tests,
show me the result, then stop.
El “then stop” está ahí para que pueda leer el diff y hacer commit antes del siguiente paso. Cuando el agente se desvía, vuelvo un commit atrás en vez de desenredar cinco recursos.
5. Esquema y migraciones
El esquema es lo más difícil de cambiar después, así que léelo con calma:
- Restricciones en la base de datos, no solo en el código.
NOT NULL, emails únicos, claves foráneas con un comportamientoON DELETEelegido a propósito. - Índices para las consultas que haces. Cada clave foránea por la que filtras y cada columna por la que ordenas en un listado.
- Timestamps (
createdAt,updatedAt) en todas las tablas; ayudan a depurar y a paginar por cursor. - El dinero en enteros o decimales, nunca en coma flotante.
Cada cambio pasa por una migración:
npx prisma migrate dev --name add_tasks # crear y aplicar en local
npx prisma migrate reset # reconstruir la base de desarrollo desde cero
npx prisma migrate deploy # aplicar migraciones pendientes en producción
En cuanto haya datos reales, lee cada archivo de migración que escriba el agente. Para y pregunta ante DROP COLUMN, DROP TABLE, renombrados (Prisma puede convertirlos en borrar y añadir), UPDATE o DELETE sin WHERE, y nuevas columnas NOT NULL sin valor por defecto en tablas grandes.
6. Autenticación: usa algo que ya exista
No dejes que el agente invente el almacenamiento de contraseñas ni formatos de token. Usa un proveedor de autenticación alojado o una librería conocida, y limita el trabajo del agente a integrarlo:
- Contraseñas con hash argon2 o bcrypt mediante una librería mantenida, nunca guardadas ni registradas en texto plano.
- Sesiones o tokens: para un cliente web en el mismo sitio, una cookie de sesión
HttpOnly,SecureySameSitees la opción segura más sencilla. Para clientes móviles o de terceros, tokens de acceso de vida corta con tokens de refresco. - Verifica bien los tokens. Firma, caducidad, emisor y audiencia; rechaza el algoritmo
none. Pide al agente que te enseñe exactamente dónde ocurre esto. - Limita la frecuencia del login y del restablecimiento de contraseña para que nadie pueda probar miles de contraseñas.
7. Autorización: el fallo que más escriben los agentes
La autenticación responde a “¿quién es?”. La autorización responde a “¿puede tocar este registro?”. Las APIs escritas por agentes suelen acertar con lo primero y fallar con lo segundo. El caso clásico es un GET /v1/tasks/:id que comprueba que el token es válido y luego carga la tarea por ID. Cualquier usuario con sesión puede leer cualquier tarea cambiando el número. OWASP pone esto, la autorización rota a nivel de objeto (BOLA), en el primer puesto de su API Security Top 10.
La solución es un hábito, no una librería:
- Cada consulta que carga un registro por ID filtra también por lo que puede ver quien llama:
where: { id, project: { members: { some: { userId } } } }. - “No existe” y “no es tuyo” devuelven el mismo
404, para que no se puedan sondear IDs. - Pruébalo con dos usuarios en cada endpoint que reciba un ID. Son los tests más valiosos del proyecto.
Un error parecido es la asignación masiva (mass assignment): el agente pasa el cuerpo de la petición directamente a update() y un usuario envía "role": "admin" u "ownerId": "...". Los esquemas de validación que solo listan los campos permitidos lo evitan.
Pídele al agente: “List every endpoint and the authorization check it performs. Mark any endpoint that loads a record by ID without filtering by the caller.”
8. Validación y errores
Toda entrada que cruza la frontera se valida: cuerpo, query string, parámetros de ruta y las cabeceras que uses. Con Zod y Fastify, la ruta declara sus esquemas y las peticiones inválidas se rechazan con un 400 antes de que se ejecute tu código.
- Rechaza los campos desconocidos en vez de ignorarlos en silencio; así los fallos del cliente salen antes.
- Limita tamaños: longitud de cadenas, de arrays, del cuerpo de la petición y de la página.
- Una sola forma de error para todo, y un manejador global que registre los detalles en el servidor pero devuelva al cliente solo
codeymessage. Las trazas y los errores SQL en las respuestas le dan un mapa al atacante. - Solo consultas parametrizadas. El ORM lo hace por defecto; vigila los helpers de SQL en crudo con concatenación de cadenas, como
$queryRawUnsafede Prisma.
9. Los tests que importan
app.inject() de Fastify envía peticiones a la app sin abrir un puerto, así que los tests de API son rápidos. No necesitas un 100 % de cobertura; necesitas estos:
- El camino feliz de cada endpoint, comprobando código de estado y forma de la respuesta.
- Una entrada inválida devuelve
400con tu formato de error. - Sin token devuelve
401; el registro de otro devuelve404. - La paginación respeta el máximo.
- Las migraciones se aplican desde cero sobre una base de datos limpia.
Los tests se ejecutan contra una base de datos de test separada que se reinicia entre ejecuciones. Nunca dejes que la suite de tests se acerque a credenciales de producción.
Vigila que el agente no “arregle” un test que falla cambiando la aserción. Si en un diff aparece un archivo de test que no pediste, pregunta por qué. En revisión de código con IA tienes una checklist para esto.
10. Documentación OpenAPI desde los mismos esquemas
La documentación escrita a mano se queda desfasada. Genérala desde los esquemas de validación para que describa exactamente lo que acepta el código. En Fastify, @fastify/swagger construye un documento OpenAPI a partir de los esquemas de las rutas y @fastify/swagger-ui lo sirve; FastAPI lo hace de serie.
Add @fastify/swagger and @fastify/swagger-ui. Generate the OpenAPI
document from the existing route schemas, add descriptions and
examples, document the error format and auth. Serve the UI at /docs
only when NODE_ENV is not production.
El archivo OpenAPI sirve para más que documentar: los agentes de frontend pueden generar un cliente tipado a partir de él, y otras herramientas pueden descubrir tus endpoints con él.
11. Despliegue
Una API pequeña funciona bien como contenedor en una plataforma como Fly.io, Railway o Render, o en cualquier servidor con Docker. Una checklist:
- Un Dockerfile con build multi-stage y un usuario que no sea root. Pídeselo al agente y luego léelo.
- Migraciones como paso aparte (
prisma migrate deploy) antes de que arranque la nueva versión, no dentro de cada instancia al iniciarse. - Variables de entorno para la URL de la base de datos, los secretos y los orígenes CORS permitidos, configuradas en la plataforma, no en la imagen.
- Un endpoint de salud (
/health) que la plataforma pueda comprobar. - CORS limitado a tus frontends reales; nunca
*con credenciales. - Solo HTTPS, cabeceras de seguridad (por ejemplo con
@fastify/helmet) y logs de peticiones sin contraseñas, tokens ni datos personales. - Copias de seguridad activadas en la base de datos de producción, y al menos una restauración probada.
- Seguimiento de errores como Sentry, para enterarte de los
500antes de que te lo digan tus usuarios.
Lo que la IA hace mal
- Devolver filas enteras de la base de datos. Hashes de contraseñas, flags internos y emails de otros usuarios en el JSON. Los esquemas de respuesta explícitos lo arreglan.
- Inventar APIs. Funciones de librería que no existen o que cambiaron hace varias versiones. Dale al agente documentación actual y pégale los errores tal cual.
- Sin paginación. Un
findMany()sin límite funciona hasta que la tabla tiene 200.000 filas. - Consultas N+1. Una consulta por elemento dentro de un bucle. Pide al agente que registre las consultas en desarrollo y busca repeticiones.
- Errores tragados. Un
catch {}que devuelve200de todos modos. - Secretos en el código. Un secreto JWT escrito a mano “solo por ahora” que acaba en producción.
- Alcance que crece solo. “También he añadido un endpoint de administración”. Revértelo o muévelo a su propia rama.
Seguridad: breve pero seria
- Mantén
.enven.gitignore; rota cualquier clave que se haya subido alguna vez. - No pegues claves de API en el chat; el agente solo necesita el nombre de la variable.
- Apunta el agente a una base de datos local o de staging, nunca a producción.
- Lee los comandos que ejecuta el agente, sobre todo los de base de datos,
rm, los force push ycurl … | sh. - Ejecuta
pnpm audit(o el equivalente de tu gestor de paquetes) de vez en cuando y pásale la salida al agente. - Limita la frecuencia de los endpoints públicos, no solo del login.
Para saber más sobre cómo mantener seguros a los agentes en tu máquina, lee seguridad de los agentes de programación con IA.
Cuándo incorporar a un desarrollador humano
Unas horas de revisión de un desarrollador backend con experiencia se pagan solas:
- Antes de que se registren usuarios reales, para la autenticación y la autorización
- Cuando la API guarda datos personales, de salud o de pago
- Cuando un socio o un cliente empresarial vaya a integrarse con ella
- Cuando el rendimiento se cae: consultas lentas, índices que faltan, límites de conexiones
- Cuando tú y el agente habéis fallado tres veces al arreglar el mismo bug
Pónselo fácil: el archivo de especificación, el documento OpenAPI, la carpeta de migraciones y una ejecución de tests en verde.
Cómo lo hago con AgentVera
Todo esto funciona en una sola terminal. Con varios agentes, AgentVera me ayuda en estos puntos:
- Trabajo en paralelo en worktrees. En el espacio de trabajo tengo un agente con la API y otro con el frontend o los tests, cada uno en su propio git worktree para que no se pisen los archivos. El método está en git worktrees para agentes en paralelo.
- Probar endpoints sin otra herramienta. El cliente de API encuentra los endpoints del proyecto en archivos OpenAPI y en rutas de Express, Fastify, Hono, Next.js, FastAPI, Django, NestJS y otros. Envío una petición con parámetros, cabeceras y cuerpo, y si la respuesta es un error, le paso la petición y la respuesta a un agente con un clic.
- Ver qué ha llegado a la base de datos. El gestor de bases de datos abre el Postgres local para que revise las filas que ha creado el agente, y DB Guard valora el riesgo de cada migración cuando el agente termina su turno y puede probarla en una base de datos sombra.
- Contenedores. La página de Docker muestra el contenedor de Postgres y sus logs junto a los agentes.
- Revisar los turnos. La revisión de código comprueba cada turno terminado antes de que haga merge.
Esto acelera el ciclo; lo que hace segura la API siguen siendo la especificación, los tests y tu propia revisión.
Para terminar
Para crear una API con IA, decide primero el contrato, escribe las reglas en CLAUDE.md o AGENTS.md, elige un stack muy extendido y haz que el agente construya un recurso cada vez, con tests. Valida cada entrada, cambia la base de datos solo con migraciones, comprueba la propiedad de cada registro, genera OpenAPI desde los mismos esquemas y ejecuta las migraciones como un paso aparte del despliegue. Consigue una revisión humana antes de que lleguen usuarios reales. Si quieres tener en paralelo los agentes de backend, frontend y tests, descarga AgentVera gratis.