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.

Guía NestJS MongoDB con Mongoose, agregaciones y preguntas de entrevista

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.

Referencia Rápida

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:

bash
# Instalar la integración de Mongoose
npm install @nestjs/mongoose mongoose

Registro de la conexión en el módulo raíz:

app.module.tstypescript
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.

user.schema.tstypescript
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.

Punto de Entrevista

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.

user.service.tstypescript
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.

analytics.service.tstypescript
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.

transfer.service.tstypescript
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.

product.schema.tstypescript
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.

create-user.dto.tstypescript
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;
}
user.controller.tstypescript
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.

typescript
// 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.

typescript
// 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.

typescript
// 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.

typescript
// 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.

user.service.spec.tstypescript
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.

Reto diario

¿Sabrías detectar el bug en Node.js / NestJS?

Un fragmento real, un bug oculto, un intento al día. Sin cuenta para probar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador 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

#nestjs
#mongodb
#mongoose
#nodejs
#backend

Compartir

Artículos relacionados