MongoDB: Integração do Express com o Mongoose

Última atualização: 2026-08-26

O Express + Mongoose é a pilha de desenvolvimento web mais madura do ecossistema Node.js — dominá-la permite que você crie APIs prontas para produção.

A posição do Express entre as estruturas web do Node.js: As estruturas web do Node.js são divididas em três níveis — 1. Baixo nível (módulo http): a opção mais leve, mas exige o gerenciamento manual de roteamento, análise sintática e tratamento de erros, o que a torna impraticável; 2. Nível intermediário (Express/Koa): Oferece recursos básicos, como roteamento, middleware e tratamento de erros; é flexível, mas requer montagem personalizada (ORM, validação, registro em log etc. devem ser selecionados); 3. Nível superior (NestJS/AdonisJS): Soluções completas integradas (DI/ORM/validação/registro em log), prontas para uso imediato, mas com uma curva de aprendizado íngreme. Razões pelas quais o Express detém a maior participação de mercado — 1. Núcleo minimalista (lida apenas com roteamento e middleware; outros componentes são livremente selecionáveis); 2. O ecossistema de middleware mais rico (o número de pacotes de middleware do Express no npm excede em muito o de outras estruturas); 3. Baixa curva de aprendizado (domine as APIs principais em 2 horas). Recomendação: Projetos pessoais/pequenas equipes → Express (flexível e rápido); projetos de nível empresarial → NestJS (padronizado e abrangente).

Vantagens e limitações da combinação Express + Mongoose: As vantagens dessa combinação — 1. Maior comunidade: você encontrará mais respostas ao procurar soluções; as perguntas sobre Express + Mongoose no Stack Overflow abrangem a mais ampla variedade de tópicos; 2. Flexibilidade: o Express não está vinculado a nenhum ORM, portanto, você pode substituir o Mongoose pelo Prisma ou pelo TypeORM a qualquer momento; 3. Progressivo: Começando pelo mais simples app.get, você pode adicionar gradualmente middleware, camadas e validação, resultando em uma curva de aprendizado suave. Limitações — 1. Ausência de segurança de tipos (projetos em JavaScript não possuem verificação de tipos em tempo de compilação; requer TypeScript e definições de interface); 2. Ausência de convenções embutidas (a estrutura do projeto, a nomenclatura e a divisão em camadas dependem inteiramente das convenções da equipe; iniciantes podem facilmente acabar com um código espaguete); 3. Estilo baseado em callbacks (o Express depende de callbacks; erros assíncronos exigem encapsulamento ou o uso de express-async-errors). Compreender essas limitações ajuda a estabelecer especificações técnicas logo no início do projeto para evitá-las.

1. O que você vai aprender


2. Noções básicas do Express 4.x

O que é o Express? O Express é a estrutura de aplicativos web mais popular para Node.js, oferecendo um sistema de roteamento simples, um mecanismo de middleware e métodos utilitários HTTP. Não se trata de uma “estrutura full-stack”, mas sim de uma camada minimalista de roteamento e middleware — e é justamente esse o seu ponto forte: é flexível, composível e conta com o apoio de um ecossistema rico.

Princípios fundamentais de design do Express: O Express se baseia no modelo de “pilha de middleware” — cada solicitação HTTP passa por uma série de funções de middleware, cada uma das quais pode ler a solicitação (req), modificar a resposta (res) ou passar o controle para a próxima função de middleware (next). Esse modelo em camadas permite que os recursos sejam combinados como blocos de construção: registro em log, autenticação, validação, lógica de negócios e tratamento de erros têm, cada um, seu lugar.

A filosofia de middleware do Express: A filosofia central de design do Express é “minimalismo + composibilidade” — o próprio framework fornece apenas abstração HTTP e um mecanismo de middleware, enquanto todas as funcionalidades (roteamento, autenticação, validação, registro em log) são implementadas por meio de middleware. Ao contrário da abordagem “tudo em um” do Django e do Rails, o Express não inclui um ORM integrado, autenticação ou mecanismo de modelos — os desenvolvedores escolhem os componentes conforme necessário. Vantagens: Curva de aprendizado baixa, alta flexibilidade e forte substituibilidade. Desvantagens: Exige a construção de sua própria pilha de tecnologias, carece de padrões unificados e os iniciantes podem facilmente escolher os componentes errados. O Express é adequado para “equipes experientes que desenvolvem soluções sob demanda”, enquanto o Django e o Rails são adequados para “pequenas equipes que precisam entregar rapidamente”.

Critérios para escolher uma pilha tecnológica: Por que escolher Express + Mongoose? 1. O ecossistema do Express é o mais maduro (com muito mais middleware do que o Koa ou o Fastify), facilitando a busca por soluções quando surgem problemas; 2. O Mongoose oferece recursos avançados, como validação de esquema, middleware e populate, reduzindo significativamente o código repetitivo em comparação com o driver nativo; 3. A curva de aprendizado é suave — o modelo req/res é intuitivo e fácil de entender; 4. A comunidade é ativa (downloads semanais no npm: mais de 30 milhões para o Express e mais de 1 milhão para o Mongoose). O Koa é mais elegante (suporte nativo a async/await), o Fastify é mais rápido (otimizado para desempenho) e o NestJS é mais padronizado (estilo Angular), mas as vantagens gerais do Express — ecossistema, documentação e base de talentos — fazem dele a melhor escolha tanto para o aprendizado quanto para a produção.

100%
sequenceDiagram
    participant Client
    participant Express as Express Server
    participant MW1 as Logging Middleware
    participant MW2 as Authentication Middleware
    participant MW3 as Validation Middleware
    participant Ctrl as Controller
    participant DB as MongoDB

    Client->>Express: HTTP Request
    Express->>MW1: req → res → next()
    MW1->>MW2: next()
    MW2->>MW3: Certification Approved
    MW3->>Ctrl: Verification Passed
    Ctrl->>DB: mongooseSearch
    DB-->>Ctrl: Search Results
    Ctrl-->>Client: JSON Response

Express x Outras estruturas do Node.js:

Dimension Express Koa Fastify NestJS
Conceitos Fundamentais Pilha de Middleware Modelo Cebola Plug-ins de Alto Desempenho Decoradores + DI
Desempenho Teste de benchmark Um pouco mais lento 2 a 3 vezes mais rápido Um pouco mais lento
Maturidade do ecossistema ★★★★★ ★★★ ★★★ ★★★★
Curva de aprendizagem Baixa Baixa Média Alta
TypeScript Requer configuração Requer configuração Suporte nativo Suporte nativo
Casos de uso APIs de uso geral Serviços leves APIs de alta simultaneidade Aplicativos corporativos

Por que Express + Mongoose é a melhor combinação? O modelo req/res do Express se alinha naturalmente às consultas assíncronas do Mongoose — as funções do controlador aceitam req, consultam o Mongoose e retornam res.json(), resultando em um código intuitivo e fácil de manter.

Principais middlewares no ecossistema do Express: O design minimalista do Express significa que o núcleo lida apenas com o roteamento, enquanto outros recursos são fornecidos por middlewares — 1. Segurança: helmet (define cabeçalhos HTTP seguros), cors (Compartilhamento de Recursos entre Origens), express-rate-limit (limitação da taxa de solicitações); 2. Análise: express.json() (analisa corpos de solicitação JSON), express.urlencoded() (analisa dados de formulário), multer (envios de arquivos); 3. Registros: morgan (registros de solicitações HTTP), winston (registros da aplicação); 4. Compressão: compression (comprime corpos de resposta com gzip, reduzindo o uso de largura de banda em mais de 60%); 5. Verificações de integridade: express-healthcheck (ponto de extremidade /health, detecção de balanceador de carga). Aplicativos em produção devem instalar, no mínimo, helmet, cors, morgan e compressão, pois esses fornecem segurança básica e garantias de desempenho.

(1) Instalar dependências

BASH
npm install express mongoose dotenv

(2) Aplicações básicas

JAVASCRIPT
// === Basic Express Applications ===
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());

