HTTP e consumo de APIs no Ionic: serviços, interceptors e tratamento de erros

Capítulo 10

Tempo estimado de leitura: 13 minutos

+ Exercício

O que significa “consumir uma API” no Ionic (Angular)

No Ionic com Angular, o consumo de APIs REST normalmente é feito com o HttpClient. A ideia é simples: sua tela (page) não deve “conhecer” detalhes de URL, headers, autenticação ou como tratar erros. Em vez disso, você cria serviços responsáveis por conversar com a API e devolver dados tipados (interfaces). Assim, a página apenas chama métodos como listar(), criar(), atualizar() e remover() e lida com estados de carregamento e mensagens de erro.

Organização por camadas: environments, models e services

1) Environment: baseUrl e flags

Centralize URLs e configurações por ambiente. Isso evita “strings mágicas” espalhadas e facilita alternar entre mock e API real.

// src/environments/environment.ts
export const environment = {
  production: false,
  apiUrl: 'https://api.exemplo.com',
  useMockApi: true,
  enableHttpLogs: true
};
// src/environments/environment.prod.ts
export const environment = {
  production: true,
  apiUrl: 'https://api.exemplo.com',
  useMockApi: false,
  enableHttpLogs: false
};

2) Models: tipagem com interfaces

Crie interfaces para tipar respostas e payloads. Isso melhora autocomplete, reduz bugs e deixa o contrato mais explícito.

// src/app/models/todo.model.ts
export interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

export interface CreateTodoDto {
  title: string;
  completed?: boolean;
}

export interface UpdateTodoDto {
  title?: string;
  completed?: boolean;
}

3) Services: um serviço por recurso

Um padrão comum é ter um serviço por “recurso” (ex.: TodoService). Ele encapsula as chamadas HTTP e devolve Observable tipado.

Passo a passo: habilitando HttpClient

Garanta que o HttpClient esteja disponível no app.

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

Se seu projeto usa Angular com módulos (mais comum em projetos Ionic mais antigos)

// src/app/app.module.ts
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  imports: [
    HttpClientModule
  ]
})
export class AppModule {}

Se seu projeto usa Angular standalone (mais comum em projetos recentes)

// src/main.ts
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(withInterceptorsFromDi())
  ]
});

CRUD REST no serviço: GET/POST/PUT/DELETE

A seguir, um serviço completo com métodos REST típicos. Ele usa environment.apiUrl e tipa tudo com interfaces.

// src/app/services/todo.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
import { Todo, CreateTodoDto, UpdateTodoDto } from '../models/todo.model';

@Injectable({ providedIn: 'root' })
export class TodoService {
  private readonly baseUrl = `${environment.apiUrl}/todos`;

  constructor(private http: HttpClient) {}

  list(): Observable<Todo[]> {
    return this.http.get<Todo[]>(this.baseUrl);
  }

  getById(id: number): Observable<Todo> {
    return this.http.get<Todo>(`${this.baseUrl}/${id}`);
  }

  create(dto: CreateTodoDto): Observable<Todo> {
    return this.http.post<Todo>(this.baseUrl, dto);
  }

  update(id: number, dto: UpdateTodoDto): Observable<Todo> {
    return this.http.put<Todo>(`${this.baseUrl}/${id}`, dto);
  }

  remove(id: number): Observable<void> {
    return this.http.delete<void>(`${this.baseUrl}/${id}`);
  }
}

Tratamento de erros: padronizando mensagens e evitando “subscribe aninhado”

Erros HTTP podem ocorrer por falta de internet, timeout, 401/403 (autenticação), 404 (recurso não encontrado), 500 (erro no servidor) etc. Um bom padrão é: transformar o erro em uma mensagem amigável e deixar a tela decidir como exibir (toast/alert/inline).

Função utilitária para mapear erros

// src/app/utils/http-error.util.ts
import { HttpErrorResponse } from '@angular/common/http';

export function mapHttpError(err: unknown): string {
  if (err instanceof HttpErrorResponse) {
    if (err.status === 0) return 'Sem conexão com a internet ou servidor indisponível.';
    if (err.status === 400) return 'Requisição inválida. Verifique os dados enviados.';
    if (err.status === 401) return 'Sessão expirada. Faça login novamente.';
    if (err.status === 403) return 'Você não tem permissão para executar esta ação.';
    if (err.status === 404) return 'Recurso não encontrado.';
    if (err.status >= 500) return 'Erro no servidor. Tente novamente em instantes.';
    return `Erro HTTP ${err.status}.`;
  }
  return 'Erro inesperado.';
}

