Definindo limites de módulos no Nx com Module Boundaries

Andrew Rosário
5 min readDec 11, 2023

O desenvolvimento de software moderno frequentemente envolve projetos complexos, equipes multidisciplinares e uma variedade de tecnologias. Nesse cenário, a abordagem tradicional de separação de código de forma monolítica muitas vezes se mostra inadequada para manter a escalabilidade, a manutenibilidade e a agilidade do projeto.

Quando um sistema possui diversos escopos e diversas camadas de arquitetura, fica difícil de delimitar os relacionamentos entre essas camadas, principalmente para novos desenvolvedores que acabaram de entrar no projeto. Nesses casos o caos se instaura e temos muitos problemas de escala.

É aqui que entram os “Module Boundaries” (Limites de Módulos), uma prática de design que desempenha um papel crucial na arquitetura e organização do código. No contexto do Nx Build System, a adoção de Module Boundaries se torna ainda mais relevante, permitindo um desenvolvimento mais estruturado e colaborativo, além de deixar bem claro o que um módulo pode importar ou não.

O que é o Nx Build System?

O Nx é um conjunto de ferramentas de desenvolvimento projetado para auxiliar no desenvolvimento de aplicações em escala, permitindo a criação de monorepos eficientes que abrigam diversos projetos relacionados. Com o Nx, é possível compartilhar código, estabelecer dependências claras entre projetos e executar tarefas como build, testes e linting de maneira otimizada.

Se você nunca utilizou o Nx antes, então não deixe de conferir meus artigos sobre Como eu inicio projetos Angular e Como eu inicio projetos React. Eu garanto que você também vai começar a criar seus projetos com ele. 😉

O que são Module Boundaries?

Module Boundaries se referem à prática de definir limites claros e bem definidos entre diferentes partes de um projeto. Em vez de uma abordagem monolítica ou hierárquica, em que todas as funcionalidades e módulos estão interconectados, os Module Boundaries promovem a criação de fronteiras entre módulos com propósitos específicos. Esses limites delimitam onde determinada funcionalidade ou conjunto de funcionalidades pertence e estabelecem interfaces claras de comunicação.

E tudo isso é definido pelo ESLint com a análise estática do código. Portanto se um módulo tentar importar indevidamente um outro módulo, seremos avisados imediatamente pela nossa IDE e também ao rodar o lint do projeto.

Projeto Nx modularizado

Utilizaremos como exemplo o projeto do artigo Como eu inicio projetos Angular. Este é o repositório do projeto. Nele estamos utilizando o Nx e separando a aplicação em diversos módulos. Nossas áreas de domínio incluem produtos, pedidos e alguns componentes UI (User Interface) mais genéricos.

Rodando o comando npx nx graph podemos visualizar o desenho de suas dependências:

A lib modules-shared-ui é importada pelas nossas duas libs de negócio: modules-order e modules-products. E a nossa app representada pelo my-app importa nossas libs de negócio.

Precisamos impor alguns limites para esses módulos:

  • Queremos permitir que modules-orders importe modules-shared-uimas não o contrário;
  • Queremos permitir que modules-products importe modules-shared-uimas não o contrário;
  • Queremos permitir que modules-orders importe modules-productsmas não o contrário;
  • Queremos permitir que todas as libs importem modules-shared-uimas não o contrário.

Definindo os limites

Ao construir essas barreiras, é importante nomearmos nossos escopos. Você pode nomear da forma que se adeque melhor ao seu projeto. Porém o Nx sugere dois tipos de dimensões:

  • type: Qual o tipo da sua lib. Exemplos: feature, data-access, ui, utils(veja mais em Library types)
  • scope (domínio): qual área de domínio essa lib representa. No exemplo do nosso projeto podemos ter: orders, products, shared.

Agora que já temos as definições dos tipos e escopos de nossas libs, vamos criar tags para elas. Cada lib no Nx possui um arquivo project.json onde podemos definir suas tags.

// modules/orders/project.json
{
...
"tags": ["type:feature", "scope:orders"],
...
}
// modules/products/project.json
{
...
"tags": ["type:feature", "scope:products"],
...
}
// modules/shared/ui/project.json
{
...
"tags": ["type:ui", "scope:shared"],
...
}

A seguir, vamos criar um conjunto de regras baseadas nestas tags:

  • type:featuredeve ser capaz de importar de type:feature e type:ui
  • type:ui só deve ser capaz de poder importar de type:ui
  • scope:orders deve ser capaz de importar de scope:orders, scope:shared e scope:products
  • scope:productsdeve ser capaz de importar de scope:products e scope:shared

Para cumprir as regras, o Nx disponibiliza uma regra do ESLint personalizada. Vamos abrir o arquivo .eslintrc.base.json na raiz do workspace e adicionar o array depConstraints em @nx/enforce-module-boundaries.

{
...
"overrides": [
{
...
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "*",
"onlyDependOnLibsWithTags": ["*"]
},
{
"sourceTag": "type:feature",
"onlyDependOnLibsWithTags": ["type:feature", "type:ui"]
},
{
"sourceTag": "type:ui",
"onlyDependOnLibsWithTags": ["type:ui"]
},
{
"sourceTag": "scope:orders",
"onlyDependOnLibsWithTags": [
"scope:orders",
"scope:products",
"scope:shared"
]
},
{
"sourceTag": "scope:products",
"onlyDependOnLibsWithTags": ["scope:products", "scope:shared"]
},
{
"sourceTag": "scope:shared",
"onlyDependOnLibsWithTags": ["scope:shared"]
}
]
}
]
}
},
...
]
}

Testando as barreiras

Agora que já realizamos nossas configurações, vamos testar se essas restrições estão funcionando.

A partir das nossas regras, sabemos que scope:products não deve depender de scope:orders. Então para testar, vamos importar OrdersModule dentro de ProductsModule.

Imediatamente visualizamos um erro pelo Plugin do ESLint: um projeto tagueado com scope:products somente pode depender de libs tagueadas com scope:products ou scope:shared. Isso é muito poderoso!

Além disso, se rodarmos o lint do nosso projeto, teremos o seguinte erro:

Portanto, se integrarmos o lint dentro do seu Continuous Integration (CI) garantimos que nada saia do controle.

Conclusão

A utilização de Module Boundaries é uma prática essencial para projetos de software bem-sucedidos e escaláveis. No contexto do Nx Build System, a definição clara de limites de módulos facilita a criação, manutenção e colaboração em projetos complexos.

Com Module Boundaries, você está construindo uma arquitetura que promove escalabilidade, manutenibilidade e colaboração eficaz, criando um ambiente propício para o desenvolvimento ágil e eficiente.

--

--

Andrew Rosário

Desenvolvedor Front-end, mentor e palestrante. Apaixonado por tecnologia e por compartilhar conhecimento.