app.get('/', (req, res) => {
  res.json({ message: 'Welcome to ShopHub API' });
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

3. Configuração da conexão com o Mongoose

Como funciona o pool de conexões do Mongoose: Por padrão, o Mongoose utiliza o pool de conexões do driver do MongoDB para Node.js. O pool de conexões mantém um conjunto de conexões TCP estabelecidas; novas solicitações reutilizam conexões ociosas em vez de criar novas a cada vez — evitando, assim, a sobrecarga do handshake de três etapas do TCP e da autenticação do MongoDB. O tamanho do pool de conexões determina quantas operações de banco de dados o aplicativo pode executar simultaneamente.

Ciclo de vida do pool de conexões: Cada conexão no pool de conexões passa pelo seguinte ciclo de vida: “Criação → Inativa → Ativa → Inativa → Fechamento” — 1. Quando o aplicativo é iniciado, ele cria minPoolSize conexões (para aquecer o sistema e evitar atrasos na primeira solicitação); 2. Quando uma solicitação chega, uma conexão ociosa é retirada do pool (estado ativo); 3. Após a conclusão da solicitação, a conexão é devolvida ao pool (estado ocioso); 4. A conexão é fechada após o tempo limite de inatividade (maxIdleTimeMS) expirar (para economizar recursos); 5. Quando o pool de conexões se esgota, novas solicitações são colocadas na fila de espera (ocorre um erro após o tempo limite da fila de espera, waitQueueTimeoutMS, expirar). Compreender o ciclo de vida ajuda a diagnosticar vazamentos de conexão — se o número de conexões ativas continuar a crescer sem que sejam devolvidas, isso indica que uma solicitação não liberou a conexão corretamente (normalmente por ter esquecido de usar await ou por uma exceção não interceptada).

Conflito entre pools de conexão e arquitetura serverless: Os pools de conexão tradicionais partem do princípio de que as aplicações são executadas por longos períodos e que as conexões podem ser reutilizadas. Em ambientes serverless (como o AWS Lambda), cada invocação é executada como um novo processo — os pools de conexão não podem ser reutilizados entre invocações, e uma nova conexão é estabelecida a cada inicialização a frio. Soluções: 1. Use o MongoDB Atlas Serverless (que gerencia automaticamente as conexões); 2. Inicialize o mongoose fora do manipulador Lambda (para reutilizar a conexão da instância do contêiner); 3. Use middleware de gerenciamento de conexões (como o cache de conexões do mongoose); 4. Reduza maxPoolSize (ambientes sem servidor têm baixa simultaneidade, portanto, um grande pool de conexões desperdiça recursos). O gerenciamento de conexões para ambientes sem servidor + MongoDB é um dos pontos críticos operacionais.

Como funcionam os pools de conexão:

100%
graph LR
    App1[Request 1] -->|Borrow| Pool[(Connection Pool<br/>min=5 max=50)]
    App2[Request 2] -->|Borrow| Pool
    App3[Request 3] -->|Waiting| Queue[Waiting Queue<br/>waitQueueTimeout]
    Pool -->|Connect 1| DB1[mongod]
    Pool -->|Connect 2| DB2[mongod]
    Pool -->|Connect N| DB3[mongod]
    App1 -.->|Return| Pool
    App3 -.->|Get the link| Pool

    style Pool fill:#d4edda
    style Queue fill:#fff3cd

Explicação dos principais parâmetros de conexão:

Parâmetro Valor padrão Descrição Recomendações de produção
maxPoolSize 100 Número máximo de conexões 50 (ajuste de acordo com a simultaneidade)
minPoolSize 0 Número mínimo de conexões inativas 5 (conexões de aquecimento)
serverSelectionTimeoutMS 30000 Tempo limite para seleção do servidor 5000 (falha rápida)
socketTimeoutMS 0 Tempo limite do soquete 45000 (para evitar conexões “zumbis”)
maxIdleTimeMS 0 Tempo limite de inatividade da conexão 30000 (reutilização de conexões)

Dicas para ajustar os parâmetros de conexão: Não há valores universais para os parâmetros de conexão; eles devem ser ajustados com base no cenário real — 1. maxPoolSize: Fórmula = (aproximadamente 1 MB de memória por conexão) + (número de solicitações simultâneas × tempo médio de consulta). Para um servidor de 4 núcleos e 8 GB, recomenda-se um valor entre 50 e 100; 2. serverSelectionTimeoutMS: Defina como 5000 (5 segundos) em vez dos 30 segundos padrão — esperar 30 segundos antes de relatar um erro quando o banco de dados está indisponível é muito lento; um tempo limite de 5 segundos garante uma detecção rápida de falha e aciona um alerta; 3. heartbeatFrequencyMS: Defina como 10000 (10 segundos) para detectar trocas de nó mestre mais rapidamente (o padrão de 10 segundos já é razoável); 4. retryWrites: true: Repita automaticamente uma operação de gravação uma vez (recupera automaticamente de breves interrupções de rede, de forma transparente para o negócio); 5. Métricas de monitoramento: Utilização do pool de conexões (ativa/máxima), comprimento da fila de espera, tempo médio de empréstimo. Se a utilização exceder consistentemente 80%, faça o escalonamento horizontal (aumente o maxPoolSize ou adicione servidores).

Monitoramento de eventos de conexão do Mongoose: O objeto de conexão do Mongoose (mongoose.connection) emite vários eventos para fins de monitoramento — 1. connected: Conexão estabelecida (entrada no log); 2. error: Erro de conexão (aciona um alerta); 3. disconnected: Conexão perdida (aciona um alerta; pode ser necessária uma reconexão); 4. reconnected: Reconexão automática bem-sucedida (entrada no log; o aplicativo pode precisar revalidar o status); 5. close: Conexão encerrada (acionada durante o desligamento ordenado). Em ambientes de produção, todos os eventos devem ser monitorados e registrados — uma conexão interrompida é um problema grave que pode afetar todas as operações do banco de dados. Solução de monitoramento: mongoose.connection.on('error', logger.error) + mongoose.connection.on('disconnected', alertOps). | autoIndex | true | Indexação automática | Desativado em produção: false |

Casos de uso: Aplicações pequenas (< 1.000 DAU) maxPoolSize=10–20; aplicações de médio porte (1.000–100.000 DAU) 30–50; aplicações grandes (> 100.000 DAU) 50–100. Tenha cuidado ao definir o valor acima de 100, pois cada conexão consome aproximadamente 1 MB de memória.

(1) Funções de conexão

Considerações de projeto para funções de conexão: Uma função de conexão pronta para produção é mais do que apenas uma única linha de código — mongoose.connect() —; ela precisa lidar com: 1. Leitura de variáveis de ambiente (recuperar MONGODB_URI de .env; não codifique-a diretamente); 2. Configurar opções de conexão (poolSize, timeout, autoIndex); 3. Monitorar eventos de conexão (registrar eventos connected, error e disconnected); 4. Encerramento suave ao término do processo (tratamento dos sinais SIGINT/SIGTERM, mongoose.disconnect()); 5. Estratégia de repetição em caso de falha de conexão (retardamento exponencial para reconexão). A função de conexão é normalmente colocada em um módulo separado, db.js, e é a primeira função chamada quando o aplicativo é iniciado.

JAVASCRIPT
// db.js
const mongoose = require('mongoose');

async function connectDB() {
  try {
    const conn = await mongoose.connect(process.env.MONGODB_URI, {
      serverSelectionTimeoutMS: 5000,
      maxPoolSize: 50,        // Maximum Number of Connections
      minPoolSize: 5,         // Minimum Number of Connections
      socketTimeoutMS: 45000,
      autoIndex: true         // Automatic Indexing(Production can be shut down)
    });
    console.log(`✅ MongoDB connected: ${conn.connection.host}`);
    return conn;
  } catch (err) {
    console.error('❌ Connection failed:', err.message);
    process.exit(1);
  }
}

module.exports = { connectDB };

100%
graph TB
    Client[Client] -->|HTTP Request| Router[Express Router]

    Router -->|JWT Verification| AuthMW[Authentication Middleware]
    AuthMW -->|joi Verification| ValidateMW[Validation Middleware]
    ValidateMW --> Controller[Controller<br/>Business Logic]

    Controller -->|CRUD| Model[mongoose Model]
    Model -->|Search/Write| DB[(MongoDB)]

    Controller -->|Error| ErrorMW[Error-handling middleware]
    ErrorMW -->|Standard Response| Client

    style Model fill:#d4edda
    style Controller fill:#cce5ff

4. O Padrão MVC

O Princípio da Estrutura em Camadas do MVC: O valor central do MVC é a separação de interesses — o Modelo concentra-se exclusivamente nos dados (“o quê”), a Visualização concentra-se exclusivamente na apresentação (“como aparece”) e o Controlador concentra-se exclusivamente na coordenação (“como fazer”). Em projetos de API, a camada de Visualização se resume à serialização em JSON (res.json), mas o princípio de separação permanece o mesmo: para modificar o banco de dados, basta alterar o Modelo; para modificar o roteamento, basta alterar a Rota; para modificar a lógica de negócios, basta alterar o Controlador. Um código que não segue essa separação mistura toda a lógica, obrigando você a vasculhar o projeto inteiro apenas para alterar um único campo.

Direção da dependência no MVC: Em uma arquitetura MVC estrita, a direção da dependência é unidirecional — Rota → Controlador → Modelo. A Rota depende do Controlador (para chamar a função manipuladora), e o Controlador depende do Modelo (para recuperar dados), mas o Modelo não depende do Controlador (a camada de dados não tem conhecimento de quem a está utilizando). Essa dependência unidirecional permite que cada camada seja testada de forma independente: o Modelo é testado por meio de testes unitários, o Controlador por meio de testes de integração e a Rota por meio de testes HTTP.

Quando introduzir a camada de serviços: Quando um controlador se torna “pesado” (ultrapassando 50 linhas ou contendo várias operações de banco de dados), a lógica de negócios deve ser extraída para a camada de serviços. Características de um serviço: 1. Não depende do Express (não tem conhecimento de req ou res); 2. Aceita parâmetros de dados puros e retorna resultados de dados puros; 3. Pode ser reutilizado por vários controladores (por exemplo, createOrder é chamado tanto pela API da Web quanto pelo painel de administração); 4. Pode ser testado unitariamente de forma independente (simulando o modelo; não são necessárias solicitações HTTP). Sinais de que um serviço deve ser introduzido: instruções if/else aninhadas nos controladores, lógica de transação que abrange vários modelos e lógica computacional reutilizável.

A filosofia de design do “modelo robusto” na camada de modelo: O Mongoose defende o “modelo robusto” — encapsulando a lógica de negócios nos métodos estáticos, métodos de instância e middleware do Modelo. Em comparação com o “modelo fino” (em que o Modelo define apenas o esquema e toda a lógica reside no Controlador), as vantagens do modelo gordo são: 1. Coesão lógica (a validação da senha é tratada dentro do método User, em vez de estar espalhada por vários Controladores); 2. Reutilização de código (User.comparePassword() pode ser compartilhado tanto pelo método de login quanto pelo de alteração de senha); 3. Encapsulamento (os Controladores não precisam saber como as senhas são verificadas; eles simplesmente chamam o método). Limitações da abordagem do “modelo robusto”: a lógica que abrange vários Modelos não pertence a nenhum Modelo específico e deve ser colocada na camada de Serviço.

Princípios de projeto para a camada do controlador: O controlador atua como coordenador do processamento de solicitações — ele recupera dados de req, chama o modelo/serviço e retorna res. O Controlador deve permanecer “enxuto”: 1. Deve apenas extrair parâmetros e realizar conversões de tipo (req.body → parâmetros do Serviço); 2. Deve fazer apenas uma chamada ao Modelo/Serviço (a lógica complexa é transferida para o Serviço); 3. Tratar erros de maneira uniforme (envolver em try/catch e passar para o middleware de erros via next(err)); 4. Padronizar o formato da resposta (res.json({success: true, data})). Critérios para um Controlador “enxuto”: cada função do Controlador deve ter menos de 30 linhas e não conter lógica de negócios (as instruções if/else devem se limitar à validação de parâmetros e ao tratamento de erros).

Ordem de execução do middleware do Express: A ordem em que o middleware é registrado determina a ordem de execução — app.use(auth) → app.use(validate) → app.use(router) → app.use(errorHandler). Erros comuns: 1. Colocar o middleware de tratamento de erros antes do roteador (fazendo com que todas as solicitações retornem uma página de erro); 2. Colocar o middleware de autenticação após rotas que não exigem autenticação (fazendo com que algumas rotas sejam interceptadas incorretamente); 3. Colocar o middleware de análise de JSON (express.json()) após o roteador (resultando em req.body sempre sendo undefined). Regras para a ordem do middleware: coloque o middleware genérico primeiro (json, cors, helmet) e o middleware específico da rota dentro do roteador (auth, validate).

Padrões de combinação de middleware: O middleware do Express suporta combinações flexíveis — 1. Combinação serial: autenticar → autorizar → controlador, em que a saída da etapa anterior serve como entrada para a seguinte (autenticar define req.user e autorizar verifica req.user.role); 2. Encadeamento condicional: certas rotas exigem middleware adicional (por exemplo, POST /products requer validação + autenticação, enquanto GET /products não requer autenticação), implementado por meio de middleware no nível da rota; 3. Propagação de erros: qualquer middleware que chame next(err) ignora todos os middlewares regulares subsequentes e prossegue diretamente para o middleware de tratamento de erros — isso significa que falhas de validação, falhas de autenticação e erros de banco de dados são todos tratados de maneira uniforme dentro do middleware de erros. Compreender os padrões de composição de middleware permite que você projete fluxos de trabalho flexíveis e fáceis de manter para o tratamento de solicitações.

A fronteira entre middleware e serviços: O middleware lida com questões transversais (autenticação, registro em log, tratamento de erros), enquanto os serviços lidam com a lógica de negócios (criação de pedidos, cálculo de classificações). Sinais de limites difusos: 1. Consultas ao banco de dados no middleware (por exemplo, é razoável que um middleware de autenticação consulte o banco de dados para verificar um usuário, mas não é razoável que um middleware de validação consulte o banco de dados para verificar um produto); 2. Serviços que leem req ou res (os serviços devem aceitar apenas parâmetros de dados puros); 3. Controladores compostos por apenas 2 a 3 linhas de código (indicando que o middleware está fazendo tarefas demais). Limites claros: o middleware lida com tarefas no “nível da solicitação” (quem pode acessar, se a solicitação é válida), enquanto os serviços lidam com tarefas no “nível de negócios” (como os dados são processados, como as regras são aplicadas).

100%
graph TB
    subgraph "Route Layer"
        R1[GET /api/products]
        R2[POST /api/products]
        R3[GET /api/products/:sku]
    end

    subgraph "Controller Layer"
        C1[listProducts]
        C2[createProduct]
        C3[getProduct]
    end

    subgraph "Model Layer"
        M1[Product.find]
        M2[Product.create]
        M3[Product.findOne]
    end

    subgraph "MongoDB"
        DB1[(products Gathering)]
    end

    R1 --> C1 --> M1 --> DB1
    R2 --> C2 --> M2 --> DB1
    R3 --> C3 --> M3 --> DB1

    style R1 fill:#cce5ff
    style C1 fill:#d4edda
    style M1 fill:#fff3cd

(1) Estrutura do diretório do projeto

Estratégias para expandir a estrutura de diretórios: A estrutura MVC básica (modelos/controladores/rotas/middlewares) é adequada para projetos de pequeno e médio porte. À medida que um projeto cresce, é necessário expandi-lo — 1. Adicionar um diretório validators/ (separando o esquema do controlador para permitir a reutilização); 2. Adicione um diretório services/ (extraindo a lógica de negócios do controlador para os serviços, com o controlador lidando apenas com a conversão de solicitação para resposta); 3. Adicione um diretório config/ (para gerenciamento centralizado de conexões com bancos de dados e variáveis de ambiente); 4. Adicione um diretório utils/ (para funções utilitárias genéricas, como registradores de log e ferramentas JWT). Princípio de expansão: cada camada deve fazer apenas uma coisa — o Princípio da Responsabilidade Única.

TEXT 📖 Somente leitura
models/      # Data Model(mongoose Schema)
controllers/ # Business Logic
routes/      # Route Definitions
middlewares/ # Middleware
services/    # Service Layer(Optional)
utils/       # Utility Functions

(1) Camada do modelo

Princípios de projeto da camada de modelo: A camada de modelo é a única fonte de verdade para os dados — todas as definições de dados, regras de validação e métodos de consulta são declarados no modelo; os controladores e as rotas não devem conter nenhuma lógica de dados. Princípios específicos: 1. As definições de esquema incluem todas as regras de validação (os controladores não realizam mais validações duplicadas); 2. Os índices são declarados no esquema, em vez de serem criados manualmente (o Mongoose cria índices automaticamente); 3. Campos virtuais, métodos de instância e métodos estáticos são definidos no esquema (a lógica de negócios é encapsulada no Modelo); 4. select: false oculta campos confidenciais (por exemplo, passwordHash não é retornado por padrão).

Reutilização e herança de esquemas: Em projetos de grande porte, vários modelos podem compartilhar a mesma subestrutura — por exemplo, tanto User quanto Admin possuem um campo address. Métodos de reutilização: 1. Definir um esquema filho (const AddressSchema = new Schema({...})) e, em seguida, referenciar AddressSchema tanto em UserSchema quanto em AdminSchema; 2. Adicionar campos dinamicamente usando Schema.add(); 3. Herdar por meio de discriminadores (classe base Schema + campos de extensão da subclasse, como a classe base Product + as subclasses Book e Product). Os subesquemas são o padrão de reutilização mais comum — esquemas independentes podem ter suas próprias regras de validação e middleware.

Estratégias de declaração de índices: O Mongoose oferece três maneiras de declarar índices — 1. Índices no nível do campo ({sku: {type: String, index: true, unique: true}}, que são simples, intuitivos e adequados para índices de campo único); 2. Índices compostos no nível do esquema (schema.index({category: 1, price: -1}), adequados para índices compostos com vários campos); 3. Índices de texto (schema.index({title: 'text', content: 'text'}), especificamente para pesquisa de texto completo). Observação para ambientes de produção: defina autoIndex como false (para evitar a criação de índices a cada inicialização; os índices devem ser criados manualmente ou por meio de scripts de migração). Defina-o como true em ambientes de desenvolvimento para facilitar a sincronização automática.

JAVASCRIPT
// models/Product.js
const mongoose = require('mongoose');

const ProductSchema = new mongoose.Schema({
  sku: { type: String, required: true, unique: true, index: true },
  title: { type: String, required: true },
  price: { type: mongoose.Schema.Types.Decimal128, required: true },
  category: { type: String, required: true, index: true },
  stock: { type: Number, default: 0 }
}, { timestamps: true });

module.exports = mongoose.model('Product', ProductSchema);

(2) Camada do controlador

Princípios de projeto da camada do controlador: O controlador atua como coordenador do processamento de solicitações — ele extrai parâmetros de req, chama o modelo para consultar dados e retorna uma resposta por meio de res. O Controlador deve ser “enxuto” em vez de “pesado” — 1. Ele não contém lógica de negócios (a lógica de negócios reside nos métodos do Modelo ou na camada de Serviço); 2. Ele não manipula diretamente o banco de dados (opera indiretamente por meio dos métodos do Modelo); 3. Ele não realiza transformação de dados (retorna o resultado de lean() diretamente usando res.json); 4. Os erros são repassados ao middleware de tratamento de erros por meio de next(err) (não use try-catch dentro do Controlador nem defina res.status diretamente).

JAVASCRIPT
// controllers/productController.js
const Product = require('../models/Product');

exports.listProducts = async (req, res) => {
  const { page = 1, limit = 20, category } = req.query;
  const query = { isActive: true };
  if (category) query.category = category;

  const products = await Product.find(query)
    .select('sku title price thumbnail')
    .limit(limit * 1)
    .skip((page - 1) * limit)
    .lean();

  res.json({ data: products, page, limit });
};

exports.getProduct = async (req, res) => {
  const product = await Product.findOne({ sku: req.params.sku })
    .select('-__v')
    .lean();

  if (!product) {
    return res.status(404).json({ error: 'Product not found' });
  }
  res.json(product);
};

exports.createProduct = async (req, res) => {
  const product = await Product.create(req.body);
  res.status(201).json(product);
};

(3) Camada de rota

Princípios de projeto da camada de rota: A camada de rota lida apenas com o mapeamento de URL para controlador — ela não contém nenhuma lógica. No entanto, é possível inserir middleware nesse mapeamento — a linha router.post('/', authenticate, authorize('admin'), validate(createSchema), ctrl.create) representa a cadeia completa de processamento da solicitação: autenticação → autorização → validação → lógica de negócios. Responsabilidades da camada de rotas: 1. Definir padrões de URL (estilo RESTful); 2. Mapear métodos HTTP para métodos de controlador; 3. Anexar middleware (autenticação, autorização, validação); 4. Não conter nenhuma lógica de processamento de dados.

Organização modular das rotas: O roteamento em grandes projetos requer uma abordagem modular — 1. Divida os arquivos por recurso (routes/products.js, routes/orders.js, routes/users.js), com cada arquivo definindo rotas para apenas um tipo de recurso; 2. Crie rotas modulares usando o Express Router (const router = express.Router()) e monte-as com app.use('/api/products', productRouter); 3. Use rotas aninhadas para expressar hierarquias de recursos (/api/posts/:postId/comments é tratada pela rota comments); 4. Implementar o controle de versões das rotas (app.use('/api/v1', v1Router), app.use('/api/v2', v2Router)). O roteamento modular torna a colaboração em equipe mais eficiente — cada pessoa é responsável por um arquivo de rota, garantindo que não haja conflitos.

Estratégias de combinação de middleware: O middleware do Express pode ser combinado para implementar cadeias complexas de tratamento de solicitações — 1. Middleware global (nível app.use): express.json(), cors(), helmet(), morgan() (registro de logs), rateLimit() (limitação de taxa); 2. Middleware de grupo de roteadores (nível router.use): authenticate (para grupos de roteadores que exigem login), authorize('admin') (para o grupo de roteadores de administração); 3. Middleware de rota única (nível de parâmetro router.get): validate(schema) (lógica de validação para um endpoint específico). O poder da composição de middleware — uma única chamada router.post('/', auth, validate, ctrl.create) implementa a tríplice proteção de “autenticação + validação + lógica de negócios”, eliminando a necessidade de duplicar essa lógica no controlador.

JAVASCRIPT
// routes/products.js
const express = require('express');
const router = express.Router();
const ctrl = require('../controllers/productController');

router.get('/', ctrl.listProducts);
router.get('/:sku', ctrl.getProduct);
router.post('/', ctrl.createProduct);

module.exports = router;

5. Middleware para tratamento de erros

Cadeias de middleware e propagação de erros: No Express, os middlewares formam uma cadeia de chamadas com base na ordem em que são registrados — next() passa o controle para o próximo middleware, enquanto next(err) ignora todos os middlewares regulares e segue diretamente para o middleware de tratamento de erros. Esse mecanismo centraliza o tratamento de erros, eliminando a necessidade de escrever blocos try-catch em cada rota. No entanto, observe que exceções dentro das funções async não acionam automaticamente o next(err); elas devem ser envolvidas em um bloco try-catch ou tratadas automaticamente pelo express-async-errors.

Segurança da informação para respostas de erro: Nunca exponha mensagens de erro em bruto aos clientes em um ambiente de produção — isso poderia vazar strings de conexão de banco de dados, caminhos de arquivos ou rastreamentos de pilha. Regras de mapeamento do middleware de tratamento de erros: ValidationError → 400 (retorna detalhes do erro no nível do campo), CastError → 400 (retorna “Formato de ID inválido”), E11000 → 409 (retorna nome de campo duplicado), outros erros → 500 (retorna apenas “Erro interno”; os detalhes são registrados no log).

A necessidade do express-async-errors: O middleware do Express 4.x não oferece suporte a funções assíncronas — se uma exceção for lançada dentro de um middleware assíncrono, o Express não a interceptará, e a solicitação ficará pendente até atingir o tempo limite. Soluções: 1. try-catch manual + next(err) (redundância de código); 2. O pacote express-async-errors (uma única linha require envolve automaticamente todas as funções de manipuladores de rota); 3. O Express 5.x oferece suporte nativo a middleware assíncrono. A opção 2 é fortemente recomendada para ambientes de produção; basta adicionar require('express-async-errors') no início de app.js — sem necessidade de modificar nenhum código existente.

Política de registro em logs: O middleware de tratamento de erros não deve apenas retornar uma resposta, mas também registrar eventos em logs — no entanto, o nível de detalhe nos logs deve variar de acordo com o ambiente: no ambiente de desenvolvimento, deve-se registrar o rastreamento completo da pilha (para facilitar a depuração); no ambiente de produção, deve-se registrar apenas o tipo de erro e o ID da solicitação (para evitar que informações confidenciais sejam gravadas nos arquivos de log). Recomendamos o uso do Winston ou do Pino em vez do console.error — eles oferecem suporte a níveis de registro (erro/aviso/informação/depuração), rotação de registros e saída estruturada (no formato JSON para facilitar a recuperação pelo ELK).

JAVASCRIPT
// middlewares/errorHandler.js
const errorHandler = (err, req, res, next) => {
  console.error(err);

  if (err.name === 'ValidationError') {
    return res.status(400).json({
      error: 'ValidationError',
      details: Object.fromEntries(
        Object.entries(err.errors).map(([k, v]) => [k, v.message])
      )
    });
  }

  if (err.code === 11000) {
    return res.status(409).json({ error: 'Duplicate key', field: err.keyValue });
  }

  if (err.name === 'CastError') {
    return res.status(400).json({ error: 'Invalid ID format' });
  }

  res.status(err.status || 500).json({
    error: err.message || 'Internal Server Error'
  });
};

module.exports = errorHandler;

6. Estrutura completa do aplicativo

Análise aprofundada do gerenciamento do pool de conexões: O pool de conexões do Mongoose é um mecanismo central do driver do MongoDB para Node.js — maxPoolSize determina o número máximo de operações simultâneas no banco de dados, enquanto minPoolSize pré-aquece as conexões ociosas para evitar atrasos na inicialização a frio. Quando o pool de conexões se esgota, novas solicitações são colocadas em uma fila de espera (controlada por waitQueueTimeoutMS), e um erro é relatado após o tempo limite expirar. Ajuste em produção: 1. maxPoolSize = simultaneidade da aplicação × 1,5 (para permitir margem de segurança); 2. minPoolSize = maxPoolSize × 0,1 (para pré-aquecimento); 3. Defina serverSelectionTimeoutMS como 5.000 (para falha rápida em vez de uma espera de 30 segundos); 4. Defina autoIndex como false em produção (a criação de índices bloqueia as conexões).

Diagnóstico do esgotamento do pool de conexões: Quando o aplicativo exibe a mensagem “Mongoose: tempo limite de conexão”, as possíveis causas incluem: 1. O pool de conexões é muito pequeno (maxPoolSize < número real de conexões simultâneas; aumente o tamanho do pool); 2. Consultas lentas estão ocupando as conexões (se uma consulta demorar 10 segundos sem liberar a conexão, use db.currentOp() para identificar a consulta lenta e encerrá-la); 3. Vazamentos de conexão (por exemplo, chamadas de db.currentOp() sem await no código ou cursores não fechados); 4. Carga excessiva no servidor MongoDB (verifique a CPU, a memória e a E/S do disco). Etapas de diagnóstico: 1. Verifique mongoose.connection.readyState (0 = desconectado, 1 = conectado, 2 = conectado, 3 = desconectando); 2. Verifique o número de conexões ativas usando db.serverStatus().connections; 3. Use uma ferramenta de APM (New Relic/Datadog) para monitorar a duração das consultas ao banco de dados.

Conflito entre pools de conexão e arquitetura sem servidor: Em um ambiente sem servidor (AWS Lambda/Cloud Functions), cada instância de função cria seu próprio pool de conexões — se 100 solicitações simultâneas acionarem 100 funções Lambda, e cada função Lambda tiver 5 conexões, um total de 500 conexões sobrecarregará o MongoDB. Soluções: 1. Reduzir significativamente maxPoolSize (recomenda-se 5–10 para cenários Lambda); 2. Use o proxy de pool de conexões do MongoDB Atlas; 3. Inicialize mongoose.connect fora do manipulador Lambda (para reutilizar o pool de conexões e reduzir o número de conexões durante inicializações a frio); 4. Considere usar uma API HTTP em vez de conexões diretas (por exemplo, Data API ou Realm Web SDK).

Monitoramento de eventos de conexão: O objeto de conexão do Mongoose emite vários eventos que podem ser usados para monitoramento — 1. mongoose.connection.on('connected', ...): Conexão bem-sucedida com o MongoDB (registro em log); 2. mongoose.connection.on('error', ...): Erro de conexão (alerta + nova tentativa); 3. mongoose.connection.on('disconnected', ...): Conexão perdida (possivelmente devido a um problema de rede ou reinicialização do MongoDB; o mecanismo de reconexão automática tentará restaurar a conexão); 4. mongoose.connection.on('reconnected', ...): Conexão restabelecida com sucesso (registre isso; as operações de negócios podem ter sido interrompidas e precisam ser restauradas); 5. mongoose.connection.on('close', ...): Conexão encerrada (encerramento do aplicativo ou mongoose.disconnect()). Esses eventos devem ser monitorados em ambientes de produção; caso contrário, desconexões silenciosas podem levar a incidentes de produção em que “todas as consultas ao banco de dados expiram”.

Melhores práticas para encerramento controlado: Quando o aplicativo for encerrado, ele deve fechar a conexão com o MongoDB de forma controlada — 1. Ficar atento aos sinais SIGINT/SIGTERM (Ctrl+C ou sinais de encerramento de pod do Kubernetes); 2. Parar de aceitar novas solicitações (server.close()); 3. Aguardar a conclusão das solicitações em andamento (consultas ativas no pool de conexões); 4. Desconecte-se do Mongoose (mongoose.disconnect()); 5. Encerre o processo (process.exit(0)). Consequências de um desligamento não ordenado: as operações em andamento no banco de dados são interrompidas, o que pode levar a inconsistências nos dados (como documentos que foram gravados apenas parcialmente). O Kubernetes concede aos pods 30 segundos para um desligamento ordenado; o aplicativo deve concluir as etapas acima em até 30 segundos.

100%
sequenceDiagram
    participant Main as app.js
    participant Dotenv as dotenv
    participant DB as connectDB
    participant Express as Express App
    participant Listen as app.listen

    Main->>Dotenv: Loading .env
    Dotenv-->>Main: Environment Variables Are Ready
    Main->>Express: Create app + Middleware
    Main->>Express: Registration Routes
    Main->>Express: Handling Registration Errors
    Main->>DB: await connectDB()
    DB-->>Main: ✅ MongoDB connected
    Main->>Listen: app.listen(PORT)
    Listen-->>Main: 🚀 Server running

Ordem de registro do middleware: O Express executa os middlewares na ordem em que são registrados, e essa ordem é fundamental: JSON parsing → logging → routes → 404 handling → error handling. O tratamento de erros deve ser colocado por último.

Detalhes da implementação do middleware 404: O middleware 404 é um middleware comum, sem parâmetros de caminho — ele é colocado após todas as rotas e é executado quando nenhuma das rotas anteriores corresponde. Implementação: app.use((req, res) => res.status(404).json({error: 'Não encontrado'})). Observação: O middleware 404 não é um middleware de erro (que recebe 4 parâmetros), mas um middleware comum (que recebe 3 parâmetros). Ele não requer next() porque não há funções de middleware subsequentes. Erros comuns: Colocar o middleware 404 antes das rotas (fazendo com que todas as solicitações retornem um 404) ou esquecer de incluir o middleware 404 (as rotas não correspondentes são tratadas pelo Express por padrão, retornando uma mensagem no formato HTML “Cannot GET /xxx” em vez de JSON).

Ordem Middleware Função
1 express.json() Analisar o corpo da solicitação
2 cors() Suporte entre domínios
3 Middleware de registro Registros de solicitações
4 Roteamento Processamento de negócios
5 404 Middleware Nenhuma rota correspondente
6 Tratamento de erros Respostas unificadas a erros

Gerenciamento de Configurações de Ambiente: Os projetos Express + Mongoose precisam gerenciar configurações para vários ambientes (desenvolvimento/teste/produção) — 1. O arquivo .env armazena as configurações locais de desenvolvimento (URL do banco de dados, porta, segredos) e não é enviado para o Git; 2. O arquivo .env.example é enviado para o Git como um modelo; 3. As configurações de produção são injetadas por meio de variáveis de ambiente (Docker/K8s/plataformas em nuvem) e não utilizam um arquivo .env; 4. O diretório config/ carrega diferentes configurações com base no ambiente (config/development.js, config/production.js). Princípio fundamental: nunca codifique chaves ou senhas diretamente no código e nunca as envie para o Git.

Desligamento gradual: As aplicações Express em produção devem suportar o desligamento gradual — ao receber um sinal SIGTERM ou SIGINT: 1. Interromper a aceitação de novas solicitações (server.close()); 2. Aguardar a conclusão das solicitações pendentes (definir um tempo limite, como 10 segundos); 3. Fechar a conexão com o Mongoose (mongoose.disconnect()); 4. Encerre o processo. Consequências da falha em realizar um desligamento ordenado: as operações do banco de dados em andamento são interrompidas à força, o que pode levar a inconsistências nos dados. As atualizações contínuas do Kubernetes/Docker dependem de desligamentos ordenados — os Pods antigos devem concluir todas as solicitações antes que o tempo limite expire.

Endpoint de verificação de integridade: O ambiente de produção deve fornecer um endpoint /health — utilizado por balanceadores de carga para determinar se a aplicação está em bom estado, bem como pelas sondas de integridade/prontidão do Kubernetes. A verificação de integridade deve incluir: 1. Uma resposta HTTP 200 (o processo da aplicação está ativo); 2. mongoose.connection.readyState === 1 (a conexão com o banco de dados está normal); 3. Verificações opcionais: conexão com o Redis, espaço em disco e uso de memória. Implementação simples: app.get('/health', (req, res) => { if (mongoose.connection.readyState === 1) res.json({status: 'ok'}); else res.status(503).json({status: 'db disconnected'}); }). Configuração do Kubernetes: O livenessProbe verifica a cada 10 segundos; se falhar três vezes seguidas, o Pod é reiniciado. O readinessProbe verifica a cada 5 segundos; se falhar duas vezes seguidas, o Pod é removido do Serviço.

Sistema de Defesa de Segurança em Camadas: A segurança das aplicações Express não se baseia em uma única medida, mas em uma abordagem de defesa em camadas — 1. Middleware Helmet (define 12 cabeçalhos HTTP de segurança: X-Content-Type-Options, X-Frame-Options, CSP, etc.); 2. Política CORS (a função cors() coloca as origens permitidas na lista de permissões; não defina Access-Control-Allow-Origin: *); 3. Limitação de taxa (express-rate-limit para prevenir ataques de força bruta e DDoS); 4. Validação de entrada (joi/express-validator para prevenir ataques de injeção); 5. Autenticação e autorização (JWT + RBAC para impedir operações não autorizadas); 6. HTTPS (transmissão criptografada por TLS para impedir ataques man-in-the-middle). Cada camada oferece proteção independente; uma violação em qualquer camada isolada não compromete a eficácia das demais camadas.

Endpoints de verificação de integridade: As aplicações em produção devem fornecer os endpoints /health e /ready/health verifica se o processo está ativo (um simples 200 OK), e /ready verifica se ele está pronto para aceitar solicitações (incluindo se a conexão com o MongoDB está funcionando). O Kubernetes usa esses dois endpoints para sondas de atividade e sondas de prontidão. Implementação de /ready: try { await mongoose.connection.db.admin().ping() } catch { return res.status(503).json({ready: false}) }. Se o MongoDB estiver inacessível, /ready retorna um código de status 503, e o Kubernetes suspende o tráfego para aquele Pod, mas não o reinicia (ao contrário da sonda de atividade).

Projeto avançado de verificação de integridade: As verificações de integridade em ambiente de produção vão além de simplesmente enviar um ping ao banco de dados — 1. Verificações de dependências: Verifique o MongoDB (ping), o Redis (ping) e as APIs externas (solicitação HEAD) em sequência; se algum deles estiver inacessível, marque o sistema como “não pronto”; 2. Atraso na inicialização: Não marque o sistema como “pronto” imediatamente após o início da aplicação (a conexão com o Mongoose ainda pode estar sendo estabelecida); aguarde até que connection.on('connected') seja acionado antes de marcá-lo como pronto; 3. Controle de tempo limite: A verificação de integridade em si não deve bloquear (defina um tempo limite de 5 segundos; se o tempo limite for atingido, marque como “não pronto”); 4. Modo degradado: Se o MongoDB estiver inacessível, mas o Redis estiver disponível, retorne um status “degradado” (fornecendo dados em cache em vez de ficar completamente indisponível). As verificações de integridade servem como uma ponte entre as operações e o desenvolvimento — os desenvolvedores fornecem endpoints, as operações configuram sondas e os SREs configuram alertas.

JAVASCRIPT
// app.js
require('dotenv').config();
const express = require('express');
const { connectDB } = require('./db');
const productRoutes = require('./routes/products');
const errorHandler = require('./middlewares/errorHandler');

const app = express();

app.use(express.json());

app.get('/healthz', (req, res) => {
  res.json({ status: 'ok', uptime: process.uptime() });
});

app.use('/api/products', productRoutes);

app.use(errorHandler);

(async () => {
  await connectDB();
  app.listen(process.env.PORT || 3000);
})();

7. Variáveis de ambiente do dotenv

Por que precisamos de variáveis de ambiente? Strings de conexão com bancos de dados, chaves JWT e números de porta codificados diretamente no código estão espalhados por todo o código — o que significa que: ① Um vazamento de código leva a um vazamento de chave; ② A mudança de ambiente (dev/staging/prod) exige a modificação do código; ③ Ao colaborar, cada pessoa tem uma configuração diferente. A solução do dotenv é carregar variáveis do arquivo .env para o process.env; o código apenas lê as variáveis de ambiente, e ambientes diferentes utilizam arquivos .env distintos.

Classificação de segurança das variáveis de ambiente: As variáveis de ambiente são classificadas em três níveis com base na sensibilidade — 1. Configurações públicas (PORT, NODE_ENV, LOG_LEVEL): Podem ser submetidas ao Git; não são sensíveis; 2. Configurações internas (MONGODB_URI, REDIS_URL, API_BASE_URL): Não são submetidas ao Git; compartilhadas dentro da equipe; injetadas por meio do arquivo .env ou de CI/CD; 3. Credenciais secretas (JWT_SECRET, AWS_ACCESS_KEY, DATABASE_PASSWORD): Sensibilidade máxima; não são submetidas ao Git nem armazenadas no arquivo .env; injetadas por meio de um serviço de gerenciamento de segredos (AWS Secrets Manager/HashiCorp Vault). Essa abordagem de gerenciamento em camadas mitiga o risco de “toda a configuração ser armazenada no .env, levando a um comprometimento total caso o .env vaze”.

Melhores práticas para o gerenciamento de variáveis de ambiente:

Exercício Descrição
Não envie o arquivo .env para o repositório Adicione-o ao .gitignore para evitar que seja divulgado
Enviando o arquivo .env.example Fornece um modelo; não contém valores reais
Gerenciamento de chaves de produção AWS Secrets Manager / HashiCorp Vault
Verificar variáveis obrigatórias Verificar, na inicialização, se MONGODB_URI e outras variáveis existem
Separação de ambientes NODE_ENV=desenvolvimento/produção

Validação de variáveis de ambiente na inicialização: Quando o aplicativo é iniciado, ele deve verificar se todas as variáveis de ambiente necessárias estão presentes — se MONGODB_URI não estiver definida, todas as operações de banco de dados falharão após o início do aplicativo; portanto, é melhor gerar um erro e encerrar o processo na inicialização. Abordagens de validação: 1. Validação simples (if (!process.env.MONGODB_URI) throw new Error('MONGODB_URI is required')); 2. Usar o dotenv-safe (compara automaticamente .env e .env.example; gera um erro se uma variável estiver faltando); 3. Usar o joi para validação (definir configSchema para verificar se os valores em process.env são válidos; por exemplo, PORT deve ser um número). Em um ambiente de produção, recomendamos a abordagem 3 — ela não apenas verifica a existência, mas também valida os valores (por exemplo, PORT deve ser um número entre 1024 e 65535, e NODE_ENV deve ser um dos seguintes: development, test ou production).

Hierarquia de configuração: Variáveis de ambiente > arquivo .env > valores padrão. O código deve ser escrito da seguinte forma: process.env.PORT || 3000 — As variáveis de ambiente têm precedência; se nenhuma estiver definida, os valores padrão são utilizados.

BASH
# .env
MONGODB_URI=mongodb://localhost:27017/shopdb
PORT=3000
NODE_ENV=development
JWT_SECRET=your-secret-key
JAVASCRIPT
// config.js
require('dotenv').config();

module.exports = {
  port: parseInt(process.env.PORT) || 3000,
  mongoUri: process.env.MONGODB_URI,
  jwtSecret: process.env.JWT_SECRET,
  nodeEnv: process.env.NODE_ENV || 'development'
};

8. Prática: Uma API CRUD completa

Princípios de projeto de roteamento RESTful: Use substantivos no plural para nomes de recursos (por exemplo, “produtos” em vez de “produto”); use caminhos hierárquicos para recursos aninhados (por exemplo, “/produtos/:id/avaliações”); use métodos HTTP para expressar a semântica das operações (GET para leitura, POST para gravação, DELETE para exclusão). Use o prefixo “/auth” para rotas de autenticação e o prefixo “/api” para rotas de negócios.

Controvérsias comuns no projeto de roteamento: Existem várias controvérsias comuns no projeto de roteamento RESTful — 1. Plural x singular: /products ou /product? A convenção do setor favorece o plural (para denotar uma coleção), mas a API do GitHub usa o singular. A consistência é mais importante do que a escolha em si; 2. Profundidade de aninhamento: /products/:id/reviews/:reviewId ou /reviews/:reviewId? Recomenda-se um máximo de dois níveis de aninhamento; para mais de dois níveis, use um recurso de nível superior mais parâmetros de filtro (por exemplo, /reviews?productId=xxx); 3. Endpoints de ação: /users/:id/activate é RESTful? Estritamente falando, não, mas é comum em projetos reais (é mais intuitivo do que PUT /users/:id {status: 'active'}). REST é uma diretriz, não um dogma; priorize a legibilidade e a praticidade.

Pontos-chave sobre segurança de APIs:

Medidas de segurança Implementação
Autenticação Token JWT Bearer
Autorização Verificação de função RBAC (admin/cliente)
Validação de entrada Validação do esquema joi
Limitação de taxa express-rate-limit
CORS Domínios na lista de permissões
Limite do corpo da solicitação express.json({ limit: '1mb' })

Estratégia de defesa em camadas para segurança: A segurança de APIs não é uma medida isolada, mas sim um sistema de defesa em várias camadas — 1. Camada de rede (criptografia HTTPS, firewall WAF, lista de IPs autorizados); 2. Camada de aplicação (autenticação + autorização + validação de entrada + limitação de taxa); 3. Camada de dados (validação de esquema do Mongoose + $jsonSchema como alternativa + select: false para ocultar campos confidenciais); 4. Camada de operações (auditoria de logs + detecção de anomalias + bloqueio automático). Cada camada oferece proteção independente; mesmo que uma camada seja violada, as outras permanecem intactas. Um erro comum é confiar exclusivamente em uma única camada (como implementar autenticação sem autorização ou realizar validação de esquema sem validação de entrada).

Estratégias de projeto para limitação de taxa: As estratégias de limitação de taxa para express-rate-limit precisam ser adaptadas a cenários específicos — 1. Limitação de taxa global (compartilhada entre todas as APIs, por exemplo, 100 solicitações por minuto para prevenir ataques DDoS); 2. Limitação de taxa rigorosa para endpoints autenticados (5 solicitações de login/cadastro por IP por minuto para prevenir ataques de força bruta); 3. Limites moderados em endpoints de gravação (30 solicitações por usuário por minuto para operações de criação/atualização, para prevenir spam); 4. Limites flexíveis em endpoints de leitura (200 solicitações por usuário por minuto para operações de listagem/detalhes, sem bloqueio durante o uso normal). A limitação de taxa é aplicada com base em uma abordagem bidimensional de endereço IP + ID de usuário — vários usuários que compartilham o mesmo endereço IP não afetam uns aos outros, e um único usuário que alterna de endereço IP continua sujeito ao limite.

Princípios de segurança para a configuração do CORS: A configuração do CORS (Cross-Origin Resource Sharing) determina quais domínios front-end podem acessar a API — nunca utilize Access-Control-Allow-Origin: * em um ambiente de produção. Configuração correta: 1. Uma lista de domínios na lista de permissões (por exemplo, ['https://shop.example.com', 'https://admin.example.com']); 2. Métodos HTTP permitidos (GET/POST/PUT/DELETE; nenhum método especial além de OPTIONS); 3. Cabeçalhos de solicitação permitidos (Content-Type, Authorization); 4. Suporte a credenciais (quando credentials: true, domínios específicos devem ser indicados; curingas não são permitidas). O curinga * pode ser usado em ambientes de desenvolvimento, mas deve ser diferenciado por meio de NODE_ENV.

JAVASCRIPT
// routes/reviews.js
const express = require('express');
const router = express.Router();
const Review = require('../models/Review');
const { authenticate } = require('../middlewares/auth');

router.get('/products/:productId/reviews', async (req, res) => {
  const reviews = await Review.find({ productId: req.params.productId })
    .populate('userId', 'username avatar')
    .sort({ createdAt: -1 })
    .limit(20)
    .lean();
  res.json(reviews);
});

router.post('/products/:productId/reviews', authenticate, async (req, res) => {
  const review = await Review.create({
    productId: req.params.productId,
    userId: req.user._id,
    content: req.body.content,
    rating: req.body.rating
  });
  res.status(201).json(review);
});

module.exports = router;

Aplicação de middleware no nível da rota: O middleware do Express pode ser aplicado em diferentes níveis — 1. Nível de aplicação (app.use(cors())), que se aplica a todas as rotas; 2. Nível de rota (router.use(authenticate)), que se aplica a todos os endpoints dessa rota; 3. Nível de endpoint (router.post('/', authenticate, validate, ctrl.create)), que se aplica apenas a esse endpoint. Quanto mais fina for a granularidade, mais preciso será o controle, mas maior será a duplicação de código. Estratégia recomendada: coloque o middleware genérico (cors/json/logging) no nível da aplicação, a autenticação no nível do roteador e a validação e autorização no nível do endpoint.

Tratamento de erros assíncronos em controladores: O Express 4.x não captura automaticamente exceções em funções assíncronas — se um erro for lançado por uma instrução await dentro da função assíncrona de um controlador, o Express não chamará next(err), e a solicitação ficará travada. Três soluções: 1. express-async-errors (resolve o problema globalmente com uma única linha require; altamente recomendado); 2. Envolver manualmente com try-catch + next(err) (redundante, mas explícito); 3. Usar a função de ordem superior wrapAsync(fn) para envolvimento automático (flexível, mas requer envolvimento manual para cada rota). A solução 1 é não intrusiva; uma vez instalada, todas as rotas assíncronas ganham automaticamente a capacidade de propagação de erros.

Chamadas encadeadas no Construtor de Consultas: O construtor de consultas do Mongoose suporta chamadas encadeadas — Model.find(query).select(fields).populate(ref).sort(order).skip(n).limit(m).lean(). A ordem das chamadas encadeadas não afeta os resultados da consulta (o Mongoose otimiza isso internamente), mas a ordem mais legível é: find → select → populate → sort → skip → limit → lean. Essa ordem se alinha à lógica de “primeiro definir o que consultar e, depois, decidir como apresentá-lo”. O método lean() deve sempre ser colocado por último — pois ele converte os resultados da consulta de um documento do Mongoose em um objeto JavaScript simples, após o que você não pode mais encadear métodos do documento.

Desempenho das operações em massa: Quando você precisa criar, atualizar ou excluir vários registros, as operações em massa são de 10 a 100 vezes mais rápidas do que as operações individuais — 1. Model.insertMany([...]) insere N registros em uma única viagem de ida e volta, o que é N vezes mais rápido do que N chamadas para Model.create(); 2. Model.updateMany(filter, update) atualiza todos os documentos correspondentes em uma única operação, o que é muito mais rápido do que executar operações updateOne individuais; 3. Model.deleteMany(filter) realiza a exclusão em massa. No entanto, as operações em massa apresentam as seguintes limitações: 1. Elas não acionam o middleware pré ou pós-gravação (apenas o middleware de validação é acionado durante o insertMany); 2. Elas não retornam objetos de documento completos (apenas são retornadas confirmações de gravação); 3. O volume de dados por operação é limitado a 16 MB pelo BSON.


▶ Exemplo 1: Conexões básicas e roteamento com Express e Mongoose

JAVASCRIPT
// === 1. Minimum Express + mongoose Applications ===
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');

const app = express();
app.use(express.json());

// mongoose Connect
mongoose.connect(process.env.MONGODB_URI, {
  maxPoolSize: 10,
  serverSelectionTimeoutMS: 5000
}).then(() => console.log('✅ MongoDB connected'))
  .catch(err => { console.error('❌ Connection failed:', err.message); process.exit(1); });

// The simplest CRUD
app.get('/api/products', async (req, res) => {
  const products = await mongoose.model('Product').find().lean();
  res.json({ data: products });
});

app.post('/api/products', async (req, res) => {
  const product = await mongoose.model('Product').create(req.body);
  res.status(201).json({ data: product });
});

app.use((err, req, res, next) => {
  res.status(500).json({ error: err.message });
});

app.listen(3000, () => console.log('🚀 Server running on port 3000'));

Resultado: Uma aplicação mínima e executável em Express + Mongoose que pode ser iniciada com apenas três arquivos (app.js, .env e package.json).

▶ Exemplo: Completo REST API com Authentication e Error Heling (Difficulty ⭐⭐)

JAVASCRIPT
// Scene: ShopHub product API with JWT auth, validation, and centralized error handling
const express = require('express');
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

// Models
const Product = mongoose.model('Product', new mongoose.Schema({
  sku: { type: String, unique: true, required: true },
  title: { type: String, required: true },
  price: { type: Number, required: true, min: 0 },
  category: String,
  stock: { type: Number, default: 0 }
}));

// Auth Middleware
const authMiddleware = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'No token provided' });
  
  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch {
    res.status(401).json({ error: 'Invalid token' });
  }
};