Aplicando no serviço com catchError

Você pode tratar no serviço e lançar um erro “amigável” para a UI, ou tratar na UI. Aqui vamos retornar um erro com mensagem já pronta.

// src/app/services/todo.service.ts (trecho)
import { catchError, throwError } from 'rxjs';
import { mapHttpError } from '../utils/http-error.util';

list(): Observable<Todo[]> {
  return this.http.get<Todo[]>(this.baseUrl).pipe(
    catchError((err) => throwError(() => new Error(mapHttpError(err))))
  );
}

Repita o mesmo padrão nos demais métodos, ou centralize via interceptor (veremos adiante).

Estados de carregamento: UX com spinner e “pull to refresh”

Em telas que consomem API, você normalmente precisa de três estados: carregando, sucesso e erro. Um padrão simples é controlar isso com variáveis na page.

  • loading = true enquanto busca dados
  • errorMessage = '' para exibir erro
  • items: Todo[] = [] para renderizar lista

Interceptors: headers comuns e logging controlado

Interceptors permitem interceptar todas as requisições/respostas HTTP. Isso é ideal para: adicionar headers (ex.: token), definir Content-Type, correlacionar logs, medir tempo, e padronizar tratamento de erros.

Interceptor de headers (ex.: Authorization e Accept)

// src/app/interceptors/auth-header.interceptor.ts
import { Injectable } from '@angular/core';
import {
  HttpInterceptor,
  HttpRequest,
  HttpHandler,
  HttpEvent
} from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class AuthHeaderInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    const token = localStorage.getItem('token');

    const cloned = req.clone({
      setHeaders: {
        Accept: 'application/json',
        ...(token ? { Authorization: `Bearer ${token}` } : {})
      }
    });

    return next.handle(cloned);
  }
}

Interceptor de logging (ativado por environment)

// src/app/interceptors/http-logging.interceptor.ts
import { Injectable } from '@angular/core';
import {
  HttpInterceptor,
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpResponse,
  HttpErrorResponse
} from '@angular/common/http';
import { Observable, tap } from 'rxjs';
import { environment } from 'src/environments/environment';

@Injectable()
export class HttpLoggingInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    const start = performance.now();

    return next.handle(req).pipe(
      tap({
        next: (event) => {
          if (!environment.enableHttpLogs) return;
          if (event instanceof HttpResponse) {
            const ms = Math.round(performance.now() - start);
            console.log('[HTTP]', req.method, req.urlWithParams, event.status, `${ms}ms`);
          }
        },
        error: (err) => {
          if (!environment.enableHttpLogs) return;
          const ms = Math.round(performance.now() - start);
          if (err instanceof HttpErrorResponse) {
            console.warn('[HTTP ERROR]', req.method, req.urlWithParams, err.status, `${ms}ms`);
          } else {
            console.warn('[HTTP ERROR]', req.method, req.urlWithParams, `${ms}ms`);
          }
        }
      })
    );
  }
}

Interceptor de erro (opcional) para padronizar mensagens

Se você preferir centralizar o catchError em um único lugar, pode transformar o erro aqui. Assim, seus serviços ficam mais “limpos”.

// src/app/interceptors/http-error.interceptor.ts
import { Injectable } from '@angular/core';
import {
  HttpInterceptor,
  HttpRequest,
  HttpHandler,
  HttpEvent
} from '@angular/common/http';
import { Observable, catchError, throwError } from 'rxjs';
import { mapHttpError } from '../utils/http-error.util';

@Injectable()
export class HttpErrorInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    return next.handle(req).pipe(
      catchError((err) => throwError(() => new Error(mapHttpError(err))))
    );
  }
}

Registrando interceptors

Em projetos com módulos:

// src/app/app.module.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { AuthHeaderInterceptor } from './interceptors/auth-header.interceptor';
import { HttpLoggingInterceptor } from './interceptors/http-logging.interceptor';
import { HttpErrorInterceptor } from './interceptors/http-error.interceptor';

providers: [
  { provide: HTTP_INTERCEPTORS, useClass: AuthHeaderInterceptor, multi: true },
  { provide: HTTP_INTERCEPTORS, useClass: HttpLoggingInterceptor, multi: true },
  { provide: HTTP_INTERCEPTORS, useClass: HttpErrorInterceptor, multi: true }
]

Em projetos standalone, basta garantir provideHttpClient(withInterceptorsFromDi()) e registrar os interceptors no DI (por exemplo, em app.config.ts ou providers do bootstrap).

Exemplo completo: tela listando e criando “Todos” com mock e depois API

