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.

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.
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:
# Instalar a integração do Mongoose
npm install @nestjs/mongoose mongooseRegistro da conexão no módulo raiz:
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.
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.
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.
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.
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.
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.
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.
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);
}
}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.
// 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.
// 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.
// 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.
// 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.
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.
Você saberia encontrar o bug em Node.js / NestJS?
Um trecho real, um bug escondido, uma tentativa por dia. Sem conta para testar.

Escrito por
Anthony Fillion-MailletFundador 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
Compartilhar
Artigos relacionados

NestJS + Prisma: o stack backend moderno para Node.js
Guia completo para construir uma API backend moderna com NestJS e Prisma. Configuração, modelos, serviços, transações e boas práticas explicadas.

NestJS: Construindo uma API REST Completa
Tutorial completo para construir uma API REST profissional com NestJS. Controllers, Services, Modules, validacao com class-validator e tratamento centralizado de erros.

Perguntas de entrevista Node.js Backend: Guia completo 2026
As 25 perguntas mais comuns em entrevistas de backend Node.js. Event loop, async/await, streams, clustering e performance explicados com respostas detalhadas.