Express.js: o framework web do Node

[112] Express.js: o framework web do Node

O mesmo endpoint que ocupava trinta linhas de if e regex cabe em quatro com Express. O artigo mostra o que o framework acrescenta ao http nativo: roteamento com parâmetros, a pilha de middlewares e o next, Router para separar por recurso, o tratador de erros de quatro parâmetros e os middlewares de terceiros.
Javascript

24 min de leitura

Módulo 4 — Node.js e Back-end

Introdução

No artigo Criando um servidor HTTP com Node.js puro construímos uma API completa com o módulo http puro. Funcionou — mas ficou claro o quanto de código repetitivo precisamos escrever para tarefas simples como parsear o corpo de uma requisição ou extrair parâmetros da URL.

O Express.js é o framework web mais popular do ecossistema Node. Ele não substitui o http — ele é construído sobre ele. O que o Express faz é pegar todo aquele código repetitivo que escrevemos e transformar em APIs limpas, expressivas e extensíveis.

Instalando e configurando

mkdir api-express && cd api-express
npm init -y
npm install express
npm install -D nodemon
// package.json — adicione os scripts
{
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js"
  }
}

Hello World com Express

// src/index.js
const express = require("express");

const app = express();
const PORTA = process.env.PORT || 3000;

app.get("/", (req, res) => {
  res.json({ mensagem: "Olá, Express!" });
});

app.listen(PORTA, () => {
  console.log(`🚀 Servidor rodando em http://localhost:${PORTA}`);
});
npm run dev

Compare com o artigo Criando um servidor HTTP com Node.js puro. A diferença é imediata.

Comparativo direto — http puro vs Express

// ── http puro ───────────────────────────────────────
const http = require("http");

const servidor = http.createServer(async (req, res) => {
  const url = new URL(req.url, `http://${req.headers.host}`);

  if (url.pathname === "/usuarios" && req.method === "GET") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify(usuarios));
  }

  if (url.pathname.match(/^\/usuarios\/\d+$/) && req.method === "GET") {
    const id = Number(url.pathname.split("/")[2]);
    const usuario = usuarios.find(u => u.id === id);
    if (!usuario) {
      res.writeHead(404);
      res.end(JSON.stringify({ erro: "Não encontrado" }));
    } else {
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(JSON.stringify(usuario));
    }
  }
});

// ── Express ──────────────────────────────────────────
const express = require("express");
const app = express();

app.get("/usuarios", (req, res) => {
  res.json(usuarios);
});

app.get("/usuarios/:id", (req, res) => {
  const usuario = usuarios.find(u => u.id === Number(req.params.id));
  if (!usuario) return res.status(404).json({ erro: "Não encontrado" });
  res.json(usuario);
});

Mesmo resultado. Metade do código. Muito mais legível.

Middlewares — o coração do Express

Middleware é qualquer função com a assinatura (req, res, next). O Express processa uma requisição passando-a por uma pilha de middlewares em sequência:

Requisição →  [MW1] → [MW2] → [MW3] → Rota → Resposta
                ↓ next()  ↓ next()  ↓ next()
// Estrutura de um middleware
function meuMiddleware(req, res, next) {
  // Faz algo com req ou res
  console.log("Passou pelo middleware");

  // Chama o próximo middleware ou rota
  next();

  // Se NÃO chamar next(), a requisição para aqui
}

// Usando
app.use(meuMiddleware); // aplica em TODAS as rotas
app.use("/admin", meuMiddleware); // aplica apenas em /admin
app.get("/rota", meuMiddleware, handler); // aplica em uma rota específica

Middlewares nativos do Express

const express = require("express");
const app = express();

// Parse automático de JSON no corpo das requisições
// Antes: lerCorpo(req) com streams manual
// Agora: req.body já está disponível
app.use(express.json());

// Parse de formulários HTML (application/x-www-form-urlencoded)
app.use(express.urlencoded({ extended: true }));

// Servir arquivos estáticos
app.use(express.static("public"));
// GET /style.css → serve public/style.css automaticamente

req e res no Express — o que ganhou

// ── req — muito mais rico ────────────────────────

