Conectar Ionic con una API REST
En una app Ionic, el consumo de APIs REST suele centralizarse en servicios (por ejemplo, ApiService) para mantener las páginas limpias y reutilizar lógica: URL base, headers, autenticación, reintentos, timeouts y manejo de errores. En el stack Angular (Ionic Angular), el cliente recomendado es HttpClient, que devuelve Observables (RxJS). Esto permite componer asincronía con operadores como map, switchMap, catchError y retry.
Instalación y configuración del cliente HTTP
En Ionic Angular, habilita HttpClient importando HttpClientModule en el módulo principal (o en el módulo correspondiente si tu app está modularizada).
// app.module.ts (o core.module.ts)\nimport { HttpClientModule } from '@angular/common/http';\n\n@NgModule({\n imports: [\n HttpClientModule,\n // ...otros imports\n ]\n})\nexport class AppModule {}Define una URL base por entorno para no “quemar” endpoints en el código. En Angular se suele usar environment.ts.
// environment.ts\nexport const environment = {\n production: false,\n apiUrl: 'https://api.ejemplo.com'\n};Modelo de datos y contrato con la API
Para evitar errores por cambios en el backend, tipa las respuestas con interfaces. Esto mejora autocompletado y validación en tiempo de compilación.
// models/todo.model.ts\nexport interface Todo {\n id: string;\n title: string;\n done: boolean;\n}\n\nexport interface ApiListResponse<T> {\n data: T[];\n total?: number;\n}Servicio API: GET/POST/PUT/DELETE, parámetros y headers
Un patrón práctico es crear un servicio que exponga métodos por recurso. Así, las páginas solo se suscriben y renderizan.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
// services/todos-api.service.ts\nimport { Injectable } from '@angular/core';\nimport { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';\nimport { Observable } from 'rxjs';\nimport { environment } from '../../environments/environment';\nimport { Todo, ApiListResponse } from '../models/todo.model';\n\n@Injectable({ providedIn: 'root' })\nexport class TodosApiService {\n private baseUrl = `${environment.apiUrl}/todos`;\n\n constructor(private http: HttpClient) {}\n\n list(params?: { q?: string; done?: boolean; page?: number; pageSize?: number }): Observable<ApiListResponse<Todo>> {\n let httpParams = new HttpParams();\n if (params?.q) httpParams = httpParams.set('q', params.q);\n if (params?.done !== undefined) httpParams = httpParams.set('done', String(params.done));\n if (params?.page) httpParams = httpParams.set('page', String(params.page));\n if (params?.pageSize) httpParams = httpParams.set('pageSize', String(params.pageSize));\n\n return this.http.get<ApiListResponse<Todo>>(this.baseUrl, { params: httpParams });\n }\n\n getById(id: string): Observable<Todo> {\n return this.http.get<Todo>(`${this.baseUrl}/${id}`);\n }\n\n create(payload: { title: string }): Observable<Todo> {\n return this.http.post<Todo>(this.baseUrl, payload);\n }\n\n update(id: string, payload: Partial<Pick<Todo, 'title' | 'done'>>): Observable<Todo> {\n return this.http.put<Todo>(`${this.baseUrl}/${id}`, payload);\n }\n\n remove(id: string): Observable<void> {\n return this.http.delete<void>(`${this.baseUrl}/${id}`);\n }\n}Headers comunes y content-type
En la mayoría de APIs JSON, HttpClient ya envía/recibe JSON correctamente. Si necesitas headers específicos (por ejemplo, X-Request-Id o Accept-Language), puedes agregarlos por request o, preferiblemente, con un interceptor (ver sección de autenticación y errores).
const headers = new HttpHeaders({\n 'X-Client': 'ionic-app',\n 'Accept-Language': 'es'\n});\n\nreturn this.http.get(url, { headers });Autenticación basada en token (conceptual) y cómo aplicarla
En autenticación basada en token, el backend emite un token (por ejemplo, JWT) tras un login. La app lo guarda de forma segura y lo envía en cada request protegido, normalmente en el header Authorization: Bearer <token>. Conceptualmente, el flujo es:
- Usuario inicia sesión → backend valida credenciales.
- Backend responde con token (y opcionalmente refresh token).
- App almacena el token (idealmente en almacenamiento seguro).
- App adjunta el token en requests a endpoints protegidos.
- Si el token expira, el backend responde 401; la app puede pedir uno nuevo (refresh) o forzar login.
Interceptor para adjuntar el token automáticamente
Un interceptor evita repetir lógica de headers en cada servicio. Aquí se muestra un ejemplo conceptual que obtiene el token desde un servicio de sesión.
// interceptors/auth.interceptor.ts\nimport { Injectable } from '@angular/core';\nimport { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';\nimport { Observable } from 'rxjs';\nimport { SessionService } from '../services/session.service';\n\n@Injectable()\nexport class AuthInterceptor implements HttpInterceptor {\n constructor(private session: SessionService) {}\n\n intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {\n const token = this.session.getToken(); // síncrono para simplificar\n if (!token) return next.handle(req);\n\n const authReq = req.clone({\n setHeaders: { Authorization: `Bearer ${token}` }\n });\n\n return next.handle(authReq);\n }\n}Registra el interceptor en el módulo:
// app.module.ts\nimport { HTTP_INTERCEPTORS } from '@angular/common/http';\nimport { AuthInterceptor } from './interceptors/auth.interceptor';\n\nproviders: [\n { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }\n]Si tu token se obtiene de forma asíncrona (por ejemplo, desde un storage), puedes adaptar el interceptor usando RxJS (por ejemplo, from() + switchMap) para esperar el token antes de clonar el request.
Asincronía en Ionic Angular: Observables vs Promises
En Angular, HttpClient devuelve Observables. La diferencia práctica:
- Promise: un único valor futuro (o error). Se resuelve una vez. Útil para flujos simples o APIs que ya devuelven promesas.
- Observable: flujo que puede emitir 0..N valores, cancelable, y con operadores para transformar, reintentar, combinar y manejar errores de forma declarativa.
Cuándo convertir a Promise
Si necesitas integrar con una API que espera async/await, puedes convertir un Observable a Promise. En RxJS moderno se recomienda firstValueFrom o lastValueFrom.
import { firstValueFrom } from 'rxjs';\n\nconst todo = await firstValueFrom(this.todosApi.getById('123'));Si tu UI se beneficia de cancelación (por ejemplo, búsquedas mientras el usuario escribe), mantén Observables y usa switchMap para cancelar requests anteriores.
Transformación de datos con operadores
Es común que la API responda con estructuras que no coinciden exactamente con lo que quieres renderizar. Usa map para transformar y normalizar.
import { map } from 'rxjs/operators';\n\nlistDoneTitles$ = this.todosApi.list({ done: true }).pipe(\n map(res => res.data.map(t => t.title))\n);Reintentos, timeouts y resiliencia
En móvil, la conectividad es variable. Dos herramientas típicas:
- Timeout: corta requests que tardan demasiado para no dejar la UI “colgada”.
- Retry: reintenta ante fallos transitorios (por ejemplo, 502/503 o cortes momentáneos).
Timeout por request
import { timeout } from 'rxjs/operators';\n\nreturn this.http.get(url).pipe(\n timeout(8000)\n);Reintentos con estrategia
Evita reintentar indiscriminadamente errores 400/401. Un enfoque es reintentar solo errores de red o 5xx. Con retry simple:
import { retry } from 'rxjs/operators';\n\nreturn this.http.get(url).pipe(\n retry(2)\n);Para una estrategia más fina, usa retry con configuración (RxJS 7+) o retryWhen para backoff. Ejemplo conceptual con backoff lineal:
import { retry, timer } from 'rxjs';\nimport { mergeMap } from 'rxjs/operators';\n\nreturn this.http.get(url).pipe(\n retry({\n count: 2,\n delay: (error, retryCount) => {\n // Reintenta solo si es 0 (network) o 5xx\n const status = error?.status;\n const retryable = status === 0 || (status >= 500 && status < 600);\n if (!retryable) throw error;\n return timer(500 * retryCount);\n }\n })\n);Manejo centralizado de errores
Centralizar errores evita duplicar catchError en cada método y permite un patrón consistente: mapear errores técnicos a mensajes útiles, registrar diagnósticos y decidir acciones (por ejemplo, logout en 401).
Normalizar errores en un interceptor
Un interceptor de errores puede:
- Detectar 401/403 y disparar un flujo de sesión (por ejemplo, redirigir a login).
- Mapear errores de red (status 0) a “Sin conexión”.
- Extraer mensajes del backend (por ejemplo,
error.message).
// interceptors/error.interceptor.ts\nimport { Injectable } from '@angular/core';\nimport { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';\nimport { Observable, throwError } from 'rxjs';\nimport { catchError } from 'rxjs/operators';\n\nexport interface AppHttpError {\n status: number;\n code?: string;\n message: string;\n raw?: any;\n}\n\n@Injectable()\nexport class ErrorInterceptor implements HttpInterceptor {\n intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {\n return next.handle(req).pipe(\n catchError((err: HttpErrorResponse) => {\n const appError: AppHttpError = this.toAppError(err);\n // Aquí podrías notificar a un servicio global de UI o logging\n return throwError(() => appError);\n })\n );\n }\n\n private toAppError(err: HttpErrorResponse): AppHttpError {\n if (err.status === 0) {\n return { status: 0, message: 'No se pudo conectar. Revisa tu conexión a internet.', raw: err };\n }\n\n const backendMessage = (err.error && (err.error.message || err.error.error)) ? (err.error.message || err.error.error) : null;\n\n if (err.status === 401) return { status: 401, message: 'Tu sesión expiró. Inicia sesión nuevamente.', raw: err };\n if (err.status === 403) return { status: 403, message: 'No tienes permisos para realizar esta acción.', raw: err };\n if (err.status === 404) return { status: 404, message: 'Recurso no encontrado.', raw: err };\n if (err.status >= 500) return { status: err.status, message: 'Error del servidor. Intenta más tarde.', raw: err };\n\n return { status: err.status, message: backendMessage || 'Solicitud inválida.', raw: err };\n }\n}Regístralo igual que el interceptor de auth:
providers: [\n { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },\n { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }\n]Patrón de consumo en páginas: éxito, vacío y error
En la UI, evita mezclar lógica de red con render. Un patrón consistente es manejar tres estados: loading, empty y error, además del estado “con datos”.
| Estado | Qué mostrar | Cuándo |
|---|---|---|
| Loading | Spinner/Skeleton | Mientras la petición está en curso |
| Empty | Mensaje + acción (recargar/crear) | Respuesta OK pero lista vacía |
| Error | Mensaje claro + reintentar | Fallo de red/servidor/permiso |
| Data | Lista/Detalle | Respuesta OK con contenido |
Guía práctica paso a paso: lista de datos con loading, vacío y error
Paso 1: crear un ViewModel simple en la página
Usaremos variables de estado y un método load(). Mantén el servicio de API separado.
// pages/todos/todos.page.ts\nimport { Component } from '@angular/core';\nimport { TodosApiService } from '../../services/todos-api.service';\nimport { Todo } from '../../models/todo.model';\nimport { AppHttpError } from '../../interceptors/error.interceptor';\nimport { finalize } from 'rxjs/operators';\n\n@Component({\n selector: 'app-todos',\n templateUrl: './todos.page.html'\n})\nexport class TodosPage {\n loading = false;\n errorMessage: string | null = null;\n todos: Todo[] = [];\n\n constructor(private todosApi: TodosApiService) {}\n\n ionViewWillEnter() {\n this.load();\n }\n\n load(event?: any) {\n this.loading = true;\n this.errorMessage = null;\n\n this.todosApi.list({ page: 1, pageSize: 20 }).pipe(\n finalize(() => {\n this.loading = false;\n if (event) event.target.complete();\n })\n ).subscribe({\n next: (res) => {\n this.todos = res.data;\n },\n error: (err: AppHttpError) => {\n this.errorMessage = err.message;\n this.todos = [];\n }\n });\n }\n\n get isEmpty(): boolean {\n return !this.loading && !this.errorMessage && this.todos.length === 0;\n }\n}Paso 2: plantilla con patrón consistente
La idea es que la UI sea predecible: primero loading, luego error, luego vacío, luego datos.
<!-- todos.page.html -->\n<ion-header>\n <ion-toolbar>\n <ion-title>Todos</ion-title>\n </ion-toolbar>\n</ion-header>\n\n<ion-content>\n <ion-refresher slot="fixed" (ionRefresh)="load($event)">\n <ion-refresher-content></ion-refresher-content>\n </ion-refresher>\n\n <ng-container *ngIf="loading">\n <ion-list>\n <ion-item *ngFor="let i of [1,2,3,4,5]">\n <ion-skeleton-text animated style="width: 70%"></ion-skeleton-text>\n </ion-item>\n </ion-list>\n </ng-container>\n\n <ng-container *ngIf="!loading && errorMessage">\n <ion-card>\n <ion-card-header>\n <ion-card-title>Ocurrió un problema</ion-card-title>\n </ion-card-header>\n <ion-card-content>\n <p>{{ errorMessage }}</p>\n <ion-button expand="block" (click)="load()">Reintentar</ion-button>\n </ion-card-content>\n </ion-card>\n </ng-container>\n\n <ng-container *ngIf="isEmpty">\n <ion-card>\n <ion-card-header>\n <ion-card-title>Sin resultados</ion-card-title>\n </ion-card-header>\n <ion-card-content>\n <p>Aún no hay elementos para mostrar.</p>\n <ion-button expand="block" (click)="load()">Actualizar</ion-button>\n </ion-card-content>\n </ion-card>\n </ng-container>\n\n <ion-list *ngIf="!loading && !errorMessage && todos.length > 0">\n <ion-item *ngFor="let t of todos">\n <ion-label>\n <h2>{{ t.title }}</h2>\n <p>Estado: {{ t.done ? 'Hecho' : 'Pendiente' }}</p>\n </ion-label>\n </ion-item>\n </ion-list>\n</ion-content>Guía práctica: POST/PUT/DELETE con feedback y manejo de errores
Para operaciones de escritura, el patrón recomendado es: deshabilitar UI mientras se envía, mostrar indicador de carga, manejar error con mensaje claro y refrescar la lista al finalizar.
Crear (POST) con loader
// pages/todos/todos.page.ts (fragmento)\nimport { LoadingController, ToastController } from '@ionic/angular';\n\nconstructor(\n private todosApi: TodosApiService,\n private loadingCtrl: LoadingController,\n private toastCtrl: ToastController\n) {}\n\nasync addTodo(title: string) {\n const loader = await this.loadingCtrl.create({ message: 'Guardando...' });\n await loader.present();\n\n this.todosApi.create({ title }).pipe(\n finalize(() => loader.dismiss())\n ).subscribe({\n next: async (created) => {\n const toast = await this.toastCtrl.create({ message: 'Creado correctamente', duration: 1500 });\n await toast.present();\n this.load();\n },\n error: async (err) => {\n const toast = await this.toastCtrl.create({ message: err.message || 'No se pudo crear', duration: 2000, color: 'danger' });\n await toast.present();\n }\n });\n}Actualizar (PUT) optimista vs conservador
Dos enfoques comunes:
- Conservador: actualizas la UI solo cuando el backend confirma.
- Optimista: actualizas la UI inmediatamente y haces rollback si falla.
Ejemplo conservador para marcar como hecho:
toggleDone(todo: Todo) {\n const newValue = !todo.done;\n this.todosApi.update(todo.id, { done: newValue }).subscribe({\n next: (updated) => {\n todo.done = updated.done;\n },\n error: (err) => {\n this.errorMessage = err.message;\n }\n });\n}Eliminar (DELETE) con confirmación y reintento
import { AlertController } from '@ionic/angular';\n\nconstructor(private alertCtrl: AlertController, private todosApi: TodosApiService) {}\n\nasync confirmDelete(todo: Todo) {\n const alert = await this.alertCtrl.create({\n header: 'Eliminar',\n message: `¿Eliminar "${todo.title}"?`,\n buttons: [\n { text: 'Cancelar', role: 'cancel' },\n {\n text: 'Eliminar',\n role: 'destructive',\n handler: () => {\n this.todosApi.remove(todo.id).subscribe({\n next: () => this.load(),\n error: (err) => this.errorMessage = err.message\n });\n }\n }\n ]\n });\n await alert.present();\n}Parámetros, filtros y paginación: buenas prácticas
- Usa
HttpParamspara query params (evita concatenar strings manualmente). - Normaliza tipos: booleans y números se envían como string en query params.
- Evita enviar params vacíos: solo setea los que existan.
- Paginación: define un contrato claro con el backend (
page,pageSize,total).
Ejemplo de llamada con filtros:
this.todosApi.list({ q: 'comprar', done: false, page: 1, pageSize: 10 })\n .subscribe(res => this.todos = res.data);Patrones consistentes de UI para red
Indicadores de carga
- Skeleton para listas (mejor percepción de rendimiento).
- LoadingController para acciones puntuales (guardar/eliminar).
- Pull-to-refresh para recarga manual.
Estados vacíos accionables
Un estado vacío útil no solo dice “no hay datos”, también ofrece una acción: actualizar, cambiar filtros o crear un elemento.
Mensajes de error claros y reutilizables
Evita mostrar errores crudos (stack traces o mensajes técnicos). Mapea a mensajes orientados a acción: “Revisa tu conexión”, “Vuelve a iniciar sesión”, “Intenta más tarde”. Si necesitas consistencia global, crea un UiFeedbackService que reciba AppHttpError y decida si mostrar toast, alert o banner según severidad.
Checklist de implementación para un consumo de API robusto
- URL base por entorno (
environment.apiUrl). - Servicios por recurso con métodos GET/POST/PUT/DELETE.
- Interceptor de auth para
Authorization: Bearer. - Interceptor de errores para normalizar mensajes y estados (401/403/0/5xx).
- Timeouts razonables (por ejemplo, 8–15s según endpoint).
- Reintentos solo para fallos transitorios (status 0 o 5xx).
- UI con estados: loading, empty, error, data; y acciones de reintento/refresh.