Skip to content

Patrón de llamada API.

Las llamadas API son uno de los procesos más frecuentemente implementados en el desarrollo web, y RxJS permite implementar complejas llamadas API asíncronas de forma declarativa y robusta.

Este artículo describe patrones de implementación concretos para varios escenarios de llamadas a la API que se encuentran en la práctica, incluyendo la gestión de errores y la gestión de cancelaciones.

Qué aprenderás en este artículo.

  • Implementación básica de peticiones GET/POST
  • Invocación paralela de múltiples APIs (forkJoin)
  • Peticiones en serie que requieren ejecución secuencial (concatMap)
  • Encadenamiento de peticiones con dependencias (switchMap)
  • Reintentos y gestión de errores
  • Gestión del tiempo de espera
  • Cancelación de solicitudes
typescript
import { from, Observable, catchError } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Posttipo
// https://jsonplaceholder.typicode.com/posts
interface Post {
  id: number;
  userId: number;
  title: string;
  body: string;
}

interface CreatePostRequest {
  userId: number;
  title: string;
  body: string;
}

function createPost(postData: CreatePostRequest): Observable<Post> {
  return from(
    fetch('https://jsonplaceholder.typicode.com/posts', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(postData)
    }).then(response => {
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return response.json();
    })
  ).pipe(
    catchError((err: unknown) => {
      console.error('Error al crear una entrada:', err);
      throw err;
    })
  );
}

// Ejemplo de uso
createPost({
  userId: 1,
  title: 'RxJSAprendiendo de',
  body: 'RxJSutilizando elAPIAprendizaje del patrón de llamadas a'
}).subscribe({
  next: post => {
    console.log('Mensajes creados:', post);
    console.log('Un postID:', post.id); // JSONPlaceholderseIDles asigna (por ejemplo: 101)
  },
  error: err => console.error('Error:', err)
});

Este artículo forma parte del Capítulo 4: Operadores y Capítulo 6: Tratamiento de errores.

Llamadas básicas a la API.

Problema: Petición GET simple.

El caso más básico implementa una simple petición GET.

Ejemplo de implementación.

typescript
import { from, Observable, map, catchError, timeout } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Usertipo
// https://jsonplaceholder.typicode.com/users
interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    street: string;
    suite: string;
    city: string;
    zipcode: string;
    geo: {
      lat: string;
      lng: string;
    };
  };
  phone: string;
  website: string;
  company: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
}

// Recuperar lista de usuarios
function fetchUsers(): Observable<User[]> {
  return from(
    fetch('https://jsonplaceholder.typicode.com/users')
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
      })
  ).pipe(
    timeout(5000), // 5Tiempo de espera en segundos
    catchError((err: unknown) => {
      console.error('Error de adquisición de usuario:', err);
      throw err;
    })
  );
}

// Ejemplo de uso
fetchUsers().subscribe({
  next: users => {
    console.log('Lista de usuarios:', users);
    console.log('Primer usuario:', users[0].name); // Ejemplo: "Leanne Graham"
  },
  error: err => console.error('Error:', err)
});
typescript
import { from, Observable, catchError } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Posttipo
// https://jsonplaceholder.typicode.com/posts
interface Post {
  id: number;
  userId: number;
  title: string;
  body: string;
}

interface CreatePostRequest {
  userId: number;
  title: string;
  body: string;
}

function createPost(postData: CreatePostRequest): Observable<Post> {
  return from(
    fetch('https://jsonplaceholder.typicode.com/posts', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(postData)
    }).then(response => {
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return response.json();
    })
  ).pipe(
    catchError((err: unknown) => {
      console.error('Error al crear una entrada:', err);
      throw err;
    })
  );
}

// Ejemplo de uso
createPost({
  userId: 1,
  title: 'RxJSAprendiendo de',
  body: 'RxJSutilizando elAPIAprendizaje del patrón de llamadas a'
}).subscribe({
  next: post => {
    console.log('Mensajes creados:', post);
    console.log('Un postID:', post.id); // JSONPlaceholderseIDles asigna (por ejemplo: 101)
  },
  error: err => console.error('Error:', err)
});

