Ya tenemos conexión y modelos. Toca mover datos. CRUD son las cuatro operaciones que sostienen cualquier aplicación con estado: crear, leer, actualizar y borrar. Con Mongoose cada una tiene varias formas de escribirse, y no todas son equivalentes: unas validan y otras no, unas devuelven el documento anterior y otras el nuevo, unas ejecutan la consulta y otras solo la construyen.
Esta lección recorre las cuatro con sus trampas reales, traduce los errores de Mongoose a la jerarquía del módulo 6, y culmina con el momento anunciado desde la primera lección del módulo: src/catalogo-datos.js se retira y src/repositorios/eventos.js ocupa su lugar con la misma firma pública, sin que los controladores ni el dominio se enteren.
Contenido
- Crear documentos
- Errores de clave duplicada y la jerarquía del módulo 6
- Leer:
Queryperezosos, proyecciones ylean() - Ordenar, limitar y paginar
- Operadores de consulta esenciales
- Actualizar:
runValidators,$set,$inc - Borrar y borrado lógico
- El repositorio: jubilando
catalogo-datos.js POST /pedidoscontra la base de datos- Errores de Mongoose y su estado HTTP
Crear documentos
Hay dos caminos, y la diferencia no es estética.
// Camino 1: instanciar y guardar. Permite manipular el documento antes de escribir.
const evento = new Evento({
eventoId: 'evt-001', titulo: 'Concierto de Otono', sala: 'Teatro Almendra',
organizadorId: 'org-almendra', categoria: 'musica', estado: 'publicado', duracionMinutos: 95,
sesiones: [
{ sesionId: 'ses-001-1', fechaHora: new Date('2026-10-03T20:00:00Z'), aforo: 400, vendidas: 312, precioCentimos: 2500 },
{ sesionId: 'ses-001-2', fechaHora: new Date('2026-10-04T19:00:00Z'), aforo: 400, vendidas: 289, precioCentimos: 2500 },
],
});
// Todavia no hay nada en la base: el documento vive en memoria.
const guardado = await evento.save(); // valida, dispara pre('save') y escribe
// Camino 2: crear directamente. Equivale a new + save en una linea.
const otro = await Evento.create({ eventoId: 'evt-002', /* ... */ });
// Insercion masiva: una sola ida y vuelta para muchos documentos.
const varios = await Evento.insertMany([documentoA, documentoB], { ordered: false });save() y create() devuelven el documento ya persistido, con su _id, sus timestamps y sus virtuals. insertMany es mucho más rápido que un bucle de create, pero por defecto es ordenado: si el tercer documento falla, los dos primeros ya están escritos y los siguientes no se intentan; con { ordered: false } intenta todos y te informa de los fallidos, que es lo que querrás en una semilla. Cuál usar: new + save() te deja aplicar lógica entre construir y escribir (calcular el total, generar códigos); create() es más directo cuando los datos ya vienen completos.
Errores de clave duplicada y la jerarquía del módulo 6
eventoId y codigo tienen índice único. Si insertas un duplicado, MongoDB responde con E11000 duplicate key error. Si no lo traduces, el manejador del módulo 6 lo tratará como un bug y devolverá un 500. Pero no es un bug: es un conflicto de estado operativo, y merece un 409.
// src/repositorios/errores-mongo.js
'use strict';
const { ConflictoDeEstado, ErrorDeValidacion } = require('../errores.js');
/**
* Traduce un error de Mongoose/MongoDB a la jerarquia de la aplicacion. Si no lo
* reconoce lo devuelve tal cual: sera un 500, y bien esta, porque significa que
* es un fallo que no habiamos previsto.
*/
function traducirErrorDeMongo(error) {
// Clave duplicada: violacion de un indice unico -> 409.
if (error.code === 11000) {
const campo = Object.keys(error.keyPattern ?? {})[0] ?? 'desconocido';
return new ConflictoDeEstado(`Ya existe un registro con el mismo ${campo}`, {
codigo: 'ESTADO_INVALIDO', detalles: { campo },
});
}
// Validacion del esquema: uno o varios campos incumplen las reglas -> 400.
if (error.name === 'ValidationError') {
return new ErrorDeValidacion('El documento no cumple el esquema', {
codigo: 'DATOS_INVALIDOS',
detalles: Object.entries(error.errors).map(([campo, f]) => ({ campo, mensaje: f.message })),
});
}
// Identificador con forma invalida ('hola' no es un ObjectId) -> 400, no 500.
if (error.name === 'CastError') {
return new ErrorDeValidacion(`El valor de '${error.path}' no es valido`, {
codigo: 'DATOS_INVALIDOS', detalles: { campo: error.path, valor: String(error.value) },
});
}
return error;
}
module.exports = { traducirErrorDeMongo };Fíjate dónde vive esta traducción: en la capa de repositorio, no en el controlador. Es coherente con la arquitectura de la lección 07-01: el controlador no debe saber que existe MongoDB, y por tanto tampoco qué es un E11000. El repositorio traduce del lenguaje del motor al de la aplicación.
Leer: Query perezosos, proyecciones y lean()
Aquí está la sorpresa que descoloca a casi todo el mundo la primera vez:
// Esto NO ejecuta ninguna consulta: construye un objeto Query.
const consulta = Evento.find({ sala: 'Teatro Almendra' });
console.log(consulta.constructor.name); // 'Query'
// Query tiene .then(), asi que await lo ejecuta: es un "thenable", no una
// promesa. Por eso se puede refinar antes de ejecutar.
const publicados = await consulta.where('estado').equals('publicado').sort({ titulo: 1 }).limit(20);Que sea perezoso es una ventaja: puedes construir la consulta por partes según los filtros opcionales que llegan por query string y ejecutarla al final. La trampa es que una consulta sin await no hace nada y no da error. Regla: siempre await, o .exec(), que devuelve una promesa de verdad y mejora las trazas de pila.
const catalogo = await Evento.find({ estado: 'publicado' }); // array, vacio si no hay
const uno = await Evento.findOne({ eventoId: 'evt-001' }); // documento o null
const porId = await Evento.findById('507f1f77bcf86cd799439011'); // documento o nullNinguna lanza si no encuentra nada: devuelven null o [], y convertir ese null en un RecursoNoEncontrado es trabajo del repositorio o del controlador. Con .orFail() lanza en vez de devolver null, lo que a veces es más cómodo.
Proyección: pedir solo los campos necesarios. Menos bytes, menos memoria, menos trabajo del motor.
// Inclusiva: solo estos campos (mas _id, salvo que lo excluyas).
await Evento.find({ estado: 'publicado' }, { eventoId: 1, titulo: 1, _id: 0 });
// Exclusiva: todo menos esto. No se pueden mezclar inclusion y exclusion
// (salvo con _id, que es la unica excepcion).
await Evento.find({}).select('-sesiones');Ese último ejemplo importa: la portada del catálogo no necesita las 7 sesiones con su aforo y su precio, solo el título y la sala.
lean() devuelve objetos JavaScript planos en vez de documentos de Mongoose. Por defecto, Mongoose envuelve cada resultado en una instancia con seguimiento de cambios, getters, virtuals y métodos: construirla es caro. Un objeto lean no tiene save(), ni virtuals, ni conversión de getters —te llegan los tipos BSON crudos—, pero se construye casi gratis.
La regla: usa lean() en toda lectura que termine en res.json(); no lo uses cuando vayas a modificar el documento. En listados grandes la diferencia es de varias veces en tiempo y memoria. En Escena Viva hay un matiz: nuestro repositorio devuelve objetos del dominio, así que hará lean() y luego Evento.desdeJSON(...), más barato que hidratar dos veces.
Ordenar, limitar y paginar
// Paginacion clasica por desplazamiento.
const eventos = await Evento.find({ estado: 'publicado' })
.sort({ titulo: 1 })
.skip((pagina - 1) * porPagina)
.limit(porPagina)
.lean();Para las primeras páginas es perfecto. Pero skip se degrada linealmente: para servir la página 5.000 el motor localiza y descarta 100.000 documentos antes de empezar a devolver; no los salta mágicamente, los recorre. Además, si alguien inserta o borra entre dos peticiones, verás elementos repetidos o te saltarás alguno, porque el desplazamiento se calcula sobre un conjunto que ha cambiado. La alternativa es la paginación por cursor (o keyset): en vez de "sáltate 100.000", dices "dame los siguientes a partir de este valor".
/** Paginacion por cursor sobre un campo ordenado y unico (aqui, el _id). */
async function paginarEventos({ despuesDe = null, limite = 20 } = {}) {
const filtro = { estado: 'publicado' };
// Solo documentos posteriores al ultimo visto: usa el indice, no descarta nada.
if (despuesDe) filtro._id = { $gt: despuesDe };
const documentos = await Evento.find(filtro).sort({ _id: 1 }).limit(limite).lean();
return { documentos, siguiente: documentos.length === limite ? documentos.at(-1)._id : null };
}El coste es constante independientemente de la profundidad y es estable ante inserciones. El precio: no puedes saltar a la página 37, solo avanzar. Para un catálogo con desplazamiento infinito o una API de consumo automático, es lo correcto.
Operadores de consulta esenciales
| Operador | Significado | Ejemplo en Escena Viva |
|---|---|---|
$eq / $ne |
Igual (implícito) / distinto | { estado: { $ne: 'borrador' } } |
$gt / $gte / $lt / $lte |
Comparaciones | { precioCentimos: { $lte: 3000 } } |
$in / $nin |
Está / no está en la lista | { sala: { $in: ['Teatro Almendra', 'Auditorio Ribera'] } } |
$and / $or |
Lógicos | Publicados o del propio organizador |
$regex / $exists |
Expresión regular / el campo existe | Búsqueda por título |
$expr |
Comparar dos campos entre sí | vendidas < aforo |
$elemMatch |
Condiciones sobre el mismo elemento de un array | Sesiones en un rango |
// 1. Eventos con alguna sesion en un rango de fechas.
// OJO: sin $elemMatch, las dos condiciones pueden cumplirlas sesiones DISTINTAS.
const rango = { $gte: new Date('2026-03-01'), $lt: new Date('2026-03-16') };
await Evento.find({ sesiones: { $elemMatch: { fechaHora: rango } } }).lean();
// 2. Eventos con alguna sesion con entradas libres: comparar dos campos del
// mismo documento exige $expr, porque el filtro normal compara con literales.
await Evento.find({
estado: 'publicado',
sesiones: { $elemMatch: { $expr: { $lt: ['$vendidas', '$aforo'] } } },
}).lean();
// 3. Busqueda por titulo insensible a mayusculas. Escapamos la entrada del
// usuario: un regex sin escapar es un vector de denegacion de servicio.
await Evento.find({ titulo: { $regex: escapar(termino), $options: 'i' } }).lean();El punto 1 es un error clásico: { 'sesiones.fechaHora': { $gte: a, $lt: b } } no significa "una sesión dentro del rango", sino "alguna sesión posterior a a y alguna anterior a b", que puede cumplir un evento sin ninguna sesión en marzo.
Actualizar: runValidators, $set, $inc
// Sin traer el documento: rapido, pero no dispara pre('save').
// Devuelve { acknowledged, matchedCount, modifiedCount, upsertedId }.
await Evento.updateOne({ eventoId: 'evt-003' }, { $set: { estado: 'publicado' } });
// Actualizar y recibir el documento resultante.
const actualizado = await Evento.findByIdAndUpdate(
id,
{ $set: { duracionMinutos: 120 } },
{ new: true, runValidators: true },
);Las dos opciones son casi obligatorias:
new: truedevuelve el documento después del cambio. Por defecto —y esto sorprende a todo el mundo— devuelve el anterior. Sin ella responderás al cliente con datos viejos.runValidators: trueejecuta los validadores del esquema. Sin ella te saltas la validación por completo: podrías ponerestado: 'inventado'aunque haya unenum, oaforo: -5aunque hayamin: 1. Es el agujero más grande y silencioso de Mongoose. Actívalo globalmente conmongoose.set('runValidators', true). Y ojo con una limitación real: al validar una actualización,thises la consulta, no el documento, así que un validador que comparavendidasconaforono tiene acceso aaforosi no lo estás actualizando también. La invariante crítica no puede descansar solo ahí.
| Operador | Qué hace | Uso en Escena Viva |
|---|---|---|
$set / $unset |
Asigna / elimina un campo | Cambiar estado |
$inc |
Suma (o resta) atómicamente | Incrementar vendidas |
$push / $pull |
Añade / quita de un array | Añadir o retirar una sesión |
$addToSet / $min / $max |
Añade si no está / asigna si es menor o mayor | Etiquetas, marcas históricas |
Y aquí aparece la primera pieza del problema del aforo:
// Incremento ATOMICO del contador de una sesion concreta. El operador posicional $
// apunta al elemento del array que ha casado con el filtro.
await Evento.updateOne(
{ eventoId: 'evt-001', 'sesiones.sesionId': 'ses-001-1' },
{ $inc: { 'sesiones.$.vendidas': 2 } },
);¿Por qué es un avance? Porque $inc no hace leer-modificar-escribir en tu proceso: le manda al motor la instrucción "suma 2 a este campo", y el motor la aplica bajo su propio bloqueo de documento. Dos peticiones simultáneas con $inc: 1 dejan el contador en +2, nunca en +1. Se acabó la pérdida de actualizaciones.
Pero no basta, y conviene verlo ahora para no llevarse una falsa sensación de seguridad: $inc suma incondicionalmente. Si quedaba una entrada y llegan dos compras, ambas suman y vendidas acaba valiendo aforo + 1. Hemos resuelto la pérdida de escrituras, no la comprobación de la condición. La pieza que falta —un filtro que forme parte de la misma operación atómica— llega en la lección 07-06.
Borrar y borrado lógico
await Evento.deleteOne({ eventoId: 'evt-009' });
await Evento.findByIdAndDelete(id); // devuelve el borrado, o null
const { deletedCount } = await Entrada.deleteMany({ estado: 'anulada' });
// Pero en Escena Viva casi nunca borramos pedidos ni entradas: los marcamos.
await Pedido.findByIdAndUpdate(pedidoId, { $set: { estado: 'anulado' } }, { new: true, runValidators: true });
await Entrada.updateMany({ pedidoId }, { $set: { estado: 'anulada' } });Las razones son de negocio: contabilidad (un pedido cobrado y luego anulado debe seguir apareciendo en el cierre del mes), trazabilidad (con el idPeticion del módulo 6 podemos reconstruir qué pasó, pero no si el documento no existe), integridad referencial (sin claves foráneas, borrar un pedido deja entradas huérfanas) y errores humanos (un borrado lógico se revierte cambiando un campo; uno físico, restaurando una copia de seguridad, con suerte). El precio —todas las consultas deben filtrar por estado— es bajo y previsible.
El repositorio: jubilando catalogo-datos.js
Ha llegado el momento. src/catalogo-datos.js exponía obtenerCatalogo y obtenerEventoPorId leyendo datos/eventos.json. El nuevo módulo expone exactamente lo mismo, leyendo MongoDB.
// src/repositorios/eventos.js
'use strict';
const { Evento: ModeloEvento } = require('../modelos/evento.js');
const { Evento } = require('../dominio/evento.js');
const { traducirErrorDeMongo } = require('./errores-mongo.js');
/**
* Convierte un documento plano de Mongo en una instancia del dominio.
* Es la frontera: de aqui hacia arriba, nadie sabe que existe Mongo.
*/
function aDominio({ eventoId, sesiones, ...resto }) {
return Evento.desdeJSON({
...resto,
id: eventoId,
sesiones: sesiones.map(({ sesionId, fechaHora, ...datos }) => ({
...datos, // aforo, vendidas, precioCentimos
id: sesionId,
fechaHora: fechaHora.toISOString(),
})),
});
}
/** Catalogo completo, como instancias del dominio. */
async function obtenerCatalogo() {
try {
// lean(): solo leemos y transformamos, no necesitamos documentos de Mongoose.
const docs = await ModeloEvento.find({ estado: { $ne: 'borrador' } }).sort({ titulo: 1 }).lean();
return docs.map(aDominio);
} catch (error) {
throw traducirErrorDeMongo(error);
}
}
/** Un evento por su identificador de negocio, o null si no existe. */
async function obtenerEventoPorId(eventoId) {
const documento = await ModeloEvento.findOne({ eventoId }).lean();
return documento ? aDominio(documento) : null;
}
module.exports = { obtenerCatalogo, obtenerEventoPorId };El cambio en los controladores es literalmente una línea: donde src/controladores/eventos.js hacía require('../catalogo-datos.js'), ahora hace require('../repositorios/eventos.js'). Eso es todo. listarEventos, cargarEvento, obtenerEvento y listarSesionesDelEvento siguen funcionando sin tocar una coma, porque siguen recibiendo instancias de Evento del dominio con sus getters ocupacion, agotado y recaudacionCentimos. Esto es lo que compra el patrón repositorio, y es la misma puerta por la que entrará Sequelize en la lección 07-05.
POST /pedidos contra la base de datos
El controlador ya recibe datos validados por zod en req.datosValidados. Lo que cambia es de dónde salen y adónde van.
// src/repositorios/pedidos.js (requires omitidos por brevedad)
'use strict';
// Genera un codigo de entrada EV-<anio>-<6 digitos>.
const generarCodigo = (anio, n) => `EV-${anio}-${String(n).padStart(6, '0')}`;
async function crearPedido({ usuarioId, sesionId, cantidad, canal }) {
const evento = await Evento.findOne({ 'sesiones.sesionId': sesionId });
if (!evento) {
throw new RecursoNoEncontrado(`No existe la sesion ${sesionId}`, { codigo: 'SESION_NO_ENCONTRADA' });
}
const sesion = evento.sesiones.find((elemento) => elemento.sesionId === sesionId);
const libres = sesion.aforo - sesion.vendidas;
if (libres < cantidad) {
throw new ConflictoDeEstado('No quedan entradas suficientes', {
codigo: 'AFORO_INSUFICIENTE',
detalles: { libres, solicitadas: cantidad },
});
}
try {
// ATENCION: entre la comprobacion de aforo y este $inc hay una ventana en la
// que otra peticion puede colarse. Sigue existiendo sobreventa. Lo resolvemos
// en la leccion 07-06 con una actualizacion atomica condicional.
await Evento.updateOne(
{ eventoId: evento.eventoId, 'sesiones.sesionId': sesionId },
{ $inc: { 'sesiones.$.vendidas': cantidad } },
);
const pedido = await Pedido.create({
usuarioId, eventoId: evento.eventoId, sesionId, cantidad, canal,
totalCentimos: sesion.precioCentimos * cantidad,
estado: 'pagado',
});
const anio = new Date().getUTCFullYear();
const base = await Entrada.countDocuments();
const entradas = await Entrada.insertMany(
Array.from({ length: cantidad }, (unused, i) => ({
codigo: generarCodigo(anio, base + i + 1), pedidoId: pedido._id, sesionId, estado: 'valida',
})),
);
return { pedido, entradas };
} catch (error) {
throw traducirErrorDeMongo(error);
}
}
module.exports = { crearPedido };Deja ese comentario en mayúsculas donde está. Es el problema del curso, ahora con nombre y línea concreta: hemos ganado atomicidad por operación, pero no por conjunto, y además el pedido y las entradas pueden crearse sin que el aforo se haya reservado, o al revés. En la 07-06 esta función se reescribirá entera y el comentario desaparecerá.
Errores de Mongoose y su estado HTTP
| Error | Cuándo aparece | Traducción | Estado |
|---|---|---|---|
ValidationError |
Un campo incumple el esquema | DATOS_INVALIDOS |
400 |
CastError |
findById('hola'), fecha no parseable |
DATOS_INVALIDOS |
400 |
E11000 |
Índice único violado | ESTADO_INVALIDO |
409 |
DocumentNotFoundError |
orFail() sin resultado |
..._NO_ENCONTRADO |
404 |
MongoServerSelectionError / MongoNetworkError |
La base no responde | SERVICIO_EXTERNO_CAIDO |
503 |
El CastError merece atención especial porque es el fallo más frecuente en producción: un cliente pide GET /pedidos/12345 con un identificador que no es un ObjectId, Mongoose no puede convertirlo y lanza. Sin traducción, tu aplicación devuelve un 500 y tu panel de alertas se llena de errores que no son bugs: son peticiones mal formadas y merecen un 400. La tabla ESTADO_POR_CODIGO del módulo 6 hace el resto; solo hay que darle el código correcto.
Errores Comunes y Consejos
- Olvidar
runValidators: true. Tuenumy tusmindejan de existir en las actualizaciones. - Olvidar
new: true. Devuelves el estado anterior al cambio y pasas media tarde buscando el bug en el frontend. - No usar
await. UnQuerysin ejecutar no falla: simplemente no pasa nada, o pasa a destiempo. - Hidratar documentos completos para solo serializarlos.
lean()y una proyección son la optimización más barata que existe. - Paginar con
skipen un catálogo grande. Funciona hasta que no funciona, y falla justo cuando tienes tráfico. - Interpolar entrada de usuario en un
$regex. Un patrón como(a+)+$puede colgar el motor. Escapa siempre. - Consejo: usa
.orFail()cuando la ausencia sea un error real, y activamongoose.set('debug', true)en desarrollo: verás cada consulta que se envía y descubrirás consultas que no sabías que hacías.
Ejercicios
Ejercicio 1: pedidos por usuario
Añade a src/repositorios/pedidos.js una función listarPedidosDeUsuario(usuarioId, { pagina, porPagina }) que devuelva los pedidos no anulados de un usuario, del más reciente al más antiguo, con proyección de sesionId, cantidad, totalCentimos, estado y createdAt, usando lean(). Indica qué índice necesita.
Ejercicio 2: buscador del catálogo
Escribe buscarEventos({ sala, categoria, desde, hasta, soloConEntradas }) que construya el filtro de forma incremental (añadiendo solo las condiciones recibidas) y devuelva los eventos publicados que casen. Usa $elemMatch para el rango de fechas y $expr para las entradas libres.
Ejercicio 3: anular un pedido
Implementa anularPedido(pedidoId) que compruebe que el pedido existe y está en estado pagado o emitido (si no, ConflictoDeEstado con ESTADO_INVALIDO), lo marque como anulado, marque sus entradas como anuladas y devuelva el aforo. Anota qué pasa si el proceso muere entre dos de esas operaciones.
Soluciones
Ejercicio 1.
async function listarPedidosDeUsuario(usuarioId, { pagina = 1, porPagina = 20 } = {}) {
return Pedido.find({ usuarioId, estado: { $ne: 'anulado' } })
.select('sesionId cantidad totalCentimos estado createdAt')
.sort({ createdAt: -1 })
.skip((pagina - 1) * porPagina)
.limit(porPagina)
.lean();
}Índice: { usuarioId: 1, estado: 1, createdAt: -1 }, siguiendo la regla ESR — igualdad primero, ordenación después y en el mismo sentido.
Ejercicio 2.
async function buscarEventos({ sala, categoria, desde, hasta, soloConEntradas = false } = {}) {
const filtro = { estado: 'publicado' };
if (sala) filtro.sala = sala;
if (categoria) filtro.categoria = categoria;
const condiciones = {};
if (desde) condiciones.fechaHora = { $gte: new Date(desde) };
if (hasta) condiciones.fechaHora = { ...condiciones.fechaHora, $lte: new Date(hasta) };
if (soloConEntradas) condiciones.$expr = { $lt: ['$vendidas', '$aforo'] };
if (Object.keys(condiciones).length > 0) filtro.sesiones = { $elemMatch: condiciones };
return Evento.find(filtro).sort({ titulo: 1 }).lean();
}Ejercicio 3.
async function anularPedido(pedidoId) {
const pedido = await Pedido.findById(pedidoId).orFail();
if (!['pagado', 'emitido'].includes(pedido.estado)) {
throw new ConflictoDeEstado(`Un pedido ${pedido.estado} no se puede anular`, { codigo: 'ESTADO_INVALIDO' });
}
pedido.estado = 'anulado';
await pedido.save();
await Entrada.updateMany({ pedidoId }, { $set: { estado: 'anulada' } });
await Evento.updateOne(
{ eventoId: pedido.eventoId, 'sesiones.sesionId': pedido.sesionId },
{ $inc: { 'sesiones.$.vendidas': -pedido.cantidad } },
);
// Si el proceso muere entre estas tres escrituras, el estado queda incoherente:
// pedido anulado con entradas validas, o aforo nunca devuelto. Cada operacion es
// atomica por separado, pero el CONJUNTO no lo es. Eso son las transacciones (07-06).
return pedido;
}Conclusión
Ya sabes mover datos con Mongoose de verdad: crear con save() o create() sabiendo qué devuelve cada uno, leer entendiendo que un Query es perezoso y que lean() y las proyecciones son la optimización más rentable, paginar sin caer en la trampa de skip, filtrar con los operadores adecuados —incluidos $elemMatch y $expr, que evitan errores sutiles sobre arrays—, actualizar sin saltarte la validación y borrar lógicamente para no perder la historia. Y sabes traducir los errores del motor a la jerarquía del módulo 6 desde la capa correcta. Sobre todo, src/catalogo-datos.js está jubilado: src/repositorios/eventos.js ocupa su lugar con la misma firma, los controladores no se han enterado y el dominio sigue recibiendo sus instancias de Evento. La arquitectura de la lección 07-01 acaba de demostrar que servía para algo.
Queda una deuda anotada en mayúsculas dentro de crearPedido: $inc nos ha dado atomicidad por operación, pero no por conjunto, y la sobreventa sigue viva. En la lección siguiente aparcamos ese problema un poco más para atacar el modelado avanzado: incrustar frente a referenciar, populate y el problema N+1, desnormalización deliberada, el framework de agregación aplicado a informes reales de Escena Viva —que sustituirán a los que en el módulo 3 hacíamos leyendo CSV— e índices en serio, con explain() para ver si tus consultas los usan o están recorriendo la colección entera.
Curso de Node.js: De Principiante a Avanzado
Módulo 1: Introducción a Node.js
- ¿Qué es Node.js?
- Instalación y Configuración del Entorno
- Tu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Moderno para Node.js
- El Proyecto del Curso: la Plataforma Escena Viva
Módulo 2: Conceptos Básicos
- Arquitectura de Node.js
- El Bucle de Eventos (Event Loop)
- Callbacks y Programación Asíncrona
- Promesas y async/await
- Eventos y EventEmitter
- Módulos CommonJS y require()
- Módulos ES e Interoperabilidad
Módulo 3: Sistema de Archivos y E/S
- Lectura y Escritura de Archivos
- El Módulo fs a Fondo
- Rutas Multiplataforma con el Módulo path
- Trabajando con Streams
- Streams de Transformación y pipeline
- Buffers y Datos Binarios
Módulo 4: HTTP y Servidores Web
- Creando un Servidor HTTP Simple
- Manejo de Solicitudes y Respuestas
- Enrutamiento Manual
- Sirviendo Archivos Estáticos
- Recibiendo Datos: Cuerpos de Petición y JSON
- Consumiendo APIs Externas desde Node.js
Módulo 5: NPM y Gestión de Paquetes
- Introducción a NPM y package.json
- Instalación y Uso de Paquetes
- Versionado Semántico y package-lock
- Scripts de npm y Automatización del Proyecto
- Creación y Publicación de Paquetes
- Seguridad y Mantenimiento de Dependencias
Módulo 6: Framework Express.js
- Introducción a Express.js
- Configuración de una Aplicación Express
- Enrutamiento en Express
- Middleware
- Middleware de Terceros Esenciales
- Validación de Datos de Entrada
- Manejo de Errores
Módulo 7: Bases de Datos y ORMs
- Introducción a las Bases de Datos
- Usando MongoDB con Mongoose
- Operaciones CRUD
- Relaciones, Poblado y Consultas Avanzadas
- Usando Bases de Datos SQL con Sequelize
- Migraciones, Transacciones y Datos de Prueba
Módulo 8: Autenticación y Autorización
- Introducción a la Autenticación
- Registro de Usuarios y Hash de Contraseñas
- Sesiones y Cookies con Passport.js
- Autenticación con JWT
- Control de Acceso Basado en Roles
- Buenas Prácticas de Seguridad en APIs
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con Mocha y Chai
- Dobles de Prueba con Sinon
- Pruebas de Integración
- Cobertura y Automatización de las Pruebas
- Depuración de Aplicaciones Node.js
Módulo 10: Temas Avanzados
- El Módulo Cluster
- Hilos de Trabajo (Worker Threads)
- Caché y Colas de Trabajo con Redis
- Optimización del Rendimiento
- Construcción de APIs RESTful
- GraphQL con Node.js
Módulo 11: Despliegue y DevOps
- Configuración y Variables de Entorno
- Registro y Monitorización en Producción
- Usando PM2 para la Gestión de Procesos
- Empaquetado con Docker
- Desplegando en Heroku y Otras PaaS
- Integración y Despliegue Continuos
