NestJS e MongoDB em 2026: Mongoose, Agregações e Perguntas de Entrevista

Guia completo sobre a integração do NestJS 12 com MongoDB através do Mongoose 9. Aprenda sobre schemas, pipelines de agregação e perguntas técnicas frequentes em entrevistas.

Guia NestJS MongoDB com Mongoose, agregações e perguntas de entrevista

NestJS 12 combinado com MongoDB através do Mongoose 9 oferece uma stack pronta para produção para construir backends Node.js escaláveis. Este guia aborda padrões de design de schemas, pipelines de agregação e as perguntas de entrevista que diferenciam desenvolvedores seniores dos juniores.

Referência Rápida

Mongoose 9.9.4 requer Node.js 18+ e suporta MongoDB 6.0 até 8.0. NestJS 12 inclui pacotes compatíveis com ESM enquanto mantém compatibilidade retroativa com projetos CommonJS.

Configurando Mongoose em uma Aplicação NestJS 12

O pacote @nestjs/mongoose integra o Mongoose com a injeção de dependências do NestJS. Instalação das dependências necessárias:

bash
# Instalar a integração do Mongoose
npm install @nestjs/mongoose mongoose

Registro da conexão no módulo raiz:

app.module.tstypescript
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';

@Module({
  imports: [
    MongooseModule.forRoot(process.env.MONGODB_URI, {
      // Tamanho do pool de conexões para produção
      maxPoolSize: 10,
      // Timeout após 10 segundos se a conexão falhar
      serverSelectionTimeoutMS: 10000,
    }),
  ],
})
export class AppModule {}

O método forRoot aceita todas as opções de conexão do Mongoose. Configurar maxPoolSize previne o esgotamento de conexões sob carga, um problema comum em produção.

Design de Schemas com Decoradores TypeScript

Os schemas do Mongoose no NestJS utilizam decoradores do @nestjs/mongoose. Cada schema mapeia para uma collection do MongoDB.

user.schema.tstypescript
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument, Types } from 'mongoose';

// Tipo de documento para autocompletar no TypeScript
export type UserDocument = HydratedDocument<User>;

@Schema({
  timestamps: true, // Adiciona createdAt e updatedAt
  collection: 'users', // Nome explícito da collection
})
export class User {
  // ObjectId do MongoDB, gerado automaticamente
  _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);

O decorador @Prop define as restrições dos campos. Definir index: true em campos consultados frequentemente melhora o desempenho de leitura ao custo de escritas mais lentas.

Ponto de Entrevista

Entrevistadores perguntam sobre a diferença entre documentos incorporados e referências. Documentos incorporados são adequados para dados acessados juntos (perfil do usuário + preferências). Referências funcionam melhor para dados que crescem indefinidamente ou requerem consultas independentes (usuário + pedidos).

Padrão Repository com Serviços Injetáveis

O NestJS promove a separação da lógica de banco de dados em serviços. O decorador @InjectModel fornece acesso ao 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 do 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> {
    // Busca de email case-insensitive
    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();
  }
}

Chamar .exec() retorna uma Promise nativa em vez de um objeto Query do Mongoose. Isso é importante para o comportamento correto de async/await e stack traces de erro.

Pipelines de Agregação para Consultas Complexas

As agregações do MongoDB lidam com relatórios, análises e transformações de dados que bancos de dados SQL resolvem com joins e 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[] = [
      // Estágio 1: Filtrar pedidos por ano
      {
        $match: {
          createdAt: {
            $gte: new Date(`${year}-01-01`),
            $lt: new Date(`${year + 1}-01-01`),
          },
          status: 'completed',
        },
      },
      // Estágio 2: Agrupar por mês, somar receita
      {
        $group: {
          _id: { $month: '$createdAt' },
          totalRevenue: { $sum: '$amount' },
          orderCount: { $sum: 1 },
          avgOrderValue: { $avg: '$amount' },
        },
      },
      // Estágio 3: Ordenar por mês crescente
      { $sort: { _id: 1 } },
      // Estágio 4: Reformatar saída
      {
        $project: {
          _id: 0,
          month: '$_id',
          totalRevenue: { $round: ['$totalRevenue', 2] },
          orderCount: 1,
          avgOrderValue: { $round: ['$avgOrderValue', 2] },
        },
      },
    ];

    return this.orderModel.aggregate(pipeline).exec();
  }
}

Os pipelines de agregação processam documentos sequencialmente através dos estágios. Cada estágio transforma a saída para o próximo estágio. O estágio $match filtra cedo para reduzir os documentos processados pelos estágios seguintes.

Pronto para mandar bem nas entrevistas de Node.js / NestJS?

Pratique com nossos simuladores interativos, flashcards e testes tecnicos.

Transações para Operações Multi-Documento