Este ejemplo utiliza el método estándar fetch con from(), pero también puedes utilizar el método oficial de RxJS ajax(). ajax()` es más sofisticado y soporta cancelación de peticiones y monitorización del progreso.

Petición POST.

Patrón para crear nuevos datos.

typescript
import { from, Observable, catchError } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Posttipo
// https://jsonplaceholder.typicode.com/posts
interface Post {
  id: number;
  userId: number;
  title: string;
  body: string;
}

interface CreatePostRequest {
  userId: number;
  title: string;
  body: string;
}

function createPost(postData: CreatePostRequest): Observable<Post> {
  return from(
    fetch('https://jsonplaceholder.typicode.com/posts', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(postData)
    }).then(response => {
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return response.json();
    })
  ).pipe(
    catchError((err: unknown) => {
      console.error('Error al crear una entrada:', err);
      throw err;
    })
  );
}

// Ejemplo de uso
createPost({
  userId: 1,
  title: 'RxJSAprendiendo de',
  body: 'RxJSutilizando elAPIAprendizaje del patrón de llamadas a'
}).subscribe({
  next: post => {
    console.log('Mensajes creados:', post);
    console.log('Un postID:', post.id); // JSONPlaceholderseIDles asigna (por ejemplo: 101)
  },
  error: err => console.error('Error:', err)
});

Consejos prácticos

  • Seguridad de tipo: definir claramente el tipo de respuesta
  • Manejo de errores: comprobar adecuadamente los códigos de estado HTTP
  • Timeouts: evitar largas esperas

Peticiones paralelas (forkJoin)

Problema: Quiero llamar a múltiples APIs al mismo tiempo.

Es posible que desee llamar a varias API independientes en paralelo y proceder sólo después de haber recibido todas las respuestas.

Solución: utilice forkJoin.

forkJoin` espera a que se completen múltiples Observable y devuelve todos los resultados en un array (equivalente a Promise.all).

typescript
import { forkJoin, from, Observable, map } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Commenttipo
// https://jsonplaceholder.typicode.com/comments
interface Comment {
  postId: number;
  id: number;
  name: string;
  email: string;
  body: string;
}
interface Post {
  id: number;
  userId: number;
  title: string;
  body: string;
}
interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    street: string;
    suite: string;
    city: string;
    zipcode: string;
    geo: {
      lat: string;
      lng: string;
    };
  };
  phone: string;
  website: string;
  company: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
}
interface Dashboard {
  user: User;
  posts: Post[];
  comments: Comment[];
}

function fetchUserById(id: number): Observable<User> {
  return from(
    fetch(`https://jsonplaceholder.typicode.com/users/${id}`).then(r => r.json())
  );
}

function fetchPostsByUserId(userId: number): Observable<Post[]> {
  return from(
    fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`).then(r => r.json())
  );
}

function fetchCommentsByPostId(postId: number): Observable<Comment[]> {
  return from(
    fetch(`https://jsonplaceholder.typicode.com/comments?postId=${postId}`).then(r => r.json())
  );
}

// Recuperación paralela de los datos del tablero
function fetchDashboard(userId: number): Observable<Dashboard> {
  return forkJoin({
    user: fetchUserById(userId),
    posts: fetchPostsByUserId(userId),
    comments: fetchCommentsByPostId(1) // Un postID=1Recuperar comentarios de
  }).pipe(
    map(({ user, posts, comments }) => ({
      user,
      posts,
      comments
    }))
  );
}

// Ejemplo de uso
fetchDashboard(1).subscribe({
  next: dashboard => {
    console.log('Usuario:', dashboard.user.name); // Ejemplo: "Leanne Graham"
    console.log('Número de entradas:', dashboard.posts.length); // Ejemplo: 10Número de entradas
    console.log('Número de comentarios:', dashboard.comments.length); // Ejemplo: 5Número de entradas
  },
  error: err => console.error('Error de adquisición del panel:', err)
});

Flujo de ejecución

typescript
import { from, Observable, catchError } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Posttipo
// https://jsonplaceholder.typicode.com/posts
interface Post {
  id: number;
  userId: number;
  title: string;
  body: string;
}

interface CreatePostRequest {
  userId: number;
  title: string;
  body: string;
}

function createPost(postData: CreatePostRequest): Observable<Post> {
  return from(
    fetch('https://jsonplaceholder.typicode.com/posts', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(postData)
    }).then(response => {
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return response.json();
    })
  ).pipe(
    catchError((err: unknown) => {
      console.error('Error al crear una entrada:', err);
      throw err;
    })
  );
}

// Ejemplo de uso
createPost({
  userId: 1,
  title: 'RxJSAprendiendo de',
  body: 'RxJSutilizando elAPIAprendizaje del patrón de llamadas a'
}).subscribe({
  next: post => {
    console.log('Mensajes creados:', post);
    console.log('Un postID:', post.id); // JSONPlaceholderseIDles asigna (por ejemplo: 101)
  },
  error: err => console.error('Error:', err)
});
  • Esperar hasta que todos los Observable se hayan completado.
  • Si alguno de ellos falla, fallará el conjunto
  • Todos los Observable deben emitir al menos un valor

Gestión de errores mejorada

En peticiones paralelas, es posible que desee recuperar otros resultados incluso si algunos de ellos fallan.

typescript
import { forkJoin, of, catchError } from 'rxjs';

function fetchDashboardWithFallback(userId: number): Observable<Dashboard> {
  return forkJoin({
    user: fetchUserById(userId).pipe(
      catchError((err: unknown) => {
        console.error('Error de adquisición de usuario:', err);
        return of(null); // En caso de error, devuelvenullDevuelve
      })
    ),
    posts: fetchPostsByUserId(userId).pipe(
      catchError((err: unknown) => {
        console.error('Post error de adquisición:', err);
        return of([]); // En caso de error, devuelve un array vacío
      })
    ),
    comments: fetchCommentsByUserId(userId).pipe(
      catchError((err: unknown) => {
        console.error('Error al recuperar comentario:', err);
        return of([]); // En caso de error, devuelve un array vacío
      })
    )
  }).pipe(
    map(({ user, posts, comments }) => ({
      user: user || { id: userId, name: 'Unknown', email: '' },
      posts,
      comments
    }))
  );
}

