HTTP / API
Ionic apps reach APIs over HTTP just like any web app, but the choice of stack matters: Angular’s HttpClient, fetch + a small wrapper for Vue/React, or the Capacitor native HTTP plugin to bypass CORS in production. Get the interceptors, retry policy, and offline behaviour right up front.
Angular HttpClient + Capacitor HTTP
EXAMPLE
// 1) Angular HttpClient — most common Ionic stack
// app.module.ts
import { HttpClientModule } from '@angular/common/http';
@NgModule({
imports: [HttpClientModule, ...],
})
export class AppModule {}
// 2) Service
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
import { catchError, retry, throwError, Observable, of, map, timeout } from 'rxjs';
export interface Todo { id: number; title: string; done: boolean; }
@Injectable({ providedIn: 'root' })
export class TodoApi {
private base = 'https://api.example.com';
constructor(private http: HttpClient) {}
list(filter: 'all' | 'done' | 'open' = 'all'): Observable<Todo[]> {
return this.http.get<Todo[]>(`${this.base}/todos`, {
params: new HttpParams().set('filter', filter),
}).pipe(
timeout(8000),
retry({ count: 2, delay: 500 }),
catchError((err) => {
console.error('list failed', err);
return throwError(() => new Error('Could not load todos'));
}),
);
}
create(title: string): Observable<Todo> {
return this.http.post<Todo>(`${this.base}/todos`, { title }, {
headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
});
}
toggle(id: number, done: boolean): Observable<Todo> {
return this.http.patch<Todo>(`${this.base}/todos/${id}`, { done });
}
remove(id: number): Observable<void> {
return this.http.delete<void>(`${this.base}/todos/${id}`);
}
}
// 3) Page using it
import { Component, inject, signal } from '@angular/core';
import { TodoApi, Todo } from '../services/todo-api.service';
@Component({
selector: 'app-todos',
templateUrl: 'todos.page.html',
})
export class TodosPage {
private api = inject(TodoApi);
todos = signal<Todo[]>([]);
loading = signal(false);
ionViewWillEnter() {
this.loading.set(true);
this.api.list().subscribe({
next: (rows) => this.todos.set(rows),
error: (e) => console.error(e),
complete: () => this.loading.set(false),
});
}
add(title: string) {
this.api.create(title).subscribe((t) => this.todos.update((rows) => [...rows, t]));
}
}
// 4) Auth interceptor — attach Bearer + refresh on 401
import { HttpInterceptorFn, HttpRequest, HttpHandlerFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { from, switchMap } from 'rxjs';
import { AuthService } from './auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
return from(auth.getToken()).pipe(
switchMap((token) => {
const cloned = token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req;
return next(cloned);
}),
);
};
// Register (standalone-style)
import { provideHttpClient, withInterceptors } from '@angular/common/http';
bootstrapApplication(AppComponent, {
providers: [provideHttpClient(withInterceptors([authInterceptor]))],
});
// 5) CORS on native — use the Capacitor HTTP plugin
// On a native build, the WebView origin is 'capacitor://localhost' (iOS) or 'http://localhost' (Android).
// Most APIs reject these origins. Solutions:
// • Configure the API's CORS to allow Capacitor origins (preferred)
// • Use @capacitor/http (Capacitor 5+) — native HTTP, no CORS rules apply
// npm install @capacitor/http
import { CapacitorHttp } from '@capacitor/http';
async function nativeFetch() {
const res = await CapacitorHttp.get({
url: 'https://api.example.com/todos',
params: { filter: 'all' },
headers: { Authorization: `Bearer ${token}` },
});
return res.data as Todo[];
}
// You can also patch the global fetch to route through native:
// capacitor.config.ts
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.app',
appName: 'App',
webDir: 'www',
plugins: { CapacitorHttp: { enabled: true } }, // patches window.fetch on native
};
export default config;
// 6) File upload — multipart
async function upload(file: File) {
const form = new FormData();
form.append('file', file);
form.append('purpose', 'avatar');
return fetch('https://api.example.com/upload', { method: 'POST', body: form })
.then((r) => r.json());
}
// Capacitor HTTP (handles large files better natively)
await CapacitorHttp.post({
url: 'https://api.example.com/upload',
headers: { 'Content-Type': 'multipart/form-data' },
data: form,
});
// 7) File download to local storage
import { Filesystem, Directory } from '@capacitor/filesystem';
import { Http } from '@capacitor/http';
async function download(url: string, name: string) {
const res = await Http.get({ url, responseType: 'blob' });
await Filesystem.writeFile({
path: name,
directory: Directory.Data,
data: res.data, // base64 string
});
}
// 8) Offline + retry
import { Network } from '@capacitor/network';
Network.addListener('networkStatusChange', (status) => {
if (status.connected) flushQueue();
});
async function safePost(payload: unknown) {
const { connected } = await Network.getStatus();
if (!connected) { enqueueForLater(payload); return; }
await fetch('/api/x', { method: 'POST', body: JSON.stringify(payload) });
}
// 9) Timeouts and abort
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 8000);
fetch('/api/slow', { signal: ctrl.signal }).catch((e) => {
if (e.name === 'AbortError') console.warn('timed out');
});
// 10) Loading states + UX
// Use Ionic's IonLoading or skeletons; never spin without showing what's happening.
import { LoadingController } from '@ionic/angular';
const loading = await this.loadingCtrl.create({ message: 'Loading…' });
await loading.present();
try { await api.fetch(); } finally { await loading.dismiss(); }
// 11) Common bugs
// • CORS errors only happen on web/dev builds — they vanish on native if you use @capacitor/http
// • Forgetting to unsubscribe from long-lived observables → memory leak; use takeUntilDestroyed()
// • HttpClient request never fires until you subscribe — cold observables are a pitfall
// • Hard-coding API URLs — use environment.ts files per build target
// • Refresh token loops on 401 — guard against repeated refresh storms
// • Native fetch returns base64 for blobs — read the response type before parsing
Why it matters
On the web HttpClient or fetch are fine, but on native Ionic ships the WebView at capacitor://localhost — either configure your API’s CORS to allow that origin or enable @capacitor/http to route requests through native code and skip CORS entirely. Pair that with interceptors for auth, an interceptor or RxJS retry for transient failures, and an offline queue so users don’t lose work on the train.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Fetch is fine for web. For native + cookies, use Capacitor Http.
import { CapacitorHttp } from '@capacitor/core';
const res = await CapacitorHttp.get({ url: 'https://api.example.com/me' });
Try it Yourself »
Discussion
Loading…