Managing API layers in Vue.js with TypeScript
Tags: typescript, vue
Published: 2021-07-17 09:32:42

## Motivation

Almost every Single-Page Application at some point needs to get some data from the backend. Sometimes there are several sources of data like REST APIs, Web Sockets etc. It's important to manage the API layer in the right way to make it simple and easy to use in any place of your application no matter if it's store, component or another type of source file.

## TLDR

If you already have some experience in development and want to check the solution here is the `FancyUserCard` example. If some things would be hard to understand feel free to check the detailed step-by-step path.

### Bad
![An example of API calls in component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/84q8ge695h60osoc9w6l.png)Performing API calls in the component is bad because:
- You make your components large and filled with logic that has nothing to do with the component itself which violates SRP;
- Same API methods could be used in different components which causes code duplication and violates DRY;
- You are importing dependencies globally and it violates the DI principle;
- Whenever API changes, you need to manually change every method that is needed to be modified.

### Good
To make things work better we need to slightly change our code and move all the API calls into a separate place.
#### users.api.ts
![Users API layer file](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0dc8x9ah1oa1dfe7u86v.png)In this case we:
- Have one single `AxiosInstance` that is configured to work with `/users` API branch and our code becomes modular;
- Have all methods located in one place so it's easier to make changes and to reuse them in different components without duplicating code;
- Handle the successful request as well as request failure and make us able to work with both error and data object depending on request status;
- Provide a standardized response return type for each method so we can work with them in one way. 

#### FancyUserCard.vue
![FancyUserCard component that imports API methods](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vybn8lpk1ex9inb4shig.png)And in our component:
- We are not dealing with the HTTP layer at all so our component is only responsible for rendering data that comes from the API layer;
- Methods return both errors and data so we can notify your user if something went wrong or simply use data that was returned by a method.

### Advanced
![API methods encapsulated within a class](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zplkc8f0ytelaujbhk35.png)Some final changes:
- The API call method was moved to reduce code duplication and all the methods are called using this private method.

### Some other ideas

The approach shown above is enough to handle standard API layer workflow. If you want to make it even more flexible you could think about implementing some ideas below:

#### Creating abstraction over HTTP layer![Creating abstraction over HTTP layer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y8w0vcscagfe88o1unsn.png)About the idea:
In the example, you can see that now we have an interface for our `HttpClient` so we could have as many implementations as we need. It works if we have different HTTP clients like `axios`, `fetch`, `ky` and if we will need to migrate from one to another we would simply need to rewrite our `HttpClient` implementation in one place and it will be applied automatically in any place where we use our service;

#### Create a factory![Create a factory](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xqa5gascfzfa22s8afqm.png)About the idea:
If you have few different data sources you could use some sort of factory to create the instance with needed implementation without an explicit class declaration. In this case, you just need to provide a contract interface and then implement each API method as you want.

## About the problem
As you already know, dealing with API calls in your components is harmful because whenever the changes come you have plenty of work to do to maintain your code in the working state. Also, it can be pretty challenging to test components and API because they are directly and deeply coupled. We want to avoid those things while writing code so let's get through the example.

### Example
This is the code for the initial example of an API call. For simplicity let's omit other code and keep attention only on the method itself.

```typescript
axios
  .get<User>(`https://api.fancy-host.com/v1/users/${this.userId}`)
  .then((response) => {
    this.user = response.data;
  })
  .catch((error) => {
    console.error(error);
  });
```
As you can already see, we're accessing the component `data()` directly and use global `axios` which forces us to type more code for setting the request configuration.

### TODO list
1. Migrate the code to a separate method;
2. Move from `then` syntax to `async`/`await`;
3. Setup `axios` instance;
4. Manage methods return type;
5. Incapsulate the method in `Class`.

## Refactoring

### 1. Migrate the code to a separate method

To start with, lest move our code to the separate file and simply export a function that accepts `userId` as input parameter and return `user` object if the call was successful:
```typescript
export function getUser(userId: number) {
  axios
  .get<User>(`https://api.fancy-host.com/v1/users/${userId}`)
  .then((response) => {
    return response.data;
  })
  .catch((error) => {
    console.error(error);
  });
}
```
Already an improvement! Now we can import this function whenever we need to get `User`. We just need to specify the `userId` and we're ready to go.

### 2. Move from `then` syntax to `async`/`await`

In real world there are often situations when you need to make sequential calls. For example, when you fetch `user` you probably want to get information about posts or comments related to user, right? Sometimes you want to perform requests in parallel and it can be really tricky if we're talking about `.then` implementation. So why won't we make it better?
```typescript
export async function getUser(userId: number): Promise<User | undefined> {
  try {
    const { data } = await axios.get<User>(`https://api.fancy-host.com/v1/users/${userId}`);
    return data;
  } catch (error) {
    console.error(error);
  }
}
```
As you can see, now we are providing additional typings and using `await` to stop our code from running until the API call finishes. **remember that you're able to use `await` only inside the `async` function**.

### 3. Setup `axios` instance;

Okay, so now the longest line is the one with the end-point URL. Your server host is probably not going to change often and it's better to keep your API branch set up in one place so let's get into:
```typescript
const axiosInstance = axios.create({
  baseURL: "https://api.fancy-host.com/v1/users"
});