app.get("/exemplo/:id", (req, res) => {
  // Parâmetros de rota
  console.log(req.params.id);        // "42" de /exemplo/42

  // Query string
  console.log(req.query.busca);      // "ana" de ?busca=ana
  console.log(req.query.pagina);     // "1" de ?pagina=1

  // Corpo (requer express.json())
  console.log(req.body);             // { nome: "Ana" }

  // Headers
  console.log(req.headers["authorization"]); // "Bearer token..."
  console.log(req.get("Content-Type"));      // "application/json"

  // Outros
  console.log(req.method);          // "GET"
  console.log(req.path);            // "/exemplo/42"
  console.log(req.ip);              // "127.0.0.1"
  console.log(req.hostname);        // "localhost"
  console.log(req.protocol);        // "http"
  console.log(req.secure);          // false
});

// ── res — muito mais expressivo ──────────────────

app.get("/resposta", (req, res) => {
  // JSON
  res.json({ mensagem: "ok" });

  // Com status
  res.status(201).json({ id: 1 });
  res.status(404).json({ erro: "Não encontrado" });

  // Texto
  res.send("texto puro");

  // HTML
  res.send("<h1>Olá</h1>");

  // Status vazio (204 No Content)
  res.sendStatus(204);

  // Redirect
  res.redirect("/nova-url");
  res.redirect(301, "/nova-url-permanente");

  // Headers personalizados
  res.set("X-Total-Count", "42");
  res.set({ "X-Custom": "valor", "Cache-Control": "no-cache" });

  // Cookie
  res.cookie("sessao", "token123", { httpOnly: true, maxAge: 3600000 });

  // Download de arquivo
  res.download("/caminho/arquivo.pdf", "relatorio.pdf");

  // Arquivo estático
  res.sendFile("/caminho/absoluto/index.html");
});

Parâmetros de rota