部分的なエラーハンドリング

Aplicando catchError a cada Observable, todo el proceso puede continuar aunque alguno de ellos falle.

Solicitud de serie (concatMap)

Problema: Quiero ejecutar las APIs en orden.

Desea ejecutar la siguiente solicitud después de que la anterior se haya completado (por ejemplo, varias cargas de archivos en secuencia).

Solución: utilizar concatMap.

concatMap` ejecuta el siguiente Observable después de que el anterior se haya completado.

typescript
import { from, Observable, concatMap, tap, delay, catchError } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Posttipo
// https://jsonplaceholder.typicode.com/posts
interface Post {
  id: number;
  userId: number;
  title: string;
  body: string;
}

interface CreatePostRequest {
  userId: number;
  title: string;
  body: string;
}

function createPost(postData: CreatePostRequest): Observable<Post> {
  return from(
    fetch('https://jsonplaceholder.typicode.com/posts', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(postData)
    }).then(response => {
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return response.json();
    })
  ).pipe(
    catchError((err: unknown) => {
      console.error('Error al crear una entrada:', err);
      throw err;
    })
  );
}

// Crear varios posts en secuencia (enAPISe tiene en cuenta la limitación de velocidad)
function createPostsSequentially(posts: CreatePostRequest[]): Observable<Post> {
  return from(posts).pipe(
    concatMap((postData, index) =>
      createPost(postData).pipe(
        tap(result => console.log(`Un post${index + 1}Creación completada:`, result.title)),
        delay(100) // APITeniendo en cuenta la limitación de velocidad100msEn espera
      )
    )
  );
}

// Ejemplo de uso
const postsToCreate: CreatePostRequest[] = [
  {
    userId: 1,
    title: '1Segundo mensaje',
    body: 'Este es el1El segundo puesto.'
  },
  {
    userId: 1,
    title: '2Segundo mensaje',
    body: 'Este es el2El segundo puesto.'
  },
  {
    userId: 1,
    title: '3Segundo mensaje',
    body: 'Este es el3El segundo puesto.'
  }
];

const results: Post[] = [];

createPostsSequentially(postsToCreate).subscribe({
  next: post => {
    results.push(post);
    console.log(`Progreso: ${results.length}/${postsToCreate.length}`);
  },
  complete: () => {
    console.log('Todos los posts creados Completados:', results.length, 'Número de entradas');
  },
  error: err => console.error('Error al crear una entrada:', err)
});

Flujo de ejecución

typescript
import { from, Observable, catchError } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Posttipo
// https://jsonplaceholder.typicode.com/posts
interface Post {
  id: number;
  userId: number;
  title: string;
  body: string;
}

interface CreatePostRequest {
  userId: number;
  title: string;
  body: string;
}

function createPost(postData: CreatePostRequest): Observable<Post> {
  return from(
    fetch('https://jsonplaceholder.typicode.com/posts', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(postData)
    }).then(response => {
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return response.json();
    })
  ).pipe(
    catchError((err: unknown) => {
      console.error('Error al crear una entrada:', err);
      throw err;
    })
  );
}

// Ejemplo de uso
createPost({
  userId: 1,
  title: 'RxJSAprendiendo de',
  body: 'RxJSutilizando elAPIAprendizaje del patrón de llamadas a'
}).subscribe({
  next: post => {
    console.log('Mensajes creados:', post);
    console.log('Un postID:', post.id); // JSONPlaceholderseIDles asigna (por ejemplo: 101)
  },
  error: err => console.error('Error:', err)
});
  • concatMap: ejecución secuencial (se completa la anterior, luego la siguiente)

  • mergeMap: ejecución paralela (múltiples ejecuciones simultáneas posibles).

  • concatMap si el orden es importante, mergeMap si el orden no es necesario y la velocidad es una prioridad.

Solicitudes de dependencia (switchMap)

Problema: llamar a la siguiente API utilizando la respuesta de la API anterior.

Uno de los patrones más comunes, utilizar el resultado de la respuesta de la primera API para llamar a la siguiente API.

Solución: usar switchMap.

El switchMap` toma el valor del Observable anterior y lo convierte en un nuevo Observable.

typescript
import { from, Observable, switchMap, map } from 'rxjs';

interface UserProfile {
  user: User;
  posts: Post[];
}
interface Post {
  id: number;
  userId: number;
  title: string;
  body: string;
}
interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    street: string;
    suite: string;
    city: string;
    zipcode: string;
    geo: {
      lat: string;
      lng: string;
    };
  };
  phone: string;
  website: string;
  company: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
}

function fetchUserById(id: number): Observable<User> {
  return from(
    fetch(`https://jsonplaceholder.typicode.com/users/${id}`).then(r => r.json())
  );
}

