Vamos a construir un ejemplo práctico para ambas estructuras usando un caso sencillo: obtener una lista de usuarios.
- Ejemplo con la estructura básica.
- Ejemplo con la estructura extendida.
- Estructura extendida - Añadimos validaciones y manejo de errores
Nota: Node.js + Express + MongoDB + Mongoose. //Crear diagrama de flujo.
📦src
┣ 📂config
┃ ┗ 📜dbConfig.js
┣ 📂controllers
┃ ┗ 📜userController.js
┣ 📂models
┃ ┗ 📜Users.js
┣ 📂routes
┃ ┗ 📜userRoutes.js
┗ 📜app.js
📜index.js
-
src/config/dbConfig.js
Conexión a MongoDB usando Mongoose:const mongoose = require("mongoose"); const connectDB = async () => { try { await mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true, }); console.log("Database connected successfully!"); } catch (error) { console.error("Error connecting to database:", error.message); process.exit(1); } }; module.exports = connectDB;
-
src/models/Users.js
Define el esquema del modelo de usuario:const mongoose = require("mongoose"); const userSchema = new mongoose.Schema({ name: { type: String, required: true }, email: { type: String, required: true, unique: true }, password: { type: String, required: true }, }); module.exports = mongoose.model("User", userSchema);
-
src/controllers/userController.js
El controlador maneja la solicitud y responde al cliente:const Users = require("../models/Users"); const getUsers = async (req, res) => { try { const users = await Users.find(); // Obtiene todos los usuarios res.status(200).json(users); } catch (error) { res.status(500).json({ error: "Error fetching users" }); } }; module.exports = { getUsers };
-
src/routes/userRoutes.js
Define las rutas y las asocia al controlador:const express = require("express"); const router = express.Router(); const { getUsers } = require("../controllers/userController"); router.get("/users", getUsers); // Ruta para obtener usuarios module.exports = router;
-
src/app.js
Configura Express y registra las rutas:const express = require("express"); const connectDB = require("./config/dbConfig"); const userRoutes = require("./routes/userRoutes"); const app = express(); app.use(express.json()); app.use("/api", userRoutes); connectDB(); // Conexión a la base de datos module.exports = app;
-
index.js
Inicia el servidor:const app = require("./src/app"); const PORT = process.env.PORT || 8080; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });
- El cliente hace un
GETa/api/users. - La ruta en
userRoutes.jsdirige la solicitud al controladorgetUsersenuserController.js. - El controlador consulta al modelo
Userspara obtener los datos. - El modelo consulta la base de datos MongoDB.
- La respuesta se envía al cliente en formato JSON.
📦src
┣ 📂config
┃ ┗ 📜dbConfig.js
┣ 📂controllers
┃ ┗ 📜userController.js
┣ 📂models
┃ ┗ 📜Users.js
┣ 📂routes
┃ ┗ 📜userRoutes.js
┣ 📂services
┃ ┗ 📜userService.js
┣ 📂utils
┃ ┗ 📜dateUtils.js
┗ 📜app.js
📜index.js
-
src/services/userService.js
Centraliza la lógica de negocio:const Users = require("../models/Users"); const fetchAllUsers = async () => { // Aquí puedes agregar lógica adicional antes de consultar la base de datos const users = await Users.find(); return users; }; module.exports = { fetchAllUsers };
-
src/utils/dateUtils.js
Utilidad para formatear fechas (opcional):const formatDate = (date) => { return new Date(date).toLocaleDateString("en-US"); }; module.exports = { formatDate };
-
src/controllers/userController.js
El controlador ahora delega la lógica al servicio:const { fetchAllUsers } = require("../services/userService"); const getUsers = async (req, res) => { try { const users = await fetchAllUsers(); // Llama al servicio res.status(200).json(users); } catch (error) { res.status(500).json({ error: "Error fetching users" }); } }; module.exports = { getUsers };
-
Los demás archivos (
dbConfig,routes,app, etc.) son idénticos al ejemplo básico.
- El cliente hace un
GETa/api/users. - La ruta en
userRoutes.jsdirige la solicitud al controladorgetUsersenuserController.js. - El controlador llama al servicio
fetchAllUsersenuserService.js. - El servicio consulta al modelo
Userspara obtener los datos. - (Opcional) El servicio usa alguna utilidad (
utils) para procesar los datos (como formatear fechas). - La respuesta procesada se envía al cliente en formato JSON.
| Estructura básica | Estructura extendida |
|---|---|
| Controlador maneja toda la lógica. | Servicio maneja la lógica de negocio. |
| Menos archivos; más sencillo. | Más archivos; mejor organizado. |
| Puede volverse difícil de mantener. | Escalable y fácil de mantener. |
| Ideal para proyectos pequeños o MVPs. | Ideal para proyectos medianos o grandes. |
¡Vamos a mejorar los ejemplos! Agregaremos validaciones y un manejo de errores más robusto. Esto será útil para proyectos en crecimiento.
- Validaciones para verificar los datos que envía el cliente.
- Manejo de errores centralizado, usando un middleware para evitar duplicar lógica de manejo de errores en cada controlador.
📦src
┣ 📂config
┃ ┗ 📜dbConfig.js
┣ 📂controllers
┃ ┗ 📜userController.js
┣ 📂middlewares
┃ ┗ 📜errorHandler.js
┣ 📂models
┃ ┗ 📜Users.js
┣ 📂routes
┃ ┗ 📜userRoutes.js
┣ 📂services
┃ ┗ 📜userService.js
┣ 📂utils
┃ ┗ 📜validationUtils.js
┗ 📜app.js
📜index.js
Aquí creamos funciones reutilizables para validar datos:
const validateEmail = (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const validateUserInput = (userData) => {
const { name, email, password } = userData;
if (!name || name.length < 3) {
throw new Error("Name must be at least 3 characters long.");
}
if (!email || !validateEmail(email)) {
throw new Error("Invalid email format.");
}
if (!password || password.length < 6) {
throw new Error("Password must be at least 6 characters long.");
}
return true;
};
module.exports = { validateUserInput };Middleware para manejar errores en un solo lugar:
const errorHandler = (err, req, res, next) => {
console.error(err.message); // Registra el error en la consola
res.status(err.status || 500).json({
success: false,
message: err.message || "Internal Server Error",
});
};
module.exports = errorHandler;Validamos los datos antes de procesarlos y dejamos que el middleware maneje los errores:
const { fetchAllUsers } = require("../services/userService");
const { validateUserInput } = require("../utils/validationUtils");
const getUsers = async (req, res, next) => {
try {
const users = await fetchAllUsers();
res.status(200).json(users);
} catch (error) {
next(error); // Envía el error al middleware
}
};
const createUser = async (req, res, next) => {
try {
validateUserInput(req.body); // Valida la entrada del cliente
const newUser = await createUserService(req.body); // Llama al servicio para guardar el usuario
res.status(201).json(newUser);
} catch (error) {
next(error); // Envía el error al middleware
}
};
module.exports = { getUsers, createUser };Agregamos la ruta para crear usuarios:
const express = require("express");
const router = express.Router();
const { getUsers, createUser } = require("../controllers/userController");
router.get("/users", getUsers); // Ruta para obtener todos los usuarios
router.post("/users", createUser); // Ruta para crear un nuevo usuario
module.exports = router;Registramos el middleware de errores:
const express = require("express");
const connectDB = require("./config/dbConfig");
const userRoutes = require("./routes/userRoutes");
const errorHandler = require("./middlewares/errorHandler");
const app = express();
app.use(express.json());
app.use("/api", userRoutes);
app.use(errorHandler); // Middleware de manejo de errores
connectDB();
module.exports = app;Agregamos un servicio para guardar un usuario:
const Users = require("../models/Users");
const fetchAllUsers = async () => {
const users = await Users.find();
return users;
};
const createUserService = async (userData) => {
const user = new Users(userData);
await user.save();
return user;
};
module.exports = { fetchAllUsers, createUserService };- Cliente solicita todos los usuarios.
- Ruta
GET /usersllama al controladorgetUsers. - Controlador llama al servicio
fetchAllUsers, que consulta el modeloUsers. - Respuesta con los usuarios o error manejado por el middleware.
- Cliente envía datos (nombre, email, contraseña).
- Ruta
POST /usersllama al controladorcreateUser. - Controlador valida los datos con
validateUserInput. - Si la validación pasa, llama al servicio
createUserServicepara guardar al usuario. - Respuesta con el nuevo usuario o error manejado por el middleware.
- Validaciones centralizadas: Puedes agregar más reglas de validación en un solo archivo (
validationUtils.js). - Manejo de errores limpio: En lugar de escribir
try-catchen todos los controladores, el middleware se encarga. - Reutilización de lógica: Si necesitas validar o procesar datos, puedes reutilizar funciones y servicios.
- Escalabilidad: Agregar nuevas rutas, validaciones o servicios no afecta otras partes del código.
Vamos a analizar el flujo de trabajo en las dos estructuras que discutimos: la más sencilla (sin services y utils) y la extendida (con services y utils). De esta manera, entenderás cómo se comunican las partes del proyecto.
📦src
┣ 📂config
┃ ┗ 📜dbConfig.js
┣ 📂controllers
┃ ┗ 📜userController.js
┣ 📂models
┃ ┗ 📜Users.js
┣ 📂routes
┃ ┗ 📜userRoutes.js
┗ 📜app.js
📜index.js
-
El cliente realiza una solicitud (Request):
- Ejemplo:
GET /userspara obtener todos los usuarios.
- Ejemplo:
-
La solicitud pasa por la ruta (
routes):-
Aquí defines qué controlador manejará esta ruta.
-
Ejemplo: En
userRoutes.js:const express = require("express"); const router = express.Router(); const { getUsers } = require("../controllers/userController"); router.get("/users", getUsers); module.exports = router;
-
-
El controlador (
controllers) recibe la solicitud:-
El controlador contiene la lógica necesaria para manejar la solicitud y devolver la respuesta.
-
Puede interactuar directamente con el modelo.
-
Ejemplo: En
userController.js:const Users = require("../models/Users"); const getUsers = async (req, res) => { try { const users = await Users.find(); // Interactúa directamente con el modelo res.status(200).json(users); } catch (err) { res.status(500).json({ error: "Error fetching users" }); } }; module.exports = { getUsers };
-
-
El modelo (
models) interactúa con la base de datos:-
Si usas una base de datos como MongoDB con Mongoose, aquí defines los esquemas y consultas.
-
Ejemplo: En
Users.js:const mongoose = require("mongoose"); const userSchema = new mongoose.Schema({ name: String, email: String, password: String, }); module.exports = mongoose.model("User", userSchema);
-
-
Respuesta al cliente (Response):
- El controlador recibe los datos del modelo y los envía como respuesta al cliente.
- Cliente → Ruta (
routes) → Controlador (controllers) → Modelo (models) → Base de datos. - Base de datos → Modelo → Controlador → Respuesta al cliente.
Este flujo es simple y directo, pero puede complicarse si la lógica de negocio se vuelve muy compleja.
📦src
┣ 📂config
┃ ┗ 📜dbConfig.js
┣ 📂controllers
┃ ┗ 📜userController.js
┣ 📂models
┃ ┗ 📜Users.js
┣ 📂routes
┃ ┗ 📜userRoutes.js
┣ 📂services
┃ ┗ 📜userService.js
┣ 📂utils
┃ ┗ 📜dateUtils.js
┗ 📜app.js
📜index.js
-
El cliente realiza una solicitud (Request):
- Ejemplo:
GET /users.
- Ejemplo:
-
La solicitud pasa por la ruta (
routes):-
En
userRoutes.jsdefines qué controlador manejará la solicitud. -
Ejemplo:
const express = require("express"); const router = express.Router(); const { getUsers } = require("../controllers/userController"); router.get("/users", getUsers); module.exports = router;
-
-
El controlador (
controllers) recibe la solicitud:-
En lugar de manejar toda la lógica directamente, el controlador delega el trabajo a los
services. -
Ejemplo: En
userController.js:const { fetchAllUsers } = require("../services/userService"); const getUsers = async (req, res) => { try { const users = await fetchAllUsers(); // Llama al servicio res.status(200).json(users); } catch (err) { res.status(500).json({ error: "Error fetching users" }); } }; module.exports = { getUsers };
-
-
El servicio (
services) maneja la lógica de negocio:-
Aquí puedes centralizar lógica más compleja, como transformar datos o llamar a otros servicios externos.
-
Ejemplo: En
userService.js:const Users = require("../models/Users"); const fetchAllUsers = async () => { // Aquí podrías agregar lógica adicional antes de llamar al modelo const users = await Users.find(); return users; }; module.exports = { fetchAllUsers };
-
-
El modelo (
models) interactúa con la base de datos:- Igual que en la estructura básica, el modelo define el esquema y realiza las consultas a la base de datos.
-
Los
utilsse usan como herramientas auxiliares (opcional):- Si necesitas, por ejemplo, formatear fechas o validar datos, los
utilsse usan dentro de los servicios, controladores o incluso los modelos.
Ejemplo: Utilizar un
utilsen un servicio:const { formatDate } = require("../utils/dateUtils"); const fetchAllUsers = async () => { const users = await Users.find(); // Agregar una fecha formateada a cada usuario return users.map((user) => ({ ...user.toObject(), formattedDate: formatDate(user.createdAt), })); };
- Si necesitas, por ejemplo, formatear fechas o validar datos, los
-
Respuesta al cliente (Response):
- La información procesada por los
servicesvuelve al controlador, que la envía al cliente.
- La información procesada por los
- Cliente → Ruta (
routes) → Controlador (controllers) → Servicio (services) → Modelo (models) → Base de datos. - Base de datos → Modelo → Servicio → Controlador → Respuesta al cliente.
-
Usa la estructura básica si:
- Tu proyecto es pequeño y sencillo.
- No tienes lógica de negocio compleja.
-
Usa la estructura extendida si:
- Tu proyecto crecerá o ya tiene lógica compleja.
- Necesitas organizar y reutilizar código (con
servicesyutils).