// Error Handler
const errorHandler = (err, req, res, next) => {
  if (err.name === 'ValidationError') {
    return res.status(400).json({ error: err.message });
  }
  if (err.code === 11000) {
    return res.status(409).json({ error: 'Duplicate key error' });
  }
  console.error(err);
  res.status(500).json({ error: 'Internal server error' });
};

// Routes
const router = express.Router();

// GET /api/products - List with pagination
router.get('/', async (req, res, next) => {
  try {
    const { page = 1, limit = 10, category } = req.query;
    const filter = category ? { category } : {};
    
    const [products, total] = await Promise.all([
      Product.find(filter).skip((page - 1) * limit).limit(limit).lean(),
      Product.countDocuments(filter)
    ]);
    
    res.json({ products, total, page, pages: Math.ceil(total / limit) });
  } catch (err) { next(err); }
});

// POST /api/products - Create (requires auth)
router.post('/', authMiddleware, async (req, res, next) => {
  try {
    const product = await Product.create(req.body);
    res.status(201).json(product);
  } catch (err) { next(err); }
});

module.exports = { router, errorHandler };

Saída:

TEXT 📖 Somente leitura
GET /api/products?category=Electronics
{"products":[{"sku":"PHONE-001","title":"Smartphone X","price":999}],...}