function fetchPostsByUserId(userId: number): Observable<Post[]> {
  return from(
    fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`).then(r => r.json())
  );
}

// Recuperar los datos de los usuarios y sus mensajes
function fetchUserProfile(userId: number): Observable<UserProfile> {
  return fetchUserById(userId).pipe(
    switchMap(user =>
      // Después de recuperar los detalles del usuario y sus mensajes
      fetchPostsByUserId(user.id).pipe(
        map(posts => ({
          user,
          posts
        }))
      )
    )
  );
}

// Ejemplo de uso
fetchUserProfile(1).subscribe({
  next: profile => {
    console.log('Usuario:', profile.user.name);
    console.log('Un post:', profile.posts);
  },
  error: err => console.error('Error:', err)
});

Ejemplo práctico: implementación de una función de búsqueda

Se trata de un patrón utilizado con frecuencia en la práctica, en el que se llama a la API en respuesta a la entrada de búsqueda del usuario.

typescript
import { from, fromEvent, Observable, of, map, debounceTime, distinctUntilChanged, switchMap, catchError } from 'rxjs';

// JSONPlaceholder (después de un sustantivo) basándose en ... Post como resultados de búsqueda.
interface SearchResult {
  id: number;
  userId: number;
  title: string;
  body: string;
}

function searchAPI(query: string): Observable<SearchResult[]> {
  return from(
    fetch('https://jsonplaceholder.typicode.com/posts')
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
      })
  ).pipe(
    // Filtrado del lado del cliente por título
    map((posts: SearchResult[]) =>
      posts.filter(post =>
        post.title.toLowerCase().includes(query.toLowerCase())
      )
    )
  );
}

// Traditional approach (commented for reference)
// const searchInput = document.querySelector<HTMLInputElement>('#search');

// Self-contained: creates search input and results container dynamically
const searchInput = document.createElement('input');
searchInput.id = 'search';
searchInput.type = 'text';
searchInput.placeholder = 'Introduzca palabras clave de búsqueda (al menos2caracteres o más)';
searchInput.style.padding = '10px';
searchInput.style.margin = '10px';
searchInput.style.width = '400px';
searchInput.style.fontSize = '16px';
searchInput.style.border = '2px solid #ccc';
searchInput.style.borderRadius = '4px';
searchInput.style.display = 'block';
document.body.appendChild(searchInput);

const resultsContainer = document.createElement('div');
resultsContainer.id = 'results';
resultsContainer.style.padding = '10px';
resultsContainer.style.margin = '10px';
resultsContainer.style.minHeight = '100px';
resultsContainer.style.border = '1px solid #ddd';
resultsContainer.style.borderRadius = '4px';
resultsContainer.style.backgroundColor = '#f9f9f9';
document.body.appendChild(resultsContainer);

const search$ = fromEvent(searchInput, 'input').pipe(
  map(event => (event.target as HTMLInputElement).value),
  debounceTime(300),           // Después de introducirlas300msEsperar
  distinctUntilChanged(),      // Ignorado si el valor es el mismo que la última vez
  switchMap(query => {
    if (query.length < 2) {
      return of([]); // 2Si tiene menos de 1 carácter, matriz vacía
    }
    return searchAPI(query).pipe(
      catchError((err: unknown) => {
        console.error('Error de búsqueda:', err);
        return of([]); // Matriz vacía en caso de error
      })
    );
  })
);

search$.subscribe(results => {
  console.log('Buscar resultados:', results);
  // UIMostrar resultados en
  displayResults(results, resultsContainer);
});

function displayResults(results: SearchResult[], container: HTMLElement): void {
  // Muestra los resultados enDOMProceso que muestra los resultados en
  container.innerHTML = results
    .map(r => `<div style="padding: 8px; margin: 4px; border-bottom: 1px solid #eee;">${r.title}</div>`)
    .join('');

  if (results.length === 0) {
    container.innerHTML = '<div style="padding: 8px; color: #999;">Sin resultados de búsqueda</div>';
  }
}

クライアントサイドフィルタリング

La API de JSONPlaceholder no tiene un punto final de búsqueda, por lo que todas las entradas se recuperan y filtran en el lado del cliente. En la práctica, este patrón se utiliza cuando el back-end no tiene una función de búsqueda o cuando la cantidad de datos es pequeña.

Ejemplo de búsqueda:

  • Búsqueda de "sunt" → varios mensajes encontrados.
  • Búsqueda con "qui est esse" → resultados con títulos que contienen "qui est esse"
  • Búsqueda con "zzz" → No hay resultados (no procede).

Flujo de ejecución

typescript
import { from, Observable, map, catchError, timeout } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Usertipo
// https://jsonplaceholder.typicode.com/users
interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    street: string;
    suite: string;
    city: string;
    zipcode: string;
    geo: {
      lat: string;
      lng: string;
    };
  };
  phone: string;
  website: string;
  company: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
}

// Recuperar lista de usuarios
function fetchUsers(): Observable<User[]> {
  return from(
    fetch('https://jsonplaceholder.typicode.com/users')
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
      })
  ).pipe(
    timeout(5000), // 5Tiempo de espera en segundos
    catchError((err: unknown) => {
      console.error('Error de adquisición de usuario:', err);
      throw err;
    })
  );
}

// Ejemplo de uso
fetchUsers().subscribe({
  next: users => {
    console.log('Lista de usuarios:', users);
    console.log('Primer usuario:', users[0].name); // Ejemplo: "Leanne Graham"
  },
  error: err => console.error('Error:', err)
});
typescript
import { from, Observable, catchError } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Posttipo
// https://jsonplaceholder.typicode.com/posts
interface Post {
  id: number;
  userId: number;
  title: string;
  body: string;
}

interface CreatePostRequest {
  userId: number;
  title: string;
  body: string;
}

function createPost(postData: CreatePostRequest): Observable<Post> {
  return from(
    fetch('https://jsonplaceholder.typicode.com/posts', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(postData)
    }).then(response => {
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return response.json();
    })
  ).pipe(
    catchError((err: unknown) => {
      console.error('Error al crear una entrada:', err);
      throw err;
    })
  );
}

// Ejemplo de uso
createPost({
  userId: 1,
  title: 'RxJSAprendiendo de',
  body: 'RxJSutilizando elAPIAprendizaje del patrón de llamadas a'
}).subscribe({
  next: post => {
    console.log('Mensajes creados:', post);
    console.log('Un postID:', post.id); // JSONPlaceholderseIDles asigna (por ejemplo: 101)
  },
  error: err => console.error('Error:', err)
});

** Cancela automáticamente el Observable anterior cuando llega un nuevo valor. ** Esto asegura que las respuestas a peticiones API más antiguas sean ignoradas aunque lleguen más tarde (Race Condition avoidance).

switchMap vs mergeMap vs concatMap

El uso de operadores de mapeo de orden superior.

typescript
import { from, Observable, catchError } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Posttipo
// https://jsonplaceholder.typicode.com/posts
interface Post {
  id: number;
  userId: number;
  title: string;
  body: string;
}

interface CreatePostRequest {
  userId: number;
  title: string;
  body: string;
}

function createPost(postData: CreatePostRequest): Observable<Post> {
  return from(
    fetch('https://jsonplaceholder.typicode.com/posts', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(postData)
    }).then(response => {
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return response.json();
    })
  ).pipe(
    catchError((err: unknown) => {
      console.error('Error al crear una entrada:', err);
      throw err;
    })
  );
}

// Ejemplo de uso
createPost({
  userId: 1,
  title: 'RxJSAprendiendo de',
  body: 'RxJSutilizando elAPIAprendizaje del patrón de llamadas a'
}).subscribe({
  next: post => {
    console.log('Mensajes creados:', post);
    console.log('Un postID:', post.id); // JSONPlaceholderseIDles asigna (por ejemplo: 101)
  },
  error: err => console.error('Error:', err)
});
typescript
import { from, Observable, map, catchError, timeout } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Usertipo
// https://jsonplaceholder.typicode.com/users
interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    street: string;
    suite: string;
    city: string;
    zipcode: string;
    geo: {
      lat: string;
      lng: string;
    };
  };
  phone: string;
  website: string;
  company: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
}

// Recuperar lista de usuarios
function fetchUsers(): Observable<User[]> {
  return from(
    fetch('https://jsonplaceholder.typicode.com/users')
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
      })
  ).pipe(
    timeout(5000), // 5Tiempo de espera en segundos
    catchError((err: unknown) => {
      console.error('Error de adquisición de usuario:', err);
      throw err;
    })
  );
}

// Ejemplo de uso
fetchUsers().subscribe({
  next: users => {
    console.log('Lista de usuarios:', users);
    console.log('Primer usuario:', users[0].name); // Ejemplo: "Leanne Graham"
  },
  error: err => console.error('Error:', err)
});

Reintentos y gestión de errores

Problema: Quiero manejar errores temporales de red

En caso de que se produzca un error de red o se agote el tiempo de espera, es posible que desee reintentarlo automáticamente.

Solución: use retry y retryWhen.

typescript
import { from, Observable, map, catchError, timeout } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Usertipo
// https://jsonplaceholder.typicode.com/users
interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    street: string;
    suite: string;
    city: string;
    zipcode: string;
    geo: {
      lat: string;
      lng: string;
    };
  };
  phone: string;
  website: string;
  company: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
}

// Recuperar lista de usuarios
function fetchUsers(): Observable<User[]> {
  return from(
    fetch('https://jsonplaceholder.typicode.com/users')
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
      })
  ).pipe(
    timeout(5000), // 5Tiempo de espera en segundos
    catchError((err: unknown) => {
      console.error('Error de adquisición de usuario:', err);
      throw err;
    })
  );
}

// Ejemplo de uso
fetchUsers().subscribe({
  next: users => {
    console.log('Lista de usuarios:', users);
    console.log('Primer usuario:', users[0].name); // Ejemplo: "Leanne Graham"
  },
  error: err => console.error('Error:', err)
});

Ejemplo de backoff exponencial en acción:*

リトライ戦略の選択

  • Immediate retry: retry(3) - simple, útil para caídas de red
  • Intervalo fijo: retryWhen + delay(1000) - tiene en cuenta la carga del servidor RetryWhen" + "timer" - la mejor práctica para AWS, etc.

Reintentar sólo en errores específicos

No todos los errores deben ser reintentados (por ejemplo, 401 No autorizado no requiere un reintento).

typescript
import { from, Observable, map, catchError, timeout } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Usertipo
// https://jsonplaceholder.typicode.com/users
interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    street: string;
    suite: string;
    city: string;
    zipcode: string;
    geo: {
      lat: string;
      lng: string;
    };
  };
  phone: string;
  website: string;
  company: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
}

// Recuperar lista de usuarios
function fetchUsers(): Observable<User[]> {
  return from(
    fetch('https://jsonplaceholder.typicode.com/users')
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
      })
  ).pipe(
    timeout(5000), // 5Tiempo de espera en segundos
    catchError((err: unknown) => {
      console.error('Error de adquisición de usuario:', err);
      throw err;
    })
  );
}

// Ejemplo de uso
fetchUsers().subscribe({
  next: users => {
    console.log('Lista de usuarios:', users);
    console.log('Primer usuario:', users[0].name); // Ejemplo: "Leanne Graham"
  },
  error: err => console.error('Error:', err)
});
typescript
import { forkJoin, from, Observable, map } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Commenttipo
// https://jsonplaceholder.typicode.com/comments
interface Comment {
  postId: number;
  id: number;
  name: string;
  email: string;
  body: string;
}
interface Post {
  id: number;
  userId: number;
  title: string;
  body: string;
}
interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    street: string;
    suite: string;
    city: string;
    zipcode: string;
    geo: {
      lat: string;
      lng: string;
    };
  };
  phone: string;
  website: string;
  company: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
}
interface Dashboard {
  user: User;
  posts: Post[];
  comments: Comment[];
}

function fetchUserById(id: number): Observable<User> {
  return from(
    fetch(`https://jsonplaceholder.typicode.com/users/${id}`).then(r => r.json())
  );
}

