MEN — Express + Nodejs + MongoDB con mongoose

Mauricio Garcia
11 min readOct 7, 2024

--

Introducción

En la story pasada [API CRUD — Node.js y Express], implementamos una versión funcional del CRUD con una simulación de datos en memoria. Esto significa que, aunque la API funcionaba, los datos se perdían cada vez que se reiniciaba el servidor, ya que no había persistencia en una base de datos real.

En esta nueva story, vamos a conectar la API con MongoDB para almacenar los datos de manera permanente. Para lograr esto, utilizaremos el paquete [Mongoose].

Antes de comenzar, te sugiero primero revisar:

mongoDB

Recordemos que MongoDB es una base de datos de documentos que ofrece una gran escalabilidad y flexibilidad, y un modelo de consultas e indexación avanzado [ref].

::Levantando el servicio

Antes de continuar, necesitamos verificar que el servicio de MongoDB esté ejecutándose. Para ello, ejecutamos la siguiente línea en la consola:

ps aux | grep -v grep | grep mongod

Si el servicio está funcionando, deberías ver algo similar a la siguiente imagen:

En caso de que no veas este mensaje, asegúrate de iniciar el servicio. La documentación oficial de MongoDB [ref][ref] recomienda utilizar Homebrew (si estás en macOS) para gestionar e iniciar MongoDB fácilmente con las configuraciones por defecto.

Para ejecutar el servicio de MongoDB con brew, utiliza el siguiente comando (en este caso, estamos ejecutando la versión 5):

brew services start mongodb-community@5.0

:: Configurando y conectando MongoDB

— Obtener URL de la base de datos

Para conectar tu aplicación a MongoDB, primero necesitas obtener la URL de la base de datos. Abre una terminal y ejecuta el comando mongo o mongosh, dependiendo de la versión que estés utilizando. Al conectarte, aparecerá un mensaje en la consola que te mostrará la dirección a la que se ha conectado. Verás algo similar a lo siguiente:

