DevTools

Cheatsheet Express.js

Framework web minimalista para Node.js

Voltar às linguagens
Express.js
105 cards encontrados
Categorias:
Versões:

Instalação e Setup


10 cards
Instalação
mkdir minha-api && cd minha-api
npm init -y
npm install express
npm install -D nodemon

npm init -y cria o package.json. express é a dependência principal. nodemon (dev) reinicia o servidor automaticamente quando ficheiros mudam.

Separar app e server
// app.js
const app = express();
app.use(express.json());
// ... rotas e middleware
module.exports = app;

// server.js
const app = require("./app");
const porta = process.env.PORT || 3000;
app.listen(porta, () => {
  console.log(`Ativo na porta ${porta}`);
});

Separar a configuração (app.js) do arranque (server.js) facilita testes — pode importar app sem abrir porta. Padrão recomendado para testes com supertest.

Variáveis de ambiente
npm install dotenv

// server.js
require("dotenv").config();

const porta = process.env.PORT || 3000;
const dbUrl = process.env.DB_URL;

// .env (NÃO commitar)
PORT=3000
DB_URL=mongodb://localhost/app

dotenv carrega variáveis de .env para process.env. Adicione .env ao .gitignore. Use para portas, URLs de BD, segredos e chaves de API.

Servidor básico
const express = require("express");
const app = express();

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

app.listen(3000, () => {
  console.log("Servidor em :3000");
});

express() cria a aplicação. app.get() regista uma rota. app.listen() inicia o servidor HTTP na porta indicada. Callback confirma quando está activo.

ES Modules
// package.json
{ "type": "module" }

// app.js
import express from "express";
const app = express();

import { userRoutes } from "./routes/users.js";
app.use("/users", userRoutes);

export default app;

"type": "module" activa import/export (ESM) em vez de require. Ficheiros precisam extensão .js nos imports. Alternativa: usar .mjs.

Configuração por ambiente
const env = process.env.NODE_ENV || "development";

if (env === "production") {
  app.set("trust proxy", 1);
  app.use(helmet());
} else {
  app.use(morgan("dev"));
}

app.listen(porta);

NODE_ENV distingue ambientes. Em produção: helmet, trust proxy, sem logs verbosos. Em desenvolvimento: morgan("dev"), erros detalhados. Defina via NODE_ENV=production node server.js.

Parsers de body
// JSON (mais comum em APIs)
app.use(express.json());

// Formulários HTML
app.use(express.urlencoded({ extended: true }));

// Texto puro
app.use(express.text());

// Limite de tamanho:
app.use(express.json({ limit: "5mb" }));

Sem parsers, req.body é undefined. express.json() faz parse de JSON. urlencoded para forms HTML. limit previne payloads enormes (DoS).

Scripts do package.json
{
  "scripts": {
    "dev": "nodemon src/server.js",
    "start": "node src/server.js",
    "test": "jest --watchAll"
  }
}

dev usa nodemon para desenvolvimento (auto-reload). start é para produção (Node puro). test corre testes. Execute com npm run dev, npm start, npm test.

Estrutura recomendada
projeto/
  src/
    routes/       # definições de rotas
    controllers/  # lógica dos handlers
    middleware/   # funções intermediárias
    models/       # acesso a dados
    services/     # regras de negócio
    app.js        # configuração da app
  server.js       # arranque do servidor
  package.json

Separação por responsabilidade: routes/ define URLs, controllers/ processa pedidos, services/ contém lógica, models/ acede à BD. Escalável e testável.

express-generator
npx express-generator minha-app
cd minha-app
npm install
npm start

// Com EJS:
npx express-generator --view=ejs app

express-generator cria uma estrutura completa com rotas, views e middleware pré-configurados. Inclui morgan, cookie-parser e engine de templates. Bom para começar rápido.

Rotas e Parâmetros


11 cards
Métodos HTTP
app.get("/users", listar);
app.post("/users", criar);
app.put("/users/:id", substituir);
app.patch("/users/:id", atualizar);
app.delete("/users/:id", remover);

// Todos os métodos:
app.all("/rota", handler);

Cada verbo HTTP tem um método correspondente. GET (ler), POST (criar), PUT (substituir), PATCH (actualizar parcial), DELETE (remover). all aceita qualquer verbo.

Encadeamento com route()
app.route("/livros")
  .get((req, res) => {
    res.json(livros);
  })
  .post((req, res) => {
    livros.push(req.body);
    res.status(201).end();
  });

app.route() agrupa múltiplos verbos na mesma rota. Evita repetir o caminho. Útil quando GET e POST partilham o mesmo endpoint. Também suporta .put(), .delete(), etc.

Rotas com sub-app
const admin = express();
admin.use(authAdmin);
admin.get("/dashboard", (req, res) => {
  res.send("Painel Admin");
});

// Montar na app principal:
app.use("/admin", admin);

Uma instância express() pode ser montada como sub-aplicação. Herda middleware próprio. Útil para secções isoladas com configuração diferente (ex: admin, API v2).

Parâmetros de rota
app.get("/users/:id", (req, res) => {
  const id = req.params.id;
  res.send(`User ${id}`);
});

// Múltiplos parâmetros:
app.get("/lojas/:lojaId/produtos/:prodId",
  (req, res) => {
    const { lojaId, prodId } = req.params;
  });

:id define um segmento dinâmico na URL. O valor fica em req.params.id. Múltiplos params são suportados. Sempre strings — converta com parseInt() ou Number() se necessário.

Parâmetros opcionais e regex
// Opcional (:sufixo?)
app.get("/users/:id?", handler);

// Padrão regex
app.get("/files/:nome(\\d+)", handler);

// Múltiplos caminhos:
app.get(["/a", "/b", "/c"], handler);

:id? torna o parâmetro opcional. (\d+) restringe a dígitos (regex inline). Array de caminhos regista a mesma handler em múltiplas URLs. Flexibilidade total no routing.

req.baseUrl e mount path
// app.use("/api/v1", router);

router.get("/users", (req, res) => {
  req.baseUrl;    // "/api/v1"
  req.path;       // "/users"
  req.originalUrl; // "/api/v1/users"
});