POST /api/products (without token)
{"error":"No token provided"}

POST /api/products (with valid token + invalid data)
{"error":"Product validation failed: price: Path `price` (-10) is less than minimum (0)"}

▶ Exemplo 2: Arquitetura completa de API de comércio eletrônico usando Express e Mongoose

JAVASCRIPT
// === Complete Project Structure ===
// shophub-api/
// ├── src/
// │   ├── app.js              # Express App Portal
// │   ├── config/
// │   │   ├── db.js          # mongoose Connect
// │   │   └── index.js       # Environment Configuration
// │   ├── models/
// │   │   ├── User.js
// │   │   ├── Product.js
// │   │   └── Order.js
// │   ├── controllers/
// │   │   ├── authController.js
// │   │   ├── productController.js
// │   │   └── orderController.js
// │   ├── routes/
// │   │   ├── auth.js
// │   │   ├── products.js
// │   │   └── orders.js
// │   ├── middlewares/
// │   │   ├── auth.js        # JWT Verification
// │   │   ├── errorHandler.js
// │   │   └── validate.js    # joi Verification
// │   └── utils/
// │       └── logger.js
// ├── .env
// └── package.json

// === 1. db.js - Connection Configuration ===
const mongoose = require('mongoose');

async function connectDB() {
  const conn = await mongoose.connect(process.env.MONGODB_URI, {
    serverSelectionTimeoutMS: 5000,
    maxPoolSize: 50,
    minPoolSize: 5,
    socketTimeoutMS: 45000,
    autoIndex: process.env.NODE_ENV !== 'production'  // Production Shutdown autoIndex
  });
  console.log(`✅ MongoDB connected: ${conn.connection.host}`);
  return conn;
}