export async function getUser(userId: number): Promise<User | undefined> {
  try {
    const { data } = await axiosInstance.get<User>(`/users/${userId}`);
    return data;
  } catch (error) {
    console.error(error);
  }
}
```
Much better. Now if your `/users` API branch will change, you could simply rewrite it in the instance configuration and it will be applied to every call made using this `AxiosInstance`. Also, now you could use something called [Interceptors](https://github.com/axios/axios#interceptors) which allows you to make some additional changes to requests/responses or perform logic when a request is made or response is back. Check out the link to get more details!

### 4. Manage methods return type

What if I will say to you that your user doesn't understand if (and why) something went wrong .. until! Until you provide some information about "what went wrong". UX is really important to keep your user happy and make the workflow better at all. So how are we going to do that? Simply by returning both `data` and `error` from our API call. You could also return as many things as you need (if you need them, right?):
```typescript
export type APIResponse = [null, User] | [Error];

export async function getUser(userId: number): Promise<APIResponse> {
  try {
    const { data } = await axiosInstance.get<User>(`/${userId}`);
    return [null, data];
  } catch (error) {
    console.error(error);
    return [error];
  }
}
```
And how it will look when we use it, for example in our `created()` callback:
```typescript
async created() {
  const [error, user] = await getUser(this.selectedUser);

  if (error) notifyUserAboutError(error);
  else this.user = user;
}
```
So in this case, if any error happens, you would be able to react to this and perform some actions like pushing an error notification, or submit a bug report or any other logic you put in your `notifyUserAboutError` method. Elsewise, if everything went okay, you could simply put the user object into your `Vue` component and render fresh information.

Also, if you need to return additional information (for example status code to indicate if it is `400 Bad Request` or `401 Unautorized` in case of failed request or if you want to get some response headers if everything was okay), you could add an object in your method return:
```typescript
export type Options = { headers?: Record<string, any>; code?: number };

export type APIResponse = [null, User, Options?] | [Error, Options?];

export async function getUser(userId: number): Promise<APIResponse> {
  try {
    const { data, headers } = await axiosInstance.get<User>(`/${userId}`);
    return [null, data, { headers }];
  } catch (error) {
    console.error(error);
    return [error, error.response?.status];
  }
}
```
And usage:
```typescript
  async created() {
    const [error, user, options] = await getUser(this.selectedUser);

    if (error) {
      notifyUserAboutError(error);
      
      if (options?.code === 401) goToAuth();
      if (options?.code === 400) notifyBadRequest(error);
    } else {
      this.user = user;

      const customHeader = options?.headers?.customHeader;
    }
  }
```
As you can see, your requests become more and more powerful but at the same time, you can make your components free from that logic and work only with those details you need.

### 5. Incapsulate the method in `Class`
And now there is time for the final touch. Our code is already doing a great job but we can make it even better. For example, there are cases when we want to test how our components interact with other layers. At the same time, we don't want to perform real requests and it's enough to ensure that we make them correctly at all. To achieve this result we want to be able to mock our HTTP client. To make it possible, we want to "inject" a mocked instance into our module and it's hard to imagine a better way to do that than with `Class` and its `constructor`.
```typescript
export class UserService {
  constructor(private httpClient: AxiosInstance) {}

  async getUser(userId: number): Promise<APIResponse> {
    try {
      const { data } = await this.httpClient.get<User>(`/${userId}`);
      return [null, data];
    } catch (error) {
      console.error(error);
      return [error];
    }
  }
}
```
And the usage:
```typescript
const axiosInstance = axios.create({
  baseURL: "https://api.fancy-host.com/v1/users"
});

export const userService = new UserService(axiosInstance);
```
In this case, you don't expose your `AxiosInstance` and provide access only thru your service public API.

## Conclusions
Hope that this article was useful for you. Do not hesitate to leave a comment if you have some other ideas or if there are any questions about the content of this post. I will update this post with detailed information about the problem, the solutions and the refactoring process soon.
Cheers!