
Publicado originalmente en cesalberca.com
Adaptado para Codemotion Magazine por César Alberca, Arquitecto Frontend y miembro del Comité de Codemotion.
Estamos entrando en una era en la que los sistemas frontend no solo son utilizados por humanos, sino también analizados, generados e interpretados por herramientas de inteligencia artificial.
Frameworks modernos como shadcn/ui, herramientas de diseño a código como V0.dev y agentes en entornos de desarrollo como Junie están redefiniendo la forma en que construimos. Pero con esta aceleración, surge una mayor necesidad de estructura.
Esta guía presenta una arquitectura práctica para:
- Potenciar la creación de interfaces con herramientas de IA
- Diseñar protocolos de colaboración entre desarrolladores y sistemas de IA
- Mantener la capacidad de testing y la escalabilidad
Define pautas de IA para tu base de código
Las herramientas de IA carecen de contexto, por lo tanto tenemos que hacer un esfuerzo por establecer un contexto para que sean más efectivas. En el caso de Junie (que es el agente de IA que uso en Webstorm) podemos crear un archivo de guidelines.md
para describir las reglas, el stack y los patrones usados en tu frontend.
Ejemplo:
Framework: Next.js
Styling: Tailwind CSS 4
Language: TypeScript
Tests: Vitest + Playwright
Architecture:
- Use Case pattern
- CQRS
- Middleware chaining
Standards:
- Named exports only
- No enums, use unions
- One component per file
- ?? instead of ||
- Avoid unnecessary useEffect
- FC with PropsWithChildren
Herramientas de IDE como Junie usarán esto para seguir tus convenciones sin que tengas que repetir instrucciones. Puedes ver un ejemplo real aquí.
2. Abraza una UI Basada en Componentes con Asistencia de IA
Con herramientas como V0.dev, puedes pasar de un prompt a JSX en segundos:
“Crea un formulario de registro en modo oscuro con dos campos de entrada y un botón CTA”
V0 responde con JSX accesible y estilizado usando shadcn/ui, listo para integrar en tu repo. Combinado con Storybook, documentación MDX y las pautas de Junie, tienes un ciclo de feedback entre la IA y tu sistema de diseño.
La IA genera. Tu sistema valida.
3. Patrón Use Case para la Lógica de Negocio
En el desarrollo potenciado por IA, generar la UI es solo una parte. El verdadero valor está en desacoplar la lógica de negocio de las capas de interfaz, para que agentes de IA (o humanos) puedan desarrollar de una forma predecible sin tener que reinventar la rueda con cada nueva iteración. Por eso, para mí, el patrón Use Case es esencial.
¿Qué es el patrón Use Case?
Cada caso de uso encapsula una única operación de negocio, por ejemplo: RegisterUser, CreatePost, SubmitFeedback, etc. Define una interfaz así:
export interface UseCase<In = unknown, Out = unknown> {
handle(param?: In, meta?: UseCaseOptions): Promise<Out>
}
Cada caso de uso es una unidad aislada, testeable e injectable que recibe una entrada y devuelve una salida. Está totalmente desacoplado de frameworks UI (o eso debería), por lo que puede ser activado por:
- Un botón en la UI
- Un trabajo en segundo plano
- Un agente de IA
- Un webhook
- O una combinación de todos
¿Por qué es importante para la IA?
Al ofrecer una interfaz estable y ocultar la lógica interna, el patrón Use Case permite a la IA orquestar el comportamiento de la aplicación sin conocer los detalles de implementación.
Esto evita que las herramientas de IA (o humanos) acoplen la lógica con componentes o vistas, lo que suele generar código frágil e inmantenible.
Ejecución centralizada con useCaseService
Para ejecutar casos de uso, empleamos una capa de servicio que abstrae la lógica de orquestación y permite componer middlewares:
await useCaseService.execute(RegisterUserUseCase, {
email: 'test@example.com',
password: 'secure-password',
})
Lenguaje del código: CSS (css)
Esta llamada no expone la lógica interna de RegisterUserUseCase: la IA solo sabe que debe enviar un payload y recibir un resultado.
Beneficios en sistemas aumentados por IA
- Testabilidad: cada caso se prueba aisladamente con mocks o stubs.
- Seguridad: control de acceso y permisos se añade via middleware.
- Extensibilidad: nuevos comportamientos (analíticas, reporte de errores) se añaden sin tocar la lógica del caso.
- Preparación para IA: agentes interactúan con tu backend de forma segura mediante una API estable y bien definida.
4. Cadenas de Middleware: Escalando la Lógica con Composabilidad
En una arquitectura frontend bien estructurada, piezas transversales como logging, manejo de errores o caching pueden ensuciar la lógica de negocio si no se gestionan bien. Los Middlewares ofrecen un enfoque modular inspirado en el patrón Chain of Responsibility.
En lugar de poner esta lógica en cada caso de uso, la envuelves con clases middleware que interceptan la ejecución antes y después de la lógica principal:
interface Middleware {
intercept<In, Out>(param: In, next: UseCase<In, Out>, options: UseCaseOptions): Promise<Out>
}
Lenguaje del código: TypeScript (typescript)
Cada middleware se ocupa de una única responsabilidad y pasa el control a la siguiente capa.
Middleware de Errores
En lugar de repetir bloques try/catch, delegas el manejo de errores a un middleware. Cuando ocurre una excepción, se dispara un evento que la UI puede escuchar — mostrando un toast o registrando el error — sin afectar la lógica de negocio.
export class ErrorMiddleware implements Middleware {
constructor(private readonly eventEmitter: EventEmitter) {}
async intercept(params: unknown, next: UseCase, options: UseCaseOptions): Promise<unknown> {
try {
return await next.handle(params)
} catch (error) {
if (!options.silentError) {
this.eventEmitter.dispatch(EventType.ERROR, error)
}
throw error
}
}
}
Lenguaje del código: TypeScript (typescript)
Middleware de Logs
Este middleware registra cada ejecución del caso de uso, con nombre y parámetros. Puedes usar diferentes implementaciones de logger — consola en desarrollo, logging remoto en producción — sin cambiar nada en los casos de uso.
export class LogMiddleware implements Middleware {
constructor(private readonly logger: Logger) {}
intercept(params: unknown, useCase: UseCase): Promise {
this.logger.log(
[${DateTime.fromNow().toISO()}] ${this.getName(useCase)} / ${this.printResult(params)}
)
return useCase.handle(params)
}
private getName(useCase: UseCase): string {
if (useCase instanceof UseCaseHandler) {
return this.getName(useCase.useCase)
}
return useCase.constructor.name
}
private printResult(result: unknown) {
return JSON.stringify(result, null, 2)
}
}
Lenguaje del código: TypeScript (typescript)
Middleware de Caché
Si usas CQRS para separar queries y comandos, este middleware puede cachear automáticamente resultados de consultas. Los casos de uso que provean una opción cacheKey
devolverán resultados cacheados por un tiempo definido, reduciendo procesamiento innecesario.
type CacheEntry = {
value: unknown
expiresAt: number
}
export class CacheMiddleware implements Middleware {
private readonly store = new Map()constructor(private readonly ttlInSeconds: number = 60) {}async intercept(
params: In,
next: UseCase,
options: UseCaseOptions,
): Promise {
const key = options.cacheKey
if (!key) return next.handle(params, options)const now = Date.now()
const cached = this.store.get(key)
if (cached && now < cached.expiresAt) {
return cached.value as Out
}
const result = await next.handle(params, options)
this.store.set(key, { value: result, expiresAt: now + this.ttlInSeconds * 1000 })
return result
}
}
Lenguaje del código: TypeScript (typescript)
Composición de Middleware con UseCaseService
Para aplicar varios middlewares, los envuelves alrededor de tus casos de uso usando un servicio que compone la cadena desde la capa más externa a la interna:
export class UseCaseService {
constructor(
private middlewares: Middleware[],
private readonly container: Container,
) {}
async execute(useCase: Type<UseCase>, param?: In, options?: UseCaseOptions): Promise {
const requiredOptions = options ?? { silentError: false }
let next = UseCaseHandler.create({
next: this.container.create(useCase),
middleware: this.container.get<EmptyMiddleware>(EmptyMiddleware.name),
options: requiredOptions,
})
for (let i = this.middlewares.length - 1; i >= 0; i--) {
const current = this.middlewares[i]
next = UseCaseHandler.create({
next,
middleware: current,
options: requiredOptions,
})
}
return next.handle(param) as Promise<Out>
}
}
Lenguaje del código: JavaScript (javascript)
¿Por qué es importante esto?
Con middlewares, tu sistema gana:
- Manejo centralizado de errores
- Logging consistente
- Caché automática
- Casos de uso más limpios, sin duplicación de código
Escalas funcionalidad sin añadir complejidad al núcleo de la lógica de negocio.
Tabla resumen: Arquitectura aumentada por IA
Capa | Herramienta / Patrón | Rol de la IA |
---|---|---|
Prototipado UI | V0.dev + shadcn/ui | Generación de JSX a partir del prompt |
Documentación UI | MDX + Storybook | Aprender a partir de ejemplos |
Capa lógica | Patrón Use Case | Crear e invocar lógica según patrones definidos |
Capa de unión | Cadena de Middleware | Mejoras sin acoplamiento |
Colaboración IDE | .junie/guidelines.md | Alinear IA a tus convenciones |
Reflexiones finales
Ya no solo construimos para navegadores: estamos creando sistemas que humanos y agentes inteligentes pueden entender y ampliar.
El futuro del frontend no es automatizado. Es aumentado.
Diseña tu arquitectura pensando en:
- Interpretabilidad (para humanos y máquinas)
- Estructura (a través de convenciones y patrones)
- Escalabilidad (mediante casos de uso y middleware)
Así es como programamos con IA, no a pesar de ella.
¿Quieres ayuda para implementar esta arquitectura en tu equipo? Ofrezco consultoría en arquitectura frontend y flujos de trabajo potenciados por IA. Contáctame o lee más sobre mi enfoque en cesalberca.com.