module.exports = { connectDB };

// === 2. middlewares/auth.js - JWT Verification ===
const jwt = require('jsonwebtoken');

exports.authenticate = (req, res, next) => {
  const token = req.header('Authorization')?.replace('Bearer ', '');
  if (!token) return res.status(401).json({ error: 'No token' });
  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
};

exports.authorize = (...roles) => (req, res, next) => {
  if (!req.user || !roles.includes(req.user.role)) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  next();
};

// === 3. controllers/productController.js ===
const Product = require('../models/Product');

exports.list = async (req, res, next) => {
  try {
    const { page = 1, limit = 20, category, search, sort = 'createdAt', order = 'desc' } = req.query;
    const query = { isActive: true };
    if (category) query.category = category;
    if (search) query.title = new RegExp(search, 'i');

    const [products, total] = await Promise.all([
      Product.find(query)
        .select('sku title price thumbnail rating')
        .sort({ [sort]: order === 'desc' ? -1 : 1 })
        .skip((page - 1) * limit)
        .limit(+limit)
        .lean(),
      Product.countDocuments(query)
    ]);

    res.json({
      success: true,
      data: products,
      meta: { page: +page, limit: +limit, total, pages: Math.ceil(total / limit) }
    });
  } catch (err) { next(err); }
};

exports.get = async (req, res, next) => {
  try {
    const product = await Product.findOne({ sku: req.params.sku }).lean();
    if (!product) return res.status(404).json({ success: false, error: { code: 'NOT_FOUND' } });
    res.json({ success: true, data: product });
  } catch (err) { next(err); }
};

