Isolating user data logic with a UserService
Tags: angular, tutorial, refactoring, services
Published: 2024-06-20 12:08:44

## Introduction

In this article, I'll shift the focus to refactor the **user data management**. Currently, the logic for fetching and saving user data is handled within the component. To create a cleaner, more maintainable architecture, I'll extract this logic into a dedicated service, named `UserService`.

_**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 `15.http-rxjs-operators` branch of the repository https://github.com/cezar-plescan/user-profile-editor/tree/15.http-rxjs-operators._

#### What I'll cover

In this article, I'll guide you through a step-by-step refactoring process to create a dedicated `UserService` and enhance our Angular application architecture:
- **Analyzing the `UserProfileComponent`**: Pinpoint specific areas where data access and manipulation logic can be separated from UI concerns.
- **Creating the `UserService`**: Generate a dedicated service to handle all interactions with the user data API.
- **Handling form data**: Introduce a helper service, `HttpHelperService`, to convert raw form data into the format required for API requests.
- **Eliminating hardcoded URLs**: Replace hardcoded URLs with dynamic configuration using Angular environment files.
- **Managing API paths**: Establish a clear pattern for organizing and maintaining API paths within the `UserService`.
- **Addressing the User ID challenge**: Explore strategies for dynamically determining the user ID without hardcoding it in the service.

By the end of this article, you'll have a deeper understanding of how to structure your Angular applications to achieve better separation of concerns, improved maintainability, and more reusable code.

## Identifying the problem

