Node.js

Node

Tutorial Node: Estructura de carpetas

Aprende a organizar proyectos Node.js nativos usando MVC para separar controladores, modelos y vistas de forma profesional y escalable.

Aprende Node y certifícate

MVC (Model-View-Controller) aplicado a Node.js nativo

La arquitectura MVC en entornos Node.js nativos permite organizar aplicaciones en tres bloques que colaboran de forma modular. Después de haber trabajado con CRUD, autenticación y manejo de archivos en lecciones anteriores, es momento de ver cómo estructurar estos conceptos de manera profesional. El controlador recibe las peticiones y decide qué acción ejecutar, el modelo gestiona la información y la vista se encarga de presentar los resultados al cliente.

En el modelo, se define cómo se accede y manipula la información. Similar a las operaciones con MySQL que hemos visto, aquí centralizamos todas las consultas y operaciones de datos. Se recomienda mantener las funciones de lectura y escritura fuera de los controladores para que sea más escalable el código.

La vista se encarga de dar forma a la respuesta que llega al navegador u otra aplicación cliente. En las APIs que hemos estado construyendo, esto equivale a formatear respuestas JSON. En un proyecto basado en Node.js sin frameworks adicionales, la vista es esencialmente la serialización de objetos que facilitan la interoperabilidad con distintos clientes.

Por último, el controlador es el punto de entrada principal para cada petición. Empleando el módulo nativo http, un ejemplo simplificado podría lucir así:

import { createServer } from "node:http";

// Controlador que gestiona la petición
function mainController(req, res) {
    if (req.url === "/datos" && req.method === "GET") {
        const data = getModelData(); // Llama a un método del modelo
        const output = createView(data); // Genera la vista
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(output);
    } else {
        res.writeHead(404);
        res.end("Recurso no encontrado");
    }
}

// Ejemplo de funciones del modelo y la vista
function getModelData() {
    return { mensaje: "Saludo desde el modelo" };
}

function createView(data) {
    return JSON.stringify(data);
}

// Configuración del servidor
const server = createServer(mainController);
server.listen(3000, () => console.log("Servidor en marcha en el puerto http://localhost:3000"));

Este ejemplo muestra cómo la lógica de cada parte se mantiene separada. El controlador decide qué datos del modelo obtener y luego invoca a la vista para formatearlos. De este modo, el mantenimiento del proyecto resulta más flexible y la aplicación conserva una estructura clara que facilita la detección de posibles mejoras o errores.

Separación de responsabilidad (controlador vs lógica de negocio)

Al estructurar un proyecto de Node.js, es recomendable que el controlador solo gestione las interacciones directas con la petición y la respuesta. Este enfoque permite que las validaciones iniciales y el enrutamiento queden concentrados en un lugar concreto, mientras que la lógica de negocio reside en módulos o capas más específicas. De esta manera, el controlador evita sobrecargas y promueve la reutilización del código.

Resulta útil contar con un servicio o capa independiente que encapsule toda la lógica relacionada con la manipulación de datos y las reglas de negocio. Así, el controlador se limita a delegar la responsabilidad a ese servicio y a procesar la información de entrada para enviarla de forma coherente. Este patrón mejora la mantenibilidad y facilita la realización de pruebas automatizadas.

En el ejemplo siguiente, el controlador deriva todo el tratamiento a la capa de negocio, limitándose a controlar la petición y a emitir una respuesta adecuada:

import { createUser } from "./user.service.mjs";

// controlador: user.controller.mjs
export async function userController(req, res) {
    try {
        if (req.url === "/users" && req.method === "POST") {
            const requestBody = await parseRequestBody(req);
            const newUser = await createUser(requestBody);
            res.writeHead(201, { "Content-Type": "application/json" });
            res.end(JSON.stringify(newUser));
        }
    } catch (error) {
        res.writeHead(500, { "Content-Type": "text/plain" });
        res.end("Error interno");
    }
}