Atenção à versão. Os exemplos abaixo usam a sintaxe do Express 4. O npm install express hoje instala o Express 5, que trocou o analisador de rotas: o curinga solto * e o parâmetro opcional :nome? deixaram de ser aceitos e derrubam a aplicação no boot, com TypeError: Missing parameter name. No 5, o curinga precisa de nome — /docs/*caminho, lido em req.params.caminho como array de segmentos — e o opcional vira /arquivos/:nome{.:extensao}. Para acompanhar o artigo como está, instale a versão 4 explicitamente: npm i express@4.

// Parâmetro simples
app.get("/usuarios/:id", (req, res) => {
  const id = Number(req.params.id);
  // /usuarios/42 → req.params.id = "42"
});

// Múltiplos parâmetros
app.get("/usuarios/:usuarioId/pedidos/:pedidoId", (req, res) => {
  const { usuarioId, pedidoId } = req.params;
  // /usuarios/1/pedidos/99 → { usuarioId: "1", pedidoId: "99" }
});

// Parâmetro opcional
app.get("/arquivos/:nome.:extensao?", (req, res) => {
  const { nome, extensao } = req.params;
  // /arquivos/relatorio.pdf → { nome: "relatorio", extensao: "pdf" }
  // /arquivos/relatorio → { nome: "relatorio", extensao: undefined }
});

// Wildcard
app.get("/docs/*", (req, res) => {
  // Captura qualquer rota abaixo de /docs/
  res.send(`Documento: ${req.params[0]}`);
});

Router — organizando rotas em módulos

Em vez de definir todas as rotas no arquivo principal, usamos Router para separar por recurso:

// src/routes/usuarios.js
const express = require("express");
const router = express.Router();

// Banco em memória (em projetos reais, viria do banco de dados)
let usuarios = [
  { id: 1, nome: "Ana Paula", email: "ana@email.com" },
  { id: 2, nome: "Carlos Silva", email: "carlos@email.com" },
];
let proximoId = 3;

// GET /usuarios
router.get("/", (req, res) => {
  const { busca, pagina = 1, por_pagina = 10 } = req.query;

  let lista = usuarios;

  if (busca) {
    const termo = busca.toLowerCase();
    lista = lista.filter(u =>
      u.nome.toLowerCase().includes(termo) ||
      u.email.toLowerCase().includes(termo)
    );
  }

  const total = lista.length;
  const inicio = (pagina - 1) * por_pagina;
  const dados = lista.slice(inicio, inicio + Number(por_pagina));

  res.json({
    dados,
    paginacao: {
      total,
      pagina: Number(pagina),
      por_pagina: Number(por_pagina),
      total_paginas: Math.ceil(total / por_pagina),
    },
  });
});

// GET /usuarios/:id
router.get("/:id", (req, res) => {
  const usuario = usuarios.find(u => u.id === Number(req.params.id));
  if (!usuario) {
    return res.status(404).json({ erro: `Usuário ${req.params.id} não encontrado.` });
  }
  res.json(usuario);
});

// POST /usuarios
router.post("/", (req, res) => {
  const { nome, email } = req.body;
  const erros = [];

  if (!nome?.trim()) erros.push("nome é obrigatório.");
  if (!email?.trim()) erros.push("email é obrigatório.");
  if (email && !email.includes("@")) erros.push("email inválido.");
  if (email && usuarios.some(u => u.email === email)) {
    erros.push("email já cadastrado.");
  }

  if (erros.length > 0) {
    return res.status(422).json({ erro: "Dados inválidos.", detalhes: erros });
  }

  const novo = {
    id: proximoId++,
    nome: nome.trim(),
    email: email.trim().toLowerCase(),
    criadoEm: new Date().toISOString(),
  };

  usuarios.push(novo);
  res.status(201).location(`/usuarios/${novo.id}`).json(novo);
});

// PUT /usuarios/:id
router.put("/:id", (req, res) => {
  const indice = usuarios.findIndex(u => u.id === Number(req.params.id));
  if (indice === -1) {
    return res.status(404).json({ erro: `Usuário ${req.params.id} não encontrado.` });
  }

  const { nome, email } = req.body;
  const erros = [];

  if (email && !email.includes("@")) erros.push("email inválido.");
  if (email && usuarios.some(u => u.email === email && u.id !== usuarios[indice].id)) {
    erros.push("email já cadastrado por outro usuário.");
  }

  if (erros.length > 0) {
    return res.status(422).json({ erro: "Dados inválidos.", detalhes: erros });
  }

  usuarios[indice] = {
    ...usuarios[indice],
    ...(nome && { nome: nome.trim() }),
    ...(email && { email: email.trim().toLowerCase() }),
    atualizadoEm: new Date().toISOString(),
  };

  res.json(usuarios[indice]);
});

// DELETE /usuarios/:id
router.delete("/:id", (req, res) => {
  const indice = usuarios.findIndex(u => u.id === Number(req.params.id));
  if (indice === -1) {
    return res.status(404).json({ erro: `Usuário ${req.params.id} não encontrado.` });
  }

  const removido = usuarios.splice(indice, 1)[0];
  res.json({ mensagem: `Usuário "${removido.nome}" removido com sucesso.` });
});

module.exports = router;

Middlewares customizados — logging, auth, erros

// src/middlewares/logger.js
function logger(req, res, next) {
  const inicio = Date.now();

  // Intercepta o momento em que a resposta é enviada
  res.on("finish", () => {
    const duracao = Date.now() - inicio;
    const cor = res.statusCode < 400 ? "\x1b[32m" : "\x1b[31m"; // verde/vermelho
    const reset = "\x1b[0m";
    console.log(
      `${cor}[${new Date().toLocaleTimeString("pt-BR")}]${reset} ` +
      `${req.method.padEnd(7)} ${req.path.padEnd(25)} ` +
      `${cor}${res.statusCode}${reset} ${duracao}ms`
    );
  });

  next();
}

module.exports = logger;
// src/middlewares/auth.js
function autenticar(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith("Bearer ")) {
    return res.status(401).json({ erro: "Token de autenticação ausente." });
  }

  const token = authHeader.split(" ")[1];

  // Em produção: verificaria JWT com jsonwebtoken
  if (token !== "token-secreto-123") {
    return res.status(401).json({ erro: "Token inválido ou expirado." });
  }

  // Adiciona dados do usuário ao req para uso nas rotas
  req.usuario = { id: 1, nome: "Admin", papel: "admin" };
  next();
}

function autorizar(...papeis) {
  return (req, res, next) => {
    if (!papeis.includes(req.usuario?.papel)) {
      return res.status(403).json({ erro: "Acesso negado." });
    }
    next();
  };
}

module.exports = { autenticar, autorizar };
// src/middlewares/validar.js
function validarJSON(req, res, next) {
  const contentType = req.headers["content-type"] || "";

  if (["POST", "PUT", "PATCH"].includes(req.method) &&
      !contentType.includes("application/json")) {
    return res.status(415).json({
      erro: "Content-Type deve ser application/json.",
    });
  }

  next();
}

module.exports = { validarJSON };
// src/middlewares/erros.js

// Middleware de rota não encontrada (deve vir após todas as rotas)
function naoEncontrado(req, res, next) {
  res.status(404).json({
    erro: `Rota ${req.method} ${req.path} não encontrada.`,
  });
}

// Middleware de erro global (4 parâmetros — Express reconhece como error handler)
function tratadorDeErros(erro, req, res, next) {
  console.error(`[Erro] ${erro.message}`, erro.stack);

  // Erros de JSON malformado
  if (erro.type === "entity.parse.failed") {
    return res.status(400).json({ erro: "JSON inválido no corpo da requisição." });
  }

  // Outros erros
  const status = erro.status || erro.statusCode || 500;
  res.status(status).json({
    erro: status === 500 ? "Erro interno do servidor." : erro.message,
  });
}

module.exports = { naoEncontrado, tratadorDeErros };

Montando tudo — o app completo

// src/index.js
const express = require("express");
const logger = require("./middlewares/logger");
const { validarJSON } = require("./middlewares/validar");
const { naoEncontrado, tratadorDeErros } = require("./middlewares/erros");
const usuariosRouter = require("./routes/usuarios");

const app = express();
const PORTA = process.env.PORT || 3000;

// ── Middlewares globais ──────────────────────────
app.use(logger);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(validarJSON);

// ── CORS básico ──────────────────────────────────
app.use((req, res, next) => {
  res.set({
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type, Authorization",
  });
  if (req.method === "OPTIONS") return res.sendStatus(204);
  next();
});

// ── Rota raiz ────────────────────────────────────
app.get("/", (req, res) => {
  res.json({
    nome: "API Express",
    versao: "1.0.0",
    status: "online",
    timestamp: new Date().toISOString(),
  });
});

// ── Rotas da API ──────────────────────────────────
app.use("/usuarios", usuariosRouter);

// ── Tratamento de erros (sempre por último) ───────
app.use(naoEncontrado);
app.use(tratadorDeErros);

// ── Inicialização ──────────────────────────────────
app.listen(PORTA, () => {
  console.log(`
🚀 API rodando em http://localhost:${PORTA}`);
  console.log(`📋 Ambiente: ${process.env.NODE_ENV || "development"}
`);
});

module.exports = app; // útil para testes

A estrutura final do projeto:

src/
├── index.js
├── routes/
│   └── usuarios.js
└── middlewares/
    ├── logger.js
    ├── auth.js
    ├── validar.js
    └── erros.js

Middleware de terceiros mais usados

npm install cors          # CORS completo e configurável
npm install helmet        # headers de segurança HTTP
npm install morgan        # logging de requisições
npm install compression   # compressão gzip/deflate
npm install rate-limiter-flexible # rate limiting
const cors = require("cors");
const helmet = require("helmet");
const morgan = require("morgan");
const compression = require("compression");

// Helmet — segurança (X-Frame-Options, CSP, etc.)
app.use(helmet());

// CORS configurado
app.use(cors({
  origin: ["https://meuapp.com", "http://localhost:3000"],
  methods: ["GET", "POST", "PUT", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization"],
  credentials: true,
}));

// Morgan — logging formatado
app.use(morgan("dev"));
// dev: GET /usuarios 200 5.123 ms - 238

// Compressão gzip para respostas grandes
app.use(compression());

Boas práticas com Express

// ✅ 1. Sempre use tratador de erros global
app.use((erro, req, res, next) => { /* ... */ });

