Qualidade e organização de código em projetos Ionic: padrões e estrutura por features

Capítulo 17

Tempo estimado de leitura: 9 minutos

+ Exercício

Por que organizar por features (e não por tipo de arquivo)

Em projetos Ionic com Angular, é comum começar organizando por tipo: pages/, services/, models/. Isso funciona no início, mas tende a gerar acoplamento e dificuldade para evoluir quando o app cresce. A organização por features agrupa tudo que pertence a uma funcionalidade (telas, componentes específicos, serviços, tipos e testes) no mesmo lugar. O resultado é um código mais fácil de encontrar, refatorar e testar.

Uma boa estrutura escalável costuma combinar quatro áreas principais:

  • core: infraestrutura e itens globais (config, interceptors, guards avançados, serviços singleton, adaptadores).
  • shared: componentes, pipes e utilitários reutilizáveis e sem dependência de uma feature específica.
  • features: cada funcionalidade do app (ex.: auth, orders, profile), contendo UI e lógica daquela área.
  • assets: imagens, ícones, fontes, etc.

Estrutura de pastas recomendada

Exemplo de estrutura (ajuste conforme o tamanho do projeto):

src/app/  core/    http/      api-client.service.ts      http-error.mapper.ts    storage/      storage.service.ts    auth/      auth.facade.ts      token.service.ts    guards/      authenticated.guard.ts    config/      app-config.ts  shared/    ui/      empty-state/        empty-state.component.ts        empty-state.component.html        empty-state.component.scss        empty-state.component.spec.ts      app-toolbar/        app-toolbar.component.ts    directives/    pipes/    utils/      date.util.ts      string.util.ts  features/    orders/      data-access/        orders.api.ts        orders.repository.ts        orders.models.ts      ui/        order-card/          order-card.component.ts      pages/        orders-list/          orders-list.page.ts          orders-list.page.html          orders-list.page.scss        order-details/          order-details.page.ts      orders.routes.ts      orders.facade.ts    profile/      ...  app.routes.ts

