Building Custom RxJS Operators for HTTP Requests
Tags: angular, tutorial, rxjs, refactoring
Published: 2024-06-14 11:34:17

## Introduction

In this article I'll focus on how to efficiently **structure the logic in the HTTP request stream pipelines** for the loading and saving of the user data. Currently, the entire logic is handled within the `UserProfileComponent` class. I'll refactor this to achieve a more declarative and reusable approach.

_**A quick note**:_
- _Before we begin, please note that this article builds on concepts and code introduced in previous articles of this series. If you're new here, I highly recommend that you check out those articles first to get up to speed._
- _The starting point for the code I'll be working with in this article can be found in the `14.image-form-control` branch of the repository https://github.com/cezar-plescan/user-profile-editor/tree/14.image-form-control._

#### In this article I'll guide you through:

- How to break down complex logic into smaller, reusable RxJS operators.
- The role of the `catchError` operator in error handling.
- Strategies for handling validation errors, upload progress, and successful responses within observable pipelines.
- The benefits of using custom operators for cleaner, more maintainable code.
- How to create custom operators like `tapValidationErrors`, `tapUploadProgress`, `tapResponseBody`, and `tapError` to make the code more streamlined and easier to manage.

By the end of this article, you'll have a deeper understanding of how to leverage custom RxJS operators to manage the intricacies of HTTP requests and responses in your Angular applications.

## Identifying the current issues