// ✅ 2. Envolva handlers assíncronos para capturar erros
// Sem este wrapper, erros em async lançam UnhandledPromiseRejection
function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

// Uso
app.get("/dados", asyncHandler(async (req, res) => {
  const dados = await buscarDados(); // se rejeitar, vai para o error handler
  res.json(dados);
}));

// ✅ 3. Separe app e server para facilitar testes
// app.js — cria e configura o app
// index.js — importa o app e chama listen()

// ✅ 4. Nunca exponha stack traces em produção
app.use((erro, req, res, next) => {
  const isProd = process.env.NODE_ENV === "production";
  res.status(500).json({
    erro: "Erro interno do servidor.",
    detalhes: isProd ? undefined : erro.message,
  });
});

// ✅ 5. Valide entradas antes de processar
// Use zod, joi ou express-validator

// ✅ 6. Use compressão em produção
// ✅ 7. Helmet para headers de segurança
// ✅ 8. Rate limiting para prevenir abusos

Tarefa para você

Expanda a API de usuários com:

1. Rota GET /usuarios/:id/posts
   Busca os posts do usuário na JSONPlaceholder:
   https://jsonplaceholder.typicode.com/posts?userId={id}
   - Use fetch (Node 18+) ou instale node-fetch
   - Trate o caso em que a API externa falha

