NestJS y MongoDB en 2026: Mongoose, Agregaciones y Preguntas de Entrevista
Guía completa sobre la integración de NestJS 12 con MongoDB mediante Mongoose 9. Aprende sobre esquemas, pipelines de agregación y preguntas técnicas frecuentes en entrevistas.

NestJS 12 combinado con MongoDB a través de Mongoose 9 proporciona un stack listo para producción para construir backends Node.js escalables. Esta guía cubre patrones de diseño de esquemas, pipelines de agregación y las preguntas de entrevista que distinguen a los desarrolladores senior de los junior.
Mongoose 9.9.4 requiere Node.js 18+ y soporta MongoDB 6.0 hasta 8.0. NestJS 12 incluye paquetes compatibles con ESM mientras mantiene compatibilidad con proyectos CommonJS.
Configuración de Mongoose en una Aplicación NestJS 12
El paquete @nestjs/mongoose integra Mongoose con la inyección de dependencias de NestJS. Instalación de las dependencias requeridas:
# Instalar la integración de Mongoose
npm install @nestjs/mongoose mongooseRegistro de la conexión en el módulo raíz:
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [
MongooseModule.forRoot(process.env.MONGODB_URI, {
// Tamaño del pool de conexiones para producción
maxPoolSize: 10,
// Timeout después de 10 segundos si la conexión falla
serverSelectionTimeoutMS: 10000,
}),
],
})
export class AppModule {}El método forRoot acepta todas las opciones de conexión de Mongoose. Configurar maxPoolSize previene el agotamiento de conexiones bajo carga, un problema común en producción.
Diseño de Esquemas con Decoradores TypeScript
Los esquemas de Mongoose en NestJS utilizan decoradores de @nestjs/mongoose. Cada esquema se mapea a una colección de MongoDB.
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument, Types } from 'mongoose';
// Tipo de documento para autocompletado en TypeScript
export type UserDocument = HydratedDocument<User>;
@Schema({
timestamps: true, // Agrega createdAt y updatedAt
collection: 'users', // Nombre explícito de la colección
})
export class User {
// ObjectId de MongoDB, generado automáticamente
_id: Types.ObjectId;
@Prop({ required: true, unique: true, index: true })
email: string;
@Prop({ required: true })
passwordHash: string;
@Prop({ type: String, enum: ['admin', 'user', 'guest'], default: 'user' })
role: string;
@Prop({ type: [String], default: [] })
permissions: string[];
}
export const UserSchema = SchemaFactory.createForClass(User);El decorador @Prop define las restricciones de los campos. Establecer index: true en campos consultados frecuentemente mejora el rendimiento de lectura a costa de escrituras más lentas.
Los entrevistadores preguntan sobre la diferencia entre documentos embebidos y referencias. Los documentos embebidos son adecuados para datos que se acceden juntos (perfil de usuario + preferencias). Las referencias funcionan mejor para datos que crecen sin límite o requieren consultas independientes (usuario + órdenes).
Patrón Repository con Servicios Inyectables
NestJS promueve la separación de la lógica de base de datos en servicios. El decorador @InjectModel proporciona acceso al modelo Mongoose.
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { User, UserDocument } from './user.schema';
@Injectable()
export class UserService {
constructor(
@InjectModel(User.name) private userModel: Model<UserDocument>,
) {}
async findById(id: string): Promise<UserDocument> {
// Validar formato de ObjectId antes de consultar
if (!Types.ObjectId.isValid(id)) {
throw new NotFoundException('Invalid user ID format');
}
const user = await this.userModel.findById(id).exec();
if (!user) {
throw new NotFoundException(`User ${id} not found`);
}
return user;
}
async findByEmail(email: string): Promise<UserDocument | null> {
// Búsqueda de email insensible a mayúsculas
return this.userModel.findOne({
email: { $regex: new RegExp(`^${email}$`, 'i') }
}).exec();
}
async create(data: Partial<User>): Promise<UserDocument> {
const user = new this.userModel(data);
return user.save();
}
}Llamar a .exec() retorna una Promise nativa en lugar de un objeto Query de Mongoose. Esto importa para el comportamiento correcto de async/await y las trazas de errores.
Pipelines de Agregación para Consultas Complejas
Las agregaciones de MongoDB manejan reportes, análisis y transformaciones de datos que las bases de datos SQL resuelven con joins y GROUP BY.
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, PipelineStage } from 'mongoose';
import { Order, OrderDocument } from './order.schema';
@Injectable()
export class AnalyticsService {
constructor(
@InjectModel(Order.name) private orderModel: Model<OrderDocument>,
) {}
async getRevenueByMonth(year: number): Promise<MonthlyRevenue[]> {
const pipeline: PipelineStage[] = [
// Etapa 1: Filtrar órdenes por año
{
$match: {
createdAt: {
$gte: new Date(`${year}-01-01`),
$lt: new Date(`${year + 1}-01-01`),
},
status: 'completed',
},
},
// Etapa 2: Agrupar por mes, sumar ingresos
{
$group: {
_id: { $month: '$createdAt' },
totalRevenue: { $sum: '$amount' },
orderCount: { $sum: 1 },
avgOrderValue: { $avg: '$amount' },
},
},
// Etapa 3: Ordenar por mes ascendente
{ $sort: { _id: 1 } },
// Etapa 4: Reformatear salida
{
$project: {
_id: 0,
month: '$_id',
totalRevenue: { $round: ['$totalRevenue', 2] },
orderCount: 1,
avgOrderValue: { $round: ['$avgOrderValue', 2] },
},
},
];
return this.orderModel.aggregate(pipeline).exec();
}
}Los pipelines de agregación procesan documentos secuencialmente a través de las etapas. Cada etapa transforma la salida para la siguiente etapa. La etapa $match filtra temprano para reducir los documentos procesados por las etapas posteriores.
¿Listo para aprobar tus entrevistas de Node.js / NestJS?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Transacciones para Operaciones Multi-Documento
MongoDB 4.0+ soporta transacciones ACID multi-documento. Las transacciones se utilizan cuando múltiples documentos deben actualizarse atómicamente.
import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectConnection, InjectModel } from '@nestjs/mongoose';
import { Connection, Model, ClientSession } from 'mongoose';
import { Account, AccountDocument } from './account.schema';
@Injectable()
export class TransferService {
constructor(
@InjectConnection() private connection: Connection,
@InjectModel(Account.name) private accountModel: Model<AccountDocument>,
) {}
async transfer(
fromId: string,
toId: string,
amount: number,
): Promise<void> {
// Iniciar una sesión para la transacción
const session: ClientSession = await this.connection.startSession();
try {
await session.withTransaction(async () => {
// Debitar la cuenta origen
const fromAccount = await this.accountModel.findByIdAndUpdate(
fromId,
{ $inc: { balance: -amount } },
{ session, new: true },
);
if (!fromAccount || fromAccount.balance < 0) {
throw new BadRequestException('Saldo insuficiente');
}
// Acreditar la cuenta destino
await this.accountModel.findByIdAndUpdate(
toId,
{ $inc: { balance: amount } },
{ session },
);
});
} finally {
await session.endSession();
}
}
}El método withTransaction maneja automáticamente el commit en caso de éxito y el rollback si se lanza una excepción. Siempre pasar el objeto session a cada operación dentro de la transacción.
Índices y Optimización de Rendimiento
El diseño de índices impacta directamente la latencia de las consultas. MongoDB soporta índices simples, compuestos y de texto.
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';
export type ProductDocument = HydratedDocument<Product>;
@Schema()
export class Product {
@Prop({ required: true, index: true })
category: string;
@Prop({ required: true })
name: string;
@Prop({ type: Number, required: true })
price: number;
@Prop({ type: Boolean, default: true })
inStock: boolean;
}
export const ProductSchema = SchemaFactory.createForClass(Product);
// Índice compuesto para consultas frecuentes
ProductSchema.index({ category: 1, price: -1 });
// Índice de texto para búsqueda
ProductSchema.index({ name: 'text' });Los índices compuestos soportan consultas que utilizan los campos prefijo. Un índice en { category: 1, price: -1 } acelera consultas que filtran por categoría o por categoría y precio, pero no aquellas que filtran solo por precio.
Manejo de Errores y Validación
Mongoose proporciona validación integrada. Combinar con los pipes de NestJS para una validación robusta de entradas.
import { IsEmail, IsString, MinLength, IsEnum, IsOptional } from 'class-validator';
export class CreateUserDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
@IsOptional()
@IsEnum(['admin', 'user', 'guest'])
role?: string;
}import { Controller, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common';
import { UserService } from './user.service';
import { CreateUserDto } from './create-user.dto';
@Controller('users')
export class UserController {
constructor(private userService: UserService) {}
@Post()
@UsePipes(new ValidationPipe({ whitelist: true }))
async create(@Body() dto: CreateUserDto) {
return this.userService.create(dto);
}
}La opción whitelist: true elimina las propiedades no decoradas del DTO, previniendo la inyección de campos no autorizados.
Preguntas Técnicas Frecuentes en Entrevistas
Las entrevistas técnicas sobre NestJS y MongoDB cubren arquitectura, rendimiento y casos límite.
Pregunta: ¿Cuándo usar $lookup versus documentos embebidos?
Respuesta esperada: $lookup realiza un join del lado del servidor entre colecciones. Usar $lookup cuando los datos referenciados son voluminosos, se actualizan independientemente, o se requieren en consultas separadas. Los documentos embebidos son adecuados cuando los datos siempre se acceden juntos y caben dentro del límite de 16MB del documento.
// Ejemplo de $lookup en una agregación
const pipeline = [
{
$lookup: {
from: 'orders',
localField: '_id',
foreignField: 'userId',
as: 'orders',
},
},
{
$project: {
email: 1,
orderCount: { $size: '$orders' },
},
},
];Pregunta: ¿Cómo manejar la paginación con grandes conjuntos de datos?
Respuesta esperada: Evitar skip() para valores grandes de offset porque MongoDB debe recorrer todos los documentos omitidos. Usar paginación por cursor con un campo indexado.
// Paginación ineficiente
await this.model.find().skip(10000).limit(20);
// Paginación por cursor (eficiente)
await this.model
.find({ _id: { $gt: lastSeenId } })
.sort({ _id: 1 })
.limit(20);Pregunta: ¿Cómo proteger las consultas contra inyección NoSQL?
Respuesta esperada: Mongoose escapa por defecto las consultas con $where deshabilitado. Siempre validar las entradas del usuario y evitar pasar objetos directamente en las condiciones de consulta.
// Vulnerable - el usuario podría inyectar { $gt: '' }
const user = await this.model.findOne({ email: userInput });
// Seguro - forzar el tipo string
const user = await this.model.findOne({ email: String(userInput) });Pregunta: ¿Cuál es la diferencia entre save() y findByIdAndUpdate()?
Respuesta esperada: save() carga el documento en memoria, aplica los middlewares (hooks pre/post), y ejecuta los validadores. findByIdAndUpdate() envía una actualización directa a MongoDB, omitiendo los middlewares a menos que se configure explícitamente.
// Dispara los middlewares
const user = await this.userModel.findById(id);
user.lastLogin = new Date();
await user.save();
// Omite los middlewares por defecto
await this.userModel.findByIdAndUpdate(id, { lastLogin: new Date() });
// Para activar los validadores
await this.userModel.findByIdAndUpdate(
id,
{ lastLogin: new Date() },
{ runValidators: true },
);Pruebas de Integración con MongoDB Memory Server
Probar los servicios de Mongoose requiere una instancia de MongoDB. mongodb-memory-server proporciona una base de datos en memoria para las pruebas.
import { Test } from '@nestjs/testing';
import { MongooseModule } from '@nestjs/mongoose';
import { MongoMemoryServer } from 'mongodb-memory-server';
import { UserService } from './user.service';
import { User, UserSchema } from './user.schema';
describe('UserService', () => {
let service: UserService;
let mongod: MongoMemoryServer;
beforeAll(async () => {
mongod = await MongoMemoryServer.create();
const uri = mongod.getUri();
const module = await Test.createTestingModule({
imports: [
MongooseModule.forRoot(uri),
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
],
providers: [UserService],
}).compile();
service = module.get<UserService>(UserService);
});
afterAll(async () => {
await mongod.stop();
});
it('should create a user', async () => {
const user = await service.create({
email: 'test@example.com',
passwordHash: 'hashed',
});
expect(user.email).toBe('test@example.com');
});
});Conclusión
NestJS y MongoDB a través de Mongoose forman una combinación potente para aplicaciones Node.js modernas. El dominio de los esquemas tipados, los pipelines de agregación y las transacciones permite construir backends de alto rendimiento y fácil mantenimiento. Las preguntas de entrevista evalúan la comprensión de los compromisos entre modelado relacional y orientado a documentos, así como las técnicas de optimización de rendimiento para aplicaciones de alta carga.
¿Sabrías detectar el bug en Node.js / NestJS?
Un fragmento real, un bug oculto, un intento al día. Sin cuenta para probar.

Escrito por
Anthony Fillion-MailletFundador de SharpSkill
Desarrollador fullstack desde hace más de 10 años. Dirige SharpSkill y responde por todo lo que se publica aquí.
Actualizado el 28 de agosto de 2026
Etiquetas
Compartir
Artículos relacionados

NestJS + Prisma: el stack backend moderno para Node.js
Guía completa para construir una API backend moderna con NestJS y Prisma. Configuración, modelos, servicios, transacciones y buenas prácticas explicadas.

NestJS: Construir una API REST Completa
Tutorial completo para construir una API REST profesional con NestJS. Controladores, Servicios, Modulos, validacion con class-validator y manejo centralizado de errores.

Preguntas de entrevista Node.js Backend: Guia completa 2026
Las 25 preguntas mas frecuentes en entrevistas de backend Node.js. Event loop, async/await, streams, clustering y rendimiento explicados con respuestas detalladas.