Skip to content

API-oproeppatroon. ​

API-oproepen zijn een van de meest geïmplementeerde processen bij webontwikkeling, en met RxJS kun je complexe asynchrone API-oproepen declaratief en robuust implementeren.

Dit artikel beschrijft concrete implementatiepatronen voor verschillende API aanroepscenario's die je in de praktijk tegenkomt, inclusief foutafhandeling en annuleringsafhandeling.

Wat je in dit artikel leert. ​

  • Basis GET/POST verzoek implementatie
  • Parallel aanroepen van meerdere API's (forkJoin)
  • Reeks verzoeken die sequentiële uitvoering vereisen (concatMap)
  • Ketens van verzoeken met afhankelijkheden (switchMap)
  • Herhaling en foutafhandeling
  • Time-out afhandeling
  • Annulering van verzoeken
typescript
import { from, Observable, catchError } from 'rxjs';

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Posttype
// 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('Fout bij het maken van een bericht:', err);
      throw err;
    })
  );
}

// Gebruiksvoorbeeld
createPost({
  userId: 1,
  title: 'RxJSLeren van',
  body: 'RxJShet gebruik van deAPILeren van het patroon van aanroepen naar'
}).subscribe({
  next: post => {
    console.log('Aangemaakte berichten:', post);
    console.log('Een berichtID:', post.id); // JSONPlaceholderworden automatischIDtoegewezen (bijv.: 101)
  },
  error: err => console.error('Fout:', err)
});

Dit artikel maakt deel uit van de Hoofdstuk 4: Operators en Hoofdstuk 6: Foutafhandeling.

Basis API-aanroepen. ​

Probleem: Eenvoudig GET-verzoek. ​

Het meest eenvoudige geval implementeert een enkel GET-verzoek.

Voorbeeldimplementatie. ​

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

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Usertype
// 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;
  };
}

// Lijst met gebruikers ophalen
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), // 5Time-out in seconden
    catchError((err: unknown) => {
      console.error('Fout bij overname gebruiker:', err);
      throw err;
    })
  );
}

// Gebruiksvoorbeeld
fetchUsers().subscribe({
  next: users => {
    console.log('Gebruikerslijst:', users);
    console.log('Eerste gebruiker:', users[0].name); // Voorbeeld: "Leanne Graham"
  },
  error: err => console.error('Fout:', err)
});
typescript
import { from, Observable, catchError } from 'rxjs';

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Posttype
// 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('Fout bij het maken van een bericht:', err);
      throw err;
    })
  );
}

// Gebruiksvoorbeeld
createPost({
  userId: 1,
  title: 'RxJSLeren van',
  body: 'RxJShet gebruik van deAPILeren van het patroon van aanroepen naar'
}).subscribe({
  next: post => {
    console.log('Aangemaakte berichten:', post);
    console.log('Een berichtID:', post.id); // JSONPlaceholderworden automatischIDtoegewezen (bijv.: 101)
  },
  error: err => console.error('Fout:', err)
});

Dit voorbeeld omwikkelt de standaard fetch met from(), maar je kunt ook de officiële RxJS ajax() gebruiken. ajax()` is geavanceerder en ondersteunt verzoekannulering en voortgangscontrole.

POST-verzoek. ​

Patroon voor het maken van nieuwe gegevens.

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

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Posttype
// 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('Fout bij het maken van een bericht:', err);
      throw err;
    })
  );
}

// Gebruiksvoorbeeld
createPost({
  userId: 1,
  title: 'RxJSLeren van',
  body: 'RxJShet gebruik van deAPILeren van het patroon van aanroepen naar'
}).subscribe({
  next: post => {
    console.log('Aangemaakte berichten:', post);
    console.log('Een berichtID:', post.id); // JSONPlaceholderworden automatischIDtoegewezen (bijv.: 101)
  },
  error: err => console.error('Fout:', err)
});
  • Typeveiligheid: duidelijk het type antwoord definiëren
  • Foutenafhandeling: HTTP-statuscodes goed controleren
  • Time-outs: lange wachttijden voorkomen

Parallelle verzoeken (forkJoin) ​

Probleem: ik wil meerdere API's tegelijk aanroepen. ​

Het kan zijn dat u meerdere onafhankelijke API's parallel wilt aanroepen en pas verder wilt gaan nadat alle antwoorden zijn ontvangen.

Oplossing: Gebruik forkJoin. ​

forkJoin` wacht tot meerdere Observable zijn voltooid en retourneert alle resultaten in een array (gelijk aan Promise.all).

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

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Commenttype
// 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())
  );
}

