Le pedí a un agente que generara la feature de listado de productos para un proyecto frontend.
Veinte segundos después tenía el código listo. Funcionaba. Los datos aparecían en pantalla.
Y entonces abrí el componente y vi esto:
// ProductListComponent.tsx — lo que el agente generó sin contexto
const ProductListComponent = () => {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch("https://api.myapp.com/products")
.then(res => res.json())
.then(data => setProducts(data));
}, []);
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
};
Una llamada HTTP directamente en el componente. Sin interface. Sin use case. Sin repository. Sin manejo de errores. La lógica de negocio mezclada con la presentación, exactamente lo que llevaba seis meses evitando en ese proyecto.
El agente no hizo lo que yo quería. Hizo lo que era más rápido de generar.
Ese es el problema real cuando usas IA en un proyecto con clean architecture frontend: la IA optimiza para el camino más corto, no para el más correcto. Y sin guía, el camino más corto siempre es el spaghetti.
(Los ejemplos de este post usan TypeScript 5.4 con Angular 19 / React 18 como referencia, y Claude Code en su versión de 2026. El CLAUDE.md y los principios aplican igualmente a Cursor y GitHub Copilot.)
Por qué la IA destroza la arquitectura si la dejas sola
Clean Architecture en frontend no es difícil de entender. Es difícil de sostener.
Cualquier developer senior entiende la separación de capas. El problema es que cuando el equipo crece, cuando hay presión de tiempo, cuando alguien nuevo entra al proyecto — las capas se erosionan. Un fetch aquí, una lógica de transformación allá directamente en el componente.
La IA acelera exactamente este problema.
Los LLMs aprenden de código real que existe en internet. Y el código real que existe en internet está lleno de llamadas HTTP en componentes, lógica de negocio en controllers, transformaciones de datos sin tipado. Los modelos han visto millones de ejemplos de ese código. Lo reproducen con total confianza porque estadísticamente es el patrón más frecuente.
Si no le dices al agente qué arquitectura sigue tu proyecto, asumirá que no tienes ninguna.
Las capas que importan en frontend
Clean Architecture en frontend es un patrón de organización de código que divide la aplicación en tres capas independientes (Domain, Data, Presentation) con una regla de dependencia estricta: las capas externas dependen de las internas, nunca al revés.
En frontend, estas tres capas se pueden modelar de forma clara — independientemente de si usas Angular, React o Vue:
Domain — El núcleo. Aquí viven las entities (los modelos de negocio), los use cases (la lógica que define qué puede hacer el sistema) y los ports (las interfaces que definen contratos sin implementación concreta).
Data — La capa de acceso a datos. Repositories (implementaciones concretas de los ports), DTOs (los objetos que llegan de la API tal como los devuelve el servidor), y adapters/mappers (la transformación de DTO a entity).
Presentation — Lo que el usuario ve. Componentes, páginas, ViewModels (la forma específica en que la presentación necesita los datos), y el estado de UI.
La regla de dependencia es simple: las capas externas dependen de las internas. La Presentation conoce el Domain. El Data implementa los contratos del Domain. El Domain no sabe que existe ninguna de las otras dos.
Presentation → Domain ← Data
El componente no habla con la API. Habla con un use case. El use case habla con un repository port. El repository concrete habla con la API y transforma los datos antes de devolverlos.
Eso es lo que el agente rompió cuando puso el fetch directamente en el componente.
Dónde la IA puede ayudarte más en Clean Architecture
La IA es extraordinariamente buena en el trabajo más aburrido de Clean Architecture.
Crear interfaces de repositorios. Generar mappers entre DTOs y entities. Escribir use cases que siguen un patrón uniforme. Crear tests unitarios de use cases que no tienen dependencias externas. Esas son tareas repetitivas, con patrones claros, donde el agente brilla.
Y son exactamente las tareas que los developers saltamos “para ir más rápido” y que luego generan deuda técnica durante meses.
| Tarea de Clean Architecture | IA sin contexto | IA con contexto |
|---|---|---|
| Generar entity con validación | Genera clase plana sin contratos | Sigue el patrón de entity existente |
| Crear repository port (interface) | Puede saltárselo e ir a la implementación | Crea interface primero, luego implementación |
| Escribir adapter/mapper DTO → Entity | Transforma inline en el componente | Crea mapper en capa Data con tipos explícitos |
| Implementar use case | Mezcla lógica de UI con lógica de negocio | Separa correctamente, inyecta el port |
| Manejo de errores en Data layer | Try/catch en el componente | Manejo en el repository, domain errors tipados |
| Test de use case | Test de integración con API real | Unit test con mock del repository port |
La diferencia entre las dos columnas no es el modelo. Es el contexto que le das.
Cómo darle contexto al agente para que respete la arquitectura
Hay tres mecanismos que uso y que funcionan en producción.
1. Estructura de carpetas que documenta la arquitectura
Si tu estructura de carpetas refleja las capas, el agente las ve antes de generar código. Cuando lee el proyecto antes de actuar, el patrón es obvio:
src/
├── domain/
│ ├── entities/
│ │ └── product.entity.ts
│ ├── use-cases/
│ │ └── get-products.use-case.ts
│ └── ports/
│ └── product.repository.port.ts
├── data/
│ ├── repositories/
│ │ └── product.repository.ts
│ ├── dtos/
│ │ └── product.dto.ts
│ └── mappers/
│ └── product.mapper.ts
└── presentation/
├── components/
│ └── product-list/
└── view-models/
└── product-list.vm.ts
Un agente que lee esta estructura sabe dónde va cada pieza. La carpeta es la arquitectura documentada.
2. CLAUDE.md con reglas de arquitectura
Si usas Claude Code, el archivo CLAUDE.md en la raíz del proyecto es leído automáticamente antes de que el agente actúe. Es tu oportunidad de definir las reglas del juego:
# Arquitectura del proyecto
Este proyecto sigue Clean Architecture con tres capas:
## Reglas de dependencia (OBLIGATORIAS)
- Los componentes en presentation/ NUNCA importan directamente de data/
- Los componentes solo usan use cases de domain/use-cases/
- Los use cases solo conocen ports de domain/ports/, nunca implementaciones concretas
- Todo acceso a API externo va en data/repositories/, nunca en componentes ni use cases
## Antes de generar código nuevo
1. Si es lógica de negocio → crea use case en domain/use-cases/
2. Si es acceso a datos → crea o modifica el repository en data/repositories/
3. Si es transformación de datos → crea mapper en data/mappers/
4. Si el port no existe → créalo en domain/ports/ antes de la implementación
## Naming conventions
- Entities: *.entity.ts
- Use cases: get-products.use-case.ts (verbo + sustantivo)
- Ports: product.repository.port.ts
- DTOs: product.dto.ts
- Mappers: product.mapper.ts
Esto no es opcional. Es la diferencia entre un agente que genera spaghetti y uno que genera código que encaja en tu arquitectura.
3. Prompt con diagrama de capas
Cuando pides una feature específica, incluye siempre la capa donde debe vivir:
Necesito implementar la feature "obtener lista de productos" siguiendo la arquitectura del proyecto.
Genera en este orden:
1. ProductDTO en data/dtos/ (tal como viene de la API)
2. ProductEntity en domain/entities/ (modelo de negocio limpio)
3. ProductMapper en data/mappers/ (transforma DTO → Entity)
4. IProductRepository port en domain/ports/ (interface del contrato)
5. ProductRepository en data/repositories/ (implementación concreta que usa fetch)
6. GetProductsUseCase en domain/use-cases/ (orquesta el repositorio, devuelve entities)
El componente ya existe — no lo modifiques. Solo genera las capas de dominio y datos.
Ejemplo práctico: de DTO a Use Case con el agente
Así es como queda el código cuando el agente tiene contexto. Le pedí exactamente las piezas del ejemplo anterior con el CLAUDE.md activo:
// domain/entities/product.entity.ts
export interface ProductEntity {
id: string;
name: string;
price: number;
inStock: boolean;
}
// data/dtos/product.dto.ts
export interface ProductDTO {
id: string;
nombre: string; // la API devuelve en español
precio_eur: number; // con otro nombre
stock: number; // número, no boolean
}
// data/mappers/product.mapper.ts
import { ProductDTO } from "../dtos/product.dto";
import { ProductEntity } from "../../domain/entities/product.entity";
export const toProductEntity = (dto: ProductDTO): ProductEntity => ({
id: dto.id,
name: dto.nombre,
price: dto.precio_eur,
inStock: dto.stock > 0,
});
// domain/ports/product.repository.port.ts
import { ProductEntity } from "../entities/product.entity";
export interface IProductRepository {
getAll(): Promise<ProductEntity[]>;
getById(id: string): Promise<ProductEntity | null>;
}
// data/repositories/product.repository.ts
import { IProductRepository } from "../../domain/ports/product.repository.port";
import { ProductEntity } from "../../domain/entities/product.entity";
import { ProductDTO } from "../dtos/product.dto";
import { toProductEntity } from "../mappers/product.mapper";
export class ProductRepository implements IProductRepository {
private readonly baseUrl = "https://api.myapp.com";
async getAll(): Promise<ProductEntity[]> {
const response = await fetch(`${this.baseUrl}/products`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const dtos: ProductDTO[] = await response.json();
return dtos.map(toProductEntity);
}
async getById(id: string): Promise<ProductEntity | null> {
const response = await fetch(`${this.baseUrl}/products/${id}`);
if (response.status === 404) return null;
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const dto: ProductDTO = await response.json();
return toProductEntity(dto);
}
}
// domain/use-cases/get-products.use-case.ts
import { IProductRepository } from "../ports/product.repository.port";
import { ProductEntity } from "../entities/product.entity";
export class GetProductsUseCase {
constructor(private readonly productRepository: IProductRepository) {}
async execute(): Promise<ProductEntity[]> {
// El use case no tiene try/catch — delega el manejo de errores al repository.
// Los errores de infraestructura suben como excepciones; la capa de presentación decide cómo mostrarlos.
return this.productRepository.getAll();
}
}
El componente ahora solo necesita instanciar el use case e invocar execute(). No sabe que existe una API. No sabe el formato de los DTOs. No hace transformaciones. Solo le habla al dominio.
Eso es Clean Architecture aplicada. Y el agente lo generó todo en un solo turno porque sabía exactamente dónde iba cada pieza.
Dónde la IA falla aunque tengas contexto
El CLAUDE.md no es una bala de plata.
Hay situaciones donde el agente ignora las reglas o las interpreta de forma inesperada. Las más comunes:
Features cross-capa sin spec previa. Si le pides “añade filtros al listado de productos”, el agente puede añadir el estado del filtro en el use case (lógica de UI en el dominio), en la URL de la API directamente, o en el componente — sin pasar por el use case. La feature es compleja y el agente toma atajos.
Refactorizaciones de archivos existentes. Al modificar código que ya existe y que no sigue la arquitectura, el agente tiende a preservar el patrón existente en lugar de corregirlo. Si el archivo ya tiene un fetch en el componente y le pides que añada una funcionalidad, lo más probable es que añada otro fetch.
Código sin tests previos. Sin tests que fallen cuando se rompe la arquitectura, el agente no recibe feedback negativo cuando viola las capas. El código compila, parece correcto, y el problema solo aparece cuando otro developer intenta extender la feature meses después.
La solución a los tres casos es la misma: spec primero, código después.
La hoja de ruta correcta: SDD + IA
Lo que marca la diferencia no es qué agente usas. Es si empiezas con una especificación o si vas directo al código.
Cuando escribes la spec primero — qué entities existen, qué use cases necesita la feature, qué contratos definen los ports — el agente tiene un mapa. No adivina la arquitectura. La sigue porque está documentada antes de que genere la primera línea.
Spec-Driven Development (SDD) es exactamente esta metodología: especificar antes de implementar, usar la spec como contrato entre el developer y el agente. He documentado todo el proceso — con plantillas, ejemplos y el flujo completo — en el Libro SDD. Si tu proyecto tiene problemas de arquitectura cuando usas IA, el libro es el punto de partida más directo que tengo para darte.
Si quieres entender primero cómo funciona el bucle interno del agente — el ciclo percibir-razonar-actuar que subyace a todo esto — el post sobre el agentic loop y la guía sobre qué es un Agentic Engineer completan el contexto antes de aplicarlo a tu arquitectura.
El flujo práctico es este:
1. Escribe la spec: entities, use cases, ports, contratos
2. Configura CLAUDE.md con las reglas de arquitectura
3. Pide al agente que genere una capa a la vez, en orden
4. Review: ¿la implementación respeta la spec?
5. Añade tests que fallen si alguien rompe las capas
6. Itera
Cada paso reduce el espacio de decisión del agente. Y reducir el espacio de decisión es reducir el riesgo de que genere spaghetti.
Si quieres ver este flujo aplicado a proyectos reales — desde la spec hasta el producto funcionando — el curso Construye con IA cubre exactamente esto: cómo trabajar con agentes de IA respetando la arquitectura, con ejemplos en TypeScript y el proceso completo desde idea hasta código en producción.
FAQ — Preguntas frecuentes
¿Clean Architecture en frontend es sobreingeniería para proyectos pequeños?
Depende del criterio de “pequeño”. Si el proyecto va a crecer, va a tener más de un developer tocando el código o va a ser mantenido más de seis meses, Clean Architecture paga su coste desde el primer mes. El problema no es la arquitectura en sí — es implementarla de forma rígida cuando no añade valor. Para un script de 200 líneas o un prototipo desechable, no la necesitas. Para cualquier producto real, la separación de capas es lo que permite que la IA ayude en lugar de crear deuda.
¿Funciona el mismo enfoque con Cursor, GitHub Copilot o cualquier otro agente?
Sí. El CLAUDE.md es específico de Claude Code, pero el principio es universal: cualquier agente que pueda leer el contexto del proyecto antes de generar código va a producir mejores resultados. En Cursor usas archivos .cursor/rules/*.mdc (la convención actual desde 2025; .cursorrules sigue funcionando por retrocompatibilidad). En GitHub Copilot puedes añadir instrucciones en el repositorio o en el prompt. La estructura de carpetas funciona con todos porque es parte del contexto que el agente lee automáticamente cuando explora el proyecto.
¿Cómo testeo que la arquitectura se está respetando?
La forma más efectiva es con import constraints a nivel de build o linting. En proyectos TypeScript puedes usar eslint-plugin-boundaries para definir reglas de qué capas pueden importar de cuáles. Cuando el agente viola la arquitectura, el linter falla antes de que el código llegue a revisión. Es la red de seguridad que hace que el enfoque sea sostenible en equipos o en proyectos donde usas IA intensivamente.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