2. Middleware de rate limiting simples
   - Limite de 10 requisições por minuto por IP
   - Retorne 429 com mensagem clara ao exceder
   - Implemente usando um Map em memória

3. Rota protegida
   - POST /admin/usuarios — cria usuário (requer token)
   - DELETE /admin/usuarios/:id — remove (requer token)
   - Use o middleware autenticar que criamos

4. Validação com Zod
   npm install zod
   - Defina schemas para criação e atualização
   - Middleware que valida o req.body antes do handler
Ver solução — posts, rate limit, rotas protegidas e validação com Zod
// npm install express zod
//
// ---- src/app.js
import express from "express";
import { z } from "zod";

const app = express();
app.use(express.json({ limit: "100kb" }));

// Atrás de proxy (Nginx, Heroku, Render), req.ip devolve o IP do
// proxy — todos os usuários viram o mesmo cliente e o rate limit
// derruba o site inteiro. Com isto, o Express lê o X-Forwarded-For.
app.set("trust proxy", 1);

const usuarios = [{ id: 1, nome: "Ana", email: "ana@exemplo.com" }];
let proximoId = 2;

// ---------------------------------------------------------------
// 2 — rate limiting com Map em memória
// ---------------------------------------------------------------
function limitarRequisicoes({ janelaMs = 60_000, maximo = 10 } = {}) {
  const registros = new Map();

  // Sem esta limpeza o Map cresce para sempre: um IP que apareceu uma
  // vez fica guardado até o processo reiniciar. `unref()` evita que o
  // timer segure o Node aberto no encerramento.
  const limpeza = setInterval(() => {
    const agora = Date.now();
    for (const [ip, registro] of registros) {
      if (agora > registro.reiniciaEm) registros.delete(ip);
    }
  }, janelaMs);
  limpeza.unref();

  return (req, res, next) => {
    const agora = Date.now();
    const chave = req.ip;

    let registro = registros.get(chave);

    if (!registro || agora > registro.reiniciaEm) {
      registro = { contagem: 0, reiniciaEm: agora + janelaMs };
      registros.set(chave, registro);
    }

    registro.contagem++;

    const restantes = Math.max(0, maximo - registro.contagem);
    const segundos = Math.ceil((registro.reiniciaEm - agora) / 1000);

    res.set({
      "RateLimit-Limit": String(maximo),
      "RateLimit-Remaining": String(restantes),
      "RateLimit-Reset": String(segundos),
    });

    if (registro.contagem > maximo) {
      // Retry-After diz ao cliente QUANDO tentar de novo. Sem ele, um
      // cliente automatizado costuma repetir imediatamente e piorar.
      res.set("Retry-After", String(segundos));

      return res.status(429).json({
        erro: "Muitas requisições",
        detalhe: `Limite de ${maximo} por minuto. Tente em ${segundos}s.`,
      });
    }

    next();
  };
}

app.use(limitarRequisicoes({ maximo: 10, janelaMs: 60_000 }));

// ---------------------------------------------------------------
// 4 — validação com Zod
// ---------------------------------------------------------------
const esquemaCriacao = z.object({
  nome: z.string().trim().min(3, "nome precisa de ao menos 3 caracteres"),
  email: z.string().email("email inválido"),
  idade: z.number().int().positive().max(120).optional(),
});

// .partial() reaproveita o esquema acima em vez de duplicá-lo, e
// .refine barra o PATCH vazio, que passaria em todas as regras.
const esquemaAtualizacao = esquemaCriacao
  .partial()
  .refine((dados) => Object.keys(dados).length > 0, {
    message: "envie ao menos um campo",
  });

