RxJS Mastery – #6 ajax

The RxJS ajax creation operator is basically a wrapper around a XMLHttpRequest. It accepts an URL or an AjaxConfig as parameter. The return value of the operator is of course an Observable. This Observable emits the notifications for the response or error that is returned during the request execution.

RxJS ajax operator explained 🎓

Remember that an Observable usually wraps an async data source. The Observable returned from the ajax function represents an HTTP request. This request is executed once we subscribe to the Observable. Let’s have a look at an easy example where we fetch users from GitHub.

const githubUsers$ = ajax(`https://api.github.com/users?per_page=5`);

githubUsers$
    .pipe(map(result => result.response))
    .subscribe(response => {
        console.log(response);
    });

We are creating the githubUsers$ Observable. In the happy case we are expecting a HTTP status 200 response with some data in it. To access the body of this response we are using the map operator. The created Observable is a cold Observable. It only begins its work when the subscription to it happens, i.e. the request is only executed when we subscribe to the Observable.

Headers, query parameters, etc. can be passed via AjaxConfig.

The following image shows what is going on. A subscription happens (^) which triggers an HTTP request towards an API. If the Observable emits, the consumer usually receives the response.

If the API call is not successful, e.g. returns an http status 404, the Observable emits an error. In the example below we introduced a spelling mistake in the URL’s path which causes a 404.

const githubUsers$ = ajax(`https://api.github.com/usders?per_page=5`);

githubUsers$
    .pipe(map(result => result.response))
    .subscribe(response => {
        console.log(response);
    }, error => {
        console.log('An error occurred');
        console.log(error);
    });

Hence, we see in the console ‘An error occurred’. Beside that the emitted value also contains information about the error. The AjaxErrorImpl object gives us all the data about the executed request and the received response in this case.

AjaxErrorImpl {
      message: 'ajax error 404',
      name: 'AjaxError',
      xhr: XMLHttpRequest {},
      request: {
        async: true,
        crossDomain: true,
        withCredentials: false,
        method: 'GET',
        timeout: 0,
        responseType: 'json',
        url: 'https://api.github.com/usders?per_page=5',
        headers: { 'x-requested-with': 'XMLHttpRequest' },
        body: undefined
      },
      status: 404,
      responseType: 'json',
      response: {
        message: 'Not Found',
        documentation_url: 'https://docs.github.com/rest'
      }
    }

What problems does the RxJS ajax operator solve? 🚧

Well, as said, it wraps an async data source. In this case the response of an HTTP request. So, we can say it allows us to execute HTTP requests in a reactive way. Subscribers to the Observable created by ajax are notified once the response is returned or an error happened.

If we are working with JSON and TypeScript we can improve a few things compared to the example above. Because the operator allows us also to be type safe. We are using the ajax.getJSON variant to directly access the response body and we are typing our response (here GitHubUser[]):


const githubUsers$: Observable<GitHubUser[]> =
    ajax.getJSON<GitHubUser[]>(`https://api.github.com/users?per_page=5`);

githubUsers$
    .subscribe({
        next: (responseBody) => {
            console.log(responseBody[0].id);
        }
    });

Like that the type in the response body is not unknown and we can safely access the properties, in this case id.

How to test the ajax operator🚦

As the data source in this case is usually an external service, we want to mock. Furthermore, we do not really want to test the ajax operator itself, but only the code that uses it. So, it makes sense to put the ajax operation into a service and test only the code that handles the response.

class GitHubService {
    getUsers$(): Observable<GitHubUser[]> {
       return ajax.getJSON<GitHubUser[]>(`https://api.github.com/users?per_page=5`);
    }
}

it('should mock the service', () => {
    testScheduler.run((helpers) => {
       const { cold, expectObservable } = helpers;

       const service = new GitHubService();

       // some mocking approach:
       service.getUsers$ = (): Observable<GitHubUser[]> => cold('-a', { a: [{ id: 1 }]});

       const firstUser$ = service.getUsers$().pipe(map(r => r[0]));
       expectObservable(firstUser$, '^----').toEqual(cold('-a', { a: { id: 1 }}));
    });
});

Similarly the error case can be tested. We are replacing the service method by a method that returns a cold error Observable. The catchError operator does some mapping. Finally, we test for the output by using expectObservable.

it('should return an error', () => {
    testScheduler.run((helpers) => {
        const { cold, expectObservable } = helpers;

        const service = new GitHubService();

        // some mocking approach:
        service.getUsers$ = (): Observable<GitHubUser[]> => cold('---#', undefined, new Error('Unauthorized'));

        const firstUser$ = service.getUsers$()
            .pipe(catchError((error: Error) =>
                throwError(() => new Error(`Request failed. Message: ${error.message}`)))
            );
        expectObservable(firstUser$).toBe('---#', null, new Error('Request failed. Message: Unauthorized'));
    });
});

Exercise for the ajax operator 💪

Execute a POST request and test it. A possible solution can be found on GitHub.