Javascript

Criando um servidor HTTP com Node.js puro Já leu

24 min de leitura

Criando um servidor HTTP com Node.js puro
Antes de usar o Express — o framework que simplifica tudo — é fundamental entender o que acontece por baixo dos panos. Todo servidor web em Node.js, seja Express, Fastify ou qualquer outro, é construído sobre o módulo na

Antes de usar o Express — o framework que simplifica tudo — é fundamental entender o que acontece por baixo dos panos. Todo servidor web em Node.js, seja Express, Fastify ou qualquer outro, é construído sobre o módulo nativo http. Entender esse módulo faz você um desenvolvedor melhor, capaz de depurar problemas que frameworks escondem.

Neste artigo vamos construir um servidor HTTP do zero, entender o ciclo de requisição e resposta, e gradualmente adicionar funcionalidades até termos uma mini API funcional.


O protocolo HTTP em 60 segundos

Antes do código, o conceito:

CLIENTE (navegador/fetch)         SERVIDOR (Node.js)
─────────────────────────────────────────────────────

1. Cliente envia uma REQUISIÇÃO:
   GET /usuarios HTTP/1.1
   Host: localhost:3000
   Accept: application/json

2. Servidor processa e envia uma RESPOSTA:
   HTTP/1.1 200 OK
   Content-Type: application/json

   [{ "id": 1, "nome": "Ana" }]

Toda comunicação HTTP segue este padrão:
  Método + URL + Headers → Corpo (opcional)
  Status + Headers → Corpo (opcional)

O primeiro servidor

// servidor.js
const http = require("http");

const servidor = http.createServer((req, res) => {
  // req → IncomingMessage — a requisição que chegou
  // res → ServerResponse — a resposta que vamos enviar

  res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
  res.end("Olá, mundo!");
});

servidor.listen(3000, () => {
  console.log("🚀 Servidor rodando em http://localhost:3000");
});
node servidor.js
# Abra http://localhost:3000 no navegador

Três coisas aconteceram:

  1. createServer registrou uma função que será chamada a cada requisição
  2. listen(3000) fez o servidor começar a escutar conexões na porta 3000
  3. O callback é chamado quando o servidor está pronto

Inspecionando a requisição

O objeto req contém tudo sobre o que o cliente enviou:

const http = require("http");

const servidor = http.createServer((req, res) => {
  console.log("─".repeat(40));
  console.log("Método:", req.method);         // GET, POST, PUT, DELETE...
  console.log("URL:", req.url);               // /usuarios/1?ativo=true
  console.log("Headers:", req.headers);       // { host, accept, ... }
  console.log("HTTP Version:", req.httpVersion); // 1.1

  // Parseando a URL manualmente
  const url = new URL(req.url, `http://${req.headers.host}`);
  console.log("Pathname:", url.pathname);     // /usuarios/1
  console.log("Search:", url.search);         // ?ativo=true
  console.log("Params:", url.searchParams.get("ativo")); // "true"

  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("OK");
});

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

Construindo a resposta

O objeto res controla tudo que enviamos de volta:

// Definindo status e headers
res.statusCode = 200;
res.setHeader("Content-Type", "application/json");
res.setHeader("X-Powered-By", "Node.js");

// Atalho — define status e múltiplos headers de uma vez
res.writeHead(201, {
  "Content-Type": "application/json",
  "Location": "/usuarios/42",
});

// Enviando o corpo — finaliza a resposta
res.end(JSON.stringify({ id: 42, nome: "Ana" }));

// Ou em partes (útil para streams)
res.write("parte 1");
res.write("parte 2");
res.end("parte final");

Lendo o corpo da requisição

Ao contrário da URL e dos headers, o corpo chega em pedaços (chunks) via stream:

