Deblogger

Rotas com Express

Express é o web framework mais famoso para NodeJS. As rotas são a essência do framework, uma vez que é através delas que os clientes se comunicam com a aplicação. Existem diversas formas de definir, organizar e construir arquivos de rotas com express. Esse artigo pretende apresentar apenas mais uma – que muito provavelmente não é inédita.

Padrões comuns

Em muitas aplicações express que trabalhei percebi uma certa desorganização nas definições de rotas. Muitas vezes as rotas são definidas em um único arquivo, resultando em um arquivo gigantesco a longo prazo. Em outros casos, cada contexto tem seu próprio arquivo, porém existe um arquivo central que importa todas as rotas. Nenhum dessas formas pode ou deve ser vista como ruim. Tudo depende do tamanho da aplicação, do time, qual o domínio que a aplicação se propõe a resolver, etc.

Outro ponto a ser observado é como estão dispostas as funções de callback necessárias para o funcionamento do router. Existem talvez duas formas mais comuns encontradas dentre as diversas aplicações que utilizam o express. São elas: implementação do código diretamente dentro do arquivo que define as rotas; isolamento da implementação em um arquivo a parte, normalmente nomeado como controller.

Particularmente, eu tendo a preferir o isolamento da implementação em arquivos próprios. Assim, temos arquivos com responsabilidades melhor definidas, sendo então mais fáceis de serem lidos. Além disso, melhora a organização do código, o que facilita a manutenção. Também tendo a preferir a separação de arquivos de rotas por contexto, o que também melhora a manutenibilidade.

Mas até que ponto é proveitoso ter um arquivo que centraliza os diversos contextos de rotas?. Será que esse arquivo central não acaba virando uma verbosidade desnecessária? Normalmente, esse arquivo resulta em algo mais ou menos assim:

import express from 'express';

import authRoutes from './auth/routes';
import userRoutes from './user/routes';
import productRoutes from './product/routes';

const router = express.Router();
router.use('/auth', authRoutes);
router.use('/user', userRoutes);
router.use('/product', productRoutes);

export default router;

Podemos entender, então, que o único intuito desse arquivo é unir todas as rotas em um router principal para facilitar a definição das rotas na aplicação. E é um motivo nobre, mas acabamos criando a necessidade de manter um arquivo que precise fazer todo esse controle. E assim, aumentamos a probabilidade de conflitos se muitas pessoas estiverem trabalhando ao mesmo tempo em novos endpoints de diferentes contextos.

Organização de arquivos

Uma solução que encontrei para não precisar manter esse arquivo foi criar um padrão de organização de arquivos dentro da aplicação para assim usar um método que leia os arquivos de rotas e faça a definição de forma automática. Basicamente, o padrão que tenho utilizado consiste em criar uma pasta que conterá os routes e controllers.

├── server
│   ├── application
│   │   ├── controllers
│   │   │   ├── User
│   │   │   │   ├── UserController.ts
│   │   │   │   └── routes.ts
│   │   └── middlewares
│   └── ...
└── ...

Dessa forma, no script que lerá os arquivos de rotas, eu apenas preciso procurar arquivos com o nome routes.ts para poder definir as rotas. Para poder usar o conteúdo do arquivo com importações dinâmicas e definir um path base para cada contexto, ao final dos arquivos de rota eu exporto um objeto com ambas as informações:

import { Router } from 'express';

import controller from './UserController';

const router = Router();

router.post('/', controller.create);

router.patch('/:id', controller.update);

router.delete('/:id', controller.delete);

export default { basePath: 'user', router };

Script para importações

Tendo feito isso, é hora de criar o script para poder fazer as importações e informar ao express quais rotas precisam ser definidas. Primeiro, vamos criar uma função para buscar todos os arquivos com base num caminho específico:

import { readdirSync, statSync } from 'fs';

export function getAllFilesPaths(dirPath: string, directoryController?: string, arrayOfFiles?: string[]): string[] {
  const files = readdirSync(dirPath);

  const newArrayOfFiles = arrayOfFiles || [];

  files.forEach((fileName) => {
    if (statSync(`${dirPath}/${fileName}`).isDirectory()) {
      const directoryFiles = getAllFilesPaths(`${dirPath}/${fileName}`, fileName, arrayOfFiles);

      newArrayOfFiles.push(...directoryFiles);
    } else {
      const filePath = directoryController ? `${directoryController}/${fileName}` : fileName;
      newArrayOfFiles.push(filePath);
    }
  });

  return newArrayOfFiles;
}

Essa função retornará os caminhos relativos para todos os arquivos dentro do diretório recebido como parâmetro. Agora, podemos criar a função que terá a responsabilidade de importar os módulos dentro desses arquivos e então repassar a informação para o express definir as rotas:

import { join, resolve, parse } from 'path';
import { Application } from 'express';

import { getAllFilesPaths } from './helpers';

export default async function defineRoutes(app: Application, pathToControllerAsArray: string[]): Promise<void> {
  const basePath = join(...pathToControllerAsArray);

  const routerFiles = getAllFilesPaths(basePath)
    .filter((filePath) => {
      const fileName = parse(filePath).base;
      return filePath.indexOf('.') !== 0 && (fileName === 'routes.ts' || fileName === 'routes.js');
    });

  const promises = routerFiles
    .map(async (filePath) => {
      const modulePath = resolve(basePath, filePath);
      const routeModule = (await import(modulePath)).default;
      app.use(`/${routeModule.basePath}`, routeModule.router);
    });

  await Promise.all(promises);
}

Com tudo pronto, é só usar essa função no arquivo principal do express!

import express from 'express';

import { defineRoutes } from './defineRoutes';

const app = express();
app.use(express.json());

console.info('Server is starting');

defineRoutes(app, [__dirname, 'application', 'controllers']).then(async () => {
  const { SERVER_PORT } = process.env;

  app.listen(SERVER_PORT, () => {
    console.info(`Listening on port ${SERVER_PORT}`);
  });
});

Conclusão

Como dito no início do artigo, o intuito aqui não é ditar um padrão para rotas em aplicações que utilizam o express, mas apresentar um padrão que tem dado certo e auxiliado no desenvolvimento de alguns projetos. Se o seu projeto for pequeno, com poucas rotas, ou ainda poucos módulos, esse padrão pode não fazer o menor sentido.

O que é possível pensar quando se constrói qualquer software, é que não existe fórmula mágica. Mas alguns padrões podem nos ajudar a manter um código mais limpo, com responsabilidades melhor definidas, e mais fácil de manter.

Avalie:
5 (5)

Publicado por Mauro Oliveira

Desenvolvedor apaixonado por JavaScript, especialista em VueJS. Atualmente focado em padrões de projeto e código, estudando sobre testes unitários, Clean Code e segurança em aplicações Web, Mobile e Desktop.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *