StackPractices
beginner Por Mathias Paulenko

Operaciones CRUD con MongoDB y Mongoose

Como realizar operaciones Crear, Leer, Actualizar y Eliminar en MongoDB usando Mongoose ODM con Node.js y Express

Mongoose proporciona una solucion basada en esquemas para modelar datos de aplicaciones en MongoDB. Maneja conversion de tipos, validacion, construccion de consultas y hooks de logica de negocio, haciendolo el ODM mas popular en el ecosistema Node.js.

Cuando Usar Esto

  • Necesitas una forma estructurada de interactuar con MongoDB desde Node.js. Consulta Parse JSON para manejo de documentos.
  • Quieres validacion automatica y hooks de middleware
  • Estas construyendo una API que requiere patrones relacionales en una base de datos documental. Consulta SQL Joins para patrones relacionales.

Requisitos Previos

  • MongoDB instalado localmente o un cluster de MongoDB Atlas
  • Node.js 18+

Solucion: Express + Mongoose

1. Instalar Dependencias

npm install express mongoose dotenv

2. Definir el Esquema

// models/User.js
import mongoose from 'mongoose';

const userSchema = new mongoose.Schema({
  email: {
    type: String,
    required: [true, 'El email es obligatorio'],
    unique: true,
    lowercase: true,
    trim: true,
  },
  name: {
    type: String,
    required: true,
    minlength: 2,
    maxlength: 100,
  },
  role: {
    type: String,
    enum: ['user', 'admin', 'moderator'],
    default: 'user',
  },
  isActive: {
    type: Boolean,
    default: true,
  },
}, {
  timestamps: true,
});

// Indice para consultas comunes
userSchema.index({ email: 1 });
userSchema.index({ role: 1, isActive: 1 });

export default mongoose.model('User', userSchema);

3. Conectar y Realizar CRUD

// app.js
import express from 'express';
import mongoose from 'mongoose';
import User from './models/User.js';

const app = express();
app.use(express.json());

await mongoose.connect(process.env.MONGODB_URI);

// CREAR
app.post('/users', async (req, res) => {
  try {
    const user = await User.create(req.body);
    res.status(201).json(user);
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

// LEER (con paginacion). Consulta [Pagination](/recipes/pagination/) para enfoques basados en cursor.
app.get('/users', async (req, res) => {
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const skip = (page - 1) * limit;

  const [users, total] = await Promise.all([
    User.find({ isActive: true })
      .select('-__v')
      .sort({ createdAt: -1 })
      .skip(skip)
      .limit(limit)
      .lean(),
    User.countDocuments({ isActive: true }),
  ]);

  res.json({
    data: users,
    pagination: { page, limit, total, pages: Math.ceil(total / limit) },
  });
});

// LEER UNO
app.get('/users/:id', async (req, res) => {
  const user = await User.findById(req.params.id).select('-__v');
  if (!user) return res.status(404).json({ error: 'Usuario no encontrado' });
  res.json(user);
});

// ACTUALIZAR
app.patch('/users/:id', async (req, res) => {
  const user = await User.findByIdAndUpdate(
    req.params.id,
    req.body,
    { new: true, runValidators: true }
  );
  if (!user) return res.status(404).json({ error: 'Usuario no encontrado' });
  res.json(user);
});

// ELIMINAR (eliminacion suave). Consulta [Soft Deletes](/recipes/soft-deletes/) para patrones.
app.delete('/users/:id', async (req, res) => {
  const user = await User.findByIdAndUpdate(
    req.params.id,
    { isActive: false },
    { new: true }
  );
  if (!user) return res.status(404).json({ error: 'Usuario no encontrado' });
  res.json({ message: 'Usuario desactivado' });
});

app.listen(3000, () => console.log('Servidor ejecutandose en el puerto 3000'));

4. Transacciones

// Operaciones atomicas entre colecciones. Consulta [Database Transactions](/recipes/database-transactions/) para patrones ACID.
app.post('/orders', async (req, res) => {
  const session = await mongoose.startSession();
  session.startTransaction();

  try {
    const order = await Order.create([{ ...req.body, status: 'pending' }], { session });
    await Product.updateOne(
      { _id: req.body.productId },
      { $inc: { stock: -req.body.quantity } },
      { session }
    );

    await session.commitTransaction();
    res.status(201).json(order[0]);
  } catch (err) {
    await session.abortTransaction();
    res.status(400).json({ error: err.message });
  } finally {
    session.endSession();
  }
});

Como Funciona

  1. Definicion de Esquema impone estructura mientras preserva la flexibilidad de MongoDB
  2. Middleware Hooks ejecutan validacion y transformacion antes/despues de operaciones
  3. Construccion de Consultas proporciona una API encadenable para consultas complejas
  4. Transacciones aseguran cumplimiento ACID entre multiples documentos

Consideraciones de Produccion

  • Habilita read preference secondary para cargas de trabajo intensivas en lectura en replica sets
  • Usa indices compuestos para campos de consulta frecuentemente combinados
  • Implementa paginacion basada en cursor para grandes datasets en lugar de skip/limit. Consulta Pagination para patrones de cursor.
  • Agrega plugins de Mongoose para patrones comunes (eliminacion suave, auditoria)

Tips de Rendimiento

  1. Usa createIndex con opción background. Previene que la creación de índices bloquee writes:
await User.collection.createIndex({ email: 1 }, { unique: true, background: true });
  1. Usa bulkWrite para inserts batch. Reduce round-trips:
const docs = Array.from({ length: 1000 }, (_, i) => ({
  insertOne: { document: { name: `User ${i}`, email: `user${i}@example.com` } },
}));

await User.bulkWrite(docs);
  1. Usa hint() para forzar el uso de un índice. Cuando el query planner elige el índice incorrecto:
await User.find({ status: 'active' }).hint({ status: 1, createdAt: -1 });
  1. Usa readPreference para replica sets. Enruta reads a secondaries:
mongoose.connect(uri, {
  readPreference: 'secondaryPreferred',
});
  1. Monitorea queries lentos con profiling. Habilita el profiler de base de datos:
// Habilitar profiling para queries más lentos que 100ms
await mongoose.connection.db.command({
  profile: 1,
  slowms: 100,
});

// Consultar el profiler
const slowQueries = await mongoose.connection.db
  .collection('system.profile')
  .find({ millis: { $gt: 100 } })
  .sort({ ts: -1 })
  .limit(10)
  .toArray();

Preguntas frecuentes

Debo usar Mongoose o el driver nativo?

Usa Mongoose para datos de aplicacion con necesidades de validacion. Usa el driver nativo para analiticas, pipelines de agregacion o maximo rendimiento.

Como manejo migraciones de esquema?

Usa migrate-mongo o escribe scripts de migracion idempotentes que se ejecuten en el despliegue.

Cuando debo usar referencias vs documentos embebidos?

Embebe cuando los datos se leen juntos y el crecimiento no acotado no se espera. Referencia cuando los datos se actualizan independientemente o crecen sin limite.