The code of the `saveUserData()` method of the `UserProfileComponent` class in the [user-profile.component.ts](https://github.com/cezar-plescan/user-profile-editor/blob/14.image-form-control/src/app/user-profile/user-profile.component.ts#L141-L186) file looks like this:{% embed https://gist.github.com/cezar-plescan/0ec6e15d4a7556cb584c653c7fb5cabd %}

The method is quite lengthy and handles multiple tasks, such as detecting response error types and computing upload progress. This violates the Single Responsibility Principle, making the code less maintainable. Ideally, the component should only be concerned with receiving errors, user data, and upload progress, not the intricacies of error handling or progress calculation.

To address all these, I'll create custom RxJS operators to handle different aspects of the HTTP request stream. This approach will lead to a more declarative and reusable code structure.

I'll start by implementing an operator for validation error handling, which I'll name `tapValidationErrors`.

## Handling validation errors: the `tapValidationErrors` operator

The core idea behind this operator is to apply the **extract method** refactoring technique. I'll move the validation error handling code from the component into a separate, reusable function that can be used in the observable pipe chain.

The reason behind naming this operator is that the _tap_ prefix aligns with the RxJS convention of using it for operators that perform side effects (like logging or triggering actions) without modifying the values in the stream. While this operator doesn't directly modify values, it does perform the side effect of invoking the callback function for validation errors.

### Benefits of this approach

- **Separation of concerns**: The error handling logic is decoupled from the main subscription logic, improving code organization and readability.
- **Reusability**: The operator can be easily reused across different observables and components that need to handle validation errors in a similar way, promoting a DRY (Don't Repeat Yourself) approach.
- **Flexibility**: The operator provides a clear way to customize the error handling behavior for validation errors without affecting the handling of other types of errors.

### Implementation and usage

Let's create a new file, `tap-validation-errors.ts`, in the `src/app/shared/rxjs-operators` folder with the following content:{% embed https://gist.github.com/cezar-plescan/d1b27d6f733d8080ce442581dd1b43f0 %}I've also updated the `saveUserData()` method in the `user-profile.component.ts` file:{% embed https://gist.github.com/cezar-plescan/2f15d364ca670f916ea0e980c0f16593 %}
Let's break down what I've done here. I'll first examine the method in the component. 

### Simplifying the `saveUserData()` method

The `catchError` operator is now responsible for only handling global errors. 

I've introduced the new operator `tapValidationErrors` before it. This order is crucial, as placing `catchError` before `tapValidationErrors` would cause it to catch and handle all errors, including validation errors, preventing `tapValidationErrors` from specifically addressing those validation errors. By placing `tapValidationErrors` first, I ensure that validation errors are identified and processed _before_ any other error handling logic.

### How the operator works

Now let's talk about the logic inside the `tapValidationErrors` operator.

After extracting the validation error handling code from the `catchError` block from the `saveUserData()` method, I could have simply used two separate `catchError` operators in the pipeline: one dedicated to validation errors and the other for general errors. While this would separate the logic, it wouldn't necessarily improve reusability or code organization.{% embed https://gist.github.com/cezar-plescan/0c3ffe86487b49467bc764874c634fe2 %}

Instead, I can adopt a more elegant solution by encapsulating the extracted logic into a reusable custom operator. This leverages the power of RxJS operator composition, where we can combine existing operators to create new ones with specialized behavior.

In this case, the `tapValidationErrors` operator is essentially a higher-order function that takes a callback function as an argument and returns a new RxJS operator based on `catchError`. This custom operator handles validation errors in a controlled and informative manner, allowing us to perform specific actions like displaying error messages while leaving other error types to be handled elsewhere in the pipeline.

The `callback` parameter in the operator definition will be invoked when these errors are detected. In the component method `saveUserData()` I specify the exact action to take when validation errors are received from the server, which in this instance is to display the errors in the form.

#### Error handling strategies in `catchError`

There's one crucial aspect in the implementation I want to discuss: the use of `return EMPTY` and `throw error`. The `catchError` operator requires that its inner callback either returns an observable or throws an exception.

By returning `EMPTY`, I explicitly indicate that this error in the stream has been handled within this operator. This prevents the error from propagating further down the observable chain and triggering another error handler, which is not what I expect to happen. `EMPTY` is a special observable that immediately completes without emitting any values. By returning this observable, we effectively terminate the current observable stream. This is important because, in the context of a form submission, we don't want to continue processing the response if the server indicates validation errors.

On the other hand, by using `throw error`, I'm explicitly re-throwing errors that are not validation errors. This allows these errors to be caught and handled by a higher-level `catchError` operator in our RxJS pipeline or by a global error handler in the application (like the `ErrorHandler` injection token or an HTTP interceptor).

## Tracking upload progress: the `tapUploadProgress` operator

I'll continue to refine the file upload handling by addressing the calculation of upload progress. Currently, this logic resides in the observer in the `saveUserData()` method, which, as I discussed earlier, isn't ideal. My goal is to create a separate operator that handles this task exclusively. Building upon the approach I've taken with the `tapValidationErrors` operator, I'll create a new file `tap-upload-progress.ts` in the same folder. I'll name the operator `tapUploadProgress`. Here is the content of the file:{% embed https://gist.github.com/cezar-plescan/51ebcbfe438df2e635bec186a02ab2d6 %}I've removed the extracted code from the `saveUserData()` method, and added the new operator:{% embed https://gist.github.com/cezar-plescan/a0b716b9162844bbf10825bdc886bf5a %}

By extracting the progress calculation into the `tapUploadProgress` operator, I've decluttered the `saveUserData()` method and made it more focused on its core responsibilities. This enhances code readability and maintainability while promoting reusability of the progress-tracking logic across other parts of our application.

### Configuring upload progress tracking

In order to access upload progress information, we need to configure the HTTP request with two specific options: `reportProgress: true` and `observe: 'events'`: {% embed https://gist.github.com/cezar-plescan/e17e9f9fb6738912957e8dd016a76066 %}

In `HttpClient`, methods like `put`, `post`, `get`, etc., accept an optional third argument called `options`. This argument is an object that allows us to configure various aspects of the HTTP request and how the response is handled. 

With both `reportProgress: true` and `observe: 'events'`, we get an observable that emits a stream of `HttpEvent` objects. Each event represents a different stage of the HTTP request/response lifecycle.

#### `reportProgress: true`

This option tells `HttpClient` to track the progress of the HTTP request, particularly relevant for uploads and downloads.

When `reportProgress` is true, `HttpClient` emits `HttpEventType.UploadProgress` (for uploads) or `HttpEventType.DownloadProgress` (for downloads) events as part of the observable stream. These events contain information about the progress, such as loaded bytes and total bytes.

In the context of our image upload component, this allows us to track the upload progress and provide feedback to the user through the progress bar indicator.

_**Note**: Even without this option, the code will still work fine, but we'll have no progress indicator. The `if` condition in the `tapUploadProgress` operator won't be satisfied, and the callback for updating the progress won't be invoked._

#### `observe: 'events'`

This option instructs `HttpClient` to emit the full sequence of HTTP events instead of just the final response body.

The emitted events can include:
- `HttpEventType.Sent`: The request has been sent to the server.
- `HttpEventType.UploadProgress` (for uploads): Provides progress information.
- `HttpEventType.Response`: The response has been received from the server (this contains the data we usually work with).

By observing events, we gain access to more granular information about the HTTP request lifecycle. In our case, we're using it to access the UploadProgress events to track progress and the Response event to get the final server response.

## Extracting the response body with the `tapResponseBody` operator

Let's make our code even better by introducing another handy tool: the `tapResponseBody` operator. This operator helps us grab the data we want from successful responses to our HTTP requests.

### The code

Here is the content of the `tap-response-body.ts` file: {% embed https://gist.github.com/cezar-plescan/334325ce612a2488d6388e5162ecd4d6 %}And the updated `saveUserData()` method: {% embed https://gist.github.com/cezar-plescan/4c3237e18b9a10462b23f0fbab34b9f3 %}

### Why Use `tapResponseBody`?

Right now, the `saveUserData` method handles successful responses directly inside the `subscribe` block. This works, but it can get messy as our form gets more complex. The `tapResponseBody` operator cleans things up by separating this logic into a reusable piece.

With `tapResponseBody`, we can:
- Separate response handling: Keep the code that deals with the response data away from the main part of the `saveUserData` method. This makes our code tidier and easier to read.
- Reuse the logic: Use the same response handling code for other parts of our app where we need to get data from successful responses.
- Focus on the big picture: Keep the `subscribe` part simple and focused on the main actions, while the `tapResponseBody` operator handles the fine details of dealing with the response.

### Updating the `loadUserData()` method

Now that we've successfully applied the `tapResponseBody` operator for the `saveUserData()` method, let's see how we can apply it for the `loadUserData()` method. I'll take a similar approach and move the code from the `subscribe` method into our operator inner callback:{% embed https://gist.github.com/cezar-plescan/40b72f576610b4ca5bfe7bae1f283718 %}However, when compiling it, we get a few TypeScript errors:{% embed https://gist.github.com/cezar-plescan/f1db6e2ea9b9812a5c127841cfee1305 %}

#### Understanding the challenge

Why doesn't this work smoothly? The problem lies in how our HTTP requests are set up. Let's take a closer look at the two methods:{% embed https://gist.github.com/cezar-plescan/afe9ced99e8a87b6fb2e04d4a0e3d7e7 %}There is a key difference. The save request uses `observe: 'events'`, meaning it gives a stream of `HttpEvent<UserDataResponse>` objects. But `getUserData$()` doesn't speficy this option, so it defaults to `observe: 'body'`, which gives us the response data directly. Notice the `map` operator which receives values of type `UserDataResponse`, but it converts them to `UserProfile`.

Our `tapResponseBody` operator is designed to work with `HttpEvent` objects. This mismatch is causing these TypeScript errors.

At a high level, I see primarily two ways to address this issue:
1. to modify **`getUserData$()`**, or,
2. to adapt the **`tapResponseBody`** operator.

#### Modify the `getUserData$()` stream

I could change the GET request to also use `observe: 'events'`, just like `saveUserData$()`. This would make both requests consistent, and `tapResponseBody` would work as is. However, this would also make our operator less flexible; it would only work with observables that emit `HttpEvent` objects.

Here is how this solution could be implemented:{% embed https://gist.github.com/cezar-plescan/9722e85606743fd641650da381219930 %}

This might be simpler if we only have a few places where we need to handle both `HttpEvent` and response body types. On the other hand, we might need to repeat code if we use this pattern in multiple places, and the `tapResponseBody` operator would be less reusable.

#### Adapt the `tapResponseBody` operator

This solution implies the operator to work with requests no matter the value of the `observe` option, which could be one of: `"body", "response", "events"`. This would make the operator more versatile and adaptable to different HTTP request configurations. It also offers more flexibility and reusability, especially if we have multiple observables that emit either event or response body types. It also encapsulates the type-handling logic within the operator itself. Of course, the tradeoff is that it requires more development work for adapting the logic inside the operator. 

I'll choose the more flexible route and enhance the operator to handle both scenarios. For improved clarity, I'll rename it to `tapResponseData`. Here's the updated code:{% embed https://gist.github.com/cezar-plescan/99aebd9da3ab3ffe32f1d5700524f706 %}Take a moment to read the comments in the code – they explain how I've improved the operator to handle different types of responses.

The generic type `<T>` in the operator now represents the specific type of data we expect from the response, `UserDataResponse`.

Additionally, I've created new files with different data types and interfaces:{% embed https://gist.github.com/cezar-plescan/cc6865dee1fc6b92fd9356293053c198 %}

Here is the updated component where the operator is used:{% embed https://gist.github.com/cezar-plescan/ca46bdac3d9f00d8e75f9368c1a5ad56 %}

Since `tapResponseData` now handles different kinds of responses, the other two operators `tapValidationErrors` and `tapUploadProgress` need to be updated too. The fix is to specify the new type `HttpClientResponse<T>`, instead of the old one`HttpEvent<T>`, which was available only when the `observe` option was set to `'events'`. Here are their updated code:{% embed https://gist.github.com/cezar-plescan/0bf6c70ef8bc287243e2e6e568b0eec8 %}

You can test the code with different settings for the `observe` option in both load and save requests. The code should successfully handle all scenarios.

## Error handling made easy: the `tapError` operator

To improve the error handling and make the code more compact, I'll introduce a new custom RxJS operator called `tapError`. This operator will serve as a dedicated mechanism for handling errors in HTTP request streams, like replacing the `catchError` block in the `loadUserData()` or `saveUserData()` methods.

### The purpose of `tapError`

The primary goal of the `tapError` operator is to execute specific actions when an error occurs within an observable stream. In our case, the `loadUserData()` method needs to be notified when an HTTP error happens so we can set an error flag in the UI.

### Implementation

Here's the implementation of the operator:{% embed https://gist.github.com/cezar-plescan/4b60e98cdd205634c706b4ea852475bc %}The usage in the component is straightforward:{% embed https://gist.github.com/cezar-plescan/15ebfa7d08053bb14dbbf765c676cb96 %}

One important distinction: the `tapError` operator is designed for single use within a stream. Why? Because the stream will terminate immediately after the operator handles the error.

### Error handling strategies in RxJS

There are several ways to respond to errors in RxJS streams:
- `catchError` operator: this is the most common and flexible way to handle errors. It allows us to catch errors and decide how to proceed, either by returning a new observable, emitting a fallback value, or throwing the error again.
- `tap` operator with `error` callback: executes a side effect when an error occurs but allows the error to propagate further.
- `subscribe` method's `error` callback: Handles the error at the end of the observable chain.

### Why `catchError` is the right tool

Here is an alternative implementation of the `tapError` with the `tap` operator and the `error` callback: {% embed https://gist.github.com/cezar-plescan/7a37dd1cd873535c6f53542f904ffa5f %}If you use this implementation, the behavior of the `loadUserData()` would remain the same.

In our scenario, I'm interested in both handling the error (setting the `hasLoadingError` flag) and stopping the error from propagating. This aligns perfectly with the purpose of `catchError`. Here's why `tap` with the `error` callback wouldn't be ideal:
- uncontrolled error propagation: The `tap` operator doesn't stop errors from continuing down the stream. This means the error would still reach the `subscribe` block `error` callback, potentially causing duplicate error handling and unexpected behavior.
- limited control: While `tap` allows us to perform actions in response to errors, it doesn't let us change the stream's behavior fundamentally. In our case, we want to stop the stream after an error, which `tap` can't do.

By using `catchError` with `return EMPTY`, we achieve clear error handling and explicit stream termination.

## Conclusion: A cleaner, more maintainable approach

In this article, I've shown you how custom RxJS operators can make the Angular code much cleaner and easier to work with. I created special operators like `tapValidationErrors`, `tapUploadProgress`, `tapResponseData` and `tapError` to handle different parts of HTTP requests.

By using these custom operators, we've made our code:
- easier to understand - each operator does one specific job, making it simpler to read and follow the logic.
- reusable - we can use these operators in other parts of the project, saving us time and effort.
- more flexible - we can now easily change how we handle errors or responses without affecting other parts of the code.

Feel free to explore and experiment with the code from this article, available in the `15.http-rxjs-operators` branch of the repository: https://github.com/cezar-plescan/user-profile-editor/tree/15.http-rxjs-operators.

I hope this article helps you see how awesome custom RxJS operators are. Feel free to use these ideas in your own Angular projects and let me know if you have any questions or comments. Let's keep learning and improving together as Angular developers.

Thanks for reading!