El Antipatrón del Componente Monolítico
Al construir aplicaciones con Next.js, la inercia más común entre desarrolladores es colocar toda la lógica dentro de los propios componentes de React. Esta aproximación, conocida como arquitectura guiada exclusivamente por componentes, funciona en prototipos de juguete, pero se convierte rápidamente en una pesadilla de mantenimiento en proyectos de ingeniería reales.
En esta lección analizaremos por qué los componentes sobrecargados (Fat Components) degradan la mantenibilidad del código y cómo los principios de Clean Architecture aplicados al frontend resuelven este problema de raíz.
1. Radiografía del Componente Espagueti
Imaginemos una pantalla sencilla para gestionar ejercicios de entrenamiento físico (un módulo habitual conectado a un backend de NestJS). En un enfoque ingenuo de Next.js, el archivo ExercisePage.tsx suele lucir así:
'use client';
import { useState, useEffect } from 'react';
// Tipado acoplado directamente al JSON del backend
interface ExerciseFromApi {
id: string;
exercise_name: string;
muscle_group: string | null;
difficulty_lvl?: number;
}
export default function ExercisePage() {
const [exercises, setExercises] = useState<ExerciseFromApi[]>([]);
const [loading, setLoading] = useState(true);
const [name, setName] = useState('');
const [muscle, setMuscle] = useState('');
// 1. Acceso a red directo en el componente
useEffect(() => {
fetch('http://localhost:3000/api/exercises')
.then((res) => res.json())
.then((data) => {
setExercises(data);
setLoading(false);
})
.catch((err) => console.error(err));
}, []);
// 2. Lógica de negocio e invariantes mezcladas con el evento de la vista
const handleCreate = async () => {
if (name.trim().length < 3) {
alert('El nombre debe tener al menos 3 caracteres');
return;
}
if (!['CHEST', 'BACK', 'LEGS', 'ARMS'].includes(muscle.toUpperCase())) {
alert('Grupo muscular no válido');
return;
}
// 3. Payload formateado a mano según exigencias del backend
const payload = {
exercise_name: name,
muscle_group: muscle.toUpperCase(),
difficulty_lvl: 2,
};
const res = await fetch('http://localhost:3000/api/exercises', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const created = await res.json();
setExercises((prev) => [...prev, created]);
setName('');
};
// 4. Renderizado visual con decenas de clases de Tailwind
return (
<div className="p-8 max-w-4xl mx-auto">
<h1 className="text-2xl font-bold mb-4">Gestión de Ejercicios</h1>
<div className="flex gap-2 mb-6">
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Nombre"
className="border p-2 rounded"
/>
<button onClick={handleCreate} className="bg-blue-600 text-white px-4 py-2 rounded">
Guardar
</button>
</div>
{loading ? (
<p>Cargando ejercicios...</p>
) : (
<ul className="space-y-2">
{exercises.map((item) => (
<li key={item.id} className="p-3 border rounded shadow-sm">
<span className="font-semibold">{item.exercise_name}</span> - {item.muscle_group ?? 'General'}
</li>
))}
</ul>
)}
</div>
);
}
¿Por qué este código es frágil y costoso de mantener?
Este archivo de apenas 80 líneas ya tiene cinco responsabilidades distintas totalmente entremezcladas:
- Protocolo de transporte HTTP: Conoce la URL exacta (
http://localhost:3000/api/exercises), los métodos HTTP (GET,POST) y las cabeceras (Content-Type). - Formato crudo del backend: Depende directamente de la nomenclatura del servidor (
exercise_name,muscle_group,difficulty_lvl). Si el equipo de backend renombra una propiedad acamelCaseo normaliza la base de datos, el archivo entero se rompe. - Reglas de negocio e invariantes: Valida que el nombre tenga al menos 3 caracteres y que el grupo muscular pertenezca a una lista blanca dentro de la función del botón.
- Estado reactivo local: Coordina los estados
loading,exercises,nameymuscle. - Renderizado visual: Maneja los elementos JSX, etiquetas semánticas y estilos de Tailwind CSS.
El componente tiene demasiadas razones para cambiar. Cambiará si se actualiza la API REST, si cambian las reglas de validación del negocio, si se rediseña la interfaz de usuario, o si se desea migrar de fetch a axios o react-query.
2. Contraste Visual: Monolito vs. Separación en Capas
A continuación se ilustra la diferencia estructural entre concentrar todas las responsabilidades en un solo archivo frente a distribuirlas en capas especializadas:
3. Principios SOLID Aplicados al Frontend
Para superar este antipatrón en Next.js, nos apoyamos en dos principios fundamentales de diseño de software:
3.1. Principio de Responsabilidad Única (SRP)
Cada módulo, clase o función debe tener una sola responsabilidad y una única razón para cambiar:
- La Vista (JSX) solo debe preocuparse por cómo se presentan los datos al usuario.
- El Caso de Uso solo debe encargarse de orquestar la acción requerida por el usuario y ejecutar las reglas de negocio.
- El Repositorio solo debe encargarse de la persistencia y recuperación de datos.
- El Mapper solo debe encargarse de transformar datos entre formatos incompatibles.
3.2. Principio de Inversión de Dependencias (DIP) y la Regla de Dependencia
El creador de Clean Architecture, Robert C. Martin, formula la regla de oro:
Las dependencias en el código fuente solo pueden apuntar hacia adentro, en dirección a las políticas de más alto nivel (el Dominio).
En el contexto de Next.js, esto significa:
- El Dominio no conoce a React: Ningún archivo dentro de la capa de dominio puede importar
useState,useEffect,next/navigationni dependencias de interfaz. - La UI depende de abstracciones: La vista consume casos de uso e interfaces abstractas, no clientes HTTP concretos ni URLs fijas.
4. Estructura de Proyecto Propuesta para Next.js
Adaptando las convenciones de Clean Architecture al ecosistema de Next.js y TypeScript, organizamos el código mediante Vertical Slices (Features):
Correspondencia de Directorios
src/
├── core/ # Recursos transversales de la aplicación
│ ├── errors/ # Clases de error estandarizadas
│ ├── http/ # Cliente HTTP base configurado
│ └── components/ui/ # Componentes genéricos de UI (Button, Modal, Input)
│
└── features/ # Módulos del negocio (Vertical Slices)
└── exercises/ # Feature específica de Ejercicios
├── domain/ # Reglas de negocio puras (TypeScript)
│ ├── entities/ # Entidades inmutables (exercise.ts)
│ ├── repositories/ # Interfaces abstractas (exercise.repository.ts)
│ └── usecases/ # Casos de uso / Interactors (get-exercises.usecase.ts)
│
├── infrastructure/ # Adaptadores de comunicación externa
│ ├── dtos/ # Contratos crudos de la API (exercise.dto.ts)
│ ├── mappers/ # Conversores DTO <-> Entidad (exercise.mapper.ts)
│ ├── datasources/ # Conexión HTTP o Mock en memoria
│ └── repositories/ # Implementación concreta del repositorio
│
├── di/ # Inyección de dependencias / Composition Root
│ └── exercise.container.ts # Ensamblador de dependencias
│
└── presentation/ # Capa de presentación visual (React / Next.js)
├── hooks/ # Custom Hook Presenter (use-exercises.ts)
└── components/ # Componentes visuales puros (ExerciseCard, ExerciseForm)
Organizar por carpetas técnicas globales (src/entities, src/usecases, src/components) dispersa el código de una misma funcionalidad a lo largo de todo el árbol. Agrupar por feature (features/exercises) mantiene la alta cohesión del módulo y permite aislar cambios sin afectar otras áreas del proyecto.
5. Resumen de la Lección
- El componente espagueti viola el principio SRP al mezclar red, lógica de negocio, formato JSON y JSX en un solo archivo.
- Cualquier cambio en el backend rompe la vista si los componentes consumen directamente los DTOs crudos de la API.
- Clean Architecture en Next.js garantiza que el núcleo de negocio permanezca puro, inmune a actualizaciones del framework o cambios de endpoints.
- En la siguiente lección profundizaremos en la Capa de Dominio, construyendo las entidades inmutables y los casos de uso en TypeScript puro.