The [component](https://github.com/cezar-plescan/user-profile-editor/blob/15.http-rxjs-operators/src/app/user-profile/user-profile.component.ts) handles a wide range of tasks, including performing HTTP requests, processing responses, and managing UI interactions. This approach violates the Single Responsibility Principle (SRP), a fundamental principle of software design that states that a class should have only one reason to change. In our case, the component is doing too much! 

Let's take a closer look at the HTTP related methods in the component class:
- `loadUserData()` - fetches user data from the server
- `getUserData$()` - defines the observable stream for fetching user data
- `saveUserData()` - saves updated user data to the server
- `saveUserData$()` - defines the observable stream for saving user data

The `getUserData$()` and `saveUserData$()` methods use the Angular `HttpClient` service to interact with the backend API:{% embed https://gist.github.com/cezar-plescan/9bb0257f5a5f4e754a4c5f632be6ae32 %}

These methods not only return observables but also contain hardcoded endpoint URLs. Maintaining these URLs within the component could become a headache if they change in the future. Therefore, these methods are prime candidates for extraction into a separate service class.

But what about `loadUserData()` and `saveUserData()`? They contain even more logic.{% embed https://gist.github.com/cezar-plescan/c19febf1fa47ce2f65686e6ce283566b %}

Should we move them into the service too? The answer is no, because they simply respond to requests, describing how the component UI elements will interact with the received data. These methods are tightly coupled to the component's UI. They handle aspects like: setting UI flags, updating form values, or displaying error messages. These tasks are directly related to the presentation layer and user interaction, which should ideally remain within the component's responsibilities.

I'll extract the reusable, data-centric logic, that is, HTTP requests, into a separate service, while keeping the UI-specific actions within the component. This adheres to the SRP, making the codebase more maintainable and easier to reason about.

In the next section, I'll create the `UserService` class to house the HTTP request logic, leaving the `loadUserData()` and `saveUserData()` methods in the component to manage the UI interactions based on the service's responses.

## Creating the `UserService`

I'll create a service file `user.service.ts` in the `src/app/services` folder using the Angular CLI command `ng generate service user`, then move `getUserData$()` and `saveUserData$()` methods into it.

### Initial content of the service class

Here is the content of the `UserService` class, after simply moving the code from the component:{% embed https://gist.github.com/cezar-plescan/a4cbe484dbfaff72de729973053fec6b %}

There are 2 errors here:
1. `TS2304: Cannot find name 'UserDataResponse'` - this is because the type `UserDataResponse` was defined within the component; I'll address this by creating a shared file for user data types.
2. `TS2339: Property 'form' does not exist on type 'UserService'` - this error arises because the `saveUserData$()` method references the `form` property, which belongs to the component; I'll resolve this by carefully considering where the responsibility for preparing form data should reside.

Let's see how they can be addressed.

### Creating shared user data types

To resolve the first error, I'll create a dedicated file for user related type definitions. I'll name this file `user.type.ts` and place it in the `src/app/shared/types/` folder:{% embed https://gist.github.com/cezar-plescan/fac94f461e3812c7b3e84ad385163de7 %}Then, in the `user.service.ts` file I'll import the `UserDataResponse` type:
```typescript
import { UserDataResponse } from "../shared/types/user.type";
```
This solves the first error. Let's tackle the second one now.

### Delegating form data preparation

The second error is because the `form` property doesn't exist in the new service. This makes sense, as the form is created and managed by the component. But how do we get the form data to the service for the HTTP request?

This situation raises an important design question: **where should the responsibility of processing the form data lie?**

My goal is to adhere to the Single Responsibility Principle (SRP) by grouping related functionalities and extracting them into separate entities, allowing the component to focus on its core responsibility: managing the view, as intended by the Angular team. I'll show you my approach for this situation.

I'll start by analyzing the different players involved in data manipulation and determine where this data processing should occur. I can identify 4 main actors in this process:
- _component_ - it creates and manages the raw form values.
- _raw form values_ - these are the data entered by the user into the form fields.
- _service_ - it's responsible for making the HTTP requests to save the user data.
- _processed form data_ - this is the user data transformed into a `FormData` object, suitable for sending in an HTTP request.

The data flow can be visualized like this: _Component_ ==> _Form Values_ ==> _Processed Form Data_ ==> _Service_ ==> _HTTP Request_.

The raw form values originate within the component and are manipulated by the user through the view. Logically, I can consider these values as an integral part of the component itself.

The processed form data, however, is necessary for the `UserService` to create the HTTP request. Where should the transformation from raw values to `FormData` object take place? Let's consider the options:
1. **within the component**: This would mean the component would be responsible for both managing the view and preparing the data for the request. This would violate the SRP and make the component less focused.
2. **within the service**: This seems more appropriate. The `UserService` would receive the raw form values from the component, process them into a `FormData` object, and then use it for the HTTP request. However, if we ever need another service that requires a similar data transformation, we'd have duplicate code, which isn't ideal.
3. **a separate entity**: This approach is the most flexible and reusable. I could create a helper function or separate service dedicated to generating the `FormData` object. This would allow us to reuse this logic in different parts of our application.

The remaining question is who should invoke the helper function that generates the `FormData` object: the component or the service?
- _the component_: If the component calls the function, it would mean it still has some knowledge about how the data is prepared for the request. This could lead to tight coupling and make the component less reusable.
- _the service_: Having the service invoke the helper function is a better approach. This way, the component can simply provide the raw form values, and the service takes care of the rest, including data transformation and sending the request.

In the spirit of **separation of concerns**, it's more appropriate for the **service** to invoke the helper function. Here's why:
- **data ownership**: the service is responsible for communicating with the backend, and the format of the request data is closely tied to this responsibility.
- **encapsulation**: keeping the data processing logic within the service encapsulates it, preventing the component from being burdened with unnecessary details.
- **testability**: having the service handle data processing makes it easier to write unit tests for both the component and the service independently.
- **flexibility**: this approach allows us to potentially reuse the helper function in other scenarios where we need to prepare the `FormData` for HTTP requests.

By delegating the data processing responsibility to another entity, I create a cleaner architecture where each entity focuses on its core function. The component handles the view and user interactions, while the services take care of data preparation and communication with the backend.

### Implementation of the helper service

Let's see this in practice. I'll create a new service dedicated for HTTP related operations, named `HttpHelperService`, in the `src/app/services` folder, using the Angular CLI: `ng generate service http-helper`. Here is its content:{% embed https://gist.github.com/cezar-plescan/4d1064ae60c9c4a4595db4da198901f4 %}

### Update the `UserService` class

Here is the updated `UserService` class that uses `HttpHelperService`:{% embed https://gist.github.com/cezar-plescan/674af5dd0ebd36a56df9f3b73b6adb5c %}

### Update the component class

There are a couple of changes to be made in the `user-profile.component.ts` file:
- remove the type definitions at the beginning of the file
- inject `UserService`: `private userService = inject(UserService);`
- invoke the two methods of the service
- add `this.form.value` as the argument of the `this.userService.saveUserData$` method
- remove `getUserData$()`, `saveUserData$()`, and `getFormData()` methods if not already done
- import the necessary types

Here is the updated component file:{% embed https://gist.github.com/cezar-plescan/9716a15feb30877faeafe12ca43adaf1 %}

### Check out the refactored code

The updated code incorporating the changes made so far can be found in the repository at this specific revision: https://github.com/cezar-plescan/user-profile-editor/tree/58be812de4c5c04d914c12b600cfc9c27f3cee4a. Feel free to explore the repository to see the full implementation details and how the refactored component and services interact.

## Dealing with hardcoded URLs

As I've discussed earlier, the hardcoded URLs for the API endpoints within the `UserService` are less than ideal due to their lack of flexibility, maintainability challenges, and potential security risks. I'll delve deeper into how to solve these issues using **Angular's environment files**.

### Why hardcoded URLs are problematic

- **environment specificity**: The hardcoded URL ties our service to a specific environment (e.g., http://localhost:3000). If we deploy our application to a different server or domain, the URL would be incorrect, and our service would break.
- **configuration changes**: If the base URL for our API changes (e.g., due to server migration or a change in the API version), we'll need to manually update it in every service where it's used, which is error-prone and tedious.
- **scattered configuration**: Having URLs scattered throughout our services makes it harder to manage and update them consistently. It becomes a challenge to keep track of all the places where the URL is used, increasing the risk of errors when changes are needed.
- **code duplication**: If multiple services use the same base URL, we'll likely have duplicate code, violating the DRY (Don't Repeat Yourself) principle.
- **testing challenges**: When testing the service, we'll need to mock the API endpoints. Hardcoded URLs can make it more difficult to replace the real API with a mock during testing.

To tackle these issues, let's take a closer look at the structure of our URLs and discuss who should be responsible for managing them.

### Understanding the URL structure and responsibilities

It's important to understand how our URLs are put together. Let's break down the URL we're using, `http://localhost:3000/users/1`:
- **Base URL**: This is the main address of our API (in this case, `http://localhost:3000`). This can vary depending on where the app is running (our computer, a testing server, or the live server). We shouldn't hardcode this in our service.
- **API Path**: This is the part of the URL that tells the server we want to work with users (`/users`). This should be the service's responsibility.
- **User ID**: This is the specific user we're dealing with (in this case, user number `1`). The user ID might be a _fixed value_ (e.g., when an admin updates a user's data) or represent the _currently logged-in user_. In the latter case, another service, like an `AuthService`, should be responsible for managing it.

Understanding this structure helps us determine who should build the complete URL and where each part should be defined.

### Managing the base URL

The best way to manage the base URL is to use **environment files**. We'll have different files for different environments (development, testing, production). This way, we can easily change the base URL without touching our service code.

#### Creating the environment files

Angular's environment files are a powerful mechanism for managing configuration settings across different environments. They allow us to define variables specific to each environment, keeping our codebase adaptable and maintainable.

Angular comes with a dedicated command for creating these files: `ng generate environments`. This will create two files in the `src/environments` folder: `environment.ts` and `environment.development.ts`. At their core, environment files are simply TypeScript files that export a constant object containing our configuration variables.

Add the following content to the `environment.development.ts` file:{% embed https://gist.github.com/cezar-plescan/f44692e79c952fb4b115189df009c5ee %}

Since we have only one environment right now, I won't use the `environment.ts` file.

To gain a deeper understanding of how environment files work and how to leverage them effectively in your Angular projects, you can explore the following resources:
- [Angular Environment Variables (YouTube)](https://youtu.be/o8wCvlj3IHg) - This video tutorial provides a step-by-step walkthrough of setting up and using environment files in Angular.
- [Angular Environment Variables](https://www.digitalocean.com/community/tutorials/angular-environment-variables) - This DigitalOcean tutorial offers a comprehensive guide on configuring and using environment variables in Angular projects.
- [Angular Basics: Using Environmental Variables to Organize Build Configurations](https://www.telerik.com/blogs/angular-basics-using-environmental-variables-organize-build-configurations) - This Telerik blog post explores the fundamentals of environment variables in Angular and how they can help organize your build configurations.

#### Usage in the UserService

To access the environment variables in our service, we simply need to import the `environment.ts` file:
```typescript
import { environment } from '../../environments/environment';
```
But earlier I've said that this file is not used! Under the hood, Angular replaces the content of this file with `environment.development.ts` file. This happens because `angular.json` file was automatically updated with an additional configuration:
```json
"development": {
              "optimization": false,
              "extractLicenses": false,
              "sourceMap": true,
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.development.ts"
                }
              ]
            }
```
Now, in the service class, I'll replace the hardcoded URLs with
`${environment.apiBaseUrl}/users/1`

### The `/users` path

The `/users` part of the URL is specific to our API and how it's set up. Since the `UserService` talks to the API, it makes sense for this path to live there. I'll define it as a constant within the service, so it's easy to update if needed.
```typescript
const USERS_API_PATH = 'users';
`${environment.apiBaseUrl}/${USERS_API_PATH}/1`
```

### Handling the User ID

There are primarily two main approaches to providing the user ID to the `UserService`:
1. pass as argument: the component, considering it's aware of the current user context, explicitly passes the user ID as an argument when calling service methods like `getUserData$` or `saveUserData$`.
2. service retrieval: the `UserService` itself fetches the user ID from another source; this source could be route parameters, an authentication service, or even a state management system like NgRx.

It's important to note that, regardless of the method, fetching the user ID isn't the `UserService`'s primary responsibility.

The best method depends on how the application is structured and how complex it is. In this tutorial, to keep things simple, I'll leave the user ID hardcoded for now, as there's no real context to determine which option is the most suitable. However, in a real-world project, you'd choose the option that best fits your specific needs and keeps your code clean and easy to maintain.

### See the code in action

To see all of these changes in action and get a better understanding of how the pieces fit together, check out this specific revision in the repository https://github.com/cezar-plescan/user-profile-editor/tree/308208159bd28d6ff3a5f4959ba6d867c1ae632a.

## Conclusion

In this article, we successfully refactored our `UserProfileComponent`, tackling the challenge of tightly coupled responsibilities. By extracting the data access and form data preparation logic into dedicated services – `UserService` and `HttpHelperService` – we've achieved a cleaner and more maintainable codebase.

Here's what we've accomplished:
- **Improved separation of concerns**: Our `UserProfileComponent` is now focused on its core responsibility: managing the user interface and interactions.
- **Enhanced reusability**: The extracted services can be easily reused in other parts of our application, saving us time and effort.
- **Better code organization**: Our project structure is more modular and easier to navigate.

This refactoring effort not only simplifies our current code but also paves the way for future enhancements. With a more streamlined component and reusable services, we can easily add new features or modify existing ones without worrying about unintended side effects.

Feel free to explore and experiment with the code from this article, available in the `16.user-service` branch of the [GitHub repository](https://github.com/cezar-plescan/user-profile-editor/tree/16.user-service).

______

_I hope this article has shown you the power of refactoring in improving Angular code quality and maintainability. Please leave your thoughts, questions, or suggestions in the comments below!  Let's keep learning and building better Angular applications together._

_Thanks for reading!_