function fetchPostsByUserId(userId: number): Observable<Post[]> {
  return from(
    fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`).then(r => r.json())
  );
}

function fetchCommentsByPostId(postId: number): Observable<Comment[]> {
  return from(
    fetch(`https://jsonplaceholder.typicode.com/comments?postId=${postId}`).then(r => r.json())
  );
}

// Recuperación paralela de los datos del tablero
function fetchDashboard(userId: number): Observable<Dashboard> {
  return forkJoin({
    user: fetchUserById(userId),
    posts: fetchPostsByUserId(userId),
    comments: fetchCommentsByPostId(1) // Un postID=1Recuperar comentarios de
  }).pipe(
    map(({ user, posts, comments }) => ({
      user,
      posts,
      comments
    }))
  );
}

// Ejemplo de uso
fetchDashboard(1).subscribe({
  next: dashboard => {
    console.log('Usuario:', dashboard.user.name); // Ejemplo: "Leanne Graham"
    console.log('Número de entradas:', dashboard.posts.length); // Ejemplo: 10Número de entradas
    console.log('Número de comentarios:', dashboard.comments.length); // Ejemplo: 5Número de entradas
  },
  error: err => console.error('Error de adquisición del panel:', err)
});
  • Solicitud POST: riesgo de creación de duplicados al reintentar si no hay igualdad
  • Errores de autenticación: 401/403 no reintentar, pedir re-inicio de sesión
  • ERROR DE VALIDACIÓN: 400 no reintentar, pedir al usuario que corrija