Esta URL (mongodb://127.0.0.1:27017/) es la que necesitas utilizar para que tu aplicación pueda conectarse a MongoDB.

— Almacenamiento de variables en .env

Como buena práctica, es recomendable almacenar la URL y el nombre de la base de datos en el archivo .env. Esto permite que la aplicación pueda cambiar fácilmente entre distintos entornos, como desarrollo, producción, entre otros, sin modificar el código directamente. Agrega las siguientes variables a tu archivo .env:

# Puerto del servidor
PORT = 3000

# Configuración de MongoDB
DATABASE_URL = mongodb://127.0.0.1:27017/
DATABASE = blog-messages

mongoose

Mongoose es un framework para MongoDB que actúa como un [ORM] (Object-Relational Mapping) entre la base de datos y JavaScript. Sirve para el modelado de objetos de manera elegante, facilitando la interacción con [MongoDB] en aplicaciones [Node.js].

Mongoose cuenta con las siguientes ventajas:

  • Modelado basado en esquemas: Proporciona una solución clara y estructurada para modelar los datos en MongoDB, dando forma a las colecciones.
  • Facilidad de uso: Ofrece una variedad de métodos útiles que simplifica la manipulación de datos en la aplicación.
  • Validación y conversión de tipos: Incluye características como la validación de datos, conversión de tipos y creación de consultas complejas de manera sencilla.
  • Modelado sin conexión: Permite definir los modelos de datos sin necesidad de estar conectado a la base de datos.

— ¿Qué es un ORM?

Un Object-Relational Mapping es una técnica que los desarrolladores utilizan para mapear y convertir datos entre sistemas incompatibles, como entre una base de datos y un lenguaje de programación orientado a objetos. Este enfoque facilita la manipulación de datos de la base de datos utilizando conceptos y estructuras de POO.

::Instalando y conectando a Mongodb con mongoose

El primer paso es instalar el paquete mongoose en el proyecto. Asegúrate de estar en la raíz del proyecto y ejecuta el siguiente comando en la consola:

npm install mongoose

Es importante saber que cada vez que modifiques las variables en el archivo .env, asegúrate de reiniciar el servidor para que los cambios sean aplicados.

Abre el archivo app.js y añade las siguientes importaciones, incluida la de mongoose:

require('dotenv').config();
const express = require('express');
const cors = require('cors');
// Importamos mongoose
const { mongoose, Types: { ObjectId } } = require('mongoose');

const app = express();
const port = process.env.PORT || 3000;

//-- Middlewares Globales
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
  • mongoose — Es un objeto que actúa como el cliente que se conecta a la base de datos. Proporciona métodos para realizar operaciones CRUD (crear, leer, actualizar, eliminar) sobre una base de datos MongoDB.
  • ObjectId — Se utiliza para consultar documentos en una colección por su campo _id.

A continuación, vamos a crear la conexión a MongoDB utilizando la URL configurada en el archivo .env o la URL por defecto:

// Conectar a Mongoose
const urlDB = process.env.DATABASE_URL || "mongodb://127.0.0.1:27017";

async function connectToDatabase() {
try {
await mongoose.connect(urlDB);
console.log("Conectado a la base de datos");
} catch (error) {
console.error("Error al conectar a MongoDB:", error);
}
}

connectToDatabase();

Este código establece la conexión a la base de datos utilizando Mongoose. Si hay algún problema al conectarse, se controla el error y se mostrará en la consola.

El siguiente paso es seleccionar la base de datos y la colección de mensajes:

async function connectToDatabase() {
try {
await mongoose.connect(urlDB);
console.log("Conectado a la base de datos");

// Selecciona la base de datos
mongoose.connection.useDb(process.env.DATABASE || 'blog-messages');
console.log(`Usando la base de datos`);
} catch (error) {
console.error("Error al conectar a MongoDB:", error);
}
}

connectToDatabase();

:: Creando un Modelo en Mongoose

Una de las ventajas que mencionamos al utilizar Mongoose es:

“Nos permite crear los modelos de datos sin necesidad de tener una conexión a la base de datos.”

Esto es posible gracias a la arquitectura interna de Mongoose, que utiliza un modelo de buffer. Este modelo permite definir y manipular los esquemas de datos antes de establecer una conexión con la base de datos. Sin embargo, es importante comprender cómo funciona este proceso: Mongoose no generará ningún error si el modelo nunca llega a conectarse a la base de datos. Los datos se mantendrán en un estado de buffer hasta que se realice la conexión.

Es bastante útil, pero también requiere mucha precaución, ya que puedes estar interactuando con los modelos sin que los datos se guarden realmente en la base de datos, si la conexión nunca se establece correctamente.

La sintaxis de un esquema es la siguiente[ref]:

Crea una carpeta llamada src/modelsy dentro el archivo message.js y define el siguiente esquema:

const mongoose = require('mongoose');

const messageSchema = new mongoose.Schema({
message: { type: String, required: true },
}, {
timestamps: true
});

module.exports = mongoose.model('Message', messageSchema);

Donde:

  • message— Es un campo del tipo String, que será obligatorio en cada documento. La propiedad required: true asegura que este campo debe estar presente al crear o guardar un documento.
  • timestamps: true— Esta opción agrega dos campos automáticamente a cada documento (createdAt: Fecha y hora de creación del documento, updatedAt: Fecha y hora de la última actualización del documento.)

Para facilitar el uso de la colección en las rutas, añadimos un middleware que pase la colección a las rutas a través de req.context. Importa el modelo de Message en app.js y pásalo a las rutas.


async function connectToDatabase() {
try {
await mongoose.connect(urlDB);
console.log("Conectado a la base de datos");
mongoose.connection.useDb(process.env.DATABASE || 'blog-messages');
console.log(`Usando la base de datos`);

// Importar el modelo de Message
const Message = require('./models/message');

// Middleware para inyectar el modelo en las rutas
app.use((req, res, next) => {
req.context = { models: { Message }, ObjectId };
next();
});
} catch (error) {
console.error("Error al conectar a MongoDB:", error);
}
}

connectToDatabase();

Este middleware permite acceder a la colección de mensajes en cualquier ruta mediante req.context.models.Message.

Finalmente, importa y monta las rutas de mensajes, añade el middleware de manejo de errores y el que arranca el servidor:

// Importar las rutas de mensajes
const messageRoutes = require("./routes/messages");
app.use("/messages", messageRoutes);

// Middleware de manejo de errores centralizado
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
message: err.message || "Error interno del servidor",
error: process.env.NODE_ENV === "production" ? {} : err,
});
});

