
Roteamento no Express: parâmetros, sub-rotas, controladores e boas práticas (na prática)
Resumo rápido: o roteador do Express combina método + caminho para decidir quem atende a requisição. Neste guia você vê parâmetros de rota e query, sub-routers, middleware por rota, padronização com controladores, ordem/prioridade, versionamento de API e armadilhas comuns — tudo com exemplos prontos.
Setup mínimo
mkdir express-routing && cd express-routing
npm init -y
npm i express
npm i -D nodemon
package.json (trecho):
{
"type": "module",
"scripts": { "dev": "nodemon src/server.js" },
"dependencies": { "express": "^4.19.0" }
}
Estrutura:
src/
server.js
app.js
routes/
index.js
users/
index.js
users.controller.js
users.validators.js
products/
index.js
products.controller.js
Conceitos-chave do roteamento
- Combinação método + caminho:
GET /users,POST /users, etc. - Parâmetros de rota: segmentos dinâmicos, ex.:
/users/:id. - Query string: filtros e paginação, ex.:
?page=2&limit=20. - Sub-routers: módulos de rotas montados em um prefixo.
- Ordem importa: rotas são avaliadas na sequência em que são registradas.
- Middleware de rota: lógica aplicada só em endpoints específicos.
App base com sub-routers
src/app.js
import express from "express";
import indexRouter from "./routes/index.js";
import usersRouter from "./routes/users/index.js";
import productsRouter from "./routes/products/index.js";
const app = express();
app.use(express.json());
// Montagem dos módulos de rota
app.use("/", indexRouter);
app.use("/users", usersRouter);
app.use("/products", productsRouter);
// 404 e erros
app.use((req, res) => res.status(404).json({ error: "Rota não encontrada" }));
app.use((err, req, res, next) => {
console.error("[ERRO]", err);
res.status(err.status || 500).json({ error: err.message || "Erro interno" });
});
export default app;
src/server.js
import app from "./app.js";
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`http://localhost:${PORT}`));
Rotas básicas e saúde da API
src/routes/index.js
import { Router } from "express";
const router = Router();
router.get("/", (_req, res) => res.json({ ok: true }));
router.get("/health", (_req, res) => res.status(200).json({ status: "healthy" }));
export default router;
Parâmetros de rota e query string
src/routes/users/index.js
import { Router } from "express";
import * as ctrl from "./users.controller.js";
import { validateCreateUser } from "./users.validators.js";
const router = Router();
// Querystring: /users?page=2&limit=20
router.get("/", ctrl.list);
// Parâmetro de rota: /users/:id
router.get("/:id", ctrl.getById);
// Middleware de rota (validação) só no POST
router.post("/", validateCreateUser, ctrl.create);
export default router;
src/routes/users/users.controller.js
const DB = [{ id: "1", name: "Ada" }, { id: "2", name: "Linus" }];
export async function list(req, res) {
const page = Number(req.query.page ?? 1);
const limit = Number(req.query.limit ?? 10);
const start = (page - 1) * limit;
const data = DB.slice(start, start + limit);
res.json({ page, limit, total: DB.length, data });
}
export async function getById(req, res, next) {
const { id } = req.params;
const user = DB.find(u => u.id === id);
if (!user) return next({ status: 404, message: "Usuário não encontrado" });
res.json(user);
}
export async function create(req, res) {
const { name } = req.body;
const newUser = { id: String(Date.now()), name };
DB.push(newUser);
res.status(201).json(newUser);
}
src/routes/users/users.validators.js
export function validateCreateUser(req, _res, next) {
if (!req.body?.name) return next({ status: 400, message: "Campo 'name' é obrigatório" });
next();
}
Padrões de roteamento úteis
1) Prefixos por domínio (sub-routers)
Mantenha /users e /products isolados em pastas próprias para facilitar testes e evolução.
2) Ordenação e “shadowing”
Rotas mais específicas primeiro:
router.get("/search", ctrl.search); // específico
router.get("/:id", ctrl.getById); // genérico
Se inverter, /users/search cairia no /:id.
3) Rota catch-all controlada (opcional)
Para métricas ou logs de 404 por módulo:
router.use((req, res) => res.status(404).json({ error: "Users: rota não encontrada" }));
4) Agrupamento por método
Agrupe rotas do mesmo recurso:
router
.route("/:id")
.get(ctrl.getById)
.patch(ctrl.partialUpdate)
.delete(ctrl.remove);
Versionamento de API sem dor
Monte versões com prefixo e reutilize controladores quando possível:
// app.js
import v1Users from "./routes/users/index.js";
import v2Users from "./routes/v2/users/index.js";
app.use("/v1/users", v1Users);
app.use("/v2/users", v2Users);
V2 pode trocar contratos (ex.: novos campos) sem quebrar clientes antigos.
Roteamento assíncrono com segurança
Use um wrapper para capturar exceções de async e enviar ao handler central:
export const wrap = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
Exemplo:
router.get("/:id", wrap(async (req, res) => {
const user = await findUser(req.params.id); // I/O real
if (!user) throw { status: 404, message: "Usuário não encontrado" };
res.json(user);
}));
Boas práticas e armadilhas comuns
- Não exponha dados sensíveis em parâmetros de rota (use IDs opacos).
- Valide entrada (params, query, body) perto da rota.
- Consistência de status/JSON: responda objetos, use
201para criação,400/404para erros de cliente. - Evite lógica pesada no handler: extraia para controladores/serviços.
- Documente caminhos e contratos (OpenAPI/Swagger ajuda muito).
- Teste rotas críticas (integração) simulando
req/res.