MVC Pattern en Aplicaciones Frontend Modernas
Aplica el Model-View-Controller pattern en aplicaciones React y Vue para separar datos, UI y logica de interaccion en una arquitectura de componentes mantenible
Model-View-Controller separa una aplicacion en tres componentes: Model (datos y reglas), View (presentacion) y Controller (manejo de entrada y coordinacion). Aunque frameworks como React y Vue difuminan estos limites, aplicar disciplina MVC previene que los componentes se conviertan en mezclas inmantenibles de estado, UI y efectos secundarios.
Cuando Usar Esto
- Los componentes crecen mas de 200 lineas porque mezclan fetching de datos, transformacion y renderizado. Consulta Component Testing para patrones de UI testeables.
- La misma logica de datos se duplica a traves de multiples paginas. Consulta Repository Pattern para capas de acceso a datos compartidas.
- Testear la UI requiere mockear redes, stores y DOM simultaneamente. Consulta Unit Testing para estrategias de tests aislados.
Problema
Un componente React que obtiene usuarios, filtra por busqueda, ordena por nombre, pagina resultados y renderiza tarjetas es imposible de testear o reutilizar.
Solucion
// model/UserModel.ts
interface User {
id: string;
name: string;
email: string;
role: string;
}
class UserModel {
private users: User[] = [];
setUsers(users: User[]) {
this.users = users;
}
getFilteredUsers(query: string): User[] {
if (!query) return this.users;
const lower = query.toLowerCase();
return this.users.filter(u =>
u.name.toLowerCase().includes(lower) ||
u.email.toLowerCase().includes(lower)
);
}
getSortedUsers(field: keyof User, direction: 'asc' | 'desc'): User[] {
return [...this.users].sort((a, b) => {
const cmp = String(a[field]).localeCompare(String(b[field]));
return direction === 'desc' ? -cmp : cmp;
});
}
}
// controller/UserController.ts
class UserController {
constructor(private model: UserModel) {}
async loadUsers(): Promise<void> {
const res = await fetch('/api/users');
const users = await res.json();
this.model.setUsers(users);
}
search(query: string): User[] {
return this.model.getFilteredUsers(query);
}
sort(field: keyof User, direction: 'asc' | 'desc'): User[] {
return this.model.getSortedUsers(field, direction);
}
}
// view/UserListView.tsx
import { useState, useEffect } from 'react';
function UserListView({ controller }: { controller: UserController }) {
const [users, setUsers] = useState<User[]>([]);
const [query, setQuery] = useState('');
useEffect(() => {
controller.loadUsers().then(() => {
setUsers(controller.search(''));
});
}, []);
const handleSearch = (q: string) => {
setQuery(q);
setUsers(controller.search(q));
};
return (
<div>
<input
type="search"
value={query}
onChange={e => handleSearch(e.target.value)}
placeholder="Buscar usuarios..."
/>
<ul>
{users.map(u => (
<li key={u.id}>{u.name} — {u.email}</li>
))}
</ul>
</div>
);
}
Ejemplo Vue
<!-- view/UserListView.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { UserController, UserModel } from './userMVC';
const model = new UserModel();
const controller = new UserController(model);
const users = ref<User[]>([]);
const query = ref('');
onMounted(async () => {
await controller.loadUsers();
users.value = controller.search('');
});
const handleSearch = (q: string) => {
query.value = q;
users.value = controller.search(q);
};
</script>
<template>
<div>
<input
type="search"
v-model="query"
@input="handleSearch(query)"
placeholder="Buscar usuarios..."
/>
<ul>
<li v-for="u in users" :key="u.id">{{ u.name }} — {{ u.email }}</li>
</ul>
</div>
</template>
El mismo Model y Controller funcionan sin cambios. Solo la View difiere, que es el punto de separar concerns.
Variaciones
- MVVM: ViewModel expone propiedades observables que la View enlaza directamente
- MVP: Presenter actualiza la View imperativamente en lugar de la View observando estado
- Flux/Redux: Flujo de datos unidireccional con un dispatcher central reemplazando al Controller
- MVU (Model-View-Update): Popular en Elm. Una funcion pura de update produce un nuevo Model desde mensajes, y la View renderiza desde el Model actual. Sin estado mutable.
Lo que Funciona
- Manten los Models puros — sin efectos secundarios, sin referencias al DOM
- Los Controllers orquestan pero no saben como se renderizan los datos
- Las Views son delgadas — reciben datos y emiten eventos, no contienen reglas de negocio
Como Funciona
El Model posee la forma de los datos y las reglas de negocio. Sabe filtrar, ordenar, validar y relacionar entidades, pero no conoce React, Vue ni el DOM. Mantener los Models puros los hace triviales de testear con datos simples.
El Controller posee la intencion del usuario y la coordinacion. Decide cuando obtener datos, que metodos del Model llamar y que hacer con los resultados. El Controller puede referenciar servicios, repositorios u otros controllers, pero no importa JSX ni plantillas.
La View posee la presentacion. Recibe datos, renderiza markup y reenvia eventos. Las Views se mantienen delgadas: llaman metodos del controller ante entradas del usuario y se re-renderizan cuando el estado cambia. Los hooks del framework viven aqui, pero solo para estado local de UI como foco, hover o animacion.
Mejores Practicas
- Manten los Models agnosticos al framework. Deberian compilar sin imports de React o Vue.
- Prefiere actualizaciones inmutables dentro de los Models para que los cambios sean predecibles y baratos de comparar.
- Inyecta dependencias en los Controllers en lugar de construirlas dentro. Esto simplifica testing y cambio de implementaciones.
- Usa una capa dedicada de servicio o repositorio para llamadas de red. Los Controllers orquestan servicios; no deberian contener fetch boilerplate en todos lados.
- Manten las Views sin estado cuando sea posible. El estado local es valido para concerns de UI, pero el estado de dominio pertenece al Model.
- Testea cada capa de forma aislada. Los Models necesitan solo datos de ejemplo, los controllers necesitan Models mockeados, y las Views necesitan controllers stubbeados.
- Documenta la interfaz publica de cada capa para que los companeros sepan donde agregar nuevo comportamiento.
Errores Comunes
- Poner logica de fetch dentro de la View en lugar del Controller o capa de servicio.
- Mutar estado del Model directamente desde una View, saltando metodos del Controller.
- Hacer que el Model dependa de manejo de estado especifico del framework o lifecycle hooks.
- Crear Models anemicos que son solo bolsas de datos sin comportamiento.
- Permitir que los Controllers crezcan a god objects que manejan UI, routing, validacion y persistencia.
- Saltar tests de capa porque “es mas facil testear el componente completo.”
- Mezclar logica de routing con logica de negocio en Controllers o Views.
- Ignorar estados de loading, error y vacio cuando el Controller obtiene datos asincronicamente.
- Poner validacion solo en la UI mientras el Model acepta cualquier valor.
- Acoplar Controllers a implementaciones especificas de View en lugar de tratarlas como consumidoras delgadas.
Troubleshooting
- Pattern does not fit the problem: re-evaluate the forces (performance, scalability, team size, coupling). A pattern is only appropriate when its trade-offs match your constraints.
- Too many abstractions: if adding a pattern increases complexity without a clear benefit, simplify. Not every module needs a factory, decorator, or strategy.
- Tight coupling after refactoring: check that interfaces are stable and dependencies point inward. Use dependency inversion to break accidental coupling.
- Tests break when the design changes: favor stable contracts over internal structure. Test observable behavior, not private helpers.
- Performance regression from indirection: measure before and after. Layers, decorators, and adapters can add latency; cache or inline hot paths if needed.
Errores Comunes en Producción
- Aplicar el patrón donde no se necesita abstracción, agregando complejidad accidental.
- Dejar que el patrón se filtre en módulos no relacionados y confundir los límites de responsabilidad.
- Sobre-ingeniería en la primera implementación en lugar de comenzar simple y medir el dolor.
- Saltar los tests de contrato, de modo que las refactorizaciones rompan consumidores en silencio.
- Ignorar modos de fallo que el patrón no cubre.
- Usar el patrón como opción por defecto en lugar de elegir la herramienta adecuada para la escala actual.
- Olvidar documentar cuándo dejar de usar el patrón y qué lo reemplaza.
- Carecer de observabilidad sobre rendimiento y propagación de errores del patrón.
Preguntas frecuentes
Los Models deberian ser clases planas o estado del framework?
Prefiere clases planas o estructuras de datos simples. El estado del framework pertenece a las Views o librerias de estado. Los Models puros son mas faciles de testear y reutilizar fuera de la capa de UI.
Como testeo las capas MVC de forma independiente?
Testea los Models con datos simples y aserciones. Testea los Controllers con Models y servicios mockeados. Testea las Views con controllers stubbeados y eventos de usuario falsos. Cada capa deberia ser testeable sin las otras.
Donde pertenece el routing en MVC?
El routing es un concern separado. Los Controllers pueden reaccionar a parametros de ruta, pero el parseo de rutas y la navegacion pertenecen a una capa de router. Manten la logica de URLs fuera de los Models y la logica de negocio fuera del router.
Puedo usar MVC con TypeScript?
Si. TypeScript fortalece el patron tipando los campos del Model, las interfaces del Controller y los props de la View. Los tipos fuertes hacen obvio cuando una capa filtra hacia otra.
Como manejo formularios y validacion?
Las reglas de validacion viven en el Model. La View llama metodos del controller ante cambios de input, y el Controller pregunta al Model si los datos son validos. Los mensajes de error fluyen de vuelta a la View a traves del Controller.
La View deberia llamar la API directamente?
No. Las llamadas a la API pertenecen a servicios o repositorios. La View reenvia eventos al Controller, que coordina la llamada al servicio y actualiza el Model.
Como comparto estado entre componentes no relacionados?
Eleva el estado compartido a un Controller de nivel superior o usa una libreria de manejo de estado. MVC no prohíbe stores compartidos; solo pide mantener la logica de dominio fuera de las Views.
Donde pertenecen los efectos secundarios?
Los efectos secundarios como fetch, timers o acceso a storage pertenecen a servicios o controllers. Los Models deberian permanecer puros, y las Views deberian evitar efectos secundarios mas alla del renderizado.
Como manejo errores?
Los Controllers capturan errores de servicios y actualizan un campo del Model o retornan un tipo result. Las Views renderizan el estado de error. Manten el manejo de errores fuera de event handlers crudos de UI.
MVC puede funcionar con SSR?
Si. Los Models pueden poblarse en el servidor, los Controllers pueden instanciarse por request, y las Views pueden renderizar desde props iniciales. Solo evita referenciar APIs exclusivas del navegador en los Models.
Recursos Relacionados
Repository Pattern con Generics de TypeScript
Implementa el Repository pattern con generics de TypeScript para desacoplar la lógica de acceso a datos de los servicios de dominio usando interfaces.
PatternPatrón Decorator en pipelines HTTP
Usa el patrón Decorator para componer aspectos transversales como logging, métricas y reintentos en pipelines de peticiones HTTP sin modificar la lógica central
GuideGuía de Estrategia de Testing de Software
Una guía práctica para construir una estrategia de testing en capas con tests unitarios, de integración, de contrato y end-to-end que tu equipo realmente mantendrá.
RecipeRenderizado en el servidor (SSR)
Mejora el rendimiento y el SEO con renderizado en el servidor usando Next.js, Nuxt, Astro y otros frameworks con estrategias de hidratación.
RecipeCrea un servidor WebSocket (Python, Node.js, Java)
Cómo construir un servidor WebSocket para comunicación en tiempo real bidireccional, con gestión de conexiones, difusión de mensajes y heartbeats.