exports.create = async (req, res, next) => {
  try {
    const product = await Product.create(req.body);
    res.status(201).json({ success: true, data: product });
  } catch (err) { next(err); }
};

// === 4. routes/products.js ===
const router = require('express').Router();
const ctrl = require('../controllers/productController');
const { authenticate, authorize } = require('../middlewares/auth');

router.get('/', ctrl.list);
router.get('/:sku', ctrl.get);
router.post('/', authenticate, authorize('admin'), ctrl.create);

module.exports = router;

// === 5. app.js - Main App ===
require('dotenv').config();
const express = require('express');
const { connectDB } = require('./config/db');

const app = express();
app.use(express.json({ limit: '1mb' }));

// Health Checkup
app.get('/healthz', (req, res) => {
  res.json({ status: 'ok', uptime: process.uptime() });
});

// Routing
app.use('/api/products', require('./routes/products'));
app.use('/api/orders', require('./routes/orders'));
app.use('/api/auth', require('./routes/auth'));

// Error Handling
app.use(require('./middlewares/errorHandler'));

(async () => {
  await connectDB();
  app.listen(process.env.PORT || 3000, () => {
    console.log(`🚀 Server running on port ${process.env.PORT || 3000}`);
  });
})();

Resultado: Uma arquitetura MVC completa que oferece suporte à autenticação JWT, permissões RBAC, tratamento de erros e verificações de integridade.