function lerCorpo(req) {
  return new Promise((resolve, reject) => {
    const chunks = [];

    req.on("data", (chunk) => {
      chunks.push(chunk);

      // Proteção contra corpos muito grandes (10MB)
      const totalBytes = chunks.reduce((acc, c) => acc + c.length, 0);
      if (totalBytes > 10 * 1024 * 1024) {
        reject(new Error("Corpo da requisição muito grande."));
      }
    });

    req.on("end", () => {
      const corpo = Buffer.concat(chunks).toString("utf-8");
      resolve(corpo);
    });

    req.on("error", reject);
  });
}

// Usando
const corpo = await lerCorpo(req);
const dados = JSON.parse(corpo);

Roteamento manual

O servidor precisa responder diferente dependendo da URL e do método:

const http = require("http");
const { URL } = require("url");

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

// ── Utilitários ─────────────────────────────────

function lerCorpo(req) {
  return new Promise((resolve, reject) => {
    const chunks = [];
    req.on("data", chunk => chunks.push(chunk));
    req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
    req.on("error", reject);
  });
}

function enviarJSON(res, status, dados) {
  res.writeHead(status, {
    "Content-Type": "application/json; charset=utf-8",
    "Access-Control-Allow-Origin": "*",
  });
  res.end(JSON.stringify(dados, null, 2));
}

function enviarErro(res, status, mensagem) {
  enviarJSON(res, status, { erro: mensagem, status });
}

function extrairId(pathname) {
  const partes = pathname.split("/").filter(Boolean);
  // /usuarios/42 → ["usuarios", "42"]
  const id = Number(partes[1]);
  return isNaN(id) ? null : id;
}

// ── Roteador ────────────────────────────────────