function validar(esquema) {
  return (req, res, next) => {
    const resultado = esquema.safeParse(req.body);

    if (!resultado.success) {
      return res.status(422).json({
        erro: "Dados inválidos",
        campos: resultado.error.issues.map((i) => ({
          campo: i.path.join(".") || "(raiz)",
          mensagem: i.message,
        })),
      });
    }

    // Substitui o corpo pelo DADO VALIDADO: o Zod já converteu tipos e
    // removeu o que não está no esquema. Continuar usando req.body
    // cru deixaria passar campos extras — inclusive um "admin": true.
    req.body = resultado.data;
    next();
  };
}

// ---------------------------------------------------------------
// 3 — autenticação
// ---------------------------------------------------------------
function autenticar(req, res, next) {
  const cabecalho = req.get("authorization") ?? "";
  const [tipo, token] = cabecalho.split(" ");

  if (tipo !== "Bearer" || !token) {
    // 401: não sei quem você é. (403 seria: sei, e você não pode.)
    return res.status(401).json({ erro: "Token ausente" });
  }

  if (token !== process.env.ADMIN_TOKEN) {
    return res.status(403).json({ erro: "Token inválido" });
  }

  req.usuario = { papel: "admin" };
  next();
}

// ---------------------------------------------------------------
// 1 — posts do usuário
// ---------------------------------------------------------------
app.get("/usuarios/:id/posts", async (req, res, next) => {
  const id = Number(req.params.id);
  const usuario = usuarios.find((u) => u.id === id);

  if (!usuario) return res.status(404).json({ erro: "Usuário não encontrado" });

  try {
    const resposta = await fetch(
      `https://jsonplaceholder.typicode.com/posts?userId=${id}`,
      { signal: AbortSignal.timeout(5000) } // Node 18+: timeout em uma linha
    );

    if (!resposta.ok) {
      return res.status(502).json({
        erro: "Serviço de posts indisponível",
        detalhe: `A API externa respondeu ${resposta.status}`,
      });
    }

    const posts = await resposta.json();
    res.json({ usuario: usuario.nome, total: posts.length, posts });
  } catch (erro) {
    if (erro.name === "TimeoutError") {
      return res.status(504).json({ erro: "A API de posts demorou demais" });
    }
    next(erro);
  }
});

// ---------------------------------------------------------------
// Rotas administrativas
// ---------------------------------------------------------------
const admin = express.Router();

// O middleware vale para o router inteiro: uma linha em vez de
// repetir `autenticar` em cada rota — e sem risco de esquecer numa.
admin.use(autenticar);

admin.post("/usuarios", validar(esquemaCriacao), (req, res) => {
  if (usuarios.some((u) => u.email === req.body.email)) {
    // 409, não 422: o dado está bem formado, o conflito é de estado.
    return res.status(409).json({ erro: "E-mail já cadastrado" });
  }

  const novo = { id: proximoId++, ...req.body };
  usuarios.push(novo);

  res.status(201).location(`/usuarios/${novo.id}`).json(novo);
});

admin.delete("/usuarios/:id", (req, res) => {
  const indice = usuarios.findIndex((u) => u.id === Number(req.params.id));

  if (indice === -1) return res.status(404).json({ erro: "Usuário não encontrado" });

  usuarios.splice(indice, 1);
  res.status(204).end();
});

app.use("/admin", admin);

// ---------------------------------------------------------------
// Tratamento de erro — sempre por último, sempre com 4 parâmetros
// ---------------------------------------------------------------
// O Express identifica o handler de erro pela ARIDADE. Com três
// parâmetros ele vira um middleware comum e nunca é chamado — bug
// clássico e silencioso.
app.use((erro, req, res, _next) => {
  console.error(erro);
  res.status(erro.status ?? 500).json({ erro: "Erro interno" });
});

app.listen(3000, () => console.log("http://localhost:3000"));

// ---------------------------------------------------------------
// O detalhe que morde: rate limit em memória não escala
// ---------------------------------------------------------------
// O Map vive DENTRO de um processo. Com duas instâncias atrás de um
// balanceador, o cliente tem na prática o dobro do limite; com PM2 em
// modo cluster, multiplicado pelo número de núcleos. E tudo zera a
// cada deploy.
//
// Para valer de verdade, o contador precisa ser compartilhado —
// Redis com INCR e EXPIRE, ou o `express-rate-limit` com um store.
// Em memória serve para desenvolvimento e para uma instância só.