Vamos montar uma tela que: (1) lista tarefas, (2) permite adicionar uma tarefa, (3) mostra loading e erro. Primeiro com um mock local (simulando API), depois trocando para uma API real.

1) Criando um “Mock API Service” para desenvolvimento

O mock ajuda a desenvolver UI e fluxos sem depender do backend. Ele retorna Observable e simula latência.

// src/app/services/todo-mock.service.ts
import { Injectable } from '@angular/core';
import { Observable, of, throwError, delay } from 'rxjs';
import { Todo, CreateTodoDto, UpdateTodoDto } from '../models/todo.model';

@Injectable({ providedIn: 'root' })
export class TodoMockService {
  private data: Todo[] = [
    { id: 1, title: 'Estudar HttpClient', completed: false },
    { id: 2, title: 'Implementar interceptor', completed: true }
  ];

  list(): Observable<Todo[]> {
    return of(this.data).pipe(delay(500));
  }

  create(dto: CreateTodoDto): Observable<Todo> {
    if (!dto.title || dto.title.trim().length < 3) {
      return throwError(() => new Error('Título muito curto.'));
    }
    const nextId = Math.max(...this.data.map(t => t.id), 0) + 1;
    const todo: Todo = { id: nextId, title: dto.title.trim(), completed: !!dto.completed };
    this.data = [todo, ...this.data];
    return of(todo).pipe(delay(400));
  }

  update(id: number, dto: UpdateTodoDto): Observable<Todo> {
    const idx = this.data.findIndex(t => t.id === id);
    if (idx < 0) return throwError(() => new Error('Item não encontrado.'));
    const updated: Todo = { ...this.data[idx], ...dto };
    this.data = this.data.map(t => (t.id === id ? updated : t));
    return of(updated).pipe(delay(400));
  }

  remove(id: number): Observable<void> {
    this.data = this.data.filter(t => t.id !== id);
    return of(void 0).pipe(delay(300));
  }
}

2) Criando uma “fachada” para alternar entre mock e API

Para não mudar a page quando trocar de mock para API real, crie um serviço “facade” que decide qual implementação usar com base no environment.useMockApi.

// src/app/services/todo-data.service.ts
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
import { Todo, CreateTodoDto, UpdateTodoDto } from '../models/todo.model';
import { TodoService } from './todo.service';
import { TodoMockService } from './todo-mock.service';

@Injectable({ providedIn: 'root' })
export class TodoDataService {
  constructor(
    private api: TodoService,
    private mock: TodoMockService
  ) {}

  private get impl() {
    return environment.useMockApi ? this.mock : this.api;
  }

  list(): Observable<Todo[]> {
    return this.impl.list();
  }

  create(dto: CreateTodoDto): Observable<Todo> {
    return this.impl.create(dto);
  }

  update(id: number, dto: UpdateTodoDto): Observable<Todo> {
    return this.impl.update(id, dto);
  }

  remove(id: number): Observable<void> {
    return this.impl.remove(id);
  }
}

3) Montando a tela (page): HTML com lista, spinner e erro

Exemplo de layout com Ionic Components: lista de itens, botão para adicionar e feedback visual.

<!-- src/app/pages/todos/todos.page.html -->
<ion-header>
  <ion-toolbar>
    <ion-title>Tarefas</ion-title>
    <ion-buttons slot="end">
      <ion-button (click)="reload()">Atualizar</ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-refresher slot="fixed" (ionRefresh)="reload($event)">
    <ion-refresher-content></ion-refresher-content>
  </ion-refresher>

  <ion-item>
    <ion-input
      label="Nova tarefa"
      labelPlacement="stacked"
      placeholder="Ex: Comprar pão"
      [(ngModel)]="newTitle">
    </ion-input>
  </ion-item>
  <ion-button expand="block" [disabled]="saving" (click)="add()">
    <ion-spinner *ngIf="saving" name="dots"></ion-spinner>
    <span *ngIf="!saving">Adicionar</span>
  </ion-button>

  <ion-item *ngIf="errorMessage" color="danger">
    <ion-label>{{ errorMessage }}</ion-label>
  </ion-item>

  <div style="padding: 16px" *ngIf="loading">
    <ion-spinner name="crescent"></ion-spinner>
    <p>Carregando...</p>
  </div>

  <ion-list *ngIf="!loading">
    <ion-item-sliding *ngFor="let t of todos">
      <ion-item>
        <ion-checkbox
          slot="start"
          [checked]="t.completed"
          (ionChange)="toggle(t)">
        </ion-checkbox>
        <ion-label>
          <h3 [style.textDecoration]="t.completed ? 'line-through' : 'none'">{{ t.title }}</h3>
          <p>ID: {{ t.id }}</p>
        </ion-label>
      </ion-item>
      <ion-item-options side="end">
        <ion-item-option color="danger" (click)="remove(t)">Excluir</ion-item-option>
      </ion-item-options>
    </ion-item-sliding>
  </ion-list>
