Leodario.com

Leodario.com – Tudo sobre Tecnologia

Aula 33 – API JavaScript, Node.js e Express – Error Handling Middleware – Tratamento centralizado

Imagem destacada da aula de API

Introdução

Prezados estudantes e futuros arquitetos de sistemas, sejam muito bem-vindos à Aula 33! Hoje desvendaremos um pilar inabalável na construção de APIs robustas e confiáveis: o Error Handling Middleware. Preparem-se para elevar o nível das suas aplicações!

Imagine a sua API como um grande centro de distribuição de pacotes. Os pedidos chegam, são processados e as respostas são enviadas. Mas e se um pacote estiver com o endereço errado, quebrar no caminho ou for um item proibido? Sem um sistema de tratamento de erros eficiente, esse centro entraria em colapso, com pacotes perdidos e clientes insatisfeitos.

No universo das APIs, os erros são inevitáveis. Uma requisição malformada, um recurso não encontrado, um problema de conexão com o banco de dados – tudo isso pode acontecer. O tratamento centralizado de erros é a nossa “central de monitoramento e reparo”. Ele garante que, mesmo diante de falhas, a sua API responda de forma coerente, segura e informativa, sem expor detalhes sensíveis ou deixar o usuário com uma tela em branco. Essa abordagem é vital para a robustez de qualquer serviço moderno.

Nesta aula, vamos desenvolver um poderoso middleware de tratamento de erros para suas aplicações Node.js com Express. Você aprenderá a interceptar exceções, categorizá-las e gerar respostas padronizadas, proporcionando uma experiência de uso superior e facilitando imensamente a depuração. Veremos como isso se encaixa perfeitamente no ecossistema Node.js/Express, onde a flexibilidade dos middlewares é uma de suas maiores virtudes.

Conceito Fundamental

O Error Handling Middleware no Express.js é uma função middleware especial que recebe quatro argumentos: (err, req, res, next). Sim, quatro! Diferente dos middlewares comuns que utilizam (req, res, next), a presença do primeiro argumento, err, é o que o identifica como um tratador de erros. Ele é o último recurso, o “para-raios” da sua aplicação, capturando qualquer erro que tenha sido propagado (lançado ou passado via next(err)) por rotas ou outros middlewares anteriores.

Essa arquitetura possibilita uma gestão centralizada e unificada das falhas. Em vez de espalhar blocos try...catch por toda a sua aplicação, você pode direcionar todos os erros para um único ponto. Isso simplifica a manutenção, melhora a legibilidade do código e garante que todas as respostas de erro sigam um padrão consistente.

A terminologia da indústria frequentemente diferencia entre erros operacionais e erros de programação. Erros operacionais são aqueles que a aplicação espera e pode lidar graciosamente, como uma validação de entrada falha (código HTTP 400 Bad Request) ou um recurso não encontrado (404 Not Found). Erros de programação são bugs reais, como tentar acessar uma propriedade de undefined, que indicam uma falha na lógica do código e geralmente resultam em um 500 Internal Server Error.

Em casos de uso reais em produção, este middleware é essencial para:

    • Padronizar respostas: Clientes da API (sejam navegadores, aplicativos móveis ou outros serviços) esperam uma estrutura consistente nas respostas, inclusive para erros.
    • Segurança: Evitar que detalhes internos da aplicação (como stack traces completos ou informações de banco de dados) sejam expostos ao cliente final, o que poderia ser explorado por atacantes.
    • Logging: Registrar detalhadamente todos os erros no lado do servidor para fins de depuração e auditoria, sem impactar o cliente.
    • Experiência do Desenvolvedor (DX): Facilita a vida de quem consome a API, pois sabe exatamente o que esperar quando algo dá errado.

O middleware de erro se integra de forma fluida com outras tecnologias. Por exemplo, se você usa uma biblioteca de validação como Joi ou Express-Validator, os erros gerados por elas podem ser capturados e transformados em respostas HTTP 400. Se seu código faz chamadas assíncronas (como consultas a um banco de dados), você pode usar try...catch com next(error) ou simplesmente deixar promessas rejeitadas serem capturadas por ouvintes globais (embora next(error) seja mais explícito e controlado para erros de rota).