// Parallel ophalen van dashboardgegevens
function fetchDashboard(userId: number): Observable<Dashboard> {
  return forkJoin({
    user: fetchUserById(userId),
    posts: fetchPostsByUserId(userId),
    comments: fetchCommentsByPostId(1) // Een berichtID=1Commentaar ophalen van
  }).pipe(
    map(({ user, posts, comments }) => ({
      user,
      posts,
      comments
    }))
  );
}

// Gebruiksvoorbeeld
fetchDashboard(1).subscribe({
  next: dashboard => {
    console.log('Gebruiker:', dashboard.user.name); // Voorbeeld: "Leanne Graham"
    console.log('Aantal berichten:', dashboard.posts.length); // Voorbeeld: 10Aantal berichten
    console.log('Aantal opmerkingen:', dashboard.comments.length); // Voorbeeld: 5Aantal berichten
  },
  error: err => console.error('Fout bij overname dashboard:', err)
});

Uitvoeringsstroom ​

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

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Posttype
// 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('Fout bij het maken van een bericht:', err);
      throw err;
    })
  );
}

// Gebruiksvoorbeeld
createPost({
  userId: 1,
  title: 'RxJSLeren van',
  body: 'RxJShet gebruik van deAPILeren van het patroon van aanroepen naar'
}).subscribe({
  next: post => {
    console.log('Aangemaakte berichten:', post);
    console.log('Een berichtID:', post.id); // JSONPlaceholderworden automatischIDtoegewezen (bijv.: 101)
  },
  error: err => console.error('Fout:', err)
});
  • Wacht tot alle Observable zijn voltooid.
  • Als een van hen faalt, faalt het geheel
  • Alle Observable moeten ten minste één waarde afgeven

Verbeterde foutafhandeling ​

Bij parallelle verzoeken wil je misschien andere resultaten ophalen, zelfs als sommige mislukken.

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

function fetchDashboardWithFallback(userId: number): Observable<Dashboard> {
  return forkJoin({
    user: fetchUserById(userId).pipe(
      catchError((err: unknown) => {
        console.error('Fout bij overname gebruiker:', err);
        return of(null); // Geeft bij fout terugnullGeeft  terug
      })
    ),
    posts: fetchPostsByUserId(userId).pipe(
      catchError((err: unknown) => {
        console.error('Fout na overname:', err);
        return of([]); // Geeft bij fout een lege matrix terug
      })
    ),
    comments: fetchCommentsByUserId(userId).pipe(
      catchError((err: unknown) => {
        console.error('Fout bij ophalen commentaar:', err);
        return of([]); // Geeft bij fout een lege matrix terug
      })
    )
  }).pipe(
    map(({ user, posts, comments }) => ({
      user: user || { id: userId, name: 'Unknown', email: '' },
      posts,
      comments
    }))
  );
}

oproep_26_

Door catchError toe te passen op elke Observable, kan het hele proces doorgaan, zelfs als sommige ervan mislukken.

Serieverzoek (concatMap) ​

Probleem: ik wil de API's in volgorde uitvoeren. ​

U wilt het volgende verzoek uitvoeren nadat het vorige is voltooid (bijvoorbeeld meerdere bestandsuploads achter elkaar).

Oplossing: gebruik concatMap. ​