Gestión del tiempo de espera

Problema: Quiero hacer frente a la lenta respuesta de la API

Si la red es lenta o el servidor no responde, quieres que se agote el tiempo de espera después de un cierto periodo de tiempo.

Solución: utilizar el operador timeout.

typescript
import { from, Observable, map, catchError, timeout } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Usertipo
// https://jsonplaceholder.typicode.com/users
interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    street: string;
    suite: string;
    city: string;
    zipcode: string;
    geo: {
      lat: string;
      lng: string;
    };
  };
  phone: string;
  website: string;
  company: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
}

// Recuperar lista de usuarios
function fetchUsers(): Observable<User[]> {
  return from(
    fetch('https://jsonplaceholder.typicode.com/users')
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
      })
  ).pipe(
    timeout(5000), // 5Tiempo de espera en segundos
    catchError((err: unknown) => {
      console.error('Error de adquisición de usuario:', err);
      throw err;
    })
  );
}

// Ejemplo de uso
fetchUsers().subscribe({
  next: users => {
    console.log('Lista de usuarios:', users);
    console.log('Primer usuario:', users[0].name); // Ejemplo: "Leanne Graham"
  },
  error: err => console.error('Error:', err)
});

Combinación de reintento y tiempo de espera

En la práctica, los tiempos de espera y los reintentos se utilizan de forma combinada.

