Como eu inicio novos projetos Angular
Recentemente eu criei uma enquete no Linkedin e no Twitter perguntando qual assunto as pessoas tinham mais dificuldade e/ou gostariam de se aprofundar dentro do Angular.
O resultado no Linkedin mostra que o tópico que as pessoas mais votaram foi "Arquitetura/Boas práticas".
Por mais que o Angular seja um framework bastante opinativo, que força os desenvolvedores a fazerem as coisas da maneira correta, ainda sim há muitos lugares onde as coisas podem dar errado. Soma-se a isso o fato do Angular ser uma boa opção para projetos de grande porte, ou seja, a necessidade de se pensar em uma arquitetura bem fundamentada é ainda maior.
O Design Modular
Muitas empresas adotam o padrão de Micro Frontends para seus projetos com o objetivo de alcançar maior autonomia entre as equipes e desacoplamento de código. Porém em muitas vezes esse padrão pode trazer muita complexidade para o projeto e também problemas de performance.
Devemos sim buscar uma modularização de nossos projetos. Saber separar os recursos de um sistema acarreta em uma arquitetura mais escalável e fácil de dar manutenção.
Com isso em mente, sempre que vou criar uma nova aplicação com Angular, eu gosto de utilizar o Design modular do Nx.
O que é o Nx?
O Nx é um Build System com suporte para monorepos e integrações extremamente poderosas. Se você quiser saber mais sobre monorepos e o próprio Nx, eu tenho uma Talk onde me aprofundo nesses temas.
Mas então vale a pena criar novos projetos no padrão de monorepo?
A resposta é sim e não. Pensando nisso, o Nx disponibiliza uma opção para a criação de novas aplicações Angular Standalone, ou seja, que não possui o setup de um monorepo. Se futuramente surgir a necessidade de criar novos projetos e novas libs, será muito fácil de integrá-las nesta estrutura.
Mesmo que sua aplicação não tenha a intenção de escalar muito, ainda assim vale a pena começar com esta abordagem. Neste artigo mostrarei algumas vantagens de se utilizar o Nx em comparação com a forma tradicional de criar aplicações Angular.
Ah, e não se preocupe! Neste primeiro momento você não precisa entender a fundo o que é o Nx. Apenas tenha em mente que você terá uma app Angular, mas com superpoderes. Ao longo do tempo seu projeto irá crescer, e é nesse momento que vale a pena investir em aprender o Nx e todos os recursos que ele te proporciona.
Criando a aplicação
Vamos criar nosso workspace com o Nx e Angular rodando o seguinte comando no terminal:
npx create-nx-workspace@latest my-app --preset=angular-standalone
No primeiro momento, a estrutura gerada aparenta ser bastante semelhante com a abordagem padrão utilizando o ng new
.
Mas temos algumas diferenças que este setup entrega:
- Setup de testes End-to-end com Cypress: Para garantir a qualidade logo de início, dentro da pasta
e2e
temos um ambiente todo configurado para criar testes end-to-end. Tudo isso utilizando o Cypress, que hoje é a melhor ferramenta do mercado para essa abordagem. - Jest pré-configurado: O Angular utiliza por padrão o framework de testes Jasmine integrado com o Test Runner Karma. Porém, ao utilizar o setup do Nx, a escolha padrão será o Jest. Simplesmente o mais popular framework de testes do ecossistema JavaScript. Na minha visão, ele é muito mais rápido que o Jasmine e também tem integração com CI mais simplificada.
- ESLint e Prettier pré-configurados: São duas ferramentas essenciais para qualquer projeto que deseja sempre manter a qualidade e padrões. Neste setup eles também já são pré-configurados.
Você pode ter notado que não temos o arquivo angular.json
. Neste caso as configurações do workspace serão feitas nos arquivos nx.json
e project.json
. Mais uma vez ressaltando: conforme seu projeto for crescendo, vale a pena entender as responsabilidades destes arquivos e como eles influenciam em sua arquitetura.
Servindo a aplicação
Para rodar a aplicação localmente, basta executar npm start
ou então npx nx serve
. Perceba que não estamos utilizando diretamente o Angular CLI em nenhum momento. O Nx está executando os processos, e mais pra frente entenderemos as suas vantagens.
Além do serve
, temos outros comandos úteis que serão utilizados frequentemente:
npx nx build # executa o build da aplicação
npx nx test # roda os testes utilizando o Jest
npx nx lint # roda o lint utilizando o ESLint
npx nx e2e e2e # roda os testes e2e utilizando o Cypress
Criando nosso primeiro componente
O Nx possui diversos plugins para geração de código. O que no Angular nós chamamos de Schematics, aqui chamamos de Generators. Os Generators permitem que você crie códigos, configurações ou projetos inteiros com facilidade.
Para criar nosso componente, vamos executar o seguinte comando:
npx nx g @nx/angular:component hello-world
Dica: o Nx possui muitos Generators, e nem sempre vamos lembrar os nomes de todos eles, portanto recomendo fortemente instalar a extensão Nx Console para o VSCode. Ela proporciona uma interface para poder executar qualquer tipo de comando e também ir muito além.
Modularização: a grande vantagem
Dado todo esse contexto, já conseguimos avançar no desenvolvimento da aplicação, mas você pode estar se perguntando se realmente vale a pena trocar o método padrão para utilizar o Nx. O diferencial está no momento de separar as responsabilidades, ou seja, dividir nosso sistema em vários módulos.
Quando você desenvolve um projeto Angular, geralmente toda a sua lógica fica na pasta app
. Idealmente separados por vários nomes de pastas que representam seus domínios. À medida que a app cresce, ela se torna cada vez mais monolítica.
Nesta estrutura, vamos separar nossa lógica em libs locais. Com isso temos os seguintes ganhos:
- Melhor separação de responsabilidades
- Maior reutilização
- “APIs” mais explícitas entre suas “áreas de domínio”
- Maior escalabilidade em CI habilitando comandos de test/lint/build independentes para cada lib
- Maior escalabilidade em suas equipes, permitindo que equipes diferentes trabalhem em libs separadas
Vamos supor que nossas áreas de domínio incluam produtos, pedidos e alguns componentes UI (User Interface) mais genéricos. Podemos gerar uma nova lib para cada uma dessas áreas usando o Generator de libs do Angular:
npx nx g @nx/angular:library products --directory=modules --simpleName
npx nx g @nx/angular:library orders --directory=modules --simpleName
npx nx g @nx/angular:library ui --directory=modules/shared --standalone --simpleName
Temos agora a seguinte estrutura dividida na pasta modules
:
Não se preocupe com a quantidade de arquivos que foram gerados para cada lib. O Nx já cria um setup otimizado para cada uma delas. Mas recomendo explorar os arquivos criados. Perceba que você pode realizar diferentes configurações pra cada um dos módulos. Isso proporciona muito mais flexibilidade, principalmente quando se está trabalhando com várias equipes.
Podemos pensar que todos esses módulos são independentes, por mais que no final a sua app irá importar todos eles.
Imagine um cenário onde uma equipe será responsável por desenvolver as features de produtos. Ela atuará somente na lib products
. Os componentes, services e qualquer outro tipo de código será criado dentro desta lib. E esta equipe será responsável por definir o que as outras camadas poderão enxergar (sua API pública) por meio do arquivo index.ts
.
Além disso, como o seu escopo é de produtos, não há a necessidade de escrever e rodar testes de outros escopos. Queremos executar os testes somente desta lib. E o mesmo vale para o lint.
npx nx test modules-products
npx nx lint modules-products
Integrando libs
Sabemos que a lib shared de ui pode ser importada em outros domínios. Podemos então importar um componente genérico dentro do componente de produtos.
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { ProductsRoutingModule } from './products.routing.module';
import { ProductsComponent } from './products/products.component';
import { UiComponent } from 'modules/shared/ui';
@NgModule({
imports: [CommonModule, ProductsRoutingModule, UiComponent],
declarations: [ProductsComponent],
})
export class ProductsModule {}
O Standalone Component UiComponent
pode ser importado normalmente no módulo de produtos e em qualquer outro módulo. O mais interessante aqui é o path de onde ele está sendo importado: modules/shared/ui
.
Toda vez que criamos uma lib com o Generator, o Nx automaticamente adiciona um path alias para ela. Podemos visualizar todos os alias e editá-los no arquivo tsconfig.base.json
.
{
"compilerOptions": {
...
"paths": {
"modules/orders": ["modules/orders/src/index.ts"],
"modules/products": ["modules/products/src/index.ts"],
"modules/shared/ui": ["modules/shared/ui/src/index.ts"]
}
},
...
}
Agora queremos criar uma rota /products que será carregada sob demanda via Lazy Loading. Vamos utilizar a mesma abordagem. Basta importar a lib de products
para o nosso arquivo de rotas que se encontra na app principal.
import { Route } from '@angular/router';
export const appRoutes: Route[] = [
{
path: 'products',
loadChildren: () =>
import('modules/products').then((m) => m.ProductsModule),
},
];
Se quiséssemos criar uma rota /orders com a lib de orders
é só seguir o mesmo processo.
Visualizando a estrutura do seu projeto
À medida que o seu projeto cresce e muitas libs são criadas, precisamos ter mais controle de como a arquitetura está escalando. Felizmente o Nx possui uma feature muito interessante que automaticamente detecta todas as dependências e monta uma estrutura de árvore.
Basta executar o seguinte comando:
npx nx graph
Veja que interessante este grafo. 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. Perceba que essas ligações estão com traços pontilhados pois trata-se de um Lazy import.
Conhecendo o Affected
Se com todas essas funcionalidades, você ainda não está convencido de utilizar o Nx, após entender como funciona o affected você mudará de ideia!
Imagine que a equipe responsável pela feature de orders fez uma pequena alteração em um dos componentes da lib dela. Já entendemos que temos a possibilidade de executar os comandos de build e lint somente de module-orders
. Mas isso talvez não seja o suficiente.
Se olharmos novamente o grafo gerado, percebemos que my-app
tem uma dependência de module-orders
. Pode acontecer de algum código modificado de module-orders
afetar diretamente em nossa aplicação. Como não rodamos os testes de my-app
, não ficaremos cientes disso.
É nesse momento que o affected entra em ação. Vamos gerar o mesmo grafo, mas agora queremos que o Nx nos mostre quem está sendo afetado com as modificações atuais.
npx nx affected:graph
Sensacional! O Nx é inteligente o suficiente para entender que module-orders
foi alterado e consequentemente todos os projetos que dependem dele na árvore de dependências também!
E o melhor de tudo: nós não estamos limitados a somente visualizar quem foi afetado, mas também executar qualquer comando affected!
npx nx affected:test
npx nx affected:lint
Por mais que sua aplicação seja extremamente enorme, separando-a em libs e utilizando o affected para todos os processos, ela terá sempre uma boa velocidade de execução, pois estaremos escalando ela e focando no que realmente importa, evitando executar processos desnecessários.
Configurando o Nx na sua Pipeline
Agora vem a cereja do bolo. Vamos automatizar todos esses processos que aprendemos até aqui em nosso Continuous Integration (CI). Para exemplo, utilizarei o Github Actions.
Vamos criar o arquivo .github/workflows/ci.yml
na raiz do nosso projeto com as seguintes instruções:
name: CI
on:
push:
branches:
- main
pull_request:
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: nrwl/nx-set-shas@v3
- run: npm ci
- run: npx nx format:check
- run: npx nx affected -t lint --parallel=3
- run: npx nx affected -t test --parallel=3 --configuration=ci
- run: npx nx affected -t build --parallel=3
O mais importante é focar nos steps: perceba que temos uma etapa para cada processo: lint, test e build do que foi afetado (affected).
Basicamente ao subir esse arquivo para um repositório no Github, vamos garantir que qualquer push para a branch main
ou na abertura de qualquer Pull Request, o CI será triggado executando sempre todas as etapas.
Tendo o CI configurado, vamos commitar a alteração da lib module-orders
e realizar um push para o repositório.
Feito isso, podemos acompanhar a execução do nosso CI pela aba de Actions do nosso repositório:
Vamos analisar o que o Nx executou no step de test, por exemplo:
Perfeito! o mesmo output local foi executado no CI. Não foi necessário executar os testes da lib modules-shared-ui
e nem da modules-products
porque elas não foram afetadas. Imagine o ganho que podemos ter na performance e redução de custos da nossa pipeline!
O modelo mental
O que construímos até aqui vai de encontro com a necessidade de muitos projetos que precisam crescer exponencialmente. A separação de responsabilidades e a capacidade de escala é levada a sério, tudo isso sem abrir mão da performance.
Por mais que tenhamos diversas libs, no final, o build e o deploy é somente um. As técnicas essenciais para a velocidade de uma app Angular continuam as mesmas, como por exemplo o Lazy Loading.
Se por algum motivo você precisar mover uma lib para um outro repositório, a arquitetura do seu projeto permite isso com muita facilidade. Ou você pode seguir a estrutura de monorepo, elevando ainda mais o reuso de código.
Esse ponto pode ser crucial na escolha da arquitetura do seu projeto. Você talvez não precise ter vários deploys independentes e reinventar a roda pra alcançar uma boa performance (sim, microfrontend, estou olhando para você 👀).
Na maioria dos casos o que você precisa é ter um workflow que te traga confiança nas entregas contínuas, sem abrir mão da autonomia entre equipes. E tudo isso o Nx disponibiliza com maestria.
Para se aprofundar mais
E não para por aqui. Existem muitas outras vantagens que o Nx oferece. Deixo aqui mais dois recursos que podem fazer toda a diferença para a qualidade e performance de todo o seu workflow de desenvolvimento:
- Module Boundary Rules: Definições de regras no ESLint para garantir o controle das dependências de suas libs. Exemplo: garantir que
module-orders
emodule-products
somente dependa demodule-shared-ui
e não o contrário. - Nx Cloud: É possível integrar esta solução na nuvem do Nx para realizar o cache de todos os seus outputs (lint, test, build, etc), garantindo assim uma maior performance e redução de custos do seu CI.
Conclusão
O Nx é uma solução extremamente robusta e eficiente para gerenciar projetos Angular de qualquer tamanho. Desde aplicações simples com dois módulos até projetos enormes com diversas equipes.
Seu ferramental possibilita muitos ganhos para todo o fluxo de desenvolvimento. Cada vez mais empresas vêm adotando esse padrão para garantir uma arquitetura mais robusta e escalável.
Confira o repositório no Github com o projeto de exemplo deste artigo.
E aproveita para me seguir no Linkedin e no Twitter. 😀 Estou sempre postando conteúdos sobre Angular e Frontend!