Como organizar rotas em Express.js para não virar espaguete
← Voltar para Codeshort

Como organizar rotas em Express.js para não virar espaguete

Rotas soltas no index.js funcionam no tutorial. Em produção, viram pesadelo. Veja como estruturar de verdade.

DC
Dev Code Software
17 de junho de 2026·5 min de leitura

O problema que todo mundo ignora no começo

Você cria um projeto Express, coloca tudo no index.js e segue em frente. Dez rotas depois, o arquivo tem 300 linhas. Vinte rotas depois, ninguém mais sabe onde está o quê.

Isso não é só estética. É um problema real de manutenção. Quando você precisa corrigir um bug em produção às 23h, não quer caçar uma rota num arquivo com seis middlewares e três funções anônimas encadeadas.

A boa notícia: Express tem tudo que você precisa para organizar isso direito. O problema é que a documentação oficial mostra o mínimo, e os tutoriais param onde o projeto ainda é brinquedo.


A estrutura de pastas que funciona

Não existe uma estrutura universal, mas essa aqui resolve bem pra maioria dos projetos de médio porte:

src/
├── app.js
├── server.js
├── routes/
│   ├── index.js
│   ├── users.routes.js
│   ├── products.routes.js
│   └── auth.routes.js
├── controllers/
│   ├── users.controller.js
│   ├── products.controller.js
│   └── auth.controller.js
└── middlewares/
    ├── auth.middleware.js
    └── validate.middleware.js

O server.js só sobe o servidor. O app.js configura o Express. As rotas ficam em routes/. Os handlers ficam em controllers/. Simples assim.

💡 Dica: Separe app.js de server.js. O app.js exporta a instância do Express — isso facilita testes com supertest sem precisar subir a porta de verdade.


Separando rotas com Express Router

O express.Router() existe exatamente pra isso. Cada arquivo de rota cria um mini-roteador independente:

// routes/users.routes.js
const { Router } = require('express');
const router = Router();
const { getUsers, getUserById, createUser } = require('../controllers/users.controller');

router.get('/', getUsers);
router.get('/:id', getUserById);
router.post('/', createUser);

module.exports = router;

Limpo. Cada arquivo de rota tem no máximo o que diz respeito a ele. Sem lógica de negócio misturada, sem callbacks gigantes.

❌ O que não fazer:

router.post('/users', async (req, res) => {
  try {
    const { name, email } = req.body;
    const existing = await db.query('SELECT * FROM users WHERE email = ?', [email]);
    if (existing.length) return res.status(409).json({ error: 'Email já existe' });
    const user = await db.query('INSERT INTO users SET ?', { name, email });
    res.status(201).json(user);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

Esse código acima apareceu num PR que revisei em 2023. A lógica de validação, a query e o tratamento de erro estavam todos dentro da rota. O revisor pediu pra extrair. Depois de refatorar, o arquivo de rota ficou com 4 linhas. A lógica foi pro controller, onde devia estar desde o início.


Centralizando no app.js sem poluir

Com os roteadores criados, o app.js registra tudo num arquivo central de rotas:

// routes/index.js
const { Router } = require('express');
const router = Router();

const usersRoutes = require('./users.routes');
const productsRoutes = require('./products.routes');
const authRoutes = require('./auth.routes');

router.use('/users', usersRoutes);
router.use('/products', productsRoutes);
router.use('/auth', authRoutes);

module.exports = router;
// app.js
const express = require('express');
const routes = require('./routes');

const app = express();

app.use(express.json());
app.use('/api/v1', routes);

module.exports = app;

Resultado: o app.js tem uma linha pra registrar todas as rotas. Adicionar um novo módulo é incluir duas linhas no routes/index.js. Sem tocar em mais nada.

⚠️ Atenção: Sempre prefixe suas rotas com a versão da API (/api/v1). Quando precisar lançar uma v2 sem quebrar clientes antigos, você vai agradecer por ter feito isso desde o início.


Middlewares de rota: onde colocar

Middleware de autenticação, validação de body, permissões — cada um tem um lugar certo.

Middleware global (pra todas as rotas): vai no app.js, antes do app.use('/api/v1', routes).

Middleware de grupo (pra um conjunto de rotas): vai no routes/index.js, aplicado ao roteador específico.

Middleware de rota individual: vai direto na declaração da rota.

// routes/users.routes.js
const { Router } = require('express');
const router = Router();
const authMiddleware = require('../middlewares/auth.middleware');
const validateUser = require('../middlewares/validate.middleware');
const { getUsers, createUser } = require('../controllers/users.controller');

router.get('/', authMiddleware, getUsers);
router.post('/', authMiddleware, validateUser, createUser);

module.exports = router;

Por que isso importa? Porque middleware aplicado no lugar errado cria comportamentos inesperados. Se você jogar autenticação global no app.js e esquecer que a rota de health check não deve ser protegida, vai travar o seu próprio load balancer.


Controllers fora dos arquivos de rota

O controller é só uma função. Recebe req e res, chama a lógica, responde. Sem mais.

// controllers/users.controller.js
const usersService = require('../services/users.service');

const getUsers = async (req, res) => {
  const users = await usersService.findAll();
  return res.json(users);
};

const createUser = async (req, res) => {
  const user = await usersService.create(req.body);
  return res.status(201).json(user);
};

module.exports = { getUsers, createUser };

Lógica de banco, regras de negócio, validações complexas — tudo isso vai pra camada de serviço (services/), que o controller chama. O controller não deveria saber se o dado vem do Postgres ou de uma API externa.

ResponsabilidadeOnde fica
Definição de rotas e métodos HTTProutes/
Receber req, chamar serviço, respondercontrollers/
Regras de negócio, acesso a dadosservices/
Validação de entradamiddlewares/
Configuração do Expressapp.js

Erros comuns que aparecem em code review

1. Lógica no arquivo de rotas Já coberto acima, mas é o erro mais frequente. A rota define o caminho, não executa a lógica.

2. Router sem prefixo

app.use(usersRoutes);

Sem prefixo, suas rotas de usuário e produto podem colidir. Sempre use /users, /products, etc.

3. Exportar o app sem separar o server

app.listen(3000);
module.exports = app;

Isso sobe o servidor na hora do require. Se um arquivo de teste importar o app, você vai ter um servidor rodando sem querer.

4. Middleware de erro mal posicionado O middleware de tratamento de erro (aquele com quatro parâmetros: err, req, res, next) precisa ser registrado depois de todas as rotas. Se vier antes, nunca vai capturar nada.

app.use('/api/v1', routes);

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Internal Server Error' });
});

5. Arquivos de rota sem nenhum padrão de nomenclatura userRoutes.js, users.js, routeUser.js no mesmo projeto. Escolha uma convenção e siga ela. Fica ridículo quando um novo dev entra e não consegue adivinhar o nome do arquivo.


FAQ

Preciso de uma camada de serviço desde o começo? Não necessariamente. Em projetos pequenos, colocar a lógica no controller é aceitável. Mas assim que você perceber que dois controllers precisam da mesma query ou regra de negócio, é hora de extrair pra um serviço. Não espere o código ficar grande demais pra refatorar.

Posso usar require dinâmico pra registrar rotas automaticamente? Dá pra fazer, mas cria magia desnecessária. Seu routes/index.js explícito documenta exatamente quais módulos existem. Registro automático esconde isso e dificulta o rastreamento quando uma rota some ou duplica.

Qual a diferença entre app.use e router.use? app.use registra middleware/roteador na instância principal do Express. router.use registra dentro de um roteador filho. Use router.use quando quiser que um middleware se aplique só a um grupo de rotas, sem afetar o resto da aplicação.

Como lidar com erros assíncronos sem repetir try/catch em todo controller? Crie um wrapper:

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

Aí use router.get('/', asyncHandler(getUsers)). Qualquer exceção é passada pro middleware de erro automaticamente.

Faz sentido usar TypeScript com essa estrutura? Faz, e melhora bastante. Tipagem nos controllers e nas camadas de serviço elimina uma classe inteira de bugs. Mas a estrutura de pastas continua a mesma — TypeScript não muda a arquitetura, só adiciona segurança de tipos.


Próximos passos

Você agora tem uma estrutura que escala. O que fazer a partir daqui:

  1. Implemente validação de entrada com zod ou joi como middleware antes dos controllers
  2. Configure tratamento centralizado de erros com um middleware de erro e classes de erro customizadas (NotFoundError, ValidationError)
  3. Adicione testes de integração nas rotas com supertest — a separação entre app.js e server.js foi feita exatamente pra isso
  4. Considere OpenAPI/Swagger para documentar suas rotas — com essa estrutura organizada, gerar a doc fica muito mais simples

A estrutura não resolve todos os problemas. Mas ela garante que quando surgir um bug, você vai saber exatamente onde olhar.