Três pegadinhas do Express num exercício só: o handler de erro precisa dos quatro parâmetros (com três, vira middleware comum e nunca roda), trust proxy é obrigatório atrás de proxy — senão todos os usuários compartilham o mesmo req.ip e o rate limit derruba o site — e o corpo validado deve substituir o req.body, senão campos não declarados seguem adiante.

Tudo no Express é middleware, rotas inclusive: uma pilha de funções que recebem req, res e next, executadas na ordem em que foram registradas. Essa única ideia explica por que a ordem importa tanto, por que uma rota literal precisa vir antes de uma paramétrica, por que o tratador de erro fica por último — e por que ele exige quatro parâmetros, já que é pela contagem deles que o Express o distingue dos demais.

Fontes e Referências

Exercícios

Exercício 1

Você segue o artigo à risca: npm install express e este arquivo. A aplicação não chega a subir. O que aconteceu?

const express = require("express");
const app = express();

app.get("/docs/*", (req, res) => {
  res.send(`Documento: ${req.params[0]}`);
});

app.listen(3000);
Ver resposta

✓ Resposta: O processo morre no boot com TypeError: Missing parameter name at 1, porque o npm install express sem versão instala o Express 5, e o analisador de rotas dele não aceita mais o curinga solto. A sintaxe dos exemplos do artigo é a do Express 4, e a migração mexeu exatamente nos casos de canto: no 5 o curinga precisa ser nomeado — /docs/*caminho —, e o valor chega em req.params.caminho como array de segmentos, não como a string única que o req.params[0] devolvia. O parâmetro opcional mudou junto: :extensao? virou {.:extensao}. Duas lições que valem além do Express: uma major pode quebrar no boot em vez de em produção, e é o melhor cenário possível — a falha é imediata e o stack aponta para a linha; e é por isso que material didático envelhece de forma traiçoeira, já que o texto continua correto para a versão em que foi escrito. Para acompanhar o artigo como está, fixe a versão: npm i express@4.

Exercício 2

Toda rota assíncrona que falha devolve uma página HTML de erro em vez do JSON deste tratador, e o console.error nunca imprime nada. Por quê?

app.use("/usuarios", usuariosRouter);

app.use((erro, req, res) => {
  console.error(erro);
  res.status(erro.status ?? 500).json({ erro: "Erro interno" });
});
Ver resposta

✓ Resposta: Porque o Express identifica o tratador de erro pela aridade da função — o número de parâmetros declarados. Com três, ele é registrado como middleware comum; o erro passa a receber o req, o req recebe o res, e nada nesse handler faz sentido. Como não sobra nenhum tratador de erro registrado, o Express cai no tratador padrão, que responde com uma página HTML — em desenvolvimento, com o stack trace inteiro exposto. A correção é declarar os quatro parâmetros, mesmo sem usar o último: (erro, req, res, next), ou _next para o linter não reclamar. E aqui está o detalhe que faz o bug sobreviver à revisão: remover um parâmetro não usado parece limpeza, o código continua sem erro de sintaxe, e o defeito só aparece no dia em que alguma rota falha de verdade. Vale saber ainda que, no Express 4, o tratador também não captura erro lançado dentro de função async, porque a rejeição não é repassada ao next — daí o asyncHandler do artigo. No Express 5 isso mudou: uma promise rejeitada é encaminhada ao tratador automaticamente.

Exercício 3

Estas quatro linhas estão no index.js do artigo, nesta ordem. Um formulário HTML comum faz POST para a API. O que ele recebe?

app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(validarJSON);

// validarJSON responde 415 quando o método é POST, PUT ou PATCH
// e o Content-Type não contém "application/json"
Ver resposta

✓ Resposta: Recebe 415 Unsupported Media Type, sempre. Um formulário HTML enviado pelo navegador manda Content-Type: application/x-www-form-urlencoded — ou multipart/form-data, quando tem arquivo —, nunca application/json. O validarJSON está registrado globalmente, então rejeita a requisição antes de qualquer rota, e o express.urlencoded da linha anterior fica completamente inútil: o app instala o suporte a formulário e, uma linha abaixo, proíbe formulários. É o artigo se contradizendo dentro do mesmo bloco, e o tipo de coisa que ninguém percebe enquanto testa só com curl mandando JSON. Há duas saídas, conforme a intenção. Se a API é JSON, o honesto é remover o express.urlencoded e assumir isso. Se ela também aceita formulário, a validação não pode ser global: aplique-a apenas onde JSON é obrigatório, como app.use("/api", validarJSON), ou liste os tipos aceitos em vez de exigir um só. A regra geral que vale levar: middleware global é uma decisão de produto, não um detalhe de implementação — ele passa a valer para rotas que ainda nem foram escritas.

Exercício 4

Três rotas, a mesma requisição GET /usuarios/novos. Qual delas responde?

router.get("/:id", (req, res) => {
  const usuario = usuarios.find(u => u.id === Number(req.params.id));
  if (!usuario) return res.status(404).json({ erro: "Não encontrado" });
  res.json(usuario);
});

router.get("/novos", (req, res) => {
  res.json(usuarios.filter(u => u.criadoEm > ontem()));
});

router.get("/:id/posts", (req, res) => { /* ... */ });
Ver resposta

