O que é Express?
Express é um framework web minimalista para Node.js. Ele não impõe a estrutura de pastas, o banco de dados ou a arquitetura do seu projeto. Ele oferece três coisas e deixa você trabalhar: uma forma de fazer o match de URLs, um objeto de requisição (request) e resposta (response), e uma cadeia de middleware que conecta tudo.
Essa simplicidade é proposital. O Express surgiu em 2010, quando o módulo http nativo do Node exigia que você escrevesse o roteamento e o parsing do corpo da requisição manualmente. Ele reduziu essas tarefas a poucas linhas. Quinze anos depois, ele continua sendo a escolha padrão para APIs em Node, e seus conceitos foram copiados por quase todos os frameworks que vieram a seguir.
Se você entende Express, você entende a estrutura do JavaScript no lado do servidor.
O ciclo de requisição e resposta
Em sua essência, um app Express é uma função que recebe uma requisição e envia uma resposta. Todo o restante são conveniências implementadas sobre isso.
import express from "express";
const app = express();
app.get("/", (req, res) => {
res.send("Hello, world");
});
O objeto req encapsula a mensagem de entrada do Node.js: req.params, req.query, req.body e req.headers fornecem a entrada. O objeto res encapsula a mensagem de saída: res.status(), res.json(), res.send() e res.set() moldam a saída. Um handler encerra a requisição chamando um dos métodos res.
Roteamento
Uma rota é composta por um método, um caminho e um ou mais handlers. Os caminhos podem ser estáticos, parametrizados com :name ou correspondidos via padrões.
app.get("/posts", listPosts);
app.get("/posts/:id", getPost);
app.post("/posts", createPost);
app.put("/posts/:id", replacePost);
app.patch("/posts/:id", updatePost);
app.delete("/posts/:id", deletePost);
Os parâmetros de rota chegam em req.params, e as query strings em req.query.
app.get("/posts/:id", (req, res) => {
const { id } = req.params; // "/posts/42" -> "42"
const { fields } = req.query; // "?fields=title" -> "title"
res.json({ id, fields });
});
Você também pode passar vários handlers para uma única rota. Eles são executados em ordem até que um deles finalize a resposta; é assim que você anexa middleware por rota, como validação ou autorização.
app.post("/posts", requireAuth, validatePost, createPost);
Middleware: a ideia fundamental para dominar
O middleware é o coração do Express. Ele é qualquer função que recebe (req, res, next) e, ou encerra a resposta, ou chama next() para continuar.
function logger(req, res, next) {
const start = Date.now();
res.on("finish", () => {
console.log(`${req.method} ${req.url} ${res.statusCode} ${Date.now() - start}ms`);
});
next();
}
app.use(logger);
O middleware é executado na ordem em que é registrado. Essa única regra explica a maior parte do comportamento do Express: registre o parser de JSON antes das rotas que leem req.body, registre a autenticação antes das rotas protegidas e registre o handler de 404 depois de todas as rotas.
Existem três tipos que valem a pena conhecer:
- Application middleware —
app.use(fn)é executado para cada requisição. - Router middleware —
router.use(fn)é executado apenas para os caminhos daquele router. - Route middleware —
app.get(path, fn, handler)é executado apenas para aquela rota.
Um middleware que recebe quatro argumentos é um error handler e só é executado quando algo chama next(err).
Routers mantêm apps grandes legíveis
À medida que um app cresce, um único arquivo de rotas torna-se impossível de gerenciar. express.Router() permite que você agrupe rotas relacionadas e as monte sob um prefixo.
// routes/posts.js
import { Router } from "express";
const router = Router();
router.get("/", listPosts);
router.post("/", createPost);
export default router;
// app.js
import posts from "./routes/posts.js";
app.use("/api/v1/posts", posts);
Agora, cada arquivo é dono de um recurso, e o prefixo da URL fica em um único lugar. Essa é a estrutura que impede que projetos Express se transformem em um amontoado de endpoints.
Tratamento de erros
Os erros devem fluir para um único lugar. Chame next(err) de qualquer lugar e o Express saltará diretamente para o primeiro manipulador de erros de quatro argumentos.
app.get("/users/:id", async (req, res, next) => {
try {
const user = await db.user.findById(req.params.id);
if (!user) return res.status(404).json({ error: "not_found" });
res.json(user);
} catch (err) {
next(err);
}
});
app.use((err, req, res, next) => {
console.error(err);
res.status(err.status ?? 500).json({
error: err.code ?? "internal_error",
message: err.message,
});
});
O Express 5 encaminha promises rejeitadas para o manipulador de erros automaticamente, portanto, handlers async não precisam mais de um wrapper. Ainda assim, ser explícito com try/catch torna a intenção óbvia e mantém o código portátil. Registre um manipulador de 404 após todas as rotas para que caminhos não correspondidos retornem um erro JSON adequado em vez da página HTML padrão do Express.
Lendo o corpo da requisição
O Express não faz o parse de corpos por padrão. Adicione os parsers integrados antes das rotas que precisarem deles.
app.use(express.json()); // application/json
app.use(express.urlencoded({ extended: true })); // form posts
Para upload de arquivos, utilize o multer. Para uma validação mais robusta, combine o parsing com uma biblioteca de schema como Zod ou Joi e rejeite entradas inválidas precocemente, antes que cheguem aos seus handlers.
Noções básicas de segurança e produção
O Express é deliberadamente minimalista, portanto, algumas linhas de middleware resolvem o essencial:
import helmet from "helmet";
import cors from "cors";
import rateLimit from "express-rate-limit";
app.use(helmet()); // sensible security headers
app.use(cors({ origin: "https://app.example.com" }));
app.use(rateLimit({ windowMs: 60_000, max: 100 }));
Além disso, desative o x-powered-by, mantenha as dependências atualizadas, valide e limite o tamanho das requisições e nunca confie na entrada do cliente. Essas são as mesmas preocupações de qualquer backend; o Express apenas oferece um lugar pequeno e bem conhecido para lidar com cada uma delas.
Testes
Como os handlers são funções simples, o Express é fácil de testar. supertest inicia o app no mesmo processo e faz asserções HTTP reais sem a necessidade de abrir uma porta.
import request from "supertest";
import app from "../app.js";
test("GET /posts returns a list", async () => {
const res = await request(app).get("/posts").expect(200);
expect(Array.isArray(res.body)).toBe(true);
});
Separe a definição do app (app.js) da inicialização do servidor (server.js) para que os testes possam importar o app sem que ele fique escutando requisições. Essa simples separação torna os testes de integração triviais.
Melhores práticas
- Mantenha os route handlers enxutos; mova a lógica para services e camadas de acesso a dados.
- Divida as rotas por funcionalidade com
express.Router()e monte-as sob um prefixo de versão. - Registre o middleware de forma deliberada e na ordem correta: parsers, depois auth, depois rotas, depois 404 e, por fim, erros.
- Encaminhe cada erro com
next(err)e formate as respostas em um único lugar. - Valide e limite todas as entradas antes que elas cheguem à lógica de negócio.
- Use
helmet,corse rate limiting desde o primeiro dia. - Separe a aplicação do servidor para que os testes permaneçam rápidos.
Erros comuns
- Esquecer o
express.json()e se perguntar por que oreq.bodyestá undefined. - Ignorar promises rejeitadas em handlers assíncronos no Express 4.
- Registrar o handler de 404 ou de erro antes das rotas que ele deveria capturar.
- Deixar um único arquivo de rotas crescer por centenas de linhas.
- Retornar 200 para erros em vez de utilizar status codes.
- Confiar no
req.bodysem validação. - Misturar
res.send,res.jsoneres.endde forma imprevisível.
Próximos passos
O Express ensina sobre a requisição, a resposta e a cadeia de middleware — conceitos que você reutilizará para sempre. Se você busca mais performance e validação nativa, leia o guia do Fastify a seguir. Se precisa de estrutura e injeção de dependência para equipes grandes, migre para o NestJS. Para implantações em edge e serverless, o Hono oferece o mesmo estilo em um runtime baseado em padrões web. E se tudo isso pareceu rápido demais, revise os fundamentos de Node.js que sustentam tudo isso.