</ion-content>

4) Lógica da tela (page): carregamento, erro e ações

Observe como a page chama apenas o TodoDataService (mock ou API), controla loading/saving e trata erros com mensagens amigáveis.

// src/app/pages/todos/todos.page.ts
import { Component, OnInit } from '@angular/core';
import { finalize } from 'rxjs';
import { Todo } from 'src/app/models/todo.model';
import { TodoDataService } from 'src/app/services/todo-data.service';

@Component({
  selector: 'app-todos',
  templateUrl: './todos.page.html'
})
export class TodosPage implements OnInit {
  todos: Todo[] = [];
  loading = false;
  saving = false;
  errorMessage = '';
  newTitle = '';

  constructor(private todoData: TodoDataService) {}

  ngOnInit(): void {
    this.load();
  }

  load(refresher?: any): void {
    this.loading = !refresher;
    this.errorMessage = '';

    this.todoData
      .list()
      .pipe(
        finalize(() => {
          this.loading = false;
          if (refresher) refresher.target.complete();
        })
      )
      .subscribe({
        next: (data) => (this.todos = data),
        error: (e: Error) => (this.errorMessage = e.message)
      });
  }

  reload(ev?: any): void {
    this.load(ev);
  }

  add(): void {
    this.saving = true;
    this.errorMessage = '';

    this.todoData
      .create({ title: this.newTitle })
      .pipe(finalize(() => (this.saving = false)))
      .subscribe({
        next: (created) => {
          this.todos = [created, ...this.todos];
          this.newTitle = '';
        },
        error: (e: Error) => (this.errorMessage = e.message)
      });
  }

  toggle(t: Todo): void {
    const nextCompleted = !t.completed;

    this.todoData.update(t.id, { completed: nextCompleted }).subscribe({
      next: (updated) => {
        this.todos = this.todos.map(x => (x.id === updated.id ? updated : x));
      },
      error: (e: Error) => (this.errorMessage = e.message)
    });
  }

  remove(t: Todo): void {
    this.todoData.remove(t.id).subscribe({
      next: () => {
        this.todos = this.todos.filter(x => x.id !== t.id);
      },
      error: (e: Error) => (this.errorMessage = e.message)
    });
  }
}

Trocando do mock para uma API real (sem mudar a tela)

Opção A: usar uma API pública para testes

Para testar rapidamente, você pode apontar para uma API pública como JSONPlaceholder. Ela fornece endpoints de /todos. Ajuste o environment.apiUrl e desligue o mock.

// src/environments/environment.ts
export const environment = {
  production: false,
  apiUrl: 'https://jsonplaceholder.typicode.com',
  useMockApi: false,
  enableHttpLogs: true
};

Observação: algumas APIs públicas não persistem alterações (POST/PUT/DELETE podem responder “ok” mas não salvar). Para fins didáticos, isso ainda é útil para validar fluxo, interceptors e tratamento de erros.

Opção B: sua API própria

Se você tiver um backend, mantenha o contrato do recurso /todos compatível com a interface Todo. Se o backend tiver campos diferentes (ex.: is_done), crie um mapeamento no service para converter para o modelo do app.

Checklist de boas práticas para consumo de APIs no Ionic

Problema comumSolução recomendada
URLs espalhadas no códigoCentralizar em environment.apiUrl e serviços por recurso
Dados sem tipagemCriar interfaces em models e tipar HttpClient (get<T>)
Headers repetidos em cada chamadaUsar interceptor para Authorization, Accept etc.
Logs poluindo produçãoControlar logs com flag (enableHttpLogs) no environment
Erros sem mensagem amigávelMapear HttpErrorResponse para mensagens claras (util ou interceptor)
UI sem feedbackEstados loading/saving + spinner + mensagens de erro

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

Ao consumir uma API no Ionic com Angular, qual prática ajuda a manter a página (page) desacoplada de detalhes como URL, headers e autenticação?

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

Você errou! Tente novamente.

Serviços por recurso centralizam URL, headers e autenticação (com apoio de environment e interceptors), e devolvem Observables tipados. Assim, a página foca em estados de loading/erro e na renderização.

Próximo capitúlo

Autenticação básica em apps Ionic: login, tokens e proteção de rotas

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

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.