MongoDB 4.0+ suporta transações ACID multi-documento. As transações são utilizadas quando múltiplos documentos precisam ser atualizados atomicamente.

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 uma sessão para a transação
    const session: ClientSession = await this.connection.startSession();

    try {
      await session.withTransaction(async () => {
        // Debitar a conta origem
        const fromAccount = await this.accountModel.findByIdAndUpdate(
          fromId,
          { $inc: { balance: -amount } },
          { session, new: true },
        );

        if (!fromAccount || fromAccount.balance < 0) {
          throw new BadRequestException('Saldo insuficiente');
        }

        // Creditar a conta destino
        await this.accountModel.findByIdAndUpdate(
          toId,
          { $inc: { balance: amount } },
          { session },
        );
      });
    } finally {
      await session.endSession();
    }
  }
}

O método withTransaction lida automaticamente com o commit em caso de sucesso e rollback se uma exceção for lançada. Sempre passar o objeto session para cada operação dentro da transação.

Índices e Otimização de Performance

O design de índices impacta diretamente a latência das consultas. O MongoDB suporta índices simples, compostos e 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 composto para consultas frequentes
ProductSchema.index({ category: 1, price: -1 });

// Índice de texto para busca
ProductSchema.index({ name: 'text' });

Índices compostos suportam consultas que utilizam os campos de prefixo. Um índice em { category: 1, price: -1 } acelera consultas que filtram por categoria ou por categoria e preço, mas não aquelas que filtram apenas por preço.

Tratamento de Erros e Validação

O Mongoose fornece validação integrada. Combinar com os pipes do NestJS para validação robusta de entrada.

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);
  }
}

A opção whitelist: true remove propriedades não decoradas do DTO, prevenindo a injeção de campos não autorizados.

Perguntas Técnicas Frequentes em Entrevistas

As entrevistas técnicas sobre NestJS e MongoDB cobrem arquitetura, performance e casos extremos.

Pergunta: Quando usar $lookup versus documentos incorporados?

Resposta esperada: $lookup realiza um join do lado do servidor entre collections. Usar $lookup quando os dados referenciados são volumosos, atualizados independentemente, ou necessários em consultas separadas. Documentos incorporados são adequados quando os dados são sempre acessados juntos e cabem dentro do limite de 16MB do documento.

typescript
// Exemplo de $lookup em uma agregação
const pipeline = [
  {
    $lookup: {
      from: 'orders',
      localField: '_id',
      foreignField: 'userId',
      as: 'orders',
    },
  },
  {
    $project: {
      email: 1,
      orderCount: { $size: '$orders' },
    },
  },
];

Pergunta: Como lidar com paginação em grandes conjuntos de dados?

Resposta esperada: Evitar skip() para valores grandes de offset porque o MongoDB precisa percorrer todos os documentos ignorados. Usar paginação por cursor com um campo indexado.

typescript
// Paginação ineficiente
await this.model.find().skip(10000).limit(20);

// Paginação por cursor (eficiente)
await this.model
  .find({ _id: { $gt: lastSeenId } })
  .sort({ _id: 1 })
  .limit(20);

Pergunta: Como proteger as consultas contra injeção NoSQL?

Resposta esperada: O Mongoose escapa por padrão as consultas com $where desabilitado. Sempre validar as entradas do usuário e evitar passar objetos diretamente nas condições de consulta.

typescript
// Vulnerável - o usuário poderia injetar { $gt: '' }
const user = await this.model.findOne({ email: userInput });

// Seguro - forçar o tipo string
const user = await this.model.findOne({ email: String(userInput) });

Pergunta: Qual é a diferença entre save() e findByIdAndUpdate()?

Resposta esperada: save() carrega o documento em memória, aplica os middlewares (hooks pre/post), e executa os validadores. findByIdAndUpdate() envia uma atualização direta ao MongoDB, ignorando os middlewares a menos que configurado explicitamente.

typescript
// Dispara os middlewares
const user = await this.userModel.findById(id);
user.lastLogin = new Date();
await user.save();

// Ignora os middlewares por padrão
await this.userModel.findByIdAndUpdate(id, { lastLogin: new Date() });

// Para ativar os validadores
await this.userModel.findByIdAndUpdate(
  id,
  { lastLogin: new Date() },
  { runValidators: true },
);

Testes de Integração com MongoDB Memory Server

Testar serviços Mongoose requer uma instância do MongoDB. mongodb-memory-server fornece um banco de dados em memória para os testes.

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');
  });
});

Conclusão

NestJS e MongoDB através do Mongoose formam uma combinação poderosa para aplicações Node.js modernas. O domínio de schemas tipados, pipelines de agregação e transações permite construir backends de alto desempenho e fácil manutenção. As perguntas de entrevista avaliam a compreensão dos trade-offs entre modelagem relacional e orientada a documentos, assim como técnicas de otimização de performance para aplicações de alta carga.

Desafio do dia

Você saberia encontrar o bug em Node.js / NestJS?

Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Anthony Fillion-Maillet

Escrito por

Anthony Fillion-Maillet

Fundador da SharpSkill

Desenvolvedor fullstack há mais de 10 anos. Dirige a SharpSkill e responde por tudo o que é publicado aqui.

Atualizado em 28 de agosto de 2026

Tags

#nestjs
#mongodb
#mongoose
#nodejs
#backend

Compartilhar

Artigos relacionados