✓ Resposta: Responde a primeira, com 404 Não encontrado — e a rota /novos nunca executa. O Express testa as rotas na ordem em que foram registradas e para na primeira que casa; /:id é um parâmetro, aceita qualquer segmento, e "novos" é um segmento como outro qualquer. O que acontece em seguida é o que torna o defeito difícil: Number("novos") é NaN, nenhum usuário tem id igual a NaN, o find devolve undefined e a rota responde um 404 perfeitamente plausível. Ninguém suspeita de conflito de rotas ao ver "não encontrado" — a hipótese natural é que falte o dado. A regra é colocar as rotas literais antes das paramétricas, sempre: /novos acima de /:id. Duas defesas complementares: restringir o parâmetro ao formato esperado, com router.get("/:id(\\d+)") no Express 4, o que faz /novos simplesmente não casar; e validar o parâmetro no início do handler, devolvendo 400 quando Number.isNaN(id) — porque "você mandou um id inválido" e "esse usuário não existe" são respostas diferentes, e confundir as duas custa horas de depuração de quem consome a API.

Exercício 5

Esta é a paginação do Router do artigo. O que devolve GET /usuarios?pagina=0, e o que devolve ?por_pagina=abc?

const { busca, pagina = 1, por_pagina = 10 } = req.query;

const total = lista.length;
const inicio = (pagina - 1) * por_pagina;
const dados = lista.slice(inicio, inicio + Number(por_pagina));
Ver resposta

✓ Resposta: Com ?pagina=0, o inicio vira -10 — e slice com índice negativo conta a partir do fim. A resposta traz os dez últimos usuários, apresentados como se fossem a página zero, sem erro nenhum. Com ?por_pagina=abc, o inicio vira NaN, o slice(NaN, NaN) trata os dois como zero e devolve um array vazio, enquanto o campo total continua informando o número real de registros — uma resposta internamente contraditória, que faz a interface mostrar "42 resultados" sobre uma lista em branco. A raiz dos dois é a mesma: req.query entrega strings, e os valores entram na conta sem nunca serem validados. As coerções do JavaScript mascaram isso — ("2" - 1)1 e tudo parece funcionar enquanto a entrada é bem-comportada. Todo parâmetro de paginação precisa de piso e teto: Math.max(1, Number(pagina) || 1) e Math.min(100, Math.max(1, Number(por_pagina) || 10)), exatamente como o artigo anterior fez. O teto não é preciosismo: sem ele, ?por_pagina=9999999 é um jeito barato de pedir a tabela inteira e derrubar a API.

Comentários

Mais em Javascript

Promises: resolvendo o Callback Hell
Promises: resolvendo o Callback Hell

Uma Promise é o recibo de uma operação que ainda não terminou: você guarda o…

Projeto: API REST com autenticação JWT
Projeto: API REST com autenticação JWT

O Módulo 4 fecha juntando Node, Express, MongoDB e Mongoose numa API de…

Mini Projeto: Quiz Interativo
Mini Projeto: Quiz Interativo

O fecho do Módulo 2 junta tudo num quiz de dez perguntas: timer de 20 segundos…