A evolução da arquitetura MVC: A evolução arquitetônica de um projeto, de pequeno a grande, pode ser dividida em três estágios — 1. Estágio de arquivo único (MVP): Toda a lógica está contida em app.js; adequado para demonstrações e protótipos (< 100 linhas de código); 2. Fase em camadas (Produção): arquivos de Modelo, Controlador e Rota separados + middleware + configuração; adequada para projetos de produção (100–1.000 linhas); 3. Fase modular (escalonamento): Módulos organizados por domínio de negócios (por exemplo, user, order, product, cada um contendo Modelo + Controlador + Rota), com comunicação entre os módulos por meio da camada de Serviço; adequado para projetos de grande escala (mais de 1.000 linhas). O princípio central dessa evolução é “introduzir complexidade somente quando surgirem pontos críticos” — não projete uma arquitetura modular durante a fase de MVP, nem continue usando uma estrutura de arquivo único quando a base de código ultrapassar 1.000 linhas.

Lista de verificação pré-lançamento do ambiente de produção: Deve ser verificada antes da implantação do aplicativo — 1. Variáveis de ambiente: Todas as variáveis necessárias estão definidas e são válidas (MONGODB_URI, JWT_SECRET, NODE_ENV=production); 2. Conexão com o banco de dados: o pool de conexões está configurado corretamente (maxPoolSize definido com base na simultaneidade), o tempo limite está definido (serverSelectionTimeoutMS: 5000); 3. Middleware de segurança: helmet (cabeçalhos de segurança), cors (lista de permissões entre origens), express-rate-limit (limitação de taxa), mongo-sanitize (proteção contra injeção); 4. Registros: Winston e Morgan estão configurados corretamente, e os registros de erros são gravados em um arquivo, em vez de serem exibidos apenas no console; 5. Desligamento gradual: os sinais SIGTERM e SIGINT são tratados, e o processo é encerrado somente após o fechamento da conexão com o banco de dados; 6. Verificação de integridade: o endpoint /health pode ser detectado pelo balanceador de carga. A aplicação só pode entrar em produção após passar por todas as 6 verificações.

