Path To A Clean(er) React Architecture - API Layer & Fetch Functions
Tags: react, javascript, webdev, frontend
Published: 2024-04-26 12:04:00

---
description: Mixing API and UI code quickly leads to messy and unmaintainable code. In this article, we take a step towards a cleaner React architecture by extracting fetch functions to an API layer.
---

_The unopinionated nature of React is a two-edged sword:_

- _On the one hand you get freedom of choice._
- _On the other hand many projects end up with a custom and **often messy architecture**._

_This article is the second part of_ _a series about software architecture and React apps_ _where we take a code base with lots of bad practices and refactor it step by step._

[Previously, we extracted an API client to share common configuration like the API base URL between all requests in the application.](https://profy.dev/article/react-architecture-api-client)

In this article, we want to focus on separating API related code from the UI components.

![Screenshot of code that contains a request method, response type, endpoint path and parameter handling](https://media.graphassets.com/RUDy4wgkRquKJoBUs6Pn)

{% embed https://youtu.be/tl6NuSL8euY %}

## Bad Code: Mixed API and UI code

Let’s have a look at the bad code example. Here’s the result of the first refactoring step from [the previous article](https://profy.dev/article/react-architecture-api-client): a component that fetches data from two API endpoints and renders the data.

```typescript
import { useEffect, useState } from "react";
import { useParams } from "react-router";
import { apiClient } from "@/api/client";
import { LoadingSpinner } from "@/components/loading";
import { ShoutList } from "@/components/shout-list";
import { UserResponse, UserShoutsResponse } from "@/types";
import { UserInfo } from "./user-info";

export function UserProfile() {
  const { handle } = useParams<{ handle: string }>();

  const [user, setUser] = useState<UserResponse>();
  const [userShouts, setUserShouts] = useState<UserShoutsResponse>();
  const [hasError, setHasError] = useState(false);

  useEffect(() => {
    apiClient
      .get<UserResponse>(`/user/${handle}`)
      .then((response) => setUser(response.data))
      .catch(() => setHasError(true));

    apiClient
      .get<UserShoutsResponse>(`/user/${handle}/shouts`)
      .then((response) => setUserShouts(response.data))
      .catch(() => setHasError(true));
  }, [handle]);

  if (hasError) {
    return <div>An error occurred</div>;
  }
  
  if (!user || !userShouts) {
    return <LoadingSpinner />;
  }

  return (
    <div className="max-w-2xl w-full mx-auto flex flex-col p-6 gap-6">
      <UserInfo user={user.data} />
      <ShoutList
        users={[user.data]}
        shouts={userShouts.data}
        images={userShouts.included}
      />
    </div>
  );
}
```

## Why is this code bad?

It’s simple: API requests and UI code are mixed. A bit of API code here, a bit of UI there.

![Screenshot of code that contains a request method, response type, endpoint path and parameter handling](https://media.graphassets.com/Il7QrSIMRFuFq782XMOg)

But in fact, for the UI it shouldn’t matter how it get’s the data:

	- It shouldn’t care whether a GET, a POST, or a PATCH request is sent.
	- It shouldn’t care what the exact path of the endpoint is.
	- It shouldn’t care how the request parameters are passed to the API.
	- It shouldn’t even really care whether it connects to a REST API or a websocket.

All of this should be an implementation detail from the perspective of the UI.

## Solution: Extract functions that connect to the API

Extracting functions that connect to the API to a separate place will greatly help decouple API related code from UI code.

These functions will hide the implementation details like 

	- the request method
	- the endpoint path or
	- the data types.

In order to have a clear separation we will locate these functions in a global API folder. 

```typescript
// src/api/user.ts

import { User, UserShoutsResponse } from "@/types";
import { apiClient } from "./client";

async function getUser(handle: string) {
  const response = await apiClient.get<{ data: User }>(`/user/${handle}`);
  return response.data;
}

async function getUserShouts(handle: string) {
  const response = await apiClient.get<UserShoutsResponse>(
    `/user/${handle}/shouts`
  );
  return response.data;
}

export default { getUser, getUserShouts };
```

Now we use these fetch functions inside the component.

```typescript
import UserApi from "@/api/user";

...

export function UserProfile() {
  const { handle } = useParams<{ handle: string }>();

  const [user, setUser] = useState<User>();
  const [userShouts, setUserShouts] = useState<UserShoutsResponse>();
  const [hasError, setHasError] = useState(false);

  useEffect(() => {
    if (!handle) {
      return;
    }

    UserApi.getUser(handle)
      .then((response) => setUser(response.data))
      .catch(() => setHasError(true));

    UserApi.getUserShouts(handle)
      .then((response) => setUserShouts(response))
      .catch(() => setHasError(true));
  }, [handle]);

  ...
```

## Why is this code better? Decoupling of UI and API code

You’re right. The code changes aren’t huge. We basically moved a bit of code around. The result might initially look like somewhat cleaner component code.

```typescript
// before
apiClient.get<UserResponse>(`/user/${handle}`)

// after
UserApi.getUser(handle)
```

But in fact, we started to effectively decouple the UI code from API related functionality.

I’m not getting tired of repeating myself: now the component doesn’t know a lot of API related details like 

	- the request method `GET`
	- the definition of the data type `UserResponse`
	- the endpoint path `/user/some-handle`
	- or that the handle is passed to the API as URL parameter.

Instead it calls a simple function `UserApi.getUser(handle)` that returns a typed result wrapped in a promise. 

Additionally, we can reuse these fetch functions in multiple components or for different rendering approaches like client-side or server-side rendering.

## Next refactoring steps

Yes, we took a big step forward separating our UI code from the API. We introduced an API layer and removed many implementation details from the UI component.

But we still have considerable coupling between the API layer and our components. For example, we transform the response data inside the components:

![Screenshot of code that contains response.data inside component code](https://media.graphassets.com/lJjVG80BRVi817s4vOwF)

Now, this might not be the most impressive example. So let’s look at another one from the same code base:

![Screenshot of code that filters part of the response data based on the entity’s type field](https://media.graphassets.com/6FUzRWpTSbmufwlBQQy5)

Here we can see how the response of the feed has an `included` field that contains users and images.

Not pretty, but sometimes we have to deal with such APIs.

But why should the component know?

Anyway, we’ll deal with that in the next article.

[![The React Job Simulator](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8bupb5hadajo1ok905li.png)](https://profy.dev)