MongoDB y Mongoose: Mejores Prácticas y Patrones

20 de diciembre de 2025
Osman Jimenez
MongoDB Mongoose Bases de Datos

Dominando MongoDB con Mongoose

MongoDB es la base de datos NoSQL más popular. Aprende a usarla efectivamente con Mongoose para aplicaciones Node.js escalables.

Conexión a MongoDB

import mongoose from 'mongoose';

const connectDB = async () => {
  try {
    await mongoose.connect(process.env.MONGODB_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      maxPoolSize: 10,
      serverSelectionTimeoutMS: 5000,
      socketTimeoutMS: 45000
    });
    console.log('MongoDB conectado');
  } catch (error) {
    console.error('Error de conexión:', error);
    process.exit(1);
  }
};

connectDB();

Definir Schemas

import { Schema, model } from 'mongoose';

const userSchema = new Schema({
  name: {
    type: String,
    required: [true, 'El nombre es requerido'],
    trim: true,
    minlength: [3, 'Mínimo 3 caracteres'],
    maxlength: [50, 'Máximo 50 caracteres']
  },
  email: {
    type: String,
    required: true,
    unique: true,
    lowercase: true,
    match: [/^\S+@\S+\.\S+$/, 'Email inválido']
  },
  password: {
    type: String,
    required: true,
    minlength: 8,
    select: false // No incluir en queries por defecto
  },
  role: {
    type: String,
    enum: ['user', 'admin', 'moderator'],
    default: 'user'
  },
  profile: {
    avatar: String,
    bio: { type: String, maxlength: 500 },
    socialLinks: {
      twitter: String,
      github: String
    }
  },
  isActive: {
    type: Boolean,
    default: true
  },
  lastLogin: Date
}, {
  timestamps: true, // createdAt, updatedAt
  toJSON: { virtuals: true },
  toObject: { virtuals: true }
});

// Virtual property
userSchema.virtual('fullProfile').get(function() {
  return `${this.name} (${this.email})`;
});

// Middleware pre-save
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  
  const bcrypt = await import('bcrypt');
  this.password = await bcrypt.hash(this.password, 10);
  next();
});

// Método de instancia
userSchema.methods.comparePassword = async function(candidatePassword) {
  const bcrypt = await import('bcrypt');
  return await bcrypt.compare(candidatePassword, this.password);
};

// Método estático
userSchema.statics.findByEmail = function(email) {
  return this.findOne({ email: email.toLowerCase() });
};

export const User = model('User', userSchema);

CRUD Operations

// Create
const user = await User.create({
  name: 'John Doe',
  email: 'john@example.com',
  password: 'password123'
});

// Read
const users = await User.find({ isActive: true })
  .select('name email')
  .limit(10)
  .sort({ createdAt: -1 });

const user = await User.findById(userId);

// Update
const updated = await User.findByIdAndUpdate(
  userId,
  { $set: { name: 'Jane Doe' } },
  { new: true, runValidators: true }
);

// Delete
await User.findByIdAndDelete(userId);

Queries Avanzadas

// Operadores de comparación
const users = await User.find({
  age: { $gte: 18, $lte: 65 },
  role: { $in: ['user', 'admin'] },
  name: { $regex: /john/i }
});

// Operadores lógicos
const users = await User.find({
  $or: [
    { email: 'john@example.com' },
    { name: 'John Doe' }
  ],
  $and: [
    { isActive: true },
    { role: 'admin' }
  ]
});

// Projection
const users = await User.find()
  .select('name email -_id')
  .lean(); // Retorna objetos planos (más rápido)

// Populate (relaciones)
const postSchema = new Schema({
  title: String,
  author: { type: Schema.Types.ObjectId, ref: 'User' },
  comments: [{ type: Schema.Types.ObjectId, ref: 'Comment' }]
});

const posts = await Post.find()
  .populate('author', 'name email')
  .populate({
    path: 'comments',
    select: 'text createdAt',
    options: { limit: 5, sort: { createdAt: -1 } }
  });

Aggregation Pipeline

// Estadísticas de usuarios por rol
const stats = await User.aggregate([
  { $match: { isActive: true } },
  { $group: {
    _id: '$role',
    count: { $sum: 1 },
    avgAge: { $avg: '$age' }
  }},
  { $sort: { count: -1 } }
]);

// Posts más populares
const popularPosts = await Post.aggregate([
  { $lookup: {
    from: 'comments',
    localField: '_id',
    foreignField: 'postId',
    as: 'comments'
  }},
  { $addFields: {
    commentCount: { $size: '$comments' }
  }},
  { $sort: { commentCount: -1 } },
  { $limit: 10 },
  { $project: {
    title: 1,
    commentCount: 1,
    author: 1
  }}
]);

Índices para Performance

// Índice simple
userSchema.index({ email: 1 });

// Índice compuesto
userSchema.index({ role: 1, isActive: 1 });

// Índice de texto para búsqueda
userSchema.index({ name: 'text', bio: 'text' });

// Búsqueda de texto
const results = await User.find(
  { $text: { $search: 'john developer' } },
  { score: { $meta: 'textScore' } }
).sort({ score: { $meta: 'textScore' } });

// Índice TTL (auto-delete)
const sessionSchema = new Schema({
  token: String,
  createdAt: { type: Date, default: Date.now, expires: 3600 }
});

Transactions

const session = await mongoose.startSession();
session.startTransaction();

try {
  // Operación 1
  const user = await User.create([{
    name: 'John',
    email: 'john@example.com'
  }], { session });
  
  // Operación 2
  await Account.create([{
    userId: user[0]._id,
    balance: 1000
  }], { session });
  
  await session.commitTransaction();
} catch (error) {
  await session.abortTransaction();
  throw error;
} finally {
  session.endSession();
}

Paginación

async function paginateUsers(page = 1, limit = 10) {
  const skip = (page - 1) * limit;
  
  const [users, total] = await Promise.all([
    User.find()
      .skip(skip)
      .limit(limit)
      .sort({ createdAt: -1 }),
    User.countDocuments()
  ]);
  
  return {
    users,
    pagination: {
      page,
      limit,
      total,
      pages: Math.ceil(total / limit)
    }
  };
}

Mejores Prácticas

  1. Usa índices: Para queries frecuentes
  2. Lean queries: Cuando no necesites métodos de Mongoose
  3. Projection: Solo selecciona campos necesarios
  4. Populate con cuidado: Puede ser costoso
  5. Validación: Define en el schema
  6. Middleware: Para lógica reutilizable
  7. Connection pooling: Configura maxPoolSize

Conclusión

MongoDB con Mongoose es una combinación poderosa para aplicaciones Node.js. Dominar estos patrones y mejores prácticas te permitirá construir aplicaciones escalables y performantes.