// Iniciar el servidor
app.listen(port, () => {
console.log(`Servidor escuchando en http://localhost:${port}`);
});

A partir de aquí, tus rutas podrán acceder a la base de datos de manera eficiente.

Código completo de app.js

require('dotenv').config();
const express = require('express');
const cors = require('cors');
const { mongoose, Types: { ObjectId } } = require('mongoose'); // Importamos mongoose

const app = express();
const port = process.env.PORT || 3000;

//-- Middlewares Globales
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

const urlDB = process.env.DATABASE_URL || 'mongodb://127.0.0.1:27017';

async function connectToDatabase() {
try {
// Conectar a MongoDB
await mongoose.connect(urlDB);
console.log('Conectado a la base de datos con mongoose');

// Selecciona la base de datos
mongoose.connection.useDb(process.env.DATABASE || 'blog-messages');
console.log(`Usando la base de datos`);

// Importar el modelo de Message
const Message = require('./models/message');

// Middleware para inyectar el modelo en las rutas
app.use((req, res, next) => {
req.context = { models: { Message }, ObjectId };
next();
});

// Importar las rutas de mensajes
const messageRoutes = require('./routes/messages');
app.use('/messages', messageRoutes);

// Middleware de manejo de errores centralizado
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
message: err.message || 'Error interno del servidor',
error: process.env.NODE_ENV === 'production' ? {} : err,
});
});

// Iniciar el servidor
app.listen(port, () => {
console.log(`Servidor escuchando en http://localhost:${port}`);
});
} catch (error) {
console.error('Error al conectar a MongoDB:', error);
}
}

connectToDatabase();

Si has seguido todos los pasos correctamente, las peticiones deberán de seguir funcionando sin ningún problema. Recuerda que en routes/messages.js aún estamos utilizando una simulación de datos en memoria, por lo que las operaciones no están interactuando directamente con MongoDB.

— Implementando mongoose en las rutas

El siguiente paso es actualizar las peticiones para que interactúen con la base de datos MongoDB.

Vamos a comenzar con la petición más sencilla. Abre el archivo src/routes/messages.js y actualiza la ruta de la siguiente manera:

// Obtener todos los mensajes
router.get('/', async (req, res, next) => {
try {
const messages = await req.context.models.Message.find();
return res.json(messages);
} catch (er) {
const error = new Error(
'Error al obtener los mensajes de la base de datos'
);
error.status = 500;
return next(error);
}
});

Donde:

  • req.context.models.Message— Es la referencia a los de Message de MongoDB que almacenamos previamente en req.context en la app.js.
  • find() — Para obtener todos los mensajes desde MongoDB.
  • Si ocurre un error, lo capturamos en el bloque catch y lo enviamos al middleware de manejo de errores utilizando next(error).

Este cambio asegura que los mensajes ahora se obtengan directamente de MongoDB en lugar de la simulación en memoria.

Vamos a continuar con la petición para obtener un mensaje específico por su messageId. Abre el archivo src/routes/messages.js y actualiza la siguiente ruta:

// Obtener un mensaje específico por ID
router.get('/:messageId', async (req, res, next) => {
const { messageId } = req.params;
try {
if (!req.context.ObjectId.isValid(messageId)) {
const error = new Error(`ID ${messageId} no es válido`);
error.status = 400;
return next(error);
}

const myid = new req.context.ObjectId(messageId);
const message = await req.context.models.Message.findOne({
_id: myid,
});

if (!message) {
const error = new Error(`Mensaje con ID ${messageId} no encontrado`);
error.status = 404;
return next(error);
}

return res.json(message);
} catch (er) {
const error = new Error(
`Error al obtener el mensaje con ID ${messageId} desde la base de datos`
);
error.status = 500;
return next(error);
}
});

