- O problema que todo mundo ignora no começo
- A estrutura de pastas que funciona
- Separando rotas com Express Router
- Centralizando no app.js sem poluir
- Middlewares de rota: onde colocar
- Controllers fora dos arquivos de rota
- Erros comuns que aparecem em code review
- FAQ
- Próximos passos
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.jsdeserver.js. Oapp.jsexporta a instância do Express — isso facilita testes comsupertestsem 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 umav2sem 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.
| Responsabilidade | Onde fica |
|---|---|
| Definição de rotas e métodos HTTP | routes/ |
| Receber req, chamar serviço, responder | controllers/ |
| Regras de negócio, acesso a dados | services/ |
| Validação de entrada | middlewares/ |
| Configuração do Express | app.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:
- Implemente validação de entrada com
zodoujoicomo middleware antes dos controllers - Configure tratamento centralizado de erros com um middleware de erro e classes de erro customizadas (
NotFoundError,ValidationError) - Adicione testes de integração nas rotas com
supertest— a separação entreapp.jseserver.jsfoi feita exatamente pra isso - 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.