req.baseUrl é o prefixo onde o router foi montado. req.path é o caminho dentro do router. req.originalUrl é a URL completa. Útil para gerar links absolutos.

Query string
// GET /busca?q=node&page=2&limit=10
app.get("/busca", (req, res) => {
  const q = req.query.q;         // "node"
  const page = req.query.page;   // "2"
  const limit = +req.query.limit || 10;
  res.json({ q, page, limit });
});

req.query contém os parâmetros após ? na URL. Valores são strings — use + ou Number() para converter. Ideal para filtros, paginação e ordenação.

router.param()
router.param("id", async (req, res, next, id) => {
  const user = await User.findById(id);
  if (!user) {
    return res.status(404).json({ erro: "Não encontrado" });
  }
  req.user = user;
  next();
});

router.get("/:id", (req, res) => {
  res.json(req.user); // já carregado
});

router.param() executa middleware quando um parâmetro específico aparece. Ideal para carregar recursos da BD uma vez e reutilizar em todas as rotas com :id.

Wildcard e 404
// Capturar qualquer rota não definida:
app.use("*", (req, res) => {
  res.status(404).json({
    erro: `Rota ${req.originalUrl} não existe`
  });
});

// Deve ser o ÚLTIMO middleware

* (ou app.use sem caminho) captura tudo que não foi tratado. Deve ser registado por último. Retorna 404 para rotas inexistentes. Em SPAs, pode servir index.html.

Router modular
// routes/users.js
const router = express.Router();

router.get("/", listar);
router.post("/", criar);
router.get("/:id", ver);
router.put("/:id", atualizar);

module.exports = router;

// app.js
app.use("/users", require("./routes/users"));

express.Router() cria um mini-app de rotas. app.use("/users", router) monta com prefixo. Mantém o ficheiro principal limpo. Cada recurso tem o seu próprio ficheiro de rotas.

Router com middleware
const router = express.Router();

// Aplica a todas as rotas deste router:
router.use(autenticar);
router.use(logAcesso);

router.get("/", listar);
router.post("/", criar);

module.exports = router;

router.use() aplica middleware apenas às rotas desse router. Não afecta outras rotas da app. Ideal para auth específica de secção (ex: todas as rotas /admin requerem admin).

Middleware