Vantagens

    • Consistência: Todas as respostas de erro seguem o mesmo formato.
    • Manutenibilidade: A lógica de tratamento de erros está em um único local, viabilizando revisões e atualizações simplificadas.
    • Segurança Aprimorada: Controle sobre quais informações são reveladas aos clientes em caso de falha.
    • Monitoramento Facilitado: Um ponto central para registrar e alertar sobre erros.
    • Código Mais Limpo: Rotas e outros middlewares focam na lógica de negócio, delegando o tratamento de exceções.

Desvantagens

    • Complexidade Inicial: Pode parecer um pouco mais complexo configurar no início para cenários muito específicos.
    • Sobrecarga: Se não for bem otimizado, pode haver um leve overhead de processamento para cada erro.

Implementação Prática

Vamos agora construir um sistema de tratamento de erros robusto e de nível enterprise. Este código rodará perfeitamente em qualquer ambiente Node.js, incluindo um HostGator Plano M, que suporta aplicações Node.js padrão.

Primeiro, crie um novo diretório para o seu projeto, entre nele e inicialize o npm:

mkdir api-erros
cd api-erros
npm init -y
npm install express dotenv winston

dotenv é para gerenciar variáveis de ambiente (como o modo de operação development ou production), e winston é uma biblioteca de logging profissional que nos dará mais controle sobre onde e como os logs são gravados.

Crie um arquivo .env na raiz do projeto:

NODE_ENV=development
PORT=3000

Agora, vamos estruturar nosso código. Crie um arquivo src/app.js e src/utils/customErrors.js e src/utils/logger.js.

Arquivo: src/utils/logger.js

// src/utils/logger.js
const winston = require('winston');

// Configuração do logger profissional com Winston const logger = winston.createLogger({ // Nível de log: 'info' para produção, 'debug' para desenvolvimento level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', // Formato para logs de console (mais legível para humanos) format: winston.format.combine( winston.format.colorize(), // Cores para o console winston.format.timestamp(), // Adiciona timestamp aos logs winston.format.printf(({ timestamp, level, message }) => { return ${timestamp} [${level}]: ${message}; }) ), // Transportes: onde os logs serão armazenados transports: [ new winston.transports.Console(), // Exibir logs no console // Em um ambiente de produção, adicionaríamos transportes para arquivos, DB, etc. // new winston.transports.File({ filename: 'error.log', level: 'error' }), // new winston.transports.File({ filename: 'combined.log' }), ], });

module.exports = logger;

Arquivo: src/utils/customErrors.js