Donde:

  • req.params — Se extrae el parámetro messageId de la URL y se utiliza para buscar el mensaje específico.
  • req.context.ObjectId.isValidValidamos que el parámetro messageId sea válido.
  • req.context.ObjectId — Para asegurar MongoDB reconozca el valor como un objeto de tipo ObjectId, en lugar de una cadena normal.
  • req.context.models.Message— Es la referencia a los modelos de Message de MongoDB que almacenamos previamente en req.context en la app.js.
  • findOne() — Para buscar el mensaje que tenga el _id igual al messageId.

A continuación, vamos a implementar la lógica para guardar un nuevo mensaje. Abre el archivo src/routes/messages.js y actualiza la siguiente ruta:

// Agregar un nuevo mensaje
router.post('/', async (req, res, next) => {
try {
const { message } = req.body;

if (!message) {
const error = new Error('La propiedad "message" es requerida');
error.status = 404;
return next(error);
}

// Crear el nuevo mensaje
const newMessage = new req.context.models.Message({ message });
const savedMessage = await newMessage.save();

// Validar si la inserción fue exitosa
if (!savedMessage) {
const error = new Error('Error al guardar el mensaje');
error.status = 500;
return next(error);
}

return res.status(201).json(savedMessage);
} catch (er) {
const error = new Error('Error al agregar el mensaje a la base de datos');
error.status = 500;
return next(error);
}
});

Donde:

  • req.body — Cuerpo de la solicitud para extraer la propiedad message.
  • req.context.models.Message— Es la referencia a los modelos de Message de MongoDB que almacenamos previamente en req.context en la app.js.
  • newMessage.save() — Para guardar el nuevo mensaje en la colección messages.
  • Validamos si savedMessage existe y no es null.

Finalmente, vamos a continuar con la petición para eliminar un mensaje específico por su messageId. Abre el archivo src/routes/messages.js y actualiza la siguiente ruta:

// Eliminar un mensaje por ID
router.delete('/:messageId', async (req, res, next) => {
const { messageId } = req.params;

try {
if (!req.context.ObjectId.isValid(messageId)) {
const error = new Error(`ID ${messageId} no es válido`);
error.status = 400;
return next(error);
}

const myid = new req.context.ObjectId(messageId);
// Buscar y eliminar el mensaje
const result = await req.context.models.Message.deleteOne({
_id: myid,
});

if (result.deletedCount === 0) {
const error = new Error(`Mensaje con ID ${messageId} no encontrado`);
error.status = 404;
return next(error);
}

return res.send(`Mensaje con ID ${messageId} eliminado.`);
} catch (err) {
return next(
new Error(
`Error al eliminar el mensaje de la base de datos con ID ${messageId}`
)
);
}
});

Donde:

  • req.params — Se extrae el parámetro messageId de la URL y se utiliza para buscar el mensaje específico.
  • req.context.ObjectId.isValidValidamos que el parámetro messageId sea válido.
  • req.context.ObjectId — Para asegurar MongoDB reconozca el valor como un objeto de tipo ObjectId, en lugar de una cadena normal.
  • req.context.models.Message— Es la referencia a los modelos de Message de MongoDB que almacenamos previamente en req.context en la app.js.
  • deleteOne() — Para eliminar el mensaje en la colección messages que coincida con el id proporcionado.
  • deletedCount — Para verificar si el mensaje fue correctamente eliminado. Si no lo fue, retrona 0.

— Validando las respuestas

Si has seguido los pasos correctamente, las peticiones deberán de funcionar como la siguiente animación.

— Descarga el código

Github[branch: with-mongoose][ref].

Conclusión

Has aprendido a levantar un servidor desde cero utilizando Node.js y Express, además de aplicar buenas prácticas como el uso de variables de entorno y el ruteo modular. También has logrado conectar tu aplicación a MongoDB de manera eficiente con mongoose, lo que te fue posible crear, leer, actualizar y eliminar (CRUD) registros de una base de datos.

--

--

No responses yet