Inicio / Artículos / Más allá de CRUD: Diez hábitos arquitectónicos que mantienen las aplicaciones MERN mantenibles.

Más allá de CRUD: Diez hábitos arquitectónicos que mantienen las aplicaciones MERN mantenibles.

Aprende los hábitos a nivel de sistema que mantienen una aplicación MERN saludable a medida que crece: propiedad de los datos, contratos API, estado derivado, código en capas, cargas útiles eficientes y errores consistentes.

1832 palabras

La mayoría de los tutoriales sobre MERN terminan con una aplicación CRUD funcional: MongoDB almacena los datos, Express expone algunas rutas, React muestra una lista y, a veces, hay una pantalla de inicio de sesión. Esa aplicación funciona, pero rara vez sigue funcionando sin cambios a medida que crece. A medida que se añaden funcionalidades y colaboradores, las APIs se vuelven difíciles de modificar, el estado deja de estar sincronizado, las páginas se ralentizan y depurar consume más tiempo que desarrollar. Rara vez son las herramientas la causa. Esta guía aborda diez hábitos arquitectónicos que resuelven esos problemas, para que pueda considerar una aplicación MERN como un sistema único en lugar de cuatro bibliotecas separadas.

Trate MERN como un flujo de datos, no como una lista de herramientas

La descripción habitual de esta tecnología se limita a sus componentes:

MongoDB + Express + React + Node.js

Eso es preciso, pero no dice nada sobre cómo colaboran las partes, algo similar a describir un automóvil como un motor, cuatro ruedas y un volante. Una imagen más útil muestra cómo los datos van desde el usuario hasta la base de datos y de vuelta:

User
   │
   ▼
React
   │
HTTP
   │
   ▼
Express + Node
   │
Database Queries
   │
   ▼
MongoDB

Cada flecha representa un límite con sus propias reglas: qué puede enviar el navegador, qué acepta el servidor y qué almacena la base de datos. La mayoría de las prácticas que se detallan a continuación tienen que ver con decidir qué ocurre en esos límites.

1. Asignar un responsable a cada dato

Al desarrollar una nueva funcionalidad, primero pregúntese quién es el responsable de los datos involucrados. En muchos proyectos recientes la respuesta no está clara. Los detalles del usuario actual pueden encontrarse en el estado de un componente, en una tienda Redux, en localStorage, en la respuesta de una API reciente y en algún otro caché, todo al mismo tiempo. Tarde o temprano una copia queda desactualizada con respecto a las demás, y la interfaz muestra dos versiones contradictorias del mismo hecho.

Una división más clara de responsabilidades:

  • MongoDB es la fuente de verdad para los datos persistidos.
  • El backend es responsable de las reglas de negocio que determinan cómo pueden cambiar esos datos.
  • El frontend muestra los datos y solicita cambios a través de la API; cualquier copia en el lado del cliente es un caché, no una fuente autorizada.

El principio rector es que cada dato tiene exactamente una fuente de verdad, y todas las demás copias saben que pueden estar desactualizadas. Las bibliotecas de estado del servidor existen principalmente para gestionar ese caché de forma explícita; consulte reconsiderando el estado del servidor con React Query y Redux para abordar ese aspecto del problema.

2. Diseñe APIs como contratos

Un endpoint típico se ve así:

app.get("/users", async (req, res) => {
  const users = await User.find();
  res.json(users);
});

Funciona, pero también promete silenciosamente que la respuesta siempre será un array de documentos completos del usuario, independientemente de los campos que tenga el modelo. Una vez que una aplicación móvil, un panel de control, una integración con socios u otro equipo dependa de esa estructura, cambiarla puede dañarlos.

Antes de agregar un endpoint, decida:

  • qué campos devuelve exactamente, en lugar de entregar el modelo en bruto (lo cual también podría revelar campos internos o sensibles);
  • sí se puede modificar posteriormente sin afectar a los clientes, o si es necesario implementar un sistema de versionado;
  • qué otros sistemas probablemente lo utilizarán.

Una API bien diseñada puede sobrevivir a varios interfaces de usuario; una mal diseñada se convierte en deuda técnica en cuestión de meses.

3. Almacena la menor cantidad posible de estado en React

React se presenta como una biblioteca de interfaz de usuario, pero en las aplicaciones reales la mayor parte de las dificultades radica en el estado. Un error frecuente es almacenar en el estado valores que podrían calcularse:

const [users, setUsers] = useState([]);
const [filteredUsers, setFilteredUsers] = useState([]);

Aquí filteredUsers está determinado íntegramente por users. Almacenarlo por separado implica que cada actualización debe mantener ambos sincronizados, y olvidarse una vez genera una lista desactualizada. Es mejor calcularlo durante el renderizado:

const filteredUsers = users.filter(user => user.active);

La regla es almacenar solo aquello que no se puede calcular y derivar todo lo demás. Si una derivación resulta realmente costosa, useMemo puede cachearla, pero sigue siendo datos derivados y no una segunda fuente de verdad.

4. Recuerde que CRUD es la parte fácil

Muchos proyectos se detienen en las cuatro operaciones básicas:

Create
Read
Update
Delete

Un backend de producción incluye mucho más que esas operaciones: validación, autenticación, autorización, reglas de negocio, limitación de frecuencia, registros de auditoría, registro de actividades y notificaciones. Compare un método de creación ingenuo:

await User.create(req.body);

con una versión que primero verifica la entrada

if (!isValid(req.body))
    throw new Error("Invalid input");

y confirma que quien llama tiene permiso para actuar antes de escribir:

if (!canCreateUser(req.user))
    throw new Error("Unauthorized");await User.create(req.body);

La versión ingenua también conlleva riesgo de asignación masiva: pasar req.body directamente a create permite que un cliente establezca cualquier campo aceptado por el esquema, incluyendo algo como una bandera role. Es necesario validar y seleccionar explícitamente los campos permitidos. Tenga en cuenta también que una verificación de permisos fallida es conceptualmente un 403 (prohibido), y no un 401, independientemente de lo que indique el mensaje de error. Escribir datos es sencillo; protegerlos es donde la ingeniería de backend se vuelve complicada.

5. Separar las responsabilidades en capas

La diferencia más clara entre un códigobase de aficionados y uno profesional radica en dónde reside la lógica. Mezclar responsabilidades conduce a controladores de rutas llenos de consultas a la base de datos, componentes React repletos de reglas de validación y controladores saturados de lógica empresarial, y cada uno de esos archivos sigue creciendo.

Un backend en capas asigna una tarea por capa:

Routes
   │
Controllers
   │
Services
   │
Repositories
   │
Database

Las rutas asignan URLs a los controladores, estos convierten las solicitudes HTTP en llamadas a funciones, los servicios almacenan las reglas de negocio y los repositorios se comunican con la base de datos. Las unidades pequeñas y de propósito único son más fáciles de probar y modificar. Para un análisis más detallado, consulte el diseño en capas de una API Node.js.

6. Corrija primero el rendimiento en la fuente de datos

Cuando se les pregunta cómo acelerar una aplicación React, la mayoría de los desarrolladores recurren a useMemo, React.memo y useCallback. Estos elementos son útiles, pero muchos problemas de rendimiento comienzan antes incluso de que React intervenga. Imagine una solicitud como esta:

GET /users

que devuelve

50,000 users

mientras que la pantalla solo muestra

10 users

Ninguna cantidad de memorización compensa el envío y análisis de decenas de miles de registros innecesarios. Aborde el problema en la fuente:

  • Paginar los resultados;
  • Filtrar en el servidor;
  • Proyectar solo los campos que necesita el cliente;
  • Comprimir las respuestas;
  • Cachear de forma intencionada, con un plan claro de invalidación.

El componente que se renderiza más rápido es aquel que nunca recibe datos que no necesita.

7. Hacer que el manejo de errores sea parte del diseño

El código de desarrollo a menudo ignora errores como este:

try {
   ...
}
catch(error){
   console.log(error);
}

El registro y paso a otro tema oculta el fallo tanto para el cliente como para los sistemas de monitoreo. Las APIs en producción necesitan errores que sean consistentes y legibles por máquinas:

return res.status(400).json({
    message: "Invalid email address",
    code: "INVALID_EMAIL"
});

Una estructura estable que incluye un mensaje legible para humanos y un código legible para máquinas permite que la interfaz frontal asocie códigos con mensajes específicos de la UI, que los registros se agrupen por código, que se activen alertas ante tasas inusuales y que la depuración comience a partir de una categoría conocida en lugar de un rastro de llamadas. Las fallas son inevitables; el objetivo es manejarlas de manera predecible.