// src/utils/customErrors.js
/* @class ApiError
  @extends Error
  @description Classe base para erros de API, habilitando a criação de erros padronizados.
 /
class ApiError extends Error {
  constructor(message, statusCode = 500) {
    super(message); // Chama o construtor da classe Error
    this.name = this.constructor.name; // Garante que o nome do erro seja o nome da classe
    this.statusCode = statusCode; // Código de status HTTP associado ao erro
    this.isOperational = true; // Indica que este é um erro que a aplicação espera e sabe como lidar
    Error.captureStackTrace(this, this.constructor); // Captura o stack trace
  }
}

/ @class NotFoundError @extends ApiError @description Erro para recursos não encontrados (HTTP 404). / class NotFoundError extends ApiError { constructor(message = 'Recurso não encontrado.') { super(message, 404); } }

/ @class BadRequestError @extends ApiError @description Erro para requisições malformadas ou com validação falha (HTTP 400). / class BadRequestError extends ApiError { constructor(message = 'Requisição inválida.') { super(message, 400); } }

/ @class UnauthorizedError @extends ApiError @description Erro para falhas de autenticação (HTTP 401). / class UnauthorizedError extends ApiError { constructor(message = 'Não autorizado.') { super(message, 401); } }

/ @class ForbiddenError @extends ApiError @description Erro para falhas de autorização (HTTP 403). / class ForbiddenError extends ApiError { constructor(message = 'Acesso negado.') { super(message, 403); } }

module.exports = { ApiError, NotFoundError, BadRequestError, UnauthorizedError, ForbiddenError };

Arquivo: src/app.js

// src/app.js
require('dotenv').config(); // Carrega variáveis de ambiente do .env

const express = require('express'); const logger = require('./utils/logger'); // Nosso logger profissional const { NotFoundError, ApiError } = require('./utils/customErrors'); // Nossas classes de erro customizadas

const app = express(); const PORT = process.env.PORT || 3000; const NODE_ENV = process.env.NODE_ENV || 'development';

// Middleware para processar JSON nas requisições app.use(express.json());

// ------------------------------------------ // Rotas da API - Exemplo de como gerar erros // ------------------------------------------

// Rota 1: Exemplo de sucesso app.get('/', (req, res) => { res.json({ mensagem: 'API funcionando perfeitamente!' }); });

// Rota 2: Simula um recurso não encontrado (usando NotFoundError) app.get('/usuarios/:id', (req, res, next) => { const { id } = req.params; // Simula busca em um banco de dados if (id === '123') { return res.json({ id: '123', nome: 'Alice' }); } // Se o ID não for 123, lançamos um NotFoundError next(new NotFoundError(Usuário com ID ${id} não encontrado.)); });

// Rota 3: Simula um erro de validação (usando BadRequestError) app.post('/produtos', (req, res, next) => { const { nome, preco } = req.body; // Simula validação de entrada if (!nome || typeof preco !== 'number' || preco <= 0) { // Usamos next() para passar o erro ao middleware de tratamento de erros const { BadRequestError } = require('./utils/customErrors'); return next(new BadRequestError('Nome do produto e preço válido são obrigatórios.')); } res.status(201).json({ mensagem: 'Produto criado com sucesso!', produto: { nome, preco } }); });

// Rota 4: Simula um erro inesperado (erro de programação) app.get('/erro-inesperado', (req, res, next) => { // Simula um erro de programação, como tentar acessar uma propriedade de null // Este erro não é uma instância de ApiError throw new Error('Erro de programação simulado: objeto nulo!'); });

// Rota 5: Simula um erro assíncrono (usando next) app.get('/erro-assincrono', async (req, res, next) => { try { // Simula uma operação assíncrona que pode falhar const resultado = await Promise.reject('Falha na operação assíncrona!'); res.json(resultado); } catch (error) { // Captura o erro da promessa e o passa para o middleware de erro next(error); } });

// ------------------------------------------ // Middlewares de Tratamento de Erros // ------------------------------------------

// Middleware para lidar com rotas não encontradas (404) // Este middleware deve vir ANTES do middleware de tratamento de erros global app.use((req, res, next) => { next(new NotFoundError(A rota '${req.originalUrl}' não foi encontrada.)); });

// Middleware centralizado de tratamento de erros (recebe 4 argumentos) app.use((err, req, res, next) => { // Registra o erro completo no log do servidor. // Em produção, queremos saber tudo, mas não expor tudo ao cliente. logger.error([${err.name}] ${err.message}, { stack: err.stack, statusCode: err.statusCode || 500, isOperational: err.isOperational || false, path: req.path, method: req.method, body: req.body });

// Determina o código de status e a mensagem de resposta. // Se o erro é uma instância das nossas ApiErrors, usamos o statusCode e message definidos. // Caso contrário (erro de programação), é um 500 Internal Server Error genérico. let statusCode = err.statusCode || 500; let message = err.message;

// Para erros que NÃO são operacionais (bugs de programação), // e estamos em produção, evitamos vazar detalhes. if (!err.isOperational && NODE_ENV === 'production') { statusCode = 500; message = 'Um erro interno inesperado ocorreu. Por favor, tente novamente mais tarde.'; }

// Se for um erro do tipo Error original e não tiver um statusCode, // ou se for um erro de programação em produção, setamos o status 500. if (!(err instanceof ApiError) && NODE_ENV === 'production') { statusCode = 500; message = 'Um erro interno no servidor.'; } else if (!(err instanceof ApiError) && NODE_ENV === 'development') { // Em desenvolvimento, mostramos o erro original para depuração statusCode = err.statusCode || 500; message = err.message || 'Erro interno do servidor'; }

res.status(statusCode).json({ status: 'erro', mensagem: message, // Em ambiente de desenvolvimento, facilita a depuração mostrando o stack trace. // Em produção, isso NUNCA deve ser enviado ao cliente por questões de segurança. stack: NODE_ENV === 'development' ? err.stack : undefined }); });

// ------------------------------------------ // Captura de exceções e rejeições não tratadas // (Essencial para garantir que o processo não caia) // ------------------------------------------

// Captura exceções síncronas não tratadas (ex: throw new Error) process.on('uncaughtException', (error) => { logger.error(Exceção Não Capturada: ${error.message}, { stack: error.stack }); // IMPORTANTE: Em produção, após logar, o ideal é encerrar o processo // para evitar que a aplicação continue em um estado instável. // process.exit(1); });

// Captura rejeições de promessas não tratadas (ex: Promise.reject() sem .catch()) process.on('unhandledRejection', (reason, promise) => { logger.error(Rejeição Não Tratada: ${reason}, { promise }); // Semelhante à exceção, em produção o ideal é encerrar o processo. // process.exit(1); });

// Inicia o servidor app.listen(PORT, () => { logger.info(Servidor escutando na porta ${PORT} no modo ${NODE_ENV}); });

Para rodar a aplicação, crie um arquivo server.js (ou index.js) na raiz do seu projeto que apenas importe e execute app.js:

// server.js
require('./src/app');

Adicione um script no seu package.json para facilitar a execução:

  "scripts": {
    "start": "node server.js",
    "dev": "NODE_ENV=development nodemon server.js"
  },

Se quiser nodemon (para restart automático durante o desenvolvimento), instale-o: npm install -g nodemon.

Agora você pode iniciar a aplicação com:

npm start

ou se tiver nodemon

📚 Informações da Aula

Curso: API Completo - Node.js & Express

Tempo estimado: 25 minutos

Pré-requisitos: JavaScript básico

npm run dev

Melhores Práticas Enterprise e HostGator

    • Classes de Erro Customizadas: Usar classes como ApiError, NotFoundError etc., facilita a diferenciação entre erros operacionais (esperados) e de programação (inesperados), viabilizando um tratamento mais inteligente no middleware.
    • Variáveis de Ambiente (.env):Essencial para configurar o ambiente (desenvolvimento vs. produção) sem alterar o código, permitindo que o middleware se comporte de forma diferente (e mais segura) em produção.
    • Logging Profissional (Winston): Não confie apenas em console.log. Um logger como Winston oferece controle granular sobre os níveis de log, formatos e destinos (console, arquivo, banco de dados, serviços externos). Isso é fundamental para depuração e auditoria em sistemas reais.
    • Não Expor Stack Trace em Produção: Observe no código que o stack é enviado apenas em development. Em produção, isso seria uma grave falha de segurança.
    • Captura de uncaughtException e unhandledRejection: Estes são ouvintes globais que previnem que seu processo Node.js “caia” abruptamente em caso de erros não capturados. Em produção, você geralmente irá registrar esses erros e, em seguida, encerrar o processo (process.exit(1)) para permitir que um supervisor de processo (como PM2 ou Docker restart policy) reinicie sua aplicação em um estado limpo.
    • Compatibilidade HostGator Plano M: O HostGator Plano M suporta aplicações Node.js padrão. O código apresentado é Node.js puro e Express, portanto é totalmente compatível. A única “configuração” que você precisa garantir é que seu script de inicialização (server.js) seja executado e que a porta process.env.PORT seja utilizada, que o HostGator irá injetar automaticamente para sua aplicação.

Testes Básicos (usando curl em outro terminal)

Inicie seu servidor (npm start ou npm run dev) e, em outro terminal, execute os comandos:

# Sucesso
curl http://localhost:3000

Recurso não encontrado (NotFoundError)

curl http://localhost:3000/usuarios/456

Requisição inválida (BadRequestError)

curl -X POST -H "Content-Type: application/json" -d '{"preco": 10}' http://localhost:3000/produtos

Erro inesperado (Erro de programação)

curl http://localhost:3000/erro-inesperado

Rota não existente (Middleware 404)

curl http://localhost:3000/rota-inexistente

Erro assíncrono

curl http://localhost:3000/erro-assincrono

Observe as respostas JSON e os logs no seu terminal do servidor. Em desenvolvimento, você verá o stack trace nos logs e na resposta JSON para os erros.

Exercício Hands-On

Chegou a sua vez de colocar a mão na massa e solidificar esse conhecimento valioso!

Desafio Prático

Seu desafio é desenvolver uma nova classe de erro customizada chamada ForbiddenError para representar uma falha de autorização (código HTTP 403). Em seguida, crie uma nova rota na sua API que utilize este erro customizado.

    • Crie a classe ForbiddenError em src/utils/customErrors.js, seguindo o padrão das classes existentes.
    • Adicione uma nova rota /admin/dashboard no src/app.js.
    • Nesta rota, simule uma verificação de autorização. Se o req.headers['x-admin-token'] não for igual a 'SUPER_SECRET_TOKEN', lance um ForbiddenError. Caso contrário, retorne um JSON de sucesso.

Solução Detalhada Passo a Passo

Passo 1: Criar a classe ForbiddenError

Abra o arquivo src/utils/customErrors.js e adicione a seguinte classe, se ainda não o fez:

// src/utils/customErrors.js (adicionar se não estiver lá)
// ... outras classes ...

/ @class ForbiddenError @extends ApiError @description Erro para falhas de autorização (HTTP 403). */ class ForbiddenError extends ApiError { constructor(message = 'Acesso negado. Você não tem permissão para realizar esta ação.') { super(message, 403); } }