タイムアウト値の設定

  • AIP normal: 5 - 10 segundos API rápida**: 2 - 3 segundos
  • Carga de archivos: 30 - 60 segundos
  • Procesamiento en segundo plano: más de 60 segundos

Establecido para equilibrar la experiencia del usuario y la carga del servidor.

Proceso de cancelación de solicitud.

Problema: Quiero cancelar las solicitudes de API que ya no son necesarias.

Quiero cancelar una solicitud de API en ejecución cuando se destruye una transición de página o un componente.

Solución: utilizar takeUntil.

typescript
import { from, Observable, map, catchError, timeout } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Usertipo
// https://jsonplaceholder.typicode.com/users
interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    street: string;
    suite: string;
    city: string;
    zipcode: string;
    geo: {
      lat: string;
      lng: string;
    };
  };
  phone: string;
  website: string;
  company: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
}

// Recuperar lista de usuarios
function fetchUsers(): Observable<User[]> {
  return from(
    fetch('https://jsonplaceholder.typicode.com/users')
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
      })
  ).pipe(
    timeout(5000), // 5Tiempo de espera en segundos
    catchError((err: unknown) => {
      console.error('Error de adquisición de usuario:', err);
      throw err;
    })
  );
}

// Ejemplo de uso
fetchUsers().subscribe({
  next: users => {
    console.log('Lista de usuarios:', users);
    console.log('Primer usuario:', users[0].name); // Ejemplo: "Leanne Graham"
  },
  error: err => console.error('Error:', err)
});

Cancelación controlada por el usuario

Este es un ejemplo de implementación de un botón de cancelación explícito.

キャンセルのベストプラクティス

  • Implemente siempre un proceso de cancelación - evita fugas de memoria y desperdicio de red
  • Utilizar takeUntil - más declarativo y menos olvidable que unsubscribe()
  • Al destruir componentes - dispara destroy$ para desuscribir todo

Ejemplos prácticos de clases de servicio

Este es un ejemplo de una clase de servicio completa que resume los patrones anteriores y puede utilizarse en la práctica.

typescript
import { from, Observable, map, catchError, timeout } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Usertipo
// https://jsonplaceholder.typicode.com/users
interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    street: string;
    suite: string;
    city: string;
    zipcode: string;
    geo: {
      lat: string;
      lng: string;
    };
  };
  phone: string;
  website: string;
  company: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
}

// Recuperar lista de usuarios
function fetchUsers(): Observable<User[]> {
  return from(
    fetch('https://jsonplaceholder.typicode.com/users')
      .then(response => {
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        return response.json();
      })
  ).pipe(
    timeout(5000), // 5Tiempo de espera en segundos
    catchError((err: unknown) => {
      console.error('Error de adquisición de usuario:', err);
      throw err;
    })
  );
}

// Ejemplo de uso
fetchUsers().subscribe({
  next: users => {
    console.log('Lista de usuarios:', users);
    console.log('Primer usuario:', users[0].name); // Ejemplo: "Leanne Graham"
  },
  error: err => console.error('Error:', err)
});

実践的なサービス設計

  • Configurable: configuración flexible de tiempos de espera, recuentos de reintentos, etc.
  • Funcionalidad de caché: evita peticiones duplicadas
  • Manejo de errores: manejo uniforme de errores
  • Limpieza automática: destroy() asegura la liberación de recursos

Código de prueba

Ejemplo de prueba para el patrón de invocación de la API.

typescript
import { from, Observable, catchError } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Posttipo
// https://jsonplaceholder.typicode.com/posts
interface Post {
  id: number;
  userId: number;
  title: string;
  body: string;
}

interface CreatePostRequest {
  userId: number;
  title: string;
  body: string;
}

function createPost(postData: CreatePostRequest): Observable<Post> {
  return from(
    fetch('https://jsonplaceholder.typicode.com/posts', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(postData)
    }).then(response => {
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      return response.json();
    })
  ).pipe(
    catchError((err: unknown) => {
      console.error('Error al crear una entrada:', err);
      throw err;
    })
  );
}

// Ejemplo de uso
createPost({
  userId: 1,
  title: 'RxJSAprendiendo de',
  body: 'RxJSutilizando elAPIAprendizaje del patrón de llamadas a'
}).subscribe({
  next: post => {
    console.log('Mensajes creados:', post);
    console.log('Un postID:', post.id); // JSONPlaceholderseIDles asigna (por ejemplo: 101)
  },
  error: err => console.error('Error:', err)
});

Resumen.

Dominando el patrón de invocación API usando RxJS, se pueden construir aplicaciones robustas y mantenibles.

typescript
import { forkJoin, from, Observable, map } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Commenttipo
// https://jsonplaceholder.typicode.com/comments
interface Comment {
  postId: number;
  id: number;
  name: string;
  email: string;
  body: string;
}
interface Post {
  id: number;
  userId: number;
  title: string;
  body: string;
}
interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    street: string;
    suite: string;
    city: string;
    zipcode: string;
    geo: {
      lat: string;
      lng: string;
    };
  };
  phone: string;
  website: string;
  company: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
}
interface Dashboard {
  user: User;
  posts: Post[];
  comments: Comment[];
}