❓ Perguntas Frequentes

P: O que é melhor, Express ou Koa/Fastify? R: O Express possui o ecossistema mais maduro. O Fastify oferece melhor desempenho (2 a 3 vezes), enquanto o Koa é mais leve.

P: Qual é o tamanho adequado para um pool de conexões do Mongoose? R: Ajuste-o de acordo com a simultaneidade. Geralmente, entre 10 e 50. Definir maxPoolSize em um valor muito alto esgotará as conexões com o banco de dados.

P: O dotenv é seguro? R: É adequado para desenvolvimento. Em produção, recomendamos ler os valores a partir de variáveis de ambiente ou de um serviço de gerenciamento de segredos (como o AWS Secrets Manager ou o Vault).


📖 Resumo


📝 Exercícios

  1. Exercício básico (⭐): Configure uma estrutura básica de projeto com Express + Mongoose, incluindo o connectDB e o roteamento básico.
  2. Questões básicas (⭐): Implementar um middleware de tratamento de erros (distinguindo entre ValidationError, CastError e 11000).
  3. Exercício avançado (⭐⭐): Implemente uma API CRUD completa (produtos), incluindo paginação, filtragem e projeção.
  4. Exercício avançado (⭐⭐): Use o dotenv para gerenciar variáveis de ambiente e distinguir entre as configurações de desenvolvimento e de produção.
  5. Desafio (⭐⭐⭐): Implementar a API completa do sistema de comentários (GET/POST/PUT/DELETE + middleware de autenticação + tratamento de erros).
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%