Cheat Sheet completa de MongoDB y Mongoose
Esta cheat sheet de MongoDB y Mongoose sirve como referencia practica para trabajar con documentos desde Node.js: conectar, modelar datos, consultar, actualizar, validar y evitar errores comunes.
MongoDB es la base de datos documental. Mongoose es la capa de modelado para Node.js que te da esquemas, modelos, validaciones, middleware y una API mas comoda para consultar.
Instalacion y conexion
Instala Mongoose en tu proyecto Node.js.
npm install mongoose
Conecta con una URI local o con MongoDB Atlas. La URI debe vivir en variables de entorno, no escrita directamente en el codigo.
import mongoose from 'mongoose';
const MONGO_URI = process.env.MONGO_URI;
if (!MONGO_URI) {
throw new Error('Falta MONGO_URI en las variables de entorno');
}
export async function connectDB() {
try {
await mongoose.connect(MONGO_URI);
console.log('MongoDB conectado');
} catch (error) {
console.error('Error conectando a MongoDB', error);
process.exit(1);
}
}
Ejemplos de URI:
# Local
MONGO_URI=mongodb://127.0.0.1:27017/miapp
# Atlas
MONGO_URI=mongodb+srv://usuario:password@cluster.mongodb.net/miapp
Schema y Model
El Schema define la forma del documento. El Model es la interfaz para leer y escribir en la coleccion.
import mongoose from 'mongoose';
const userSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
trim: true,
minlength: 2,
maxlength: 80,
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true,
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user',
},
age: {
type: Number,
min: 0,
},
active: {
type: Boolean,
default: true,
},
},
{
timestamps: true,
}
);
export const User = mongoose.model('User', userSchema);
Notas rapidas:
required: campo obligatorio.unique: crea un indice unico, no es una validacion normal de Mongoose.trim: elimina espacios al principio y al final.lowercase: normaliza a minusculas.enum: limita valores permitidos.timestamps: crea y mantienecreatedAtyupdatedAt.
Crear documentos
Usa create para crear y guardar directamente.
const user = await User.create({
name: 'Ada Lovelace',
email: 'ada@example.com',
age: 36,
});
console.log(user._id);
Tambien puedes instanciar y guardar.
const user = new User({
name: 'Grace Hopper',
email: 'grace@example.com',
});
await user.save();
Para insertar varios documentos:
await User.insertMany([
{ name: 'Linus', email: 'linus@example.com' },
{ name: 'Margaret', email: 'margaret@example.com' },
]);
Leer documentos
Consultas basicas:
const users = await User.find();
const activeUsers = await User.find({ active: true });
const oneUser = await User.findOne({ email: 'ada@example.com' });
const byId = await User.findById('660000000000000000000001');
Seleccionar campos:
const users = await User.find({ active: true }).select('name email role');
const withoutInternalFields = await User.find().select('-passwordHash -__v');
Ordenar, limitar y paginar:
const page = 1;
const limit = 20;
const users = await User.find({ active: true })
.sort({ createdAt: -1 })
.skip((page - 1) * limit)
.limit(limit);
Usa lean() cuando solo necesitas objetos planos y no documentos de Mongoose.
const users = await User.find({ active: true }).lean();
Filtros habituales
Operadores de comparacion:
await User.find({ age: { $gte: 18 } }); // mayor o igual
await User.find({ age: { $lt: 65 } }); // menor que
await User.find({ role: { $in: ['admin', 'user'] } });
await User.find({ email: { $ne: null } });
Combinaciones logicas:
await User.find({
$or: [{ role: 'admin' }, { age: { $gte: 18 } }],
});
await User.find({
$and: [{ active: true }, { age: { $gte: 18 } }],
});
Regex con cuidado:
await User.find({
name: { $regex: '^ada', $options: 'i' },
});
Si el texto viene del usuario, escapa caracteres especiales antes de construir una regex.
Actualizar documentos
Actualizar uno:
const updated = await User.findByIdAndUpdate(
userId,
{ $set: { name: 'Ada Byron' } },
{ new: true, runValidators: true }
);
Actualizar varios:
await User.updateMany(
{ active: false },
{ $set: { archivedAt: new Date() } },
{ runValidators: true }
);
Operadores utiles:
await User.updateOne({ _id: userId }, { $inc: { loginCount: 1 } });
await User.updateOne({ _id: userId }, { $unset: { temporaryToken: '' } });
await User.updateOne({ _id: userId }, { $set: { lastLoginAt: new Date() } });
Con upsert:
await User.updateOne(
{ email: 'ada@example.com' },
{ $set: { name: 'Ada Lovelace' } },
{ upsert: true, runValidators: true }
);
Borrar documentos
await User.findByIdAndDelete(userId);
await User.deleteOne({ email: 'ada@example.com' });
await User.deleteMany({ active: false });
En producto real, muchas veces conviene hacer borrado logico:
await User.findByIdAndUpdate(
userId,
{ $set: { active: false, deletedAt: new Date() } },
{ new: true }
);
Arrays y subdocumentos
const postSchema = new mongoose.Schema(
{
title: { type: String, required: true },
tags: [{ type: String, trim: true, lowercase: true }],
comments: [
{
body: { type: String, required: true },
author: { type: String, required: true },
createdAt: { type: Date, default: Date.now },
},
],
},
{ timestamps: true }
);
export const Post = mongoose.model('Post', postSchema);
Operadores para arrays:
await Post.updateOne({ _id: postId }, { $addToSet: { tags: 'mongodb' } });
await Post.updateOne({ _id: postId }, { $push: { comments: comment } });
await Post.updateOne({ _id: postId }, { $pull: { tags: 'draft' } });
Actualizar un subdocumento por condicion:
await Post.updateOne(
{ _id: postId, 'comments._id': commentId },
{ $set: { 'comments.$.body': 'Comentario editado' } }
);
Relaciones y populate
MongoDB no es relacional, pero puedes guardar referencias con ObjectId.
const postSchema = new mongoose.Schema(
{
title: { type: String, required: true },
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
},
{ timestamps: true }
);
export const Post = mongoose.model('Post', postSchema);
Crear un post con referencia:
await Post.create({
title: 'Introduccion a MongoDB',
author: user._id,
});
Leer con populate:
const post = await Post.findById(postId).populate('author', 'name email');
Regla practica:
- Si el dato siempre se lee junto y no crece sin limite, puede ir embebido.
- Si el dato se comparte, crece mucho o cambia de forma independiente, suele convenir referencia.
Indices
Los indices aceleran lecturas, pero encarecen escrituras. Crea indices para consultas reales.
userSchema.index({ email: 1 }, { unique: true });
userSchema.index({ role: 1, createdAt: -1 });
postSchema.index({ title: 'text', body: 'text' });
Consultar usando indice de texto:
await Post.find({ $text: { $search: 'mongodb mongoose' } });
Ver indices desde MongoDB:
db.users.getIndexes();
Analizar una consulta:
db.users.find({ email: 'ada@example.com' }).explain('executionStats');
Aggregation
Aggregation sirve para transformar, agrupar y calcular datos en la base de datos.
const result = await User.aggregate([
{ $match: { active: true } },
{
$group: {
_id: '$role',
total: { $sum: 1 },
averageAge: { $avg: '$age' },
},
},
{ $sort: { total: -1 } },
]);
Pipeline habitual:
await Order.aggregate([
{ $match: { status: 'paid' } },
{ $unwind: '$items' },
{
$group: {
_id: '$items.productId',
units: { $sum: '$items.quantity' },
revenue: { $sum: '$items.total' },
},
},
{ $sort: { revenue: -1 } },
{ $limit: 10 },
]);
Validaciones
Validaciones en el schema:
const productSchema = new mongoose.Schema({
name: { type: String, required: true, trim: true },
price: { type: Number, required: true, min: 0 },
sku: {
type: String,
required: true,
uppercase: true,
validate: {
validator(value) {
return /^[A-Z0-9-]+$/.test(value);
},
message: 'SKU invalido',
},
},
});
Validar antes de guardar:
const product = new Product({ price: -1 });
await product.validate();
En updates, recuerda runValidators: true:
await Product.findByIdAndUpdate(
productId,
{ $set: { price: -10 } },
{ runValidators: true, new: true }
);
Middleware
Middleware para ejecutar logica antes o despues de operaciones.
userSchema.pre('save', async function () {
if (!this.isModified('password')) return;
this.passwordHash = await hashPassword(this.password);
this.password = undefined;
});
userSchema.post('save', function (doc) {
console.log('Usuario guardado', doc._id);
});
No metas reglas de negocio grandes en middleware si eso hace el flujo dificil de entender. Para casos complejos suele ser mejor un servicio explicito.
Transacciones
Usa transacciones cuando varias escrituras deben confirmarse juntas.
const session = await mongoose.startSession();
try {
await session.withTransaction(async () => {
await Account.updateOne({ _id: fromId }, { $inc: { balance: -amount } }, { session });
await Account.updateOne({ _id: toId }, { $inc: { balance: amount } }, { session });
await Transfer.create([{ from: fromId, to: toId, amount }], { session });
});
} finally {
await session.endSession();
}
Las transacciones son utiles, pero no sustituyen un buen modelo de datos. Si todo depende de transacciones constantes, revisa si estas modelando demasiado relacional para MongoDB.
Errores comunes
Duplicado por indice unico:
try {
await User.create({ email: 'ada@example.com', name: 'Ada' });
} catch (error) {
if (error?.code === 11000) {
throw new Error('Ya existe un usuario con ese email');
}
throw error;
}
Validacion de Mongoose:
try {
await user.save();
} catch (error) {
if (error.name === 'ValidationError') {
const messages = Object.values(error.errors).map((item) => item.message);
console.log(messages);
}
}
ObjectId invalido:
if (!mongoose.isValidObjectId(req.params.id)) {
return res.status(400).json({ error: 'ID invalido' });
}
Seguridad
Reglas basicas:
- Guarda la URI en variables de entorno.
- No expongas usuarios, passwords ni connection strings en el frontend.
- Valida y limita el input antes de usarlo en queries.
- No pases directamente
req.bodyentero a$setsi hay campos privados. - Usa listas blancas de campos actualizables.
- Activa permisos minimos en el usuario de base de datos.
- No devuelvas
passwordHash, tokens ni datos internos. - Escapa regex si se construyen con texto del usuario.
Ejemplo de lista blanca:
const allowedFields = ['name', 'age'];
const update = {};
for (const field of allowedFields) {
if (req.body[field] !== undefined) {
update[field] = req.body[field];
}
}
const user = await User.findByIdAndUpdate(
req.params.id,
{ $set: update },
{ new: true, runValidators: true }
).select('-passwordHash');
Express CRUD basico
Ejemplo pequeno de rutas con validacion minima del ObjectId.
import express from 'express';
import mongoose from 'mongoose';
import { User } from './models/User.js';
export const router = express.Router();
router.get('/users', async (req, res, next) => {
try {
const users = await User.find({ active: true }).sort({ createdAt: -1 }).lean();
res.json(users);
} catch (error) {
next(error);
}
});
router.get('/users/:id', async (req, res, next) => {
try {
if (!mongoose.isValidObjectId(req.params.id)) {
return res.status(400).json({ error: 'ID invalido' });
}
const user = await User.findById(req.params.id).select('-passwordHash').lean();
if (!user) return res.status(404).json({ error: 'Usuario no encontrado' });
res.json(user);
} catch (error) {
next(error);
}
});
router.post('/users', async (req, res, next) => {
try {
const user = await User.create({
name: req.body.name,
email: req.body.email,
age: req.body.age,
});
res.status(201).json(user);
} catch (error) {
next(error);
}
});
router.patch('/users/:id', async (req, res, next) => {
try {
if (!mongoose.isValidObjectId(req.params.id)) {
return res.status(400).json({ error: 'ID invalido' });
}
const user = await User.findByIdAndUpdate(
req.params.id,
{ $set: { name: req.body.name, age: req.body.age } },
{ new: true, runValidators: true }
);
if (!user) return res.status(404).json({ error: 'Usuario no encontrado' });
res.json(user);
} catch (error) {
next(error);
}
});
router.delete('/users/:id', async (req, res, next) => {
try {
if (!mongoose.isValidObjectId(req.params.id)) {
return res.status(400).json({ error: 'ID invalido' });
}
await User.findByIdAndDelete(req.params.id);
res.status(204).send();
} catch (error) {
next(error);
}
});
Buenas practicas
- Disena documentos pensando en como se leen, no solo en como se guardan.
- Embebe datos pequenos y muy acoplados.
- Referencia datos grandes, compartidos o que cambian por separado.
- Crea indices para las queries frecuentes.
- Usa
lean()en lecturas que no necesitan metodos de documento. - Usa
select()para no devolver campos privados. - Usa
runValidators: trueen updates. - Usa paginacion en listados.
- Maneja errores
ValidationError, duplicados11000e IDs invalidos. - Mantén la URI y secretos fuera del repositorio.
- Prueba las queries importantes con datos parecidos a produccion.
Referencia rapida
// Crear
await User.create(data);
// Leer
await User.find(filter).select('name email').sort({ createdAt: -1 }).lean();
await User.findOne({ email });
await User.findById(id);
// Actualizar
await User.findByIdAndUpdate(id, { $set: update }, { new: true, runValidators: true });
await User.updateMany(filter, { $set: update });
// Borrar
await User.findByIdAndDelete(id);
await User.deleteMany(filter);
// Relaciones
await Post.findById(id).populate('author', 'name email');
// Agregacion
await User.aggregate([{ $match: filter }, { $group: { _id: '$role', total: { $sum: 1 } } }]);
// Transaccion
const session = await mongoose.startSession();
await session.withTransaction(async () => {
await Model.updateOne(filter, update, { session });
});
await session.endSession();