8. Organizar el código por funcionalidad

Con veinte archivos, cualquier estructura de carpetas funciona. Con quinientos, es sumamente importante. Un diseño agrupado por tipo técnico distribuye una misma funcionalidad en toda la estructura jerárquica:

routes/
controllers/
models/

Agrupar por funcionalidad mantiene todo lo relacionado con un mismo dominio en un solo lugar:

users/
    routes.js
    controller.js
    service.js
    validation.js
orders/
    routes.js
    controller.js
    service.js

Cuando cambia la lógica de los pedidos, se abre solo la carpeta orders. Las carpetas de funcionalidades también facilitan enormemente la asignación de responsabilidades, las revisiones de código y su posterior extracción a servicios separados.

9. Piense en sistemas, no en tickets

Una solicitud de funcionalidad como “añadir inicio de sesión” puede resolverse de forma limitada, con un formulario y una ruta. Un enfoque orientado a sistemas plantea las preguntas relacionadas: cómo funciona la autenticación de principio a fin, dónde se almacenan los tokens, cómo se aplican los permisos, qué ocurre cuando un token expira y cómo iniciará sesión un futuro cliente móvil. Responder a estas preguntas de antemano cuesta un poco más hoy, pero evita tener que reescribir el código más adelante.

10. Elija los compromisos de forma deliberada

Ninguna arquitectura es la mejor en todas las situaciones. Cada opción implica ciertos beneficios y también costos:

  • Arquitectura simple: más rápida de desarrollar, pero más difícil de escalar.
  • Microservicios: escalabilidad independiente, pero mayor complejidad operativa.
  • Estatuto global: fácil compartición entre componentes, pero más difícil de depurar.
  • Diseño normalizado de la base de datos: menos duplicaciones, pero más uniones o búsquedas.
  • Caché agresivo: respuestas más rápidas, pero el problema constante de la invalidación del caché.

Los ingenieros excepcionales no son aquellos que conocen todos los patrones, sino aquellos que saben explicar cuándo cada patrón merece su costo.

Cómo suelen deteriorarse los proyectos MERN en crecimiento

Etapa 1: todo es simple

La primera versión cubre lo esencial:

CRUD
Authentication
Dashboard
Deployment

El código es pequeño, y todos lo comprenden.

Etapa 2: el crecimiento revela atajos

Llegan más usuarios, funcionalidades y desarrolladores. La lógica duplicada, los puntos de extremo inconsistentes, las páginas lentas, el estado complicado y la depuración dolorosa aparecen por todo el código.

Etapa 3: se culpan las herramientas

El equipo concluye que React no es escalable o que elegir MongoDB fue un error. Por lo general, ninguna de estas afirmaciones es cierta. La arquitectura simplemente nunca evolucionó junto con la aplicación.

Una analogía de restaurante para las capas

Imagínense la pila como un restaurante. MongoDB es la despensa que almacena todos los ingredientes. Express y Node son la cocina: deciden qué se cocina, cómo se prepara y quién puede hacer pedidos. React es el área de servicio, donde se presentan los platos listos a los clientes. Los clientes no necesitan saber cómo funciona la cocina, y a la cocina no le importa cómo se colocan los platos en la mesa. Cada parte hace bien su trabajo, lo cual es exactamente la separación que necesita una aplicación MERN.

Puntos clave

Conocer MERN no se trata tanto de escribir consultas, rutas y componentes, sino más bien de entender los caminos que sigue la información, colocar las reglas empresariales en la capa adecuada, evolucionar las APIs sin dañar a los clientes, mantener el estado al mínimo y reconocer cómo las decisiones tomadas temprano tienen un impacto acumulativo.

  • Asignen un responsable a cada dato y traten todas las copias restantes como caché.
  • Trate los puntos de extremo como contratos y devuelva formas explícitas y deliberadas.
  • Derive el estado en lugar de duplicarlo.
  • Valide, autorice y incluya en la lista blanca los campos antes de escribir cualquier cosa.
  • Mantenga las cargas útiles pequeñas en la fuente antes de optimizar su renderizado.
  • Devuelva errores consistentes y codificados, y agrupe el código por funcionalidad.
  • Cuando surge una nueva funcionalidad, la pregunta más útil no es cómo construirla, sino a dónde pertenece cada responsabilidad.

    Lecturas relacionadas