function fetchUserById(id: number): Observable<User> {
  return from(
    fetch(`https://jsonplaceholder.typicode.com/users/${id}`).then(r => r.json())
  );
}

function fetchPostsByUserId(userId: number): Observable<Post[]> {
  return from(
    fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`).then(r => r.json())
  );
}

function fetchCommentsByPostId(postId: number): Observable<Comment[]> {
  return from(
    fetch(`https://jsonplaceholder.typicode.com/comments?postId=${postId}`).then(r => r.json())
  );
}

// Recuperación paralela de los datos del tablero
function fetchDashboard(userId: number): Observable<Dashboard> {
  return forkJoin({
    user: fetchUserById(userId),
    posts: fetchPostsByUserId(userId),
    comments: fetchCommentsByPostId(1) // Un postID=1Recuperar comentarios de
  }).pipe(
    map(({ user, posts, comments }) => ({
      user,
      posts,
      comments
    }))
  );
}

// Ejemplo de uso
fetchDashboard(1).subscribe({
  next: dashboard => {
    console.log('Usuario:', dashboard.user.name); // Ejemplo: "Leanne Graham"
    console.log('Número de entradas:', dashboard.posts.length); // Ejemplo: 10Número de entradas
    console.log('Número de comentarios:', dashboard.comments.length); // Ejemplo: 5Número de entradas
  },
  error: err => console.error('Error de adquisición del panel:', err)
});
  • forkJoin: ejecuta múltiples APIs en paralelo, todas a la espera de ser completadas
  • concatMap: ejecuta APIs en orden (la anterior completada, luego la siguiente)
  • switchMap: ideal para peticiones dependientes, funciones de búsqueda
  • retry/retryWhen: reintento automático en caso de error, se recomienda un backoff exponencial
  • timeout: establece siempre un timeout
  • takeUntil: cancelación automática en caso de destrucción del componente
typescript
import { forkJoin, from, Observable, map } from 'rxjs';

// JSONPlaceholder API(después de un sustantivo) basándose en ...Commenttipo
// https://jsonplaceholder.typicode.com/comments
interface Comment {
  postId: number;
  id: number;
  name: string;
  email: string;
  body: string;
}
interface Post {
  id: number;
  userId: number;
  title: string;
  body: string;
}
interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  address: {
    street: string;
    suite: string;
    city: string;
    zipcode: string;
    geo: {
      lat: string;
      lng: string;
    };
  };
  phone: string;
  website: string;
  company: {
    name: string;
    catchPhrase: string;
    bs: string;
  };
}
interface Dashboard {
  user: User;
  posts: Post[];
  comments: Comment[];
}

function fetchUserById(id: number): Observable<User> {
  return from(
    fetch(`https://jsonplaceholder.typicode.com/users/${id}`).then(r => r.json())
  );
}

function fetchPostsByUserId(userId: number): Observable<Post[]> {
  return from(
    fetch(`https://jsonplaceholder.typicode.com/posts?userId=${userId}`).then(r => r.json())
  );
}

function fetchCommentsByPostId(postId: number): Observable<Comment[]> {
  return from(
    fetch(`https://jsonplaceholder.typicode.com/comments?postId=${postId}`).then(r => r.json())
  );
}

// Recuperación paralela de los datos del tablero
function fetchDashboard(userId: number): Observable<Dashboard> {
  return forkJoin({
    user: fetchUserById(userId),
    posts: fetchPostsByUserId(userId),
    comments: fetchCommentsByPostId(1) // Un postID=1Recuperar comentarios de
  }).pipe(
    map(({ user, posts, comments }) => ({
      user,
      posts,
      comments
    }))
  );
}

// Ejemplo de uso
fetchDashboard(1).subscribe({
  next: dashboard => {
    console.log('Usuario:', dashboard.user.name); // Ejemplo: "Leanne Graham"
    console.log('Número de entradas:', dashboard.posts.length); // Ejemplo: 10Número de entradas
    console.log('Número de comentarios:', dashboard.comments.length); // Ejemplo: 5Número de entradas
  },
  error: err => console.error('Error de adquisición del panel:', err)
});
  • Seguridad de tipos: definir tipos para todas las respuestas de la API
  • Manejo de errores: implementar catchError para todas las peticiones
  • Manejo de cancelaciones: asegurar la limpieza con takeUntil.
  • Estrategia de reintento: reintentar apropiadamente según el código de estado
  • Caching: evitar peticiones duplicadas con shareReplay.

Próximos pasos.

Una vez que domines el patrón de llamadas a la API, puedes pasar a los siguientes patrones.

Secciones relacionadas.

  • Capítulo 4: Operadores](../operators/index.md) - más sobre switchMap, mergeMap y concatMap.
  • Capítulo 6: Tratamiento de errores](../error-handling/strategies.md)) - conceptos básicos de catchError, retry
  • Capítulo 2: Cold/Hot Observable](../observables/cold-and-hot-observables.md)) - Entendiendo shareReplay

Recursos de referencia

Publicado bajo licencia CC-BY-4.0.