11 cards
Middleware global
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url}`);
  next();
});

app.use() sem caminho aplica a todas as rotas. next() passa para o próximo middleware. Sem next(), o pedido fica pendente (hang). Registe antes das rotas.

next() e fluxo
app.use((req, res, next) => {
  req.usuario = { id: 1, nome: "Ana" };
  next();
});

// Passar erro para o handler:
app.use((req, res, next) => {
  if (!req.query.token) {
    return next(new Error("Token ausente"));
  }
  next();
});

next() avança para o próximo middleware. next(erro) salta directamente para o handler de erro. Pode adicionar dados a req para usar em handlers seguintes.

Terceiros essenciais
const morgan = require("morgan");
const cors = require("cors");
const helmet = require("helmet");
const compression = require("compression");

app.use(morgan("dev"));
app.use(cors());
app.use(helmet());
app.use(compression());

morgan (logs), cors (cross-origin), helmet (headers seguros), compression (gzip). Os 4 middlewares mais usados em produção. Registe no topo da app.

Middleware por rota
function autenticar(req, res, next) {
  if (!req.headers.authorization) {
    return res.status(401).json({ erro: "Não autorizado" });
  }
  next();
}

app.get("/perfil", autenticar, handler);

Middleware inline corre antes do handler final. Pode encadear múltiplos: app.get("/", auth, validar, handler). Se não chamar next(), deve enviar resposta.

Middleware com configuração
function rateLimit({ max, janela }) {
  const pedidos = new Map();
  return (req, res, next) => {
    const ip = req.ip;
    const agora = Date.now();
    // lógica de contagem...
    if (excedeu) return res.status(429).end();
    next();
  };
}

app.use(rateLimit({ max: 100, janela: 60000 }));

Função que retorna middleware (factory pattern). Aceita opções de configuração. Permite reutilizar com parâmetros diferentes. Padrão usado por cors(), helmet(), etc.

Skip e montagem
// Morgan com skip:
app.use(morgan("dev", {
  skip: (req) => req.url.startsWith("/health")
}));

// Montar em path específico:
app.use("/uploads", express.static("uploads"));

Alguns middlewares aceitam skip para ignorar certos pedidos. app.use(path, middleware) monta apenas num prefixo. Reduz overhead em rotas de health check ou estáticos.

Ordem de execução
// A ordem importa!
app.use(express.json());    // 1º - parse body
app.use(logger);            // 2º - log
app.use("/api", rotas);     // 3º - rotas
app.use(notFound);          // 4º - 404
app.use(tratadorDeErros);   // 5º - erros (último)

Middleware executa na ordem de registo via app.use(). Parsers antes das rotas. 404 depois das rotas. Handler de erros sempre no fim (4 argumentos). Ordem errada = comportamento inesperado.

Middleware condicional
// Só em desenvolvimento:
if (process.env.NODE_ENV === "development") {
  app.use(morgan("dev"));
}

// Só para /api:
app.use("/api", express.json());

// Só para POST/PUT:
app.use((req, res, next) => {
  if (["POST", "PUT"].includes(req.method)) {
    return express.json()(req, res, next);
  }
  next();
});

Middleware pode ser aplicado condicionalmente por ambiente, caminho ou método. app.use("/api", ...) limita a um prefixo. Evita processamento desnecessário.

res.locals
app.use((req, res, next) => {
  res.locals.anoAtual = new Date().getFullYear();
  res.locals.userLogado = req.user || null;
  next();
});

// Em templates (EJS):
// <%= anoAtual %>
// Em handlers:
app.get("/", (req, res) => {
  res.render("index", { ano: res.locals.anoAtual });
});

res.locals guarda dados disponíveis em templates e handlers seguintes. Diferente de req — é específico da resposta. Ideal para dados de layout (user, ano, config).

Middleware de erro
// 4 argumentos obrigatórios
app.use((erro, req, res, next) => {
  console.error(erro.stack);
  const status = erro.status || 500;
  res.status(status).json({
    erro: erro.message,
    ...(process.env.NODE_ENV !== "production" && {
      stack: erro.stack
    }),
  });
});

Handler de erro tem 4 parâmetros (err, req, res, next). Captura erros passados via next(erro) ou lançados. Nunca exponha stack em produção. Deve ser o último middleware.

Async middleware
const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

app.get("/users", asyncHandler(async (req, res) => {
  const users = await User.find();
  res.json(users);
}));

Express 4 não captura erros de async automaticamente. asyncHandler embrulha a Promise e encaminha rejeições para next(). Express 5 captura nativamente.

Request e Input


11 cards
req.body
// POST com JSON
app.post("/users", (req, res) => {
  const { nome, email } = req.body;
  res.json({ nome, email });
});

// Requer: app.use(express.json())

req.body contém o corpo parseado do pedido. Só disponível após express.json() ou urlencoded(). Em GET, é normalmente undefined ou {}.

Cookies
const cookieParser = require("cookie-parser");
app.use(cookieParser());

app.get("/", (req, res) => {
  // Ler:
  const sessao = req.cookies.sessao;
  const assinado = req.signedCookies.token;

  // Definir:
  res.cookie("tema", "escuro", {
    maxAge: 86400000, httpOnly: true
  });
});

cookie-parser faz parse dos cookies. req.cookies para normais, req.signedCookies para assinados. res.cookie() define com opções: maxAge, httpOnly, secure.

req.route e matched
app.get("/users/:id", (req, res) => {
  req.route.path;    // "/users/:id"
  req.route.methods; // { get: true }
  req.baseUrl;       // prefixo do router
});

req.route mostra a rota que fez match. path é o padrão (com :id), methods os verbos registados. Útil para debugging e logging de rotas.

req.params
// GET /users/42/posts/7
app.get("/users/:userId/posts/:postId", (req, res) => {
  req.params.userId;  // "42"
  req.params.postId;  // "7"

  // Desestruturação:
  const { userId, postId } = req.params;
});

req.params contém os segmentos dinâmicos da rota. Valores são sempre strings. Defina com :nome na rota. Múltiplos params são suportados numa mesma rota.

Propriedades do request
req.method       // "GET"
req.url          // "/users?page=2"
req.path         // "/users"
req.originalUrl  // "/api/users?page=2"
req.ip           // "127.0.0.1"
req.protocol     // "http" ou "https"
req.secure       // true se HTTPS
req.xhr          // true se AJAX

Propriedades úteis: method (verbo), path (sem query), ip (cliente), protocol, secure. req.xhr detecta pedidos XMLHttpRequest.

Body com limite e tipos
// Limitar tamanho:
app.use(express.json({ limit: "1mb" }));

// Só para certas rotas:
app.use("/api", express.json());

// Raw body (webhooks):
app.use("/webhook", express.raw({ type: "*/*" }));

limit previne payloads enormes (ataque DoS). Pode aplicar parsers só a certos caminhos. express.raw() mantém o body como Buffer — necessário para verificar assinaturas de webhooks.

req.query
// GET /produtos?categoria=livros&ordem=preco&page=2
app.get("/produtos", (req, res) => {
  const { categoria, ordem } = req.query;
  const page = parseInt(req.query.page) || 1;

  // Arrays: ?tags=a&tags=b
  // req.query.tags = ["a", "b"]
});

req.query são os parâmetros após ?. Valores são strings (ou arrays se repetidos). Ideal para filtros, ordenação e paginação. Sempre valide e sanitize.

req.is() e content-type
app.post("/upload", (req, res) => {
  if (req.is("json")) {
    // processar JSON
  } else if (req.is("multipart/form-data")) {
    // processar upload
  } else {
    res.status(415).json({ erro: "Tipo não suportado" });
  }
});

req.is(tipo) verifica o Content-Type do pedido. Retorna o tipo se corresponder, false caso contrário. 415 é o status para tipo de conteúdo não suportado.

Sanitizar input
const { body } = require("express-validator");

app.post("/users",
  body("nome").trim().escape(),
  body("email").normalizeEmail(),
  (req, res) => {
    const nome = req.body.nome; // já limpo
  }
);

trim() remove espaços. escape() converte HTML entities (previne XSS). normalizeEmail() padroniza emails. Sempre sanitize antes de guardar na BD ou renderizar.

req.headers
app.get("/info", (req, res) => {
  const auth = req.headers["authorization"];
  const tipo = req.get("Content-Type");
  const accept = req.accepts("json");
  const host = req.hostname;
  const ua = req.get("User-Agent");
  res.json({ auth, tipo, host, ua });
});

req.headers dá acesso a todos os headers (lowercase). req.get() é atalho para um header específico. req.accepts() verifica o header Accept. Headers são case-insensitive.

req.files (multer)
const upload = multer({ dest: "uploads/" });

app.post("/fotos", upload.array("fotos", 5), (req, res) => {
  req.files.forEach(f => {
    console.log(f.originalname, f.size, f.mimetype);
  });
  res.json({ total: req.files.length });
});

Com multer, req.files contém os ficheiros enviados. Cada ficheiro tem originalname, size, mimetype, path. upload.array() aceita múltiplos.

Response e Output


11 cards
res.send()
res.send("texto simples");
res.send({ ok: true });
res.send("<h1>HTML</h1>");
res.send(Buffer.from("binário"));

res.send() envia a resposta e define Content-Type automaticamente. String → text/html. Objecto → application/json. Buffer → application/octet-stream. Encerra o ciclo request-response.

Headers de resposta
res.set("X-Token", "abc123");
res.set("Cache-Control", "no-cache");
res.type("application/pdf");

// Múltiplos de uma vez:
res.set({
  "X-API-Version": "2.0",
  "X-RateLimit-Remaining": "99",
});

res.set() define headers na resposta. res.type() é atalho para Content-Type. Headers devem ser definidos antes de send()/json(). Úteis para tokens, rate limit info, versionamento.

res.cookie()
res.cookie("sessao", token, {
  httpOnly: true,
  secure: true,
  maxAge: 7 * 24 * 60 * 60 * 1000,
  sameSite: "strict",
});

// Remover:
res.clearCookie("sessao");

res.cookie() define cookies com opções. httpOnly impede acesso via JS (anti-XSS). secure só em HTTPS. sameSite previne CSRF. clearCookie() remove.

res.json()
res.json({ users: [], total: 0 });
res.status(201).json(novoUser);
res.status(404).json({ erro: "Não encontrado" });

// JSONP (legacy):
res.jsonp({ dados: [] });

res.json() serializa para JSON e define Content-Type: application/json. Mais explícito que send() para APIs. Aceita objectos, arrays, null. jsonp adiciona callback (evitar).

res.download() e sendFile()
const path = require("path");

// Download (header Content-Disposition):
res.download("./relatorios/2024.pdf", "relatorio.pdf");

// Enviar ficheiro inline:
res.sendFile(path.join(__dirname, "public", "index.html"));

res.download() força download com nome customizado. res.sendFile() serve o ficheiro inline (browser mostra). Use path.join() para caminhos seguros. Previne path traversal.

res.location() e links
// Header Location (sem redirect):
res.location("/users/42");
res.status(201).json({ id: 42 });

// Link header (paginação):
res.links({
  next: "/users?page=3",
  last: "/users?page=10",
});

res.location() define o header Location sem fazer redirect. res.links() define o header Link (paginação REST). Padrão em APIs: 201 + Location para recurso criado.

Códigos de status
res.status(200).json({ ok: true });   // sucesso
res.status(201).json(novo);           // criado
res.status(204).end();                // sem conteúdo
res.status(400).json({ erro });       // inválido
res.status(401).json({ erro });       // não autenticado
res.status(403).json({ erro });       // sem permissão
res.status(404).json({ erro });       // não encontrado
res.status(500).json({ erro });       // erro servidor

res.status() define o código HTTP (encadeável). 2xx sucesso, 4xx erro do cliente, 5xx erro do servidor. 204 não tem body — use .end().

res.render() (templates)
// Configurar engine:
app.set("view engine", "ejs");
app.set("views", "./src/views");

// Renderizar:
app.get("/", (req, res) => {
  res.render("index", {
    titulo: "Início",
    users: listaUsers,
  });
});

res.render() compila um template com dados e envia HTML. Suporta EJS, Pug, Handlebars. view engine define a engine. views define a pasta. Dados ficam disponíveis no template.

res.append() e attachment()
// Adicionar header sem sobrescrever:
res.append("Set-Cookie", "a=1");
res.append("Set-Cookie", "b=2");

// Forçar download com nome:
res.attachment("foto.png");
res.sendFile("./uploads/foto.png");

res.append() adiciona valores a um header existente (não substitui). res.attachment() define Content-Disposition para download. Útil para múltiplos cookies ou forçar download.

res.redirect()
res.redirect("/login");
res.redirect(301, "/nova-url");    // permanente
res.redirect(302, "/temporario");  // temporário
res.redirect("back");              // voltar (Referer)

// Após criar recurso:
res.redirect(`/users/${novoUser.id}`);

res.redirect() envia HTTP 302 por defeito. 301 para redirect permanente (SEO). "back" usa o header Referer. Comum após POST para evitar re-submissão (padrão PRG).

res.end() e streaming
// Sem corpo:
res.status(204).end();

// Streaming (dados grandes):
app.get("/export", (req, res) => {
  res.set("Content-Type", "text/csv");
  const stream = fs.createReadStream("dados.csv");
  stream.pipe(res);
});

res.end() encerra sem body. stream.pipe(res) envia dados em chunks (eficiente para ficheiros grandes). Não carrega tudo em memória. Ideal para exports, vídeos, downloads grandes.

API REST e CRUD


11 cards
CRUD completo
let users = [];

app.get("/users", (req, res) => res.json(users));

app.post("/users", (req, res) => {
  const u = { id: Date.now(), ...req.body };
  users.push(u);
  res.status(201).json(u);
});

app.put("/users/:id", (req, res) => {
  const i = users.findIndex(u => u.id == req.params.id);
  if (i === -1) return res.status(404).end();
  users[i] = { ...users[i], ...req.body };
  res.json(users[i]);
});

app.delete("/users/:id", (req, res) => {
  users = users.filter(u => u.id != req.params.id);
  res.status(204).end();
});

CRUD REST: GET (listar), POST (criar, 201), PUT (substituir), DELETE (remover, 204). 404 se recurso não existe. Padrão para qualquer API.

Versionamento de API
// Por URL (mais comum):
app.use("/api/v1", rotasV1);
app.use("/api/v2", rotasV2);

// Por header:
app.use((req, res, next) => {
  const versao = req.get("API-Version") || "1";
  req.apiVersion = versao;
  next();
});

Versionamento por URL (/api/v1) é o mais simples e explícito. Por header é mais limpo mas menos visível. Mantenha backward compatibility. Documente breaking changes.

Idempotency key
const processados = new Map();

app.post("/pagamentos", (req, res) => {
  const key = req.get("Idempotency-Key");
  if (processados.has(key)) {
    return res.json(processados.get(key));
  }

  const resultado = processarPagamento(req.body);
  processados.set(key, resultado);
  res.status(201).json(resultado);
});

Idempotency-Key evita processamento duplicado. Se a chave já foi vista, retorna a resposta anterior. Essencial para pagamentos e operações não-idempotentes. O cliente gera a chave (UUID).

Paginação
app.get("/users", (req, res) => {
  const page = Math.max(1, +req.query.page || 1);
  const limite = Math.min(100, +req.query.limite || 10);
  const inicio = (page - 1) * limite;

  const itens = users.slice(inicio, inicio + limite);
  res.json({
    dados: itens,
    meta: { total: users.length, page, limite },
  });
});

Paginação com page e limite via query params. Math.max/Math.min previnem valores inválidos. Retorne meta com total para o cliente calcular páginas.

HATEOAS e links
app.get("/users/:id", (req, res) => {
  const user = users.find(u => u.id == req.params.id);
  res.json({
    ...user,
    _links: {
      self: `/users/${user.id}`,
      posts: `/users/${user.id}/posts`,
      delete: `/users/${user.id}`,
    },
  });
});

HATEOAS inclui links na resposta para descoberta de recursos. _links indica acções disponíveis. Torna a API auto-descritiva. Padrão em APIs REST maduras.

API com controller
// controllers/userController.js
exports.listar = async (req, res, next) => {
  try {
    const users = await User.find();
    res.json(users);
  } catch (e) { next(e); }
};

exports.criar = async (req, res, next) => {
  try {
    const user = await User.create(req.body);
    res.status(201).json(user);
  } catch (e) { next(e); }
};

Controllers separam a lógica dos handlers. exports.metodo para cada acção. try/catch com next(e) encaminha erros. Mantém rotas limpas e código testável.

Filtros e ordenação
app.get("/produtos", (req, res) => {
  let resultado = [...produtos];

  if (req.query.categoria) {
    resultado = resultado.filter(
      p => p.categoria === req.query.categoria
    );
  }

  const ordem = req.query.ordem === "desc" ? -1 : 1;
  resultado.sort((a, b) => (a.preco - b.preco) * ordem);

  res.json(resultado);
});

Filtros via query params (?categoria=x). Ordenação com ?ordem=desc. Combine com paginação para APIs completas. Em BD real, use WHERE e ORDER BY.

PATCH parcial
app.patch("/users/:id", (req, res) => {
  const user = users.find(u => u.id == req.params.id);
  if (!user) return res.status(404).end();

  // Actualizar só campos enviados:
  const camposPermitidos = ["nome", "email", "ativo"];
  camposPermitidos.forEach(campo => {
    if (req.body[campo] !== undefined) {
      user[campo] = req.body[campo];
    }
  });

  res.json(user);
});

PATCH actualiza parcialmente (só campos enviados). PUT substitui tudo. Whitelist de campos evita actualização de id ou role. Sempre valide os campos recebidos.

Service layer
// services/userService.js
class UserService {
  async listar(filtros) {
    return User.find(filtros).lean();
  }

  async criar(dados) {
    if (await this.emailExiste(dados.email)) {
      throw new ApiError(409, "Email já existe");
    }
    return User.create(dados);
  }
}

module.exports = new UserService();

Services contêm regras de negócio (validações, lógica). Controllers só orquestram (receber, chamar service, responder). Separação clara: rota → controller → service → model. Máxima testabilidade com exports por classe.

Respostas padronizadas
// Sucesso:
res.json({
  sucesso: true,
  dados: users,
  meta: { total: 50 },
});

// Erro:
res.status(400).json({
  sucesso: false,
  erro: "Email já existe",
  campos: { email: "Deve ser único" },
});

Estrutura consistente: sucesso (boolean), dados (payload), erro (mensagem), campos (erros por campo). Facilita o parsing no frontend. Documente o formato.

Bulk operations
app.post("/users/bulk", (req, res) => {
  const novos = req.body.usuarios;
  if (!Array.isArray(novos)) {
    return res.status(400).json({ erro: "Array esperado" });
  }

  const criados = novos.map(u => ({ id: Date.now(), ...u }));
  users.push(...criados);
  res.status(201).json({ criados: criados.length });
});

Operações em massa para criar/actualizar/remover múltiplos recursos via POST. Valide que é array. Limite o tamanho (ex: max 100). Retorne resumo da operação. Útil para imports e syncs.

Recursos Avançados


10 cards
Upload com multer
const multer = require("multer");

const storage = multer.diskStorage({
  destination: "./uploads/",
  filename: (req, file, cb) => {
    cb(null, `${Date.now()}-${file.originalname}`);
  },
});

const upload = multer({
  storage,
  limits: { fileSize: 5 * 1024 * 1024 },
  fileFilter: (req, file, cb) => {
    cb(null, file.mimetype.startsWith("image/"));
  },
});

app.post("/upload", upload.single("foto"), (req, res) => {
  res.json({ path: req.file.path });
});

multer processa multipart/form-data. diskStorage controla destino e nome. limits restringe tamanho. fileFilter valida tipo. single(), array(), fields() para diferentes usos.

Agendamento (node-cron)
const cron = require("node-cron");

// Todos os dias às 3h:
cron.schedule("0 3 * * *", async () => {
  console.log("Limpar tokens expirados...");
  await Token.deleteMany({ expira: { $lt: new Date() } });
});

// A cada 5 minutos:
cron.schedule("*/5 * * * *", () => {
  verificarServicos();
});

node-cron agenda tarefas recorrentes no processo Express. Sintaxe cron standard (min hora dia mês semana). Ideal para limpeza, relatórios, syncs. Para produção pesada, use Bull/BullMQ.

Cluster mode
const cluster = require("cluster");
const os = require("os");

if (cluster.isPrimary) {
  const cpus = os.cpus().length;
  for (let i = 0; i < cpus; i++) {
    cluster.fork();
  }
  cluster.on("exit", () => cluster.fork());
} else {
  require("./server");
}

cluster cria um processo por CPU. O primary distribui conexões. Se um worker morre, outro é criado. Aproveita multi-core. Alternativa moderna: usar PM2 com -i max.

WebSockets (Socket.IO)
const http = require("http");
const { Server } = require("socket.io");

const server = http.createServer(app);
const io = new Server(server);

io.on("connection", (socket) => {
  socket.on("mensagem", (dados) => {
    io.emit("mensagem", dados);
  });
  socket.on("disconnect", () => {});
});

server.listen(3000);

Socket.IO adiciona WebSockets ao Express. io.on("connection") quando cliente conecta. socket.emit() envia para um. io.emit() para todos. Ideal para chat, notificações em tempo real.

Server-Sent Events
app.get("/eventos", (req, res) => {
  res.set({
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  });

  const id = setInterval(() => {
    res.write(`data: ${JSON.stringify({ hora: new Date() })}\n\n`);
  }, 1000);

  req.on("close", () => clearInterval(id));
});

SSE envia eventos do servidor para o cliente (unidireccional). Header text/event-stream. res.write() envia sem fechar. Mais simples que WebSockets para notificações e feeds.

Health check
app.get("/health", async (req, res) => {
  const checks = {
    uptime: process.uptime(),
    bd: "ok",
    redis: "ok",
  };

  try {
    await mongoose.connection.db.admin().ping();
  } catch {
    checks.bd = "erro";
    return res.status(503).json(checks);
  }

  res.json(checks);
});

/health verifica se o serviço está operacional. Testa conexões (BD, Redis, APIs). Retorna 503 se algo falhar. Usado por load balancers e Kubernetes para liveness/readiness probes.

Templates EJS
npm install ejs

// app.js
app.set("view engine", "ejs");

// views/index.ejs
// <h1><%= titulo %></h1>
// <% users.forEach(u => { %>
//   <p><%= u.nome %></p>
// <% }) %>

app.get("/", (req, res) => {
  res.render("index", { titulo: "Home", users });
});

EJS é a engine de templates mais simples. <%= %> imprime (escapado). <% %> executa lógica. res.render() compila com dados. Inclua partials com <%- include() %>.

Proxy reverso
const { createProxyMiddleware } = require("http-proxy-middleware");

// Encaminhar /api/legacy para outro serviço:
app.use("/api/legacy", createProxyMiddleware({
  target: "http://servico-antigo:4000",
  changeOrigin: true,
  pathRewrite: { "^/api/legacy": "" },
}));

http-proxy-middleware encaminha pedidos para outros serviços. target é o destino. pathRewrite remove o prefixo. Útil para migrações graduais, microserviços e APIs externas.

Caching com Redis
const Redis = require("ioredis");
const redis = new Redis();

async function cacheMiddleware(req, res, next) {
  const key = `cache:${req.originalUrl}`;
  const cached = await redis.get(key);
  if (cached) return res.json(JSON.parse(cached));

  const originalJson = res.json.bind(res);
  res.json = (data) => {
    redis.setex(key, 60, JSON.stringify(data));
    return originalJson(data);
  };
  next();
}

app.get("/api/produtos", cacheMiddleware, handler);

Redis cacheia respostas por URL com TTL (60s). Intercepta res.json() para guardar. Se cache existe, responde sem ir à BD. Reduz carga drasticamente em endpoints lidos frequentemente.

Graceful shutdown
const server = app.listen(3000);

process.on("SIGTERM", () => {
  console.log("Encerrando...");
  server.close(() => {
    mongoose.connection.close();
    redis.quit();
    process.exit(0);
  });

  // Forçar após 10s:
  setTimeout(() => process.exit(1), 10000);
});

SIGTERM é enviado pelo Docker/orquestrador. server.close() para de aceitar conexões e espera as activas terminarem. Feche conexões a BD/Redis. Timeout força encerramento se algo bloquear.

Boas Práticas e Ferramentas


10 cards
Logging com morgan
const morgan = require("morgan");

// Desenvolvimento (colorido):
app.use(morgan("dev"));
// GET /users 200 3.2ms - 1.2kb

// Produção (detalhado):
app.use(morgan("combined"));

// Para ficheiro:
const fs = require("fs");
app.use(morgan("combined", {
  stream: fs.createWriteStream("access.log", { flags: "a" })
}));

morgan regista todos os pedidos HTTP. "dev" é colorido e conciso. "combined" é formato Apache (para produção). stream redirecciona para ficheiro. Combine com Winston para logs de app.

Docker
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

# .dockerignore
node_modules
.env

node:20-alpine é a imagem base leve. npm ci instala exacto (lock file). .dockerignore exclui node_modules e .env. EXPOSE documenta a porta. Multi-stage para build menor.

Checklist de produção
// ✓ NODE_ENV=production
// ✓ helmet() + cors() configurados
// ✓ Rate limiting activo
// ✓ Body com limit
// ✓ HTTPS forçado
// ✓ Logs estruturados (Winston)
// ✓ Graceful shutdown
// ✓ Health check endpoint
// ✓ Sem secrets no código
// ✓ PM2 ou Docker
// ✓ Testes a passar

Antes de deploy: segurança (helmet, cors, limits), observabilidade (logs, health), resiliência (graceful shutdown, PM2), e higiene (sem secrets, testes). Revise esta lista em cada release.

Winston (logs estruturados)
const winston = require("winston");

const logger = winston.createLogger({
  level: "info",
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: "erro.log", level: "error" }),
    new winston.transports.File({ filename: "app.log" }),
    new winston.transports.Console({ format: winston.format.simple() }),
  ],
});

logger.info("Servidor iniciado", { porta: 3000 });
logger.error("Falha BD", { erro: e.message });

Winston é o logger mais popular. Níveis: error, warn, info, debug. Múltiplos transports (console, ficheiro, serviços). Formato JSON para produção (parseável por ELK, Datadog).

Organização por feature
src/
  features/
    users/
      user.routes.js
      user.controller.js
      user.service.js
      user.model.js
      user.test.js
    posts/
      post.routes.js
      post.controller.js
  shared/
    middleware/
    utils/
  app.js

Organização por feature agrupa tudo de um recurso junto. Mais escalável que separar por tipo (routes/, controllers/). Fácil de encontrar e remover features. shared/ para código comum.

Express 5 (novidades)
npm install express@5

// Async errors capturados nativamente:
app.get("/users", async (req, res) => {
  const users = await User.find(); // reject → 500 automático
  res.json(users);
});

// Wildcard mudou:
app.get("/users/{id}", handler);  // em vez de :id
app.use("/*splat", notFound);     // em vez de *

Express 5 captura erros async sem wrapper. Sintaxe de rota mudou: {id} em vez de :id, *splat em vez de *. Promises nativas em res.redirect(). Breaking changes mínimos.

Testes com supertest
const request = require("supertest");
const app = require("./app");

describe("API Users", () => {
  it("GET /users retorna lista", async () => {
    const res = await request(app)
      .get("/users")
      .expect(200)
      .expect("Content-Type", /json/);

    expect(res.body).toBeInstanceOf(Array);
  });

  it("POST /users cria", async () => {
    const res = await request(app)
      .post("/users")
      .send({ nome: "Ana", email: "ana@test.com" })
      .expect(201);
  });
});

supertest testa endpoints sem abrir porta. request(app) simula pedidos HTTP. .expect() verifica status e headers. .send() envia body. Combine com Jest ou Mocha. Testa a app inteira (integração).

Dependency injection
// Controller recebe service (testável):
function createUserController(userService) {
  return {
    async listar(req, res) {
      const users = await userService.listar();
      res.json(users);
    },
    async criar(req, res) {
      const user = await userService.criar(req.body);
      res.status(201).json(user);
    },
  };
}

// Injecção:
const ctrl = createUserController(new UserService());

Factory functions recebem dependências como argumentos via require. Facilita testes com mocks. Sem acoplamento a implementações concretas. Alternativa: usar um container DI (tsyringe, awilix).

PM2 (process manager)
npm install -g pm2

pm2 start server.js -i max
pm2 list
pm2 logs
pm2 restart all
pm2 save
pm2 startup

PM2 gere processos Node em produção. -i max usa todos os CPUs (cluster). Auto-restart em crash. pm2 logs mostra output. pm2 startup inicia no boot. Monitor com pm2 monit.

Config centralizada
// config/index.js
require("dotenv").config();

module.exports = {
  porta: process.env.PORT || 3000,
  dbUrl: process.env.DB_URL,
  jwtSecret: process.env.JWT_SECRET,
  jwtExpiry: "24h",
  cors: {
    origin: process.env.CORS_ORIGIN || "*",
  },
  isProduction: process.env.NODE_ENV === "production",
};

Centralize toda a configuração num módulo. Um único ponto para variáveis de ambiente. Defaults sensatos para desenvolvimento. Valide que segredos existem ao arrancar. Nunca espalhe process.env pelo código.

Validação e Erros


10 cards
Validação manual
function validarUser(req, res, next) {
  const { nome, email } = req.body;
  const erros = [];

  if (!nome || nome.trim().length < 2)
    erros.push("Nome deve ter 2+ caracteres");
  if (!email || !email.includes("@"))
    erros.push("Email inválido");

  if (erros.length)
    return res.status(400).json({ erros });
  next();
}

app.post("/users", validarUser, criar);

Middleware de validação verifica campos antes do handler. Retorna 400 com lista de erros se inválido. next() só se tudo passar. Simples mas repetitivo para muitos campos.

Classe de erro custom
class ApiError extends Error {
  constructor(status, mensagem, campos = {}) {
    super(mensagem);
    this.status = status;
    this.campos = campos;
    this.isOperacional = true;
  }
}

// Uso:
throw new ApiError(404, "User não encontrado");
throw new ApiError(422, "Erro de validação", {
  email: "Já existe"
});

ApiError padroniza erros com status e campos. isOperacional distingue erros esperados de bugs. O handler global formata a resposta. Evita repetir res.status().json() em todo o lado.

notFound middleware
// Depois de todas as rotas:
app.use((req, res) => {
  res.status(404).json({
    sucesso: false,
    erro: `Rota ${req.method} ${req.originalUrl} não existe`,
  });
});

// Para SPA (servir index.html):
app.get("*", (req, res) => {
  res.sendFile(path.join(__dirname, "public", "index.html"));
});

Middleware 404 captura rotas não definidas. Para APIs, retorne JSON. Para SPAs, sirva index.html (client-side routing). Deve vir depois de todas as rotas mas antes do handler de erro.

express-validator
const { body, validationResult } = require("express-validator");

app.post("/users",
  body("nome").notEmpty().isLength({ min: 2 }),
  body("email").isEmail().normalizeEmail(),
  body("idade").optional().isInt({ min: 18 }),
  (req, res) => {
    const erros = validationResult(req);
    if (!erros.isEmpty())
      return res.status(400).json({ erros: erros.array() });
    // continuar...
  }
);

express-validator valida de forma declarativa. body(), param(), query() para cada fonte. validationResult() recolhe erros. Suporta optional(), sanitização e mensagens custom.

Handler de erro global
app.use((erro, req, res, next) => {
  const status = erro.status || 500;
  const resposta = {
    sucesso: false,
    erro: erro.message,
    ...(erro.campos && { campos: erro.campos }),
  };

  if (process.env.NODE_ENV !== "production") {
    resposta.stack = erro.stack;
  }

  res.status(status).json(resposta);
});

Um único handler formata todos os erros. Erros operacionais mostram a mensagem. Bugs (500) escondem detalhes em produção. stack só em desenvolvimento. Sempre o último middleware.

try/catch em controllers
exports.criar = async (req, res, next) => {
  try {
    const user = await UserService.criar(req.body);
    res.status(201).json(user);
  } catch (erro) {
    if (erro.code === 11000) {
      return res.status(409).json({
        erro: "Email já registado"
      });
    }
    next(erro);
  }
};

try/catch permite tratar erros específicos (ex: duplicado 11000) e encaminhar o resto com next(erro). Mais controlo que asyncHandler puro. Combine ambos para máxima cobertura.

Validação com Zod
const { z } = require("zod");

const userSchema = z.object({
  nome: z.string().min(2),
  email: z.string().email(),
  idade: z.number().min(18).optional(),
});

app.post("/users", (req, res) => {
  const resultado = userSchema.safeParse(req.body);
  if (!resultado.success) {
    return res.status(400).json({
      erros: resultado.error.issues
    });
  }
  const dados = resultado.data; // tipado
});

Zod valida e faz parse com tipos. safeParse() não lança excepção. resultado.data é o input validado e transformado. Mais moderno que express-validator. Suporta schemas complexos.

Erros async (Express 4)
const asyncHandler = (fn) => (req, res, next) =>
  Promise.resolve(fn(req, res, next)).catch(next);

// Uso:
app.get("/users/:id", asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id);
  if (!user) throw new ApiError(404, "Não encontrado");
  res.json(user);
}));

Express 4 não captura rejects de async. asyncHandler embrulha e encaminha para next(). Em Express 5, erros async são capturados nativamente — sem wrapper necessário.

Validação de params e query
const { param, query } = require("express-validator");

app.get("/users/:id",
  param("id").isMongoId(),
  query("page").optional().isInt({ min: 1 }),
  handler
);

// Com Zod:
const paramsSchema = z.object({ id: z.string().uuid() });
const querySchema = z.object({ page: z.coerce.number().default(1) });

Valide também params e query — não só body. param("id").isMongoId() previne queries inválidas na BD. z.coerce.number() converte string para número automaticamente.

Erros de validação Mongoose
app.use((erro, req, res, next) => {
  if (erro.name === "ValidationError") {
    const campos = {};
    for (const [k, v] of Object.entries(erro.errors)) {
      campos[k] = v.message;
    }
    return res.status(422).json({ campos });
  }
  if (erro.name === "CastError") {
    return res.status(400).json({ erro: "ID inválido" });
  }
  next(erro);
});

Mongoose lança ValidationError e CastError. Formate-os em 422 com campos específicos. CastError ocorre com ObjectIds inválidos. Traduzir erros da BD para respostas amigáveis.

Segurança e Performance


10 cards
CORS
const cors = require("cors");

// Liberar tudo (dev):
app.use(cors());

// Configuração específica:
app.use(cors({
  origin: "http://localhost:5173",
  methods: ["GET", "POST", "PUT", "DELETE"],
  credentials: true,
}));

cors permite pedidos de outros domínios. Sem isto, o browser bloqueia (Same-Origin Policy). origin autoriza domínios específicos. credentials: true permite cookies/headers de auth.

Autenticação JWT
const jwt = require("jsonwebtoken");

function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.split(" ")[1];
  if (!token) return res.status(401).json({ erro: "Token ausente" });

  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET);
    next();
  } catch {
    res.status(401).json({ erro: "Token inválido" });
  }
}

jsonwebtoken verifica tokens JWT. O header Authorization: Bearer token é o padrão. jwt.verify() valida assinatura e expiração. req.user fica disponível para handlers seguintes.

Trust proxy e HTTPS
// Atrás de Nginx/load balancer:
app.set("trust proxy", 1);

// Forçar HTTPS:
app.use((req, res, next) => {
  if (req.headers["x-forwarded-proto"] !== "https") {
    return res.redirect(301, `https://${req.hostname}${req.url}`);
  }
  next();
});