// Función para analizar el cuerpo de la solicitud
async function parseRequestBody(req) {
    return new Promise((resolve) => {
        let body = "";
        req.on("data", (chunk) => {
            body += chunk.toString();
        });
        req.on("end", () => {
            resolve(JSON.parse(body));
        });
    });
}

// capa de negocio: user.service.mjs
export async function createUser(userData) {
    // Aquí se ubican validaciones, hash de passwords, etc.
    // Similar a las operaciones que hemos visto en autenticación
    return { id: 1, ...userData };
}

Cada controlador puede servirse de múltiples capas o servicios, asegurando que solo maneje la comunicación entre el cliente y la lógica real. Este reparto de roles es especialmente valioso cuando implementamos middleware de autorización o manejo de archivos.

Uso de servicios o capas de dominio para aislar la lógica

En muchos proyectos de Node resulta efectivo utilizar un enfoque basado en servicios o capas de dominio como intermediarios entre los controladores y la fuente de datos. Esta capa concentra todas las operaciones y reglas específicas de la aplicación, permitiendo que los controladores se mantengan ligeros y centrados en la gestión de peticiones y respuestas.

Un servicio puede englobar validaciones de integridad, interacciones con bases de datos (como las operaciones CRUD que hemos practicado), y reglas empresariales que no deben mezclarse con la inmediatez del controlador. Este aislamiento facilita la actualización o sustitución de la base de datos o de la lógica reutilizable sin afectar a otras partes del proyecto.

Para estructurar el código, se acostumbra a dedicar un directorio exclusivo a los servicios, separados por áreas de negocio. Además de clarificar la responsabilidad de cada archivo, este diseño promueve la escalabilidad y la colaboración en equipo, puesto que cada cambio en la lógica de un servicio queda claramente delimitado.

A modo de ejemplo, si el proyecto debe manejar la facturación de usuarios, se crearía un fichero como "billingService.mjs" con las funciones de cálculo, descuentos y validaciones. Luego, desde cualquier controlador, bastaría con invocar dichas funciones sin exponer los detalles internos:

// billingService.mjs
export function calcularTotal(productos) {
    // Operaciones específicas de cálculo de importe
    return productos.reduce((acum, prod) => acum + prod.precio, 0);
}

export function aplicarDescuento(total, porcentaje) {
    return total - total * (porcentaje / 100);
}

export function validarPago(datosPayment) {
    // Lógica de validación similar a las validaciones
    // que hemos implementado en autenticación
    if (!datosPayment.email || !datosPayment.amount) {
        throw new Error("Datos de pago incompletos");
    }
    return true;
}

La estructura de carpetas resultante quedaría organizada así:

proyecto/
├── controllers/
│   ├── user.controller.js
│   ├── billing.controller.js
│   └── product.controller.js
├── models/
│   ├── user.model.js
│   └── product.model.js
├── services/
│   ├── auth.service.js
│   ├── billing.service.js
│   └── file.service.js
├── middleware/
│   └── authorization.js
├── test/
└── server.js

Con esta aproximación, el desarrollo de nuevas características resulta más mantenible, ya que es sencillo localizar dónde se deben aplicar ajustes o mejoras en la lógica. Además, cada capa conserva su responsabilidad bien definida y evita la duplicidad de código en diferentes controladores.

Este patrón MVC es especialmente útil cuando trabajamos con múltiples tipos de operaciones - desde autenticación hasta manejo de archivos y operaciones de base de datos - manteniendo todo organizado y escalable.

Aprende Node online

Otras lecciones de Node

Accede a todas las lecciones de Node y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Node y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender la separación de responsabilidades en arquitectura MVC.
  • Implementar controladores que gestionen peticiones en Node.js.
  • Utilizar modelos para manipular datos de forma escalable.
  • Generar vistas en diversos formatos como HTML y JSON.
  • Aplicar servicios o capas de dominio para aislar la lógica de negocio.
  • Facilitar la mantenibilidad y escalabilidad del código.
  • Promover la separación de lógica de negocio y controladores.