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.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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 = trueenquanto busca dadoserrorMessage = ''para exibir erroitems: 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 comum | Solução recomendada |
|---|---|
| URLs espalhadas no código | Centralizar em environment.apiUrl e serviços por recurso |
| Dados sem tipagem | Criar interfaces em models e tipar HttpClient (get<T>) |
| Headers repetidos em cada chamada | Usar interceptor para Authorization, Accept etc. |
| Logs poluindo produção | Controlar logs com flag (enableHttpLogs) no environment |
| Erros sem mensagem amigável | Mapear HttpErrorResponse para mensagens claras (util ou interceptor) |
| UI sem feedback | Estados loading/saving + spinner + mensagens de erro |