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
- Usa índices: Para queries frecuentes
- Lean queries: Cuando no necesites métodos de Mongoose
- Projection: Solo selecciona campos necesarios
- Populate con cuidado: Puede ser costoso
- Validación: Define en el schema
- Middleware: Para lógica reutilizable
- 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.