concatMap` voert de volgende Observable uit nadat de vorige is voltooid.

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

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Posttype
// 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('Fout bij het maken van een bericht:', err);
      throw err;
    })
  );
}

// Meerdere berichten achter elkaar maken (inAPISnelheidsbeperking rekening gehouden)
function createPostsSequentially(posts: CreatePostRequest[]): Observable<Post> {
  return from(posts).pipe(
    concatMap((postData, index) =>
      createPost(postData).pipe(
        tap(result => console.log(`Een bericht${index + 1}Creatie voltooid:`, result.title)),
        delay(100) // APIRekening houdend met snelheidsbeperking100msWachten op
      )
    )
  );
}

// Gebruiksvoorbeeld
const postsToCreate: CreatePostRequest[] = [
  {
    userId: 1,
    title: '1Tweede bericht',
    body: 'Dit is de1De tweede post.'
  },
  {
    userId: 1,
    title: '2Tweede bericht',
    body: 'Dit is de2De tweede post.'
  },
  {
    userId: 1,
    title: '3Tweede bericht',
    body: 'Dit is de3De tweede post.'
  }
];

const results: Post[] = [];

createPostsSequentially(postsToCreate).subscribe({
  next: post => {
    results.push(post);
    console.log(`Voortgang: ${results.length}/${postsToCreate.length}`);
  },
  complete: () => {
    console.log('Alle aangemaakte berichten Voltooid:', results.length, 'Aantal berichten');
  },
  error: err => console.error('Fout bij het maken van een bericht:', err)
});

Uitvoeringsstroom ​

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

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Posttype
// 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('Fout bij het maken van een bericht:', err);
      throw err;
    })
  );
}

// Gebruiksvoorbeeld
createPost({
  userId: 1,
  title: 'RxJSLeren van',
  body: 'RxJShet gebruik van deAPILeren van het patroon van aanroepen naar'
}).subscribe({
  next: post => {
    console.log('Aangemaakte berichten:', post);
    console.log('Een berichtID:', post.id); // JSONPlaceholderworden automatischIDtoegewezen (bijv.: 101)
  },
  error: err => console.error('Fout:', err)
});
  • concatMap**: sequentiële uitvoering (vorige voltooid, dan volgende)

  • mergeMap**: parallelle uitvoering (meerdere gelijktijdige uitvoeringen mogelijk).

  • concatMap als volgorde belangrijk is, mergeMap als volgorde niet nodig is en snelheid prioriteit heeft.

Afhankelijkheid aanvragen (switchMap) ​

Probleem: de volgende API aanroepen met behulp van het vorige API-antwoord. ​

Een van de meest voorkomende patronen, waarbij het resultaat van de eerste API-respons wordt gebruikt om de volgende API aan te roepen.

Oplossing: gebruik switchMap. ​

De switchMap neemt de waarde van de vorige Observable en zet deze om in een nieuwe 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())
  );
}

// Gebruikersgegevens en hun berichten ophalen
function fetchUserProfile(userId: number): Observable<UserProfile> {
  return fetchUserById(userId).pipe(
    switchMap(user =>
      // Na het ophalen van gebruikersgegevens en hun berichten
      fetchPostsByUserId(user.id).pipe(
        map(posts => ({
          user,
          posts
        }))
      )
    )
  );
}

// Gebruiksvoorbeeld
fetchUserProfile(1).subscribe({
  next: profile => {
    console.log('Gebruiker:', profile.user.name);
    console.log('Een bericht:', profile.posts);
  },
  error: err => console.error('Fout:', err)
});

Praktisch voorbeeld: een zoekfunctie implementeren ​

Dit is een veelgebruikt patroon in de praktijk, waarbij de API wordt aangeroepen als reactie op de zoekinvoer van de gebruiker.

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

// JSONPlaceholder (na een zelfstandig naamwoord) gebaseerd op ... Post als zoekresultaten.
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(
    // Client-side filteren op titel
    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 = 'Voer zoektermen in (minstens2tekens of meer)';
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),           // Na het invoeren300msWacht op
  distinctUntilChanged(),      // Genegeerd als de waarde hetzelfde is als de vorige keer
  switchMap(query => {
    if (query.length < 2) {
      return of([]); // 2Indien minder dan 1 karakter, lege matrix
    }
    return searchAPI(query).pipe(
      catchError((err: unknown) => {
        console.error('Fout zoeken:', err);
        return of([]); // Lege matrix in geval van fout
      })
    );
  })
);

search$.subscribe(results => {
  console.log('Zoekresultaten:', results);
  // UIResultaten weergeven in
  displayResults(results, resultsContainer);
});

function displayResults(results: SearchResult[], container: HTMLElement): void {
  // Geeft de resultaten weer inDOMResultaten weergeven in
  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;">Geen zoekresultaten</div>';
  }
}
typescript
import { from, Observable, catchError } from 'rxjs';

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Posttype
// 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('Fout bij het maken van een bericht:', err);
      throw err;
    })
  );
}

// Gebruiksvoorbeeld
createPost({
  userId: 1,
  title: 'RxJSLeren van',
  body: 'RxJShet gebruik van deAPILeren van het patroon van aanroepen naar'
}).subscribe({
  next: post => {
    console.log('Aangemaakte berichten:', post);
    console.log('Een berichtID:', post.id); // JSONPlaceholderworden automatischIDtoegewezen (bijv.: 101)
  },
  error: err => console.error('Fout:', err)
});

De JSONPlaceholder API heeft geen zoekfunctie, dus alle berichten worden opgehaald en gefilterd aan de kant van de client. In de praktijk wordt dit patroon gebruikt als de back-end geen zoekfunctie heeft of als de hoeveelheid gegevens klein is.

Voorbeeld zoekopdracht:

  • Zoeken op "sunt" → meerdere berichten gevonden.
  • Zoeken met "qui est esse" → treffers met titels die "qui est esse" bevatten.
  • Zoeken met "zzz" → Geen resultaten (niet van toepassing)

Uitvoeringsstroom ​

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

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Usertype
// 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;
  };
}

// Lijst met gebruikers ophalen
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), // 5Time-out in seconden
    catchError((err: unknown) => {
      console.error('Fout bij overname gebruiker:', err);
      throw err;
    })
  );
}

// Gebruiksvoorbeeld
fetchUsers().subscribe({
  next: users => {
    console.log('Gebruikerslijst:', users);
    console.log('Eerste gebruiker:', users[0].name); // Voorbeeld: "Leanne Graham"
  },
  error: err => console.error('Fout:', err)
});
typescript
import { from, Observable, catchError } from 'rxjs';

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Posttype
// 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('Fout bij het maken van een bericht:', err);
      throw err;
    })
  );
}

// Gebruiksvoorbeeld
createPost({
  userId: 1,
  title: 'RxJSLeren van',
  body: 'RxJShet gebruik van deAPILeren van het patroon van aanroepen naar'
}).subscribe({
  next: post => {
    console.log('Aangemaakte berichten:', post);
    console.log('Een berichtID:', post.id); // JSONPlaceholderworden automatischIDtoegewezen (bijv.: 101)
  },
  error: err => console.error('Fout:', err)
});

** Annuleert automatisch de vorige Observable als er een nieuwe waarde binnenkomt. ** Dit zorgt ervoor dat antwoorden op oudere API verzoeken worden genegeerd, zelfs als ze later aankomen (Race Condition voorkomen).

switchMap vs mergeMap vs concatMap ​

Het gebruik van hogere orde mapping operatoren.

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

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Posttype
// 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('Fout bij het maken van een bericht:', err);
      throw err;
    })
  );
}

// Gebruiksvoorbeeld
createPost({
  userId: 1,
  title: 'RxJSLeren van',
  body: 'RxJShet gebruik van deAPILeren van het patroon van aanroepen naar'
}).subscribe({
  next: post => {
    console.log('Aangemaakte berichten:', post);
    console.log('Een berichtID:', post.id); // JSONPlaceholderworden automatischIDtoegewezen (bijv.: 101)
  },
  error: err => console.error('Fout:', err)
});
typescript
import { from, Observable, map, catchError, timeout } from 'rxjs';

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Usertype
// 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;
  };
}

// Lijst met gebruikers ophalen
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), // 5Time-out in seconden
    catchError((err: unknown) => {
      console.error('Fout bij overname gebruiker:', err);
      throw err;
    })
  );
}

// Gebruiksvoorbeeld
fetchUsers().subscribe({
  next: users => {
    console.log('Gebruikerslijst:', users);
    console.log('Eerste gebruiker:', users[0].name); // Voorbeeld: "Leanne Graham"
  },
  error: err => console.error('Fout:', err)
});

Herhalingen en foutafhandeling ​

Probleem: ik wil tijdelijke netwerkfouten afhandelen ​

In het geval van een netwerkfout of time-out, wil je misschien automatisch een nieuwe poging doen.

Oplossing: gebruik retry en retryWhen. ​

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

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Usertype
// 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;
  };
}

// Lijst met gebruikers ophalen
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), // 5Time-out in seconden
    catchError((err: unknown) => {
      console.error('Fout bij overname gebruiker:', err);
      throw err;
    })
  );
}

// Gebruiksvoorbeeld
fetchUsers().subscribe({
  next: users => {
    console.log('Gebruikerslijst:', users);
    console.log('Eerste gebruiker:', users[0].name); // Voorbeeld: "Leanne Graham"
  },
  error: err => console.error('Fout:', err)
});

Voorbeeld van exponentiële backoff in actie:*

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

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Commenttype
// 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())
  );
}

// Parallel ophalen van dashboardgegevens
function fetchDashboard(userId: number): Observable<Dashboard> {
  return forkJoin({
    user: fetchUserById(userId),
    posts: fetchPostsByUserId(userId),
    comments: fetchCommentsByPostId(1) // Een berichtID=1Commentaar ophalen van
  }).pipe(
    map(({ user, posts, comments }) => ({
      user,
      posts,
      comments
    }))
  );
}

// Gebruiksvoorbeeld
fetchDashboard(1).subscribe({
  next: dashboard => {
    console.log('Gebruiker:', dashboard.user.name); // Voorbeeld: "Leanne Graham"
    console.log('Aantal berichten:', dashboard.posts.length); // Voorbeeld: 10Aantal berichten
    console.log('Aantal opmerkingen:', dashboard.comments.length); // Voorbeeld: 5Aantal berichten
  },
  error: err => console.error('Fout bij overname dashboard:', err)
});
  • Immediate retry: retry(3) - eenvoudig, handig voor netwerkstoringen
  • Vast interval: retryWhen + delay(1000) - houdt rekening met serverbelasting
  • Exponentiële backoff: retryWhen + timer - best practice voor AWS etc.

Herhaal alleen bij specifieke fouten ​

Niet alle fouten moeten opnieuw worden geprobeerd (bijv. 401 Ongeautoriseerd vereist geen nieuwe poging).

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

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Usertype
// 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;
  };
}

// Lijst met gebruikers ophalen
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), // 5Time-out in seconden
    catchError((err: unknown) => {
      console.error('Fout bij overname gebruiker:', err);
      throw err;
    })
  );
}

// Gebruiksvoorbeeld
fetchUsers().subscribe({
  next: users => {
    console.log('Gebruikerslijst:', users);
    console.log('Eerste gebruiker:', users[0].name); // Voorbeeld: "Leanne Graham"
  },
  error: err => console.error('Fout:', err)
});

OPROEP_31_

  • POST-verzoek: risico op duplicaat bij opnieuw proberen indien geen gelijkheid
  • Authenticatiefouten: 401/403 niet opnieuw proberen, vraag opnieuw inloggen
  • VALIDatiefout: 400 niet opnieuw proberen, vraag gebruiker om correctie

Time-out afhandeling ​

Probleem: ik wil omgaan met trage API-respons ​

Als het netwerk traag is of de server niet reageert, wilt u na een bepaalde tijd een time-out instellen.

Oplossing: gebruik de timeout operator. ​

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

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Usertype
// 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;
  };
}

// Lijst met gebruikers ophalen
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), // 5Time-out in seconden
    catchError((err: unknown) => {
      console.error('Fout bij overname gebruiker:', err);
      throw err;
    })
  );
}

// Gebruiksvoorbeeld
fetchUsers().subscribe({
  next: users => {
    console.log('Gebruikerslijst:', users);
    console.log('Eerste gebruiker:', users[0].name); // Voorbeeld: "Leanne Graham"
  },
  error: err => console.error('Fout:', err)
});

Combinatie van opnieuw proberen en time-out ​

In de praktijk worden time-outs en herhalingen gecombineerd gebruikt.

callout_32_

  • Normale API: 5 - 10 seconden
  • Snelle API: 2 - 3 seconden
  • Bestand uploaden: 30 - 60 seconden
  • Achtergrondverwerking: meer dan 60 sec.

Ingesteld om gebruikerservaring en serverbelasting in balans te brengen.

Annuleringsproces aanvragen. ​

Probleem: Ik wil API-verzoeken annuleren die niet langer nodig zijn. ​

Ik wil een lopend API-verzoek annuleren wanneer een paginaovergang of component wordt vernietigd.

Oplossing: gebruik takeUntil. ​

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

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Usertype
// 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;
  };
}

// Lijst met gebruikers ophalen
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), // 5Time-out in seconden
    catchError((err: unknown) => {
      console.error('Fout bij overname gebruiker:', err);
      throw err;
    })
  );
}

// Gebruiksvoorbeeld
fetchUsers().subscribe({
  next: users => {
    console.log('Gebruikerslijst:', users);
    console.log('Eerste gebruiker:', users[0].name); // Voorbeeld: "Leanne Graham"
  },
  error: err => console.error('Fout:', err)
});

Door gebruiker gecontroleerde annulering ​

Dit is een voorbeeld van het implementeren van een expliciete annuleringsknop.

oproep_33_

  • implementeer altijd een cancelproces - voorkomt geheugenlekken en netwerkverspilling
  • Gebruik takeUntil - meer declaratief en minder vergeetbaar dan unsubscribe()
  • Bij het vernietigen van componenten - vuur destroy$ om alle componenten af te melden

Voorbeelden van praktische serviceklassen ​

Dit is een voorbeeld van een complete serviceklasse die de voorgaande patronen samenvat en in de praktijk gebruikt kan worden.

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

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Usertype
// 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;
  };
}

// Lijst met gebruikers ophalen
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), // 5Time-out in seconden
    catchError((err: unknown) => {
      console.error('Fout bij overname gebruiker:', err);
      throw err;
    })
  );
}

// Gebruiksvoorbeeld
fetchUsers().subscribe({
  next: users => {
    console.log('Gebruikerslijst:', users);
    console.log('Eerste gebruiker:', users[0].name); // Voorbeeld: "Leanne Graham"
  },
  error: err => console.error('Fout:', err)
});

oproep_34_

  • Configureerbaar: flexibele configuratie van time-outs, aantal nieuwe pogingen, enz.
  • Cachefunctionaliteit: voorkomt dubbele verzoeken
  • Foutenafhandeling: uniforme foutenafhandeling
  • Automatisch opruimen: destroy() zorgt ervoor dat bronnen worden vrijgegeven

Testcode ​

Voorbeeldtest voor API aanroeppatroon.

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

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Posttype
// 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('Fout bij het maken van een bericht:', err);
      throw err;
    })
  );
}

// Gebruiksvoorbeeld
createPost({
  userId: 1,
  title: 'RxJSLeren van',
  body: 'RxJShet gebruik van deAPILeren van het patroon van aanroepen naar'
}).subscribe({
  next: post => {
    console.log('Aangemaakte berichten:', post);
    console.log('Een berichtID:', post.id); // JSONPlaceholderworden automatischIDtoegewezen (bijv.: 101)
  },
  error: err => console.error('Fout:', err)
});

Samenvatting. ​

Door het API-aanroeppatroon met RxJS onder de knie te krijgen, kunnen robuuste en onderhoudbare toepassingen worden gebouwd.

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

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Commenttype
// 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())
  );
}

// Parallel ophalen van dashboardgegevens
function fetchDashboard(userId: number): Observable<Dashboard> {
  return forkJoin({
    user: fetchUserById(userId),
    posts: fetchPostsByUserId(userId),
    comments: fetchCommentsByPostId(1) // Een berichtID=1Commentaar ophalen van
  }).pipe(
    map(({ user, posts, comments }) => ({
      user,
      posts,
      comments
    }))
  );
}

// Gebruiksvoorbeeld
fetchDashboard(1).subscribe({
  next: dashboard => {
    console.log('Gebruiker:', dashboard.user.name); // Voorbeeld: "Leanne Graham"
    console.log('Aantal berichten:', dashboard.posts.length); // Voorbeeld: 10Aantal berichten
    console.log('Aantal opmerkingen:', dashboard.comments.length); // Voorbeeld: 5Aantal berichten
  },
  error: err => console.error('Fout bij overname dashboard:', err)
});
  • forkJoin**: meerdere API's parallel uitvoeren, allemaal wachtend op voltooiing
  • concatMap**: API's in volgorde uitvoeren (vorige voltooid, dan volgende) switchMap: ideaal voor afhankelijke verzoeken, zoekfuncties
  • retryWhen: automatisch opnieuw proberen bij fouten, exponentiële backoff aanbevolen
  • timeout: altijd een timeout instellen
  • takeUntil: automatische annulering bij vernietiging van component
typescript
import { forkJoin, from, Observable, map } from 'rxjs';

// JSONPlaceholder API(na een zelfstandig naamwoord) gebaseerd op ...Commenttype
// 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())
  );
}

// Parallel ophalen van dashboardgegevens
function fetchDashboard(userId: number): Observable<Dashboard> {
  return forkJoin({
    user: fetchUserById(userId),
    posts: fetchPostsByUserId(userId),
    comments: fetchCommentsByPostId(1) // Een berichtID=1Commentaar ophalen van
  }).pipe(
    map(({ user, posts, comments }) => ({
      user,
      posts,
      comments
    }))
  );
}

// Gebruiksvoorbeeld
fetchDashboard(1).subscribe({
  next: dashboard => {
    console.log('Gebruiker:', dashboard.user.name); // Voorbeeld: "Leanne Graham"
    console.log('Aantal berichten:', dashboard.posts.length); // Voorbeeld: 10Aantal berichten
    console.log('Aantal opmerkingen:', dashboard.comments.length); // Voorbeeld: 5Aantal berichten
  },
  error: err => console.error('Fout bij overname dashboard:', err)
});
  • Type veiligheid: definieer types voor alle API antwoorden
  • Foutenafhandeling: implementeer catchError voor alle verzoeken
  • Afhandeling annuleringen: zorg voor opschoning met takeUntil.
  • Retriestrategie: probeer opnieuw op gepaste wijze volgens statuscode
  • Caching: voorkom dubbele verzoeken met shareReplay.

Volgende stappen. ​

Als je het API aanroeppatroon onder de knie hebt, kun je verder met de volgende patronen.

  • form-handling](./form-handling.md) - real-time validatie, automatisch opslaan.
  • UI-gebeurtenis-afhandeling](./ui-events.md) - integratie van UI-events en API-oproepen.
  • real-time gegevensverwerking](./real-time-data.md) - WebSocket, SSE.
  • caching-strategieën](./caching-strategies.md) - Cachen van API-reacties
  • Foutbehandelingspraktijken (in voorbereiding) - Meer geavanceerde foutbehandelingsstrategieën

Gerelateerde secties. ​

  • Hoofdstuk 4: Operatoren](../operators/index.md) - meer over switchMap, mergeMap en concatMap.
  • Hoofdstuk 6: Foutafhandeling](../error-handling/strategies.md)) - basis van catchError, retry.
  • Hoofdstuk 2: Cold/Hot Observable](../observables/cold-and-hot-observables.md)) - shareReplay begrijpen

Referentiebronnen ​

Uitgebracht onder de CC-BY-4.0 licentie.