async function roteador(req, res) {
  const url = new URL(req.url, `http://${req.headers.host}`);
  const pathname = url.pathname;
  const metodo = req.method;

  console.log(`[${new Date().toLocaleTimeString("pt-BR")}] ${metodo} ${pathname}`);

  // CORS preflight
  if (metodo === "OPTIONS") {
    res.writeHead(204, {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
      "Access-Control-Allow-Headers": "Content-Type",
    });
    return res.end();
  }

  // GET /
  if (pathname === "/" && metodo === "GET") {
    return enviarJSON(res, 200, {
      mensagem: "API funcionando!",
      versao: "1.0.0",
      rotas: [
        "GET /usuarios",
        "GET /usuarios/:id",
        "POST /usuarios",
        "PUT /usuarios/:id",
        "DELETE /usuarios/:id",
      ],
    });
  }

  // GET /usuarios
  if (pathname === "/usuarios" && metodo === "GET") {
    const busca = url.searchParams.get("busca")?.toLowerCase();
    const lista = busca
      ? usuarios.filter(u =>
          u.nome.toLowerCase().includes(busca) ||
          u.email.toLowerCase().includes(busca)
        )
      : usuarios;

    return enviarJSON(res, 200, {
      dados: lista,
      total: lista.length,
    });
  }

  // GET /usuarios/:id
  if (pathname.match(/^/usuarios/d+$/) && metodo === "GET") {
    const id = extrairId(pathname);
    const usuario = usuarios.find(u => u.id === id);

    if (!usuario) return enviarErro(res, 404, `Usuário ${id} não encontrado.`);
    return enviarJSON(res, 200, usuario);
  }

  // POST /usuarios
  if (pathname === "/usuarios" && metodo === "POST") {
    let corpo;
    try {
      const texto = await lerCorpo(req);
      corpo = JSON.parse(texto);
    } catch {
      return enviarErro(res, 400, "Corpo da requisição inválido. Envie JSON válido.");
    }

    // Validação
    const erros = [];
    if (!corpo.nome?.trim()) erros.push("nome é obrigatório.");
    if (!corpo.email?.trim()) erros.push("email é obrigatório.");
    if (corpo.email && !corpo.email.includes("@")) erros.push("email inválido.");
    if (usuarios.some(u => u.email === corpo.email)) erros.push("email já cadastrado.");

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

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

    usuarios.push(novoUsuario);

    res.setHeader("Location", `/usuarios/${novoUsuario.id}`);
    return enviarJSON(res, 201, novoUsuario);
  }

  // PUT /usuarios/:id
  if (pathname.match(/^/usuarios/d+$/) && metodo === "PUT") {
    const id = extrairId(pathname);
    const indice = usuarios.findIndex(u => u.id === id);

    if (indice === -1) return enviarErro(res, 404, `Usuário ${id} não encontrado.`);

    let corpo;
    try {
      corpo = JSON.parse(await lerCorpo(req));
    } catch {
      return enviarErro(res, 400, "JSON inválido.");
    }

    const erros = [];
    if (corpo.nome !== undefined && !corpo.nome.trim()) erros.push("nome não pode ser vazio.");
    if (corpo.email !== undefined && !corpo.email.includes("@")) erros.push("email inválido.");
    if (corpo.email && usuarios.some(u => u.email === corpo.email && u.id !== id)) {
      erros.push("email já cadastrado por outro usuário.");
    }

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

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

    return enviarJSON(res, 200, usuarios[indice]);
  }

  // DELETE /usuarios/:id
  if (pathname.match(/^/usuarios/d+$/) && metodo === "DELETE") {
    const id = extrairId(pathname);
    const indice = usuarios.findIndex(u => u.id === id);

    if (indice === -1) return enviarErro(res, 404, `Usuário ${id} não encontrado.`);

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

  // Rota não encontrada
  enviarErro(res, 404, `Rota ${metodo} ${pathname} não encontrada.`);
}

// ── Servidor ─────────────────────────────────────
const servidor = http.createServer(async (req, res) => {
  try {
    await roteador(req, res);
  } catch (erro) {
    console.error("Erro interno:", erro);
    enviarErro(res, 500, "Erro interno do servidor.");
  }
});

servidor.listen(3000, () => {
  console.log("🚀 API rodando em http://localhost:3000");
  console.log("📋 Rotas disponíveis:");
  console.log("   GET  /usuarios");
  console.log("   GET  /usuarios/:id");
  console.log("   POST /usuarios");
  console.log("   PUT  /usuarios/:id");
  console.log("   DELETE /usuarios/:id");
});

Testando a API

# Instale o curl ou use o httpie para testar

# GET todos
curl http://localhost:3000/usuarios

# GET com busca
curl "http://localhost:3000/usuarios?busca=ana"

# GET por ID
curl http://localhost:3000/usuarios/1

# POST — criar
curl -X POST http://localhost:3000/usuarios 
  -H "Content-Type: application/json" 
  -d '{"nome": "Beatriz", "email": "bia@email.com"}'

# PUT — atualizar
curl -X PUT http://localhost:3000/usuarios/1 
  -H "Content-Type: application/json" 
  -d '{"nome": "Ana Paula Souza"}'

# DELETE
curl -X DELETE http://localhost:3000/usuarios/2

Ou crie um arquivo testar.js:

// testar.js
const BASE = "http://localhost:3000";

async function req(metodo, url, corpo = null) {
  const opcoes = {
    method: metodo,
    headers: { "Content-Type": "application/json" },
  };
  if (corpo) opcoes.body = JSON.stringify(corpo);

  const res = await fetch(`${BASE}${url}`, opcoes);
  const dados = await res.json();
  console.log(`
${metodo} ${url} → ${res.status}`);
  console.log(JSON.stringify(dados, null, 2));
  return dados;
}

async function main() {
  await req("GET", "/");
  await req("GET", "/usuarios");
  await req("POST", "/usuarios", { nome: "Beatriz", email: "bia@email.com" });
  await req("GET", "/usuarios/3");
  await req("PUT", "/usuarios/1", { nome: "Ana Paula Souza" });
  await req("DELETE", "/usuarios/2");
  await req("GET", "/usuarios");
  await req("GET", "/usuarios/99"); // teste de 404
}

main().catch(console.error);
node testar.js

Servindo arquivos estáticos

const http = require("http");
const fs = require("fs/promises");
const path = require("path");

const TIPOS_MIME = {
  ".html": "text/html; charset=utf-8",
  ".css":  "text/css",
  ".js":   "application/javascript",
  ".json": "application/json",
  ".png":  "image/png",
  ".jpg":  "image/jpeg",
  ".svg":  "image/svg+xml",
  ".ico":  "image/x-icon",
  ".txt":  "text/plain",
};

const PASTA_PUBLICA = path.join(__dirname, "public");

async function servirArquivo(res, caminhoRelativo) {
  const caminho = path.join(PASTA_PUBLICA, caminhoRelativo);

  // Segurança — evita path traversal (../../etc/passwd)
  if (!caminho.startsWith(PASTA_PUBLICA)) {
    res.writeHead(403);
    return res.end("Acesso negado.");
  }

  try {
    const conteudo = await fs.readFile(caminho);
    const ext = path.extname(caminho);
    const tipo = TIPOS_MIME[ext] || "application/octet-stream";

    res.writeHead(200, { "Content-Type": tipo });
    res.end(conteudo);
  } catch (erro) {
    if (erro.code === "ENOENT") {
      res.writeHead(404);
      res.end("Arquivo não encontrado.");
    } else {
      res.writeHead(500);
      res.end("Erro ao ler arquivo.");
    }
  }
}

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

  // Redireciona / para index.html
  if (pathname === "/") pathname = "/index.html";

  await servirArquivo(res, pathname);
});

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