trust proxy faz Express ler X-Forwarded-* headers (IP real, protocolo). Sem isto, req.ip é o proxy. Force HTTPS em produção com redirect 301. Essencial atrás de load balancers.

Helmet
const helmet = require("helmet");

app.use(helmet());

// Ou configurar individualmente:
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "cdn.exemplo.com"],
    },
  },
  crossOriginEmbedderPolicy: false,
}));

helmet() define headers de segurança: CSP, X-Frame-Options, HSTS, etc. Previne XSS, clickjacking, MIME sniffing. Indispensável em produção. Zero config já protege bastante.

Hash de passwords
const bcrypt = require("bcrypt");

// Registo:
const hash = await bcrypt.hash(password, 12);
await User.create({ email, password: hash });

// Login:
const user = await User.findOne({ email });
const valido = await bcrypt.compare(password, user.password);
if (!valido) return res.status(401).end();

bcrypt faz hash com salt automático. hash(senha, 12) — 12 rounds (mais seguro, mais lento). compare() verifica sem expor a senha. Nunca guarde passwords em texto simples.

DDoS e payload limits
// Limitar body:
app.use(express.json({ limit: "100kb" }));

// Limitar por IP (agressivo para login):
const loginLimit = rateLimit({
  windowMs: 60 * 1000,
  max: 5,
  message: { erro: "Muitas tentativas" },
});
app.use("/login", loginLimit);

// Timeout:
app.use((req, res, next) => {
  req.setTimeout(30000);
  next();
});

Combine limit no body parser + rate limiting por endpoint. Login com limite agressivo (5/min). Timeout previne conexões penduradas. Camadas de defesa contra abuso.

Rate limiting
const rateLimit = require("express-rate-limit");

const limitador = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 min
  max: 100,
  message: { erro: "Muitos pedidos, tente depois" },
  standardHeaders: true,
});

app.use("/api/", limitador);

express-rate-limit limita pedidos por IP/janela. max: 100 = 100 pedidos em 15 min. Retorna 429 quando excedido. standardHeaders envia RateLimit-* headers. Previne brute force.

Ficheiros estáticos seguros
app.use(express.static("public", {
  maxAge: "1d",
  etag: true,
  index: false,
  dotfiles: "ignore",
}));

// Cache imutável para assets com hash:
app.use("/assets", express.static("dist", {
  maxAge: "1y",
  immutable: true,
}));

express.static() serve ficheiros. maxAge define cache. index: false previne listagem. dotfiles: "ignore" esconde .env. immutable para assets com fingerprint no nome.

Compression
const compression = require("compression");

app.use(compression());

// Com filtro:
app.use(compression({
  filter: (req, res) => {
    if (req.headers["x-no-compress"]) return false;
    return compression.filter(req, res);
  },
  level: 6,
}));

compression faz gzip/brotli das respostas. Reduz tamanho em 60-80%. level (1-9) controla compressão vs CPU. Aplique globalmente. Em produção com Nginx, pode ser redundante.

Prevenir injection
// NUNCA:
db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);

// SEMPRE (parameterized):
db.query("SELECT * FROM users WHERE id = $1", [req.params.id]);

// Mongoose (seguro por defeito):
User.findById(req.params.id);

// Escapar output HTML:
const escapeHtml = require("escape-html");
res.send(`<p>${escapeHtml(nome)}</p>`);

Use sempre queries parametrizadas ($1, ?) — nunca interpolação. Mongoose/ORMs protegem por defeito. escape-html previne XSS no output. Valide tipos de params.