module.exports = { ApiError, NotFoundError, BadRequestError, UnauthorizedError, ForbiddenError // Não se esqueça de exportá-la! };

Passo 2: Adicionar a nova rota em src/app.js

Abra o arquivo src/app.js e adicione a seguinte rota antes dos middlewares de tratamento de erros:

// src/app.js (adicionar esta rota)
// ... após Rota 5 ...

// Rota 6: Simula um erro de autorização (ForbiddenError) app.get('/admin/dashboard', (req, res, next) => { const adminToken = req.headers['x-admin-token']; if (adminToken !== 'SUPER_SECRET_TOKEN') { const { ForbiddenError } = require('./utils/customErrors'); return next(new ForbiddenError('Token de administrador inválido ou ausente.')); } res.json({ mensagem: 'Bem-vindo ao painel de administração!', acesso: 'concedido' }); });

// ... antes dos middlewares de tratamento de erros ...

Passo 3: Testar e Validar o Resultado

Certifique-se de que seu servidor Node.js esteja rodando (npm start ou npm run dev).

Teste de acesso negado:

curl http://localhost:3000/admin/dashboard

Você deverá receber uma resposta com status 403 e a mensagem: {"status":"erro","mensagem":"Token de administrador inválido ou ausente.","stack":"..."} (se em desenvolvimento).

Teste de acesso permitido:

curl -H "x-admin-token: SUPER_SECRET_TOKEN" http://localhost:3000/admin/dashboard

Você deverá receber uma resposta com status 200 e a mensagem: {"mensagem":"Bem-vindo ao painel de administração!","acesso":"concedido"}.

Troubleshooting dos Erros Mais Comuns

    • Resposta 500 genérica, mas esperava 403/400: Verifique se você passou o erro para o next() no Express (ex: next(new ForbiddenError(...))). Se você apenas throw new ForbiddenError(...) em um contexto assíncrono sem um try...catch ao redor, ele pode se tornar uma unhandledRejection e ser tratado como um erro 500 genérico se o process.on('unhandledRejection') não estiver configurado para passar para o next (o que não é o padrão e geralmente não é recomendado para evitar a confusão do middleware de erro com os ouvintes globais de processo). Em rotas síncronas, um throw funciona, mas next(error) é mais consistente.
    • Detalhes do stack trace expostos em produção: Confirme que a variável de ambiente NODE_ENV está definida como production quando você deploya. Caso contrário, seu middleware de erro ainda enviará o stack trace, comprometendo a segurança.
    • Middleware de erro não é acionado: Certifique-se de que seu middleware de tratamento de erros global (a função app.use((err, req, res, next) => { ... })) seja o ÚLTIMO middleware registrado na sua cadeia de middlewares. Ele deve vir depois de todas as suas rotas e outros app.use()s normais.
    • Erros de digitação nas classes customizadas: Verifique os nomes das classes e se o super(message, statusCode) está correto.

Próximos Passos Sugeridos

Para ir além e refinar ainda mais o tratamento de erros:

    • Integração com Joi ou Express-Validator:Implemente uma validação de esquema para suas entradas de requisição e veja como os erros gerados por essas bibliotecas podem ser transformados em BadRequestError pelo seu middleware.
    • Monitoramento de Aplicação (APM): Explore serviços como Sentry, New Relic ou DataDog que se integram com seu logger para monitorar erros em tempo real em produção.
    • Centralização de Logs: Em ambientes de produção, configure Winston para enviar logs para um serviço centralizado (ELK Stack, CloudWatch, Loggly) em vez de apenas arquivos locais.
    • Testes de Integração:Construa testes automatizados para garantir que seu middleware de erro funciona corretamente para diferentes tipos de erros e cenários.

Parabéns por dominar este tópico significativo! O tratamento de erros é uma marca registrada de APIs bem projetadas. Continuem a praticar e a aprimorar suas habilidades!

🚀 Pronto para a próxima aula?

Continue sua jornada no desenvolvimento de APIs e domine Node.js & Express!

📚 Ver todas as aulas