Construir uma aplicação Angular que funciona é relativamente simples. Construir uma aplicação Angular que se mantém organizada, legível, escalável e fácil de manter ao fim de meses ou anos de desenvolvimento é outra coisa. A verdadeira diferença entre um projeto amador e um projeto profissional não está só no código que “faz o seu trabalho”, mas na arquitetura que permite à equipa continuar a desenvolver sem criar caos.
Quando uma aplicação cresce, aumentam as páginas, os componentes, os serviços, as chamadas HTTP, os estados a gerir, as permissões, as rotas, os formulários, as validações e as dependências entre as várias partes do sistema. Se tudo for parar a pastas genéricas como components, services e models, ao fim de pouco tempo torna-se difícil perceber onde está uma funcionalidade, quem usa o quê e que partes do código podem ser alteradas sem partir o resto.
Neste guia vemos como estruturar uma aplicação Angular moderna usando uma feature-based architecture, uma separação clara entre core e shared, os standalone components, os signals, o padrão smart vs dumb components e uma folder structure pensada para projetos reais.
Porque é que a arquitetura é importante no Angular
O Angular é um framework muito poderoso porque já oferece uma estrutura clara: componentes, serviços, injeção de dependências, routing, formulários, cliente HTTP, guards, interceptors e muito mais. No entanto, precisamente por oferecer tantas ferramentas, é fácil usá-las sem uma estratégia coerente.
Um projeto pequeno pode sobreviver mesmo com uma estrutura pouco organizada. Um projeto médio ou grande, pelo contrário, precisa de regras. Sem regras, cada programador organiza o código à sua maneira, os componentes ficam demasiado grandes, os serviços acumulam responsabilidades sem relação entre si e o projeto transforma-se lentamente num bloco difícil de alterar.
Uma boa arquitetura Angular deve ajudar a alcançar alguns objetivos fundamentais:
- Escalabilidade: acrescentar novas funcionalidades sem ter de reorganizar todo o projeto.
- Manutenibilidade: perceber rapidamente onde está o código e como alterá-lo.
- Separação de responsabilidades: cada ficheiro, componente ou serviço deve ter um papel claro.
- Testabilidade: o código deve ser fácil de testar de forma isolada.
- Desempenho: a estrutura deve favorecer o lazy loading, bundles mais pequenos e uma renderização eficiente.
- Colaboração: vários programadores devem poder trabalhar na mesma aplicação sem se atropelarem.
O ponto central é este: a arquitetura não serve para complicar o projeto, mas para o tornar mais previsível. Quando uma estrutura é previsível, cada nova funcionalidade tem um sítio natural onde viver.
Feature-based architecture: organizar o código por funcionalidade
Um dos erros mais comuns em projetos Angular é organizar o código por tipo técnico em vez de por domínio funcional. Uma estrutura deste tipo pode parecer organizada no início:
src/app/
components/
services/
models/
pipes/
directives/
pages/
O problema é que esta organização não diz nada sobre o produto. Se estás a trabalhar na secção de projetos, tens de procurar os componentes em components, os serviços em services, os modelos em models, as páginas em pages e assim por diante. Cada funcionalidade fica espalhada por todo o projeto.
Uma feature-based architecture, pelo contrário, organiza o código em torno das funcionalidades da aplicação. Por exemplo:
src/app/
features/
dashboard/
blog/
projects/
experiences/
auth/
admin/
Cada pasta representa uma parte real do produto. Tudo o que diz respeito ao blog está em features/blog. Tudo o que diz respeito aos projetos está em features/projects. Tudo o que diz respeito à administração está em features/admin.
Esta abordagem melhora enormemente a legibilidade do projeto. Quando tens de alterar uma funcionalidade, sabes onde ir. Quando tens de eliminar uma funcionalidade, sabes que ficheiros estão envolvidos. Quando um novo programador entra na equipa, pode perceber a aplicação a partir das funcionalidades e não de pastas técnicas genéricas.
Uma funcionalidade deve ser o mais autónoma possível
Uma boa funcionalidade Angular deve conter tudo o que precisa para funcionar: páginas, componentes específicos, serviços de acesso aos dados, modelos, stores locais, rotas e utilitários internos.
Exemplo de estrutura para uma funcionalidade projects:
src/app/features/projects/
projects.routes.ts
pages/
projects-list-page.component.ts
project-detail-page.component.ts
components/
project-card.component.ts
project-filters.component.ts
project-empty-state.component.ts
data-access/
projects-api.service.ts
projects-store.service.ts
models/
project.model.ts
project-filter.model.ts
utils/
project-status.util.ts
Esta estrutura torna a funcionalidade independente e fácil de perceber. As páginas estão separadas dos componentes mais pequenos, a lógica de acesso aos dados está em data-access, os tipos TypeScript estão em models e as funções de apoio específicas da funcionalidade estão em utils.
Regra importante: evitar dependências entre funcionalidades
Uma funcionalidade não deve importar diretamente componentes, serviços ou modelos de outra funcionalidade. Por exemplo, features/blog não deve importar código de features/projects. Isto cria acoplamento e torna difícil alterar uma funcionalidade sem afetar as outras.
Se duas funcionalidades precisam do mesmo componente ou utilitário, provavelmente esse elemento deve passar para shared. Se, pelo contrário, partilham lógica de domínio importante, pode ser útil criar uma pasta ou biblioteca dedicada, mas sempre com fronteiras claras.
Core e Shared: uma diferença fundamental
Em muitos projetos Angular encontramos as pastas core e shared, mas muitas vezes são mal usadas. Perceber a diferença entre estas duas áreas é essencial para manter o projeto limpo.
Core: o que pertence à aplicação global
A pasta core contém código global, usado ao nível da aplicação e muitas vezes inicializado uma única vez. Aqui encontramos elementos como autenticação, interceptors HTTP, guards globais, layout principal, gestão de erros, configurações, serviços singleton e lógica de infraestrutura.
Exemplo:
src/app/core/
auth/
auth.service.ts
auth.guard.ts
auth.interceptor.ts
http/
api-error.interceptor.ts
http-context.tokens.ts
layout/
main-layout.component.ts
admin-layout.component.ts
config/
app-config.token.ts
guards/
role.guard.ts
services/
logger.service.ts
storage.service.ts
O core não deve tornar-se um depósito de serviços aleatórios. Deve conter só o que é realmente global. Se um serviço só serve a funcionalidade do blog, não vai para core: vai para features/blog/data-access.
Shared: o que é reutilizável e sem lógica de negócio específica
A pasta shared contém elementos reutilizáveis em várias partes da aplicação, mas não ligados a uma funcionalidade específica. Aqui encontramos componentes de UI genéricos, pipes, diretivas, helpers e modelos verdadeiramente partilhados.
src/app/shared/
ui/
button/
modal/
card/
badge/
spinner/
pipes/
truncate.pipe.ts
safe-html.pipe.ts
directives/
autofocus.directive.ts
click-outside.directive.ts
utils/
date-format.util.ts
string.util.ts
models/
pagination.model.ts
api-response.model.ts
Um componente como app-button, app-modal ou app-spinner pode ficar em shared/ui. Um componente como project-card, pelo contrário, não deve ficar em shared, porque pertence ao domínio dos projetos.
Erro comum: pôr tudo em Shared
Um dos anti-padrões mais frequentes é criar uma pasta shared enorme que contém de tudo. No início parece prático, mas ao fim de alguns meses torna-se impossível perceber que componentes são realmente genéricos e quais foram lá postos só por comodidade.
A regra prática é simples: se um componente contém palavras, lógica ou conceitos ligados a uma funcionalidade específica, não é shared. Se, pelo contrário, é genérico, reutilizável e independente do domínio, pode ir para shared.
Standalone components: Angular moderno sem NgModules inúteis
No Angular moderno, os standalone components tornaram-se a forma recomendada de construir aplicações mais simples, explícitas e modulares. No passado, cada componente tinha de ser declarado dentro de um NgModule. Isto levava muitas vezes à criação de módulos muito grandes ou pouco claros.
Com os componentes standalone, cada componente declara diretamente as suas dependências através da propriedade imports. Isto torna o código mais legível: olhando para um componente percebes logo que outros componentes, diretivas ou pipes usa.
@Component({
selector: 'app-project-card',
standalone: true,
imports: [DatePipe],
template: `
<article class="project-card">
<h3>{{ project().title }}</h3>
<p>{{ project().description }}</p>
<small>Pubblicato il {{ project().createdAt | date }}</small>
<button type="button" (click)="open.emit(project().id)">
Apri progetto
</button>
</article>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProjectCardComponent {
project = input.required<Project>();
open = output<string>();
}
Esta abordagem tem várias vantagens:
- as dependências são explícitas;
- os componentes são mais fáceis de mover e testar;
- o lazy loading torna-se mais natural;
- reduz-se a necessidade de módulos intermédios;
- o projeto fica mais leve de entender.
Bootstrap da aplicação com standalone
Numa aplicação Angular moderna, o bootstrap pode ser feito sem um AppModule tradicional. A configuração global é muitas vezes definida em app.config.ts.
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(withInterceptors([
authInterceptor,
apiErrorInterceptor
]))
]
};
E no ficheiro main.ts:
bootstrapApplication(AppComponent, appConfig)
.catch((error) => console.error(error));
Esta configuração separa claramente o ponto de arranque da aplicação da lógica das funcionalidades.
Routing e lazy loading por funcionalidade
O routing é uma parte central da arquitetura Angular. Num projeto escalável, cada funcionalidade deve ter as suas próprias rotas, carregadas em lazy loading sempre que possível.
No ficheiro principal app.routes.ts podemos definir apenas as rotas principais:
export const routes: Routes = [
{
path: '',
loadComponent: () =>
import('./features/home/pages/home-page.component')
.then((m) => m.HomePageComponent)
},
{
path: 'projects',
loadChildren: () =>
import('./features/projects/projects.routes')
.then((m) => m.PROJECTS_ROUTES)
},
{
path: 'blog',
loadChildren: () =>
import('./features/blog/blog.routes')
.then((m) => m.BLOG_ROUTES)
},
{
path: 'admin',
canMatch: [authGuard],
loadChildren: () =>
import('./features/admin/admin.routes')
.then((m) => m.ADMIN_ROUTES)
}
];
Dentro da funcionalidade projects, pelo contrário, definimos as rotas específicas:
export const PROJECTS_ROUTES: Routes = [
{
path: '',
loadComponent: () =>
import('./pages/projects-list-page.component')
.then((m) => m.ProjectsListPageComponent)
},
{
path: ':id',
loadComponent: () =>
import('./pages/project-detail-page.component')
.then((m) => m.ProjectDetailPageComponent)
}
];
Esta abordagem mantém limpo o ficheiro principal das rotas e permite ao Angular carregar apenas o código necessário para a secção visitada pelo utilizador.
Signals: gestão do estado mais simples e reativa
Os signals são uma das ferramentas mais importantes do Angular moderno. Permitem gerir estado local e derivado de forma mais simples, legível e eficiente do que muitas soluções tradicionais.
Um signal representa um valor reativo. Quando o valor muda, o Angular sabe que partes da UI têm de ser atualizadas. Isto torna o estado mais explícito e reduz a complexidade em muitos cenários.
Exemplo simples:
const count = signal(0);
const double = computed(() => count() * 2);
function increment(): void {
count.update((value) => value + 1);
}
Numa aplicação escalável, os signals podem ser usados a vários níveis:
- estado local do componente, como o separador ativo, filtros ou a abertura de uma modal;
- estado derivado, como o total de elementos, os elementos filtrados ou o estado vazio;
- stores de funcionalidade, quando uma secção da aplicação tem dados partilhados entre vários componentes;
- facades, para expor à UI um estado simples e já pronto.
Exemplo de store com signals
Uma boa prática consiste em criar uma store específica para a funcionalidade, evitando pôr toda a lógica dentro dos componentes.
@Injectable()
export class ProjectsStore {
private readonly api = inject(ProjectsApiService);
private readonly _projects = signal<Project[]>([]);
private readonly _loading = signal(false);
private readonly _error = signal<string | null>(null);
private readonly _query = signal('');
readonly projects = this._projects.asReadonly();
readonly loading = this._loading.asReadonly();
readonly error = this._error.asReadonly();
readonly query = this._query.asReadonly();
readonly filteredProjects = computed(() => {
const query = this._query().toLowerCase().trim();
if (!query) {
return this._projects();
}
return this._projects().filter((project) =>
project.title.toLowerCase().includes(query)
);
});
readonly total = computed(() => this.filteredProjects().length);
async loadProjects(): Promise<void> {
this._loading.set(true);
this._error.set(null);
try {
const projects = await firstValueFrom(this.api.getProjects());
this._projects.set(projects);
} catch {
this._error.set('Impossibile caricare i progetti.');
} finally {
this._loading.set(false);
}
}
setQuery(query: string): void {
this._query.set(query);
}
}
Esta store tem uma responsabilidade clara: gerir o estado da funcionalidade de projetos. O componente não precisa de saber como os dados são carregados ou filtrados. Só tem de ler signals e chamar os métodos expostos pela store.
Onde fornecer uma store de funcionalidade
Se a store só serve uma funcionalidade específica, pode ser fornecida ao nível da rota ou do componente. Assim, o seu ciclo de vida fica ligado à própria funcionalidade e não fica global sem necessidade.
export const PROJECTS_ROUTES: Routes = [
{
path: '',
providers: [ProjectsStore, ProjectsApiService],
loadComponent: () =>
import('./pages/projects-list-page.component')
.then((m) => m.ProjectsListPageComponent)
}
];
Esta escolha evita encher o root injector com serviços que não têm de viver durante toda a duração da aplicação.
Signals e RxJS: não são inimigos
Um erro comum é pensar que os signals substituem completamente o RxJS. Na realidade, as duas ferramentas podem conviver perfeitamente. Os signals são ótimos para estado síncrono, derivações e estado de UI. O RxJS continua a ser muito útil para fluxos assíncronos, eventos complexos, WebSockets, debounce, retry, combinações de streams e cancelamento automático de pedidos.
Uma regra prática útil:
- usa signals para representar o estado atual da UI;
- usa computed para valores derivados;
- usa RxJS quando tens de modelar fluxos assíncronos no tempo;
- usa conversões como
toSignalquando queres expor um Observable ao template de forma mais simples.
Smart vs Dumb Components
O padrão smart vs dumb components é um dos mais úteis para manter uma aplicação Angular limpa. A ideia é separar os componentes que gerem lógica, dados e comunicação dos que só mostram informação.
Smart components
Os smart components, também chamados container components, conhecem a funcionalidade. Podem usar serviços, stores, o router, parâmetros de rota e lógica da aplicação. Normalmente correspondem a uma página ou a um componente principal da funcionalidade.
Exemplo de smart component:
@Component({
selector: 'app-projects-list-page',
standalone: true,
imports: [
ProjectCardComponent,
ProjectFiltersComponent,
SpinnerComponent
],
template: `
<section>
<h2>Progetti</h2>
<app-project-filters
[query]="store.query()"
(queryChange)="store.setQuery($event)"
/>
@if (store.loading()) {
<app-spinner />
} @else if (store.error()) {
<p class="error">{{ store.error() }}</p>
} @else {
<div class="projects-grid">
@for (project of store.filteredProjects(); track project.id) {
<app-project-card
[project]="project"
(open)="openProject($event)"
/>
}
</div>
}
</section>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProjectsListPageComponent implements OnInit {
readonly store = inject(ProjectsStore);
private readonly router = inject(Router);
ngOnInit(): void {
this.store.loadProjects();
}
openProject(id: string): void {
this.router.navigate(['/projects', id]);
}
}
Este componente é smart porque coordena a página: carrega os dados, lê a store, gere a navegação e passa informação aos componentes filhos.
Dumb components
Os dumb components, também chamados presentational components, são componentes simples, reutilizáveis e fáceis de testar. Recebem dados através de inputs e emitem eventos através de outputs. Não devem conhecer o router, serviços HTTP ou stores globais.
@Component({
selector: 'app-project-filters',
standalone: true,
template: `
<label>
Cerca progetto
<input
type="search"
[value]="query()"
(input)="onInput($event)"
placeholder="Cerca per titolo..."
/>
</label>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProjectFiltersComponent {
query = input('');
queryChange = output<string>();
onInput(event: Event): void {
const target = event.target as HTMLInputElement;
this.queryChange.emit(target.value);
}
}
Este componente não sabe nada da funcionalidade completa. Não sabe de onde vêm os projetos, não conhece a API, não navega e não altera diretamente a store. A sua única tarefa é mostrar um input e comunicar o novo valor.
Porque é que esta separação funciona
Separar smart e dumb components traz vantagens concretas:
- os componentes de apresentação são mais fáceis de reutilizar;
- os testes tornam-se mais simples;
- a lógica da aplicação fica nos containers ou nas stores;
- a UI torna-se mais previsível;
- reduz-se o risco de componentes enormes e difíceis de manter.
No entanto, o padrão não deve ser aplicado de forma dogmática. Para componentes muito pequenos ou funcionalidades simples, pode bastar um único componente. O objetivo não é criar o maior número possível de ficheiros, mas manter responsabilidades claras.
Folder structure recomendada para uma aplicação Angular escalável
Uma estrutura sólida para uma aplicação Angular moderna pode ser esta:
src/
app/
app.component.ts
app.config.ts
app.routes.ts
core/
auth/
auth.service.ts
auth.guard.ts
auth.interceptor.ts
http/
api-error.interceptor.ts
layout/
main-layout.component.ts
admin-layout.component.ts
services/
logger.service.ts
storage.service.ts
shared/
ui/
button/
button.component.ts
modal/
modal.component.ts
spinner/
spinner.component.ts
pipes/
truncate.pipe.ts
directives/
autofocus.directive.ts
models/
pagination.model.ts
api-response.model.ts
utils/
date.util.ts
features/
home/
pages/
home-page.component.ts
projects/
projects.routes.ts
pages/
projects-list-page.component.ts
project-detail-page.component.ts
components/
project-card.component.ts
project-filters.component.ts
data-access/
projects-api.service.ts
projects-store.service.ts
models/
project.model.ts
utils/
project-status.util.ts
blog/
blog.routes.ts
pages/
blog-list-page.component.ts
blog-detail-page.component.ts
components/
blog-card.component.ts
blog-search.component.ts
data-access/
blog-api.service.ts
blog-store.service.ts
models/
blog-post.model.ts
admin/
admin.routes.ts
pages/
admin-dashboard-page.component.ts
edit-post-page.component.ts
components/
admin-sidebar.component.ts
content-editor.component.ts
data-access/
admin-api.service.ts
admin-store.service.ts
models/
admin-user.model.ts
environments/
environment.ts
Esta estrutura é suficientemente simples para ser percebida rapidamente, mas suficientemente sólida para crescer com o tempo.
Explicação das pastas principais
app.config.ts contém os providers globais: router, cliente HTTP, interceptors, configurações globais e serviços da aplicação.
app.routes.ts contém apenas as rotas principais e delega as rotas detalhadas às funcionalidades.
core contém o que vive ao nível global: auth, interceptors, layout, logger, configurações e serviços de infraestrutura.
shared contém elementos genéricos e reutilizáveis, sem lógica de negócio específica.
features contém o coração da aplicação, organizado por domínio funcional.
pages contém componentes ligados diretamente às rotas. Normalmente são smart components.
components contém componentes específicos da funcionalidade, muitas vezes dumb ou semi-presentational.
data-access contém serviços de API, stores, facades e lógica de acesso aos dados.
models contém interfaces e tipos TypeScript ligados à funcionalidade.
utils contém funções puras específicas da funcionalidade.
Data-access layer: isolar a API e o estado
Numa aplicação Angular escalável, os componentes não devem falar diretamente com HttpClient. Se cada componente fizer as suas próprias chamadas HTTP, a lógica duplica-se e torna-se difícil gerir loading, erros, cache e transformação dos dados.
É melhor criar uma camada data-access para cada funcionalidade. Esta camada pode conter:
- serviços de API;
- stores baseadas em signals;
- facades;
- mappers entre DTOs e modelos de UI;
- a lógica de cache local da funcionalidade.
Exemplo de API service:
@Injectable()
export class ProjectsApiService {
private readonly http = inject(HttpClient);
private readonly baseUrl = '/api/projects';
getProjects(): Observable<Project[]> {
return this.http.get<Project[]>(this.baseUrl);
}
getProjectById(id: string): Observable<Project> {
return this.http.get<Project>(`${this.baseUrl}/${id}`);
}
}
O componente não precisa de conhecer endpoints, URLs ou detalhes HTTP. Deve comunicar com a store ou com uma facade. Isto mantém a UI limpa e facilita os testes.
Regras práticas de dependência
Para evitar o caos arquitetural, é útil definir regras claras sobre as dependências:
- Uma funcionalidade pode importar de shared, porque shared contém elementos genéricos.
- Uma funcionalidade pode usar serviços de core, como auth ou logger, se forem mesmo necessários.
- Shared não deve importar funcionalidades, senão deixa de ser genérico.
- Shared deve evitar dependências fortes de core, para continuar reutilizável.
- Uma funcionalidade não deve importar diretamente outra funcionalidade.
- Core não deve conter lógica específica de uma única funcionalidade.
Uma possível direção das dependências é esta:
features ---> shared
features ---> core
core ---> shared
shared ---> nessuna feature
Quanto mais estas regras forem respeitadas, mais modular a aplicação se mantém.
Convenções de nomes recomendadas
As convenções de nomes parecem pormenores, mas nos projetos grandes fazem uma grande diferença. Um nome coerente reduz o tempo necessário para perceber o papel de um ficheiro.
*.page.tspara componentes ligados a uma rota.*.component.tspara componentes de UI normais.*.service.tspara serviços genéricos.*.api.service.tspara serviços que comunicam com o backend.*.store.tsou*.store.service.tspara o estado da funcionalidade.*.guard.tspara route guards.*.interceptor.tspara interceptors HTTP.*.model.tspara interfaces e tipos principais.*.util.tspara funções puras de apoio.
Por exemplo, project-detail-page.component.ts comunica de imediato que se trata de uma página. projects-api.service.ts comunica que esse serviço fala com o backend. projects-store.service.ts comunica que esse ficheiro gere o estado da funcionalidade.
Desempenho e arquitetura
Uma boa arquitetura Angular não diz respeito apenas à organização dos ficheiros. Também influencia o desempenho da aplicação.
Usar funcionalidades em lazy loading significa evitar carregar todo o código no arranque. Se um utilizador só visita a página inicial, não faz sentido descarregar logo também o código da administração, do editor, do dashboard e de todas as secções internas.
Os standalone components ajudam porque tornam mais simples carregar componentes e rotas de forma granular. Os signals ajudam porque tornam a renderização mais precisa e o estado mais fácil de seguir. O padrão smart/dumb ajuda porque reduz componentes enormes e templates difíceis de otimizar.
Algumas boas práticas:
- usa lazy loading para funcionalidades que não são precisas de imediato;
- usa
ChangeDetectionStrategy.OnPushnos componentes; - usa
@forcomtrackpara listas eficientes; - mantém os componentes pequenos e focados;
- evita lógica pesada diretamente no template;
- usa signals e computed para valores derivados;
- carrega imagens e assets de forma otimizada;
- separa a administração do frontend público sempre que possível.
Erros comuns a evitar
1. Componentes demasiado grandes
Um componente que contém um template enorme, chamadas HTTP, gestão de formulários, estado, validações, mapeamento de dados e lógica de navegação torna-se rapidamente impossível de gerir. Quando um componente acumula demasiadas responsabilidades, é altura de o dividir.
2. Serviços globais para tudo
Nem todos os serviços têm de ser providedIn: 'root'. Se um serviço só serve uma funcionalidade, fornecê-lo ao nível da rota ou do componente pode ser mais correto. Isto reduz estado global desnecessário e torna o ciclo de vida mais previsível.
3. Shared demasiado grande
Uma pasta shared enorme é muitas vezes sinal de uma arquitetura fraca. Shared deve conter componentes e utilitários verdadeiramente genéricos, não pedaços de funcionalidades lá postos por comodidade.
4. Funcionalidades acopladas entre si
Quando uma funcionalidade importa diretamente ficheiros de outra, o projeto torna-se frágil. É melhor extrair o código partilhado para uma área comum com uma responsabilidade clara.
5. Lógica de negócio no template
O template deve continuar legível. Se uma condição se tornar demasiado complexa, passa-a para um computed, para um método claro ou para a store da funcionalidade.
6. Falta de convenções
Se cada programador criar pastas e nomes à sua maneira, o projeto perde coerência. As convenções devem ser simples, documentadas e respeitadas.
Exemplo prático: uma funcionalidade Blog bem estruturada
Imaginemos uma secção de blog com lista de artigos, detalhe do artigo, pesquisa, filtros por categoria e gestão de conteúdos do lado da administração. Uma estrutura organizada poderia ser:
src/app/features/blog/
blog.routes.ts
pages/
blog-list-page.component.ts
blog-detail-page.component.ts
components/
blog-card.component.ts
blog-search.component.ts
blog-category-filter.component.ts
blog-empty-state.component.ts
data-access/
blog-api.service.ts
blog-store.service.ts
models/
blog-post.model.ts
blog-category.model.ts
utils/
reading-time.util.ts
slug.util.ts
A página blog-list-page é smart: usa a store, carrega os dados, gere filtros e pesquisa. O componente blog-card é dumb: recebe um artigo e mostra título, excerto, imagem e data. O serviço blog-api fala com o backend. A store mantém estado, loading, erros e artigos filtrados.
Este tipo de separação permite alterar o layout dos cartões sem mexer na lógica de dados, ou mudar um endpoint da API sem alterar os componentes de apresentação.
Checklist final para uma arquitetura Angular escalável
Antes de considerares sólida a estrutura da tua aplicação Angular, podes usar esta checklist:
- As funcionalidades principais estão organizadas dentro de
features? - Cada funcionalidade tem rotas, páginas, componentes, data-access e modelos separados?
- O código global está mesmo limitado a
core? sharedcontém apenas elementos genéricos e reutilizáveis?- As funcionalidades evitam dependências diretas entre si?
- As rotas principais usam lazy loading?
- Os standalone components declaram claramente as suas dependências?
- A lógica HTTP está fora dos componentes?
- O estado da funcionalidade é gerido por stores, facades ou serviços dedicados?
- Os signals são usados para estado local e valores derivados?
- O RxJS é usado onde é mesmo preciso modelar fluxos assíncronos?
- Os smart e dumb components têm responsabilidades separadas?
- Os nomes dos ficheiros são coerentes?
- Os componentes são pequenos, legíveis e testáveis?
- Existe uma convenção partilhada pela equipa?
Conclusão
Organizar bem um projeto Angular não significa criar uma estrutura complicada. Significa criar uma estrutura clara, previsível e preparada para crescer. A feature-based architecture ajuda a raciocinar por funcionalidades reais. A separação entre core e shared evita confusão. Os standalone components tornam as dependências mais explícitas. Os signals simplificam a gestão do estado. O padrão smart vs dumb components melhora a legibilidade, a reutilização e a testabilidade.
O mais importante é não esperar que o projeto fique grande para pensar na arquitetura. As decisões tomadas no início influenciam todo o ciclo de vida da aplicação. Uma estrutura limpa permite acrescentar novas páginas, novas funcionalidades e novos programadores sem transformar o código num labirinto.
Se estás a construir uma aplicação Angular profissional, parte de uma regra simples: cada coisa deve ter um lugar preciso. As funcionalidades devem conter a lógica do produto, o core deve conter a infraestrutura global e o shared deve conter elementos verdadeiramente reutilizáveis. A partir daí, acrescenta standalone components, lazy loading, signals e uma separação clara entre smart e dumb components.
Uma arquitetura escalável não é a mais complexa, mas aquela que a equipa consegue perceber, manter e fazer evoluir ao longo do tempo. O Angular oferece todas as ferramentas necessárias: cabe-nos a nós usá-las com disciplina, coerência e bom senso.