O que vai em cada pasta

  • features/<feature>/pages: páginas (screens) Ionic. Devem conter o mínimo de lógica possível.
  • features/<feature>/ui: componentes visuais específicos da feature (cards, modais, listas).
  • features/<feature>/data-access: acesso a dados (API, storage, mapeamentos, repositórios).
  • features/<feature>/*.facade.ts: camada de orquestração (estado local, chamadas, regras simples), consumida pela UI.
  • shared/ui: componentes reutilizáveis (não devem importar coisas de features).
  • core: serviços globais e infraestrutura (ex.: cliente HTTP, log, config, auth base).

Padrões de nomenclatura e consistência

Arquivos e classes

  • Use kebab-case para nomes de arquivos e pastas: order-details.page.ts, orders.repository.ts.
  • Sufixos claros por responsabilidade: .page.ts, .component.ts, .service.ts, .facade.ts, .repository.ts, .api.ts, .models.ts.
  • Classes em PascalCase: OrdersRepository, OrdersFacade.
  • Interfaces e tipos: prefira nomes descritivos sem prefixo I (ex.: Order, CreateOrderPayload). Use type quando fizer sentido (uniões, mapeamentos) e interface para contratos extensíveis.

Imports e caminhos

Padronize imports para evitar caminhos relativos longos. Configure aliases no tsconfig (ex.: @core, @shared, @features) e use-os de forma consistente.

// bom (com alias)import { StorageService } from '@core/storage/storage.service';import { EmptyStateComponent } from '@shared/ui/empty-state/empty-state.component';

Tipagem consistente

  • Evite any. Prefira unknown quando necessário e faça narrowing.
  • Centralize modelos da feature em *.models.ts e reutilize-os em UI e data-access.
  • Crie tipos específicos para payloads e respostas: UpdateProfilePayload, OrdersListResponse.

Separação de responsabilidades (UI vs lógica vs dados)

Um padrão prático para apps Ionic é dividir em três camadas:

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

  • UI (pages/components): renderiza, captura eventos, exibe loading/erro. Não deve conhecer detalhes de HTTP, storage ou mapeamento.
  • Lógica (facade): coordena ações, validações simples, composição de chamadas, estado local (ex.: loading, error, items).
  • Acesso a dados (repository/api): conversa com API/Storage e converte DTOs em modelos do domínio.

Exemplo prático: Orders (lista e detalhes)

Imagine uma feature orders com uma lista e um detalhe. A UI chama a facade; a facade chama o repository; o repository chama a API.

1) Modelos da feature

// features/orders/data-access/orders.models.tsexport type OrderStatus = 'open' | 'paid' | 'canceled';export interface Order {  id: string;  title: string;  total: number;  status: OrderStatus;  createdAt: string;}

2) Camada de API (contrato HTTP)

// features/orders/data-access/orders.api.tsimport { Injectable } from '@angular/core';import { HttpClient } from '@angular/common/http';import { Observable } from 'rxjs';@Injectable({ providedIn: 'root' })export class OrdersApi {  constructor(private http: HttpClient) {}  list(): Observable<unknown> {    return this.http.get('/api/orders');  }  getById(id: string): Observable<unknown> {    return this.http.get(`/api/orders/${id}`);  }}

Note que a API retorna unknown aqui de propósito: isso força a camada seguinte a mapear/validar. Em projetos menores, você pode tipar diretamente, mas mantenha um ponto único de mapeamento.

3) Repository (mapeamento e regras de dados)

// features/orders/data-access/orders.repository.tsimport { Injectable } from '@angular/core';import { map, Observable } from 'rxjs';import { OrdersApi } from './orders.api';import { Order } from './orders.models';type OrderDto = {  id: string;  title: string;  total: number;  status: string;  createdAt: string;};function toOrder(dto: OrderDto): Order {  // aqui você normaliza e garante consistência  return {    id: String(dto.id),    title: dto.title ?? 'Sem título',    total: Number(dto.total ?? 0),    status: (dto.status as any) ?? 'open',    createdAt: dto.createdAt,  };}@Injectable({ providedIn: 'root' })export class OrdersRepository {  constructor(private api: OrdersApi) {}  list(): Observable<Order[]> {    return this.api.list().pipe(      map((raw) => (raw as OrderDto[]).map(toOrder))    );  }  getById(id: string): Observable<Order> {    return this.api.getById(id).pipe(      map((raw) => toOrder(raw as OrderDto))    );  }}

4) Facade (estado e orquestração)

// features/orders/orders.facade.tsimport { Injectable } from '@angular/core';import { BehaviorSubject, finalize } from 'rxjs';import { OrdersRepository } from './data-access/orders.repository';import { Order } from './data-access/orders.models';@Injectable({ providedIn: 'root' })export class OrdersFacade {  private readonly _items = new BehaviorSubject<Order[]>([]);  readonly items$ = this._items.asObservable();  private readonly _loading = new BehaviorSubject<boolean>(false);  readonly loading$ = this._loading.asObservable();  private readonly _error = new BehaviorSubject<string | null>(null);  readonly error$ = this._error.asObservable();  constructor(private repo: OrdersRepository) {}  loadList(): void {    this._loading.next(true);    this._error.next(null);    this.repo      .list()      .pipe(finalize(() => this._loading.next(false)))      .subscribe({        next: (items) => this._items.next(items),        error: () => this._error.next('Não foi possível carregar os pedidos.'),      });  }}

Essa facade facilita testes (você pode mockar o repository) e evita que a página tenha lógica de carregamento/erro espalhada.

5) Page (UI enxuta)

// features/orders/pages/orders-list/orders-list.page.tsimport { Component, OnInit } from '@angular/core';import { OrdersFacade } from '../../orders.facade';@Component({  selector: 'app-orders-list',  templateUrl: './orders-list.page.html',  styleUrls: ['./orders-list.page.scss'],})export class OrdersListPage implements OnInit {  readonly items$ = this.facade.items$;  readonly loading$ = this.facade.loading$;  readonly error$ = this.facade.error$;  constructor(private facade: OrdersFacade) {}  ngOnInit(): void {    this.facade.loadList();  }  onRefresh(): void {    this.facade.loadList();  }}
<!-- features/orders/pages/orders-list/orders-list.page.html --><ion-content>  <ng-container *ngIf="(loading$ | async) === true">    <ion-spinner></ion-spinner>  </ng-container>  <ng-container *ngIf="(error$ | async) as error">    <app-empty-state [message]="error"></app-empty-state>  </ng-container>  <ion-list>    <app-order-card      *ngFor="let item of (items$ | async)"      [order]="item">    </app-order-card>  </ion-list></ion-content>

Reaproveitamento com Shared Components (sem acoplamento)

Componentes em shared/ui devem ser genéricos e configuráveis por @Input(), sem depender de modelos de uma feature específica. Exemplo: um componente de estado vazio.

// shared/ui/empty-state/empty-state.component.tsimport { Component, Input } from '@angular/core';@Component({  selector: 'app-empty-state',  template: '<div class="empty">{{ message }}</div>',})export class EmptyStateComponent {  @Input() message = 'Nada por aqui.';}

Já um OrderCardComponent é específico de pedidos e deve ficar em features/orders/ui, pois depende do tipo Order e do layout daquela feature.

Passo a passo para refatorar um projeto existente para “por features”

Passo 1: escolha uma feature piloto

Selecione uma área pequena (ex.: profile ou orders). Evite tentar reorganizar tudo de uma vez.

Passo 2: crie a pasta da feature e mova a(s) páginas

  • Crie src/app/features/<feature>/pages.
  • Mova os arquivos da página para lá.
  • Ajuste imports relativos e rotas (se houver arquivo de rotas da feature, como orders.routes.ts).

Passo 3: extraia lógica da página para uma facade

  • Identifique: chamadas de serviço, controle de loading/erro, transformação de dados.
  • Crie <feature>.facade.ts e mova essa lógica.
  • Deixe a página apenas com bindings e handlers simples.

Passo 4: crie data-access (api/repository/models)

  • Crie data-access dentro da feature.
  • Separe o contrato HTTP (*.api.ts) do mapeamento e regras (*.repository.ts).
  • Centralize tipos em *.models.ts.

Passo 5: identifique componentes reutilizáveis e mova para shared

  • Se um componente é usado em 2+ features e não depende de modelos específicos, mova para shared/ui.
  • Se depende de modelos/fluxos de uma feature, mantenha em features/<feature>/ui.

Passo 6: padronize nomes e contratos

  • Renomeie arquivos para sufixos consistentes (.facade.ts, .repository.ts).
  • Garanta que a UI não importe diretamente api ou repository; ela deve falar com a facade.

Como evitar acoplamento (regras práticas)

SituaçãoEvitePrefira
Page precisa carregar dadosPage chamando HttpClient ou ApiService diretoPage chama Facade
Componente shared precisa de dadosImportar Order (feature) em sharedInputs genéricos (ex.: title, message)
Regras de transformaçãoMapear DTO na PageMapear no Repository
Dependências cruzadasfeatures/orders importando features/profileExtrair contrato para shared ou criar serviço em core

Organização de “core” para infraestrutura

Use core para itens que são globais e não pertencem a uma feature:

  • Config: app-config.ts (URLs, flags, chaves).
  • Storage: um StorageService com métodos genéricos (get/set/remove) e chaves centralizadas.
  • HTTP: cliente/adapter, mapeamento de erros, helpers de retry/backoff (quando aplicável).
  • Auth base: gerenciamento de token, sessão e utilitários de autenticação usados por várias features.

Regra: se algo é usado por várias features e não é UI, provavelmente é core.

Testabilidade e manutenção: como a estrutura ajuda

Com UI enxuta e dependências bem definidas, fica mais simples testar:

  • Repository: teste mapeamentos (DTO → Model) e regras de dados isoladamente.
  • Facade: teste fluxos (loading/erro/sucesso) mockando o repository.
  • UI: teste renderização e interação sem precisar conhecer detalhes de API.

Além disso, mudanças de API tendem a afetar apenas data-access, enquanto mudanças de layout afetam ui/pages, reduzindo efeitos colaterais.

Agora responda o exercício sobre o conteúdo:

Em uma estrutura de projeto Ionic organizada por features, qual abordagem melhor reduz acoplamento e melhora a manutenção ao separar responsabilidades entre UI, lógica e acesso a dados?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

A separação em UI → facade → repository/api mantém a UI enxuta, concentra orquestração e estado na facade e deixa acesso a dados/mapeamentos no data-access, reduzindo acoplamento e facilitando testes e refatorações.

Próximo capitúlo

Performance e experiência do usuário no Ionic: carregamento, feedback e otimização

Arrow Right Icon
Capa do Ebook gratuito Ionic para Iniciantes: aplicativos híbridos com HTML, CSS e TypeScript
81%

Ionic para Iniciantes: aplicativos híbridos com HTML, CSS e TypeScript

Novo curso

21 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.