Eventos do servidor

const servidor = http.createServer(handler);

// Quando uma conexão TCP é estabelecida
servidor.on("connection", (socket) => {
  console.log(`Nova conexão de ${socket.remoteAddress}`);
});

// Quando ocorre um erro no servidor
servidor.on("error", (erro) => {
  if (erro.code === "EADDRINUSE") {
    console.error(`❌ Porta 3000 já está em uso.`);
    process.exit(1);
  }
  console.error("Erro no servidor:", erro);
});

// Quando o servidor fecha
servidor.on("close", () => {
  console.log("Servidor encerrado.");
});

servidor.listen(3000);

// Encerramento gracioso — termina requisições em andamento
process.on("SIGTERM", () => {
  console.log("Encerrando servidor...");
  servidor.close(() => {
    console.log("Servidor encerrado com sucesso.");
    process.exit(0);
  });
});

As limitações do http puro — por que existe o Express

Depois de tudo que construímos, ficam claras as limitações do módulo http puro:

Problema                          Com http puro
──────────────────────────────────────────────────────
Roteamento                        if/else manual e verboso
Parâmetros de URL (/users/:id)   Regex manual
Middleware (logging, auth, cors)  Encadeamento manual
Body parsing (JSON, form)         Stream manual
Upload de arquivos                Muito complexo
Sessões e cookies                 Manual
Compressão (gzip)                 Manual
HTTPS                             Configuração trabalhosa

O Express resolve todos esses problemas com APIs limpas e elegantes. E agora que você entende o que há por baixo, vai usar o Express com muito mais consciência.


Tarefa para você

Estenda a mini API de usuários adicionando:

// 1. Persistência em arquivo JSON
//    - Ao iniciar, carrega usuarios.json se existir
//    - Após cada POST, PUT, DELETE salva no arquivo
//    - Use fs/promises para leitura e escrita

// 2. Paginação no GET /usuarios
//    GET /usuarios?pagina=1&por_pagina=10
//    Resposta deve incluir:
//    { dados, total, pagina, por_pagina, total_paginas }

// 3. Rota GET /usuarios/:id/posts
//    Busca posts do usuário na JSONPlaceholder:
//    https://jsonplaceholder.typicode.com/posts?userId={id}

// 4. Middleware de logging
//    Crie uma função que envolve o roteador e registra:
//    [timestamp] METHOD /path → STATUS (Xms)

// 5. Tratamento de Content-Type
//    Rejeite requisições POST/PUT que não enviem
//    Content-Type: application/json com erro 415

Conclusão

Neste artigo você aprendeu:

  • Como funciona o protocolo HTTP no nível mais básico
  • Como criar um servidor com o módulo http nativo do Node
  • O objeto req — método, URL, headers e corpo
  • O objeto res — status, headers e corpo da resposta
  • Como ler o corpo da requisição via streams
  • Como implementar roteamento manual com if/else e regex
  • Como construir uma API REST completa sem frameworks
  • Como servir arquivos estáticos com segurança
  • Eventos do servidor e encerramento gracioso
  • Por que frameworks como o Express existem e o que resolvem

No próximo artigo chegamos ao Express.js — onde tudo que construímos manualmente vira código limpo e expressivo.


📌 Próximo artigo: Aula 30 — Express.js: o framework web do Node


📚 Fontes e Referências

  • Node.js Docs — HTTP module: https://nodejs.org/api/http.html
  • Node.js Docs — URL module: https://nodejs.org/api/url.html
  • MDN Web Docs — HTTP overview: https://developer.mozilla.org/pt-BR/docs/Web/HTTP/Overview
  • MDN Web Docs — HTTP response status codes: https://developer.mozilla.org/pt-BR/docs/Web/HTTP/Status
  • JavaScript.info — Node.js networking: https://javascript.info/network
  • Node.js Design Patterns — Mario Casciaro (Packt Publishing)
  • Learning Node.js Development — Andrew Mead (Packt Publishing)
  • OWASP — Path Traversal: https://owasp.org/www-community/attacks/Path_Traversal

Prof. Ricardo Matos

Mestre em Programação | Série: Dominando JavaScript em 1 Ano — Artigo 29/52


Quer a Aula 30 — Express.js: o framework web do Node agora?


# you asked

Continue


# claude response


# 📘 Série: Dominando o JavaScript em 1 Ano

Artigo 30 — Express.js: o framework web do Node


Por Prof. Ricardo Matos | Mestre em Programação

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


Introdução

No artigo anterior 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 anterior. 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

// 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

Conclusão

Neste artigo você aprendeu:

  • Como o Express se relaciona com o módulo http nativo
  • O ciclo de vida de uma requisição no Express
  • Os middlewares nativos — json(), urlencoded(), static()
  • O objeto req e res enriquecidos pelo Express
  • Parâmetros de rota, query strings e corpo da requisição
  • Como usar Router para organizar rotas em módulos
  • Como criar middlewares customizados de logging, autenticação e erro
  • O tratador de erros global com quatro parâmetros
  • Os middlewares de terceiros mais importantes
  • Boas práticas — async handler, separação app/server, segurança

 

 

📚 Fontes e Referências

  • Express.js — Documentação oficial: https://expressjs.com/pt-br
  • Express.js — Guia de middlewares: https://expressjs.com/pt-br/guide/using-middleware.html
  • Express.js — Guia de roteamento: https://expressjs.com/pt-br/guide/routing.html
  • Express.js — Tratamento de erros: https://expressjs.com/pt-br/guide/error-handling.html
  • Helmet.js: https://helmetjs.github.io
  • Morgan: https://github.com/expressjs/morgan
  • CORS: https://github.com/expressjs/cors
  • Node.js Design Patterns — Mario Casciaro (Packt Publishing)
  • roadmap.sh — Express.js: https://roadmap.sh/nodejs
Comentários

Mais em Javascript

Tratamento de Erros com try, catch e finally
Tratamento de Erros com try, catch e finally

Todo programa que vai para produ&ccedil;&atilde;o vai encontrar situa&ccedil;...

MongoDB e Mongoose: banco de dados com Node
MongoDB e Mongoose: banco de dados com Node

Até agora nossa API guardava dados em um array na memória — tudo sumia quando...

Revisão  + Projeto Final: Produção Real
Revisão + Projeto Final: Produção Real

Chegamos ao fim de mais um módulo. Em três artigos cobrimos deploy, segurança...