Express.js: roteamento e middlewares

1. Introdução ao Express.js e sua arquitetura

Express.js é o framework web mais popular do ecossistema Node.js, utilizado para construir APIs e aplicações web de forma rápida e estruturada. Ele fornece uma camada minimalista sobre o módulo HTTP nativo do Node, abstraindo complexidades e oferecendo recursos poderosos como roteamento, middlewares e integração com templates.

Para começar, instale o Express em seu projeto:

npm install express

Uma aplicação mínima no Express segue esta estrutura:

const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send('Olá, mundo!');
});

app.listen(port, () => {
  console.log(`Servidor rodando em http://localhost:${port}`);
});

A arquitetura do Express é baseada em um ciclo de requisição-resposta onde cada requisição passa por uma série de funções (middlewares) até chegar ao handler final da rota.

2. Roteamento básico com Express

O Express mapeia métodos HTTP para rotas específicas. Veja como definir rotas para operações CRUD:

// GET - listar usuários
app.get('/usuarios', (req, res) => {
  res.json([{ id: 1, nome: 'João' }]);
});

// POST - criar usuário
app.post('/usuarios', (req, res) => {
  const { nome } = req.body;
  res.status(201).json({ id: 2, nome });
});

// PUT - atualizar usuário
app.put('/usuarios/:id', (req, res) => {
  const { id } = req.params;
  res.json({ id, nome: 'Atualizado' });
});

// DELETE - remover usuário
app.delete('/usuarios/:id', (req, res) => {
  const { id } = req.params;
  res.status(204).send();
});

Para acessar parâmetros de rota e query:

app.get('/produtos/:categoria/:id', (req, res) => {
  const { categoria, id } = req.params;  // /produtos/eletronicos/5
  const { page, limit } = req.query;     // ?page=1&limit=10
  res.json({ categoria, id, page, limit });
});

Para modularizar as rotas, utilize express.Router():

// routes/usuarios.js
const router = express.Router();

router.get('/', (req, res) => { /* ... */ });
router.post('/', (req, res) => { /* ... */ });

module.exports = router;

// app.js
const usuariosRouter = require('./routes/usuarios');
app.use('/usuarios', usuariosRouter);

3. Roteamento avançado e boas práticas

É possível encadear múltiplos handlers em uma mesma rota, utilizando next() para passar o controle ao próximo handler:

app.get('/admin', 
  (req, res, next) => {
    if (!req.query.token) {
      return res.status(401).json({ erro: 'Token obrigatório' });
    }
    req.usuario = { role: 'admin' }; // enriquecendo a requisição
    next();
  },
  (req, res) => {
    res.json({ mensagem: 'Acesso autorizado', usuario: req.usuario });
  }
);

Organize suas rotas em arquivos separados dentro de uma pasta routes/:

routes/
  ├── usuarios.js
  ├── produtos.js
  └── auth.js

Cada arquivo exporta um router que é montado no app.js com prefixos adequados.

4. O que são middlewares no Express.js

Middlewares são funções que têm acesso ao objeto de requisição (req), ao objeto de resposta (res) e à próxima função no ciclo (next). Eles podem:

  • Executar qualquer código
  • Modificar os objetos req e res
  • Encerrar o ciclo (enviando uma resposta)
  • Chamar o próximo middleware com next()

Existem três tipos principais:

  • Middleware de aplicação: aplicado globalmente com app.use()
  • Middleware de rota: aplicado a rotas específicas
  • Middleware de erro: recebe 4 parâmetros (err, req, res, next)

Exemplo prático de middleware de logging:

const logger = (req, res, next) => {
  console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
  next();
};

app.use(logger); // aplicado a todas as rotas

5. Middlewares embutidos e de terceiros

O Express oferece middlewares embutidos essenciais:

// Parsing de JSON no corpo da requisição
app.use(express.json());

// Parsing de dados de formulário URL-encoded
app.use(express.urlencoded({ extended: true }));

// Servir arquivos estáticos da pasta 'public'
app.use(express.static('public'));

Middlewares de terceiros populares:

const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');

app.use(cors());                    // Habilitar CORS
app.use(helmet());                  // Segurança HTTP
app.use(morgan('dev'));             // Logging detalhado

6. Middlewares personalizados e cadeia de execução

Crie um middleware de autenticação simples:

const autenticar = (req, res, next) => {
  const token = req.headers['authorization'];

  if (!token || token !== 'meu-token-secreto') {
    return res.status(401).json({ erro: 'Não autorizado' });
  }

  req.usuario = { id: 1, nome: 'Admin' };
  next();
};

// Aplicar a uma rota específica
app.get('/perfil', autenticar, (req, res) => {
  res.json({ usuario: req.usuario });
});

Middleware de validação manual:

const validarUsuario = (req, res, next) => {
  const { nome, email } = req.body;
  const erros = [];

  if (!nome || nome.length < 3) erros.push('Nome deve ter pelo menos 3 caracteres');
  if (!email || !email.includes('@')) erros.push('Email inválido');

  if (erros.length > 0) {
    return res.status(400).json({ erros });
  }

  next();
};

app.post('/usuarios', validarUsuario, (req, res) => {
  res.status(201).json({ mensagem: 'Usuário criado' });
});

Para controle de fluxo com erros, use next(err):

app.get('/dados', (req, res, next) => {
  try {
    const dados = buscarDadosDoSistema(); // pode lançar erro
    res.json(dados);
  } catch (erro) {
    next(erro); // passa para o middleware de erro
  }
});

7. Tratamento de erros e middlewares de erro

Um middleware de erro padrão deve ter 4 parâmetros:

app.use((err, req, res, next) => {
  console.error(err.stack);

  const status = err.status || 500;
  const mensagem = err.message || 'Erro interno do servidor';

  res.status(status).json({
    erro: true,
    mensagem,
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
  });
});

Centralize os erros criando classes personalizadas:

class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
  }
}

// Uso
app.get('/recurso/:id', (req, res, next) => {
  const recurso = buscarPorId(req.params.id);
  if (!recurso) {
    return next(new AppError('Recurso não encontrado', 404));
  }
  res.json(recurso);
});

8. Integração com React e dicas finais

Criando uma API REST para servir dados ao React:

// backend/server.js
const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors({ origin: 'http://localhost:5173' }));
app.use(express.json());

// Rotas da API
app.get('/api/usuarios', (req, res) => {
  res.json(usuarios);
});

app.listen(3001);

No frontend React:

// frontend/src/App.jsx
fetch('http://localhost:3001/api/usuarios')
  .then(res => res.json())
  .then(data => setUsuarios(data));

Boas práticas finais:

  • Separação de responsabilidades: mantenha rotas, controllers e middlewares em arquivos separados
  • Documentação: utilize Swagger/OpenAPI para documentar suas rotas
  • Testes: escreva testes unitários e de integração usando Jest e Supertest
  • Ambientes: configure diferentes ambientes (dev, homolog, prod) com variáveis de ambiente

Referências