Path To A Clean(er) React Architecture - API Layer & Data Transformations
Tags: react, javascript, webdev, frontend
Published: 2024-05-10 11:00:00

---
description: Mixing transformations of API data and UI code quickly leads to messy and unmaintainable code. In this article, we take a step towards a cleaner React architecture by extracting data transformations 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 third 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 created the initial API layer and extracted fetch functions](https://profy.dev/article/react-architecture-api-layer-and-fetch-functions) that can be used in the components. This way we already removed a lot of implementation details related to API requests from the UI code. 

But we’re not done yet. 

In this article, we identify data transformations in the component that can be moved to the API layer. Another step towards a cleaner architecture.

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

## Table Of Contents

1. [Bad Code - Example 1: API responses inside UI code](#bad-code-example-1-api-responses-inside-ui-code)
    1. [What is the problem?](#what-is-the-problem)
    2. [Solution: Use data transformations in the API layer](#solution-use-data-transformations-in-the-api-layer)
2. [Bad Code - Example 2: Data transformations in UI code](#bad-code-example-2-data-transformations-in-ui-code)
    1. [What is the problem?](#what-is-the-problem)
    2. [Solution: Move data transformation logic to the fetch function](#solution-move-data-transformation-logic-to-the-fetch-function)
3. [Bad Code - Example 3: Transformation of input data](#bad-code-example-3-transformation-of-input-data)
    1. [What is the problem?](#what-is-the-problem)
    2. [Solution: Move transformation of input data to API layer](#solution-move-transformation-of-input-data-to-api-layer)
4. [Next refactoring steps](#next-refactoring-steps)


[![React Architecture Course Waitlist](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/39421fp4sx3wvy4l8888.png)](https://profy.dev/article/react-architecture-api-layer-and-data-transformations#newsletter-box)

## Bad Code - Example 1: API responses inside UI code

Let’s have a look at a problematic code example. Here is a user profile component that fetches data from two different endpoints.

Can you identify the problem?

```typescript
import { useEffect, useState } from "react";
import { Navigate, useParams } from "react-router";

import UserApi from "@/api/user";
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(() => {
    if (!handle) {
      return;
    }

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

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

  if (!handle) {
    return <Navigate to="/" />;
  }

  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>
  );
}

```

For completion, these are the fetch functions used in the component: `UserAPI.getUser()` and `UserAPI.getUserShouts()` 

```typescript
export interface UserResponse {
  data: User;
}

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

export interface UserShoutsResponse {
  data: Shout[];
  included: Image[];
}

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

### What is the problem?

We have the raw response data structures inside the component. But if you think about it, the UI code 

- shouldn’t care that the `User` object is returned as `response.data`
- shouldn’t need to know that the `shouts` are in `response.data` and the images inside `response.included`.

Imagine we pass these response data structures to a dozen or more components in a larger project. Now imagine that the API changes the data structure. We’d have to adjust all of those components one by one.

Additionally, the code would be more readable if we wouldn’t have to deal with all those `data` and `included` fields anymore. Name it `user`, `shout`, or `image` instead.

To summarize: All this knowledge can be hidden inside the API layer as this is where we deal with API responses and the fetch functions should return the final `User`, `Shout`, or `Image` data structures.

### Solution: Use data transformations in the API layer

The goal is to remove all code related to API response data structures from the component. 

The solution is to use data transformations in the fetch functions:

```typescript
export interface UserResponse {
  data: User;
}

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

export interface UserShoutsResponse {
  data: Shout[];
  included: Image[];
}

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

Now the fetch functions return the actual data structures like `User`, `Shout`, or `Image`.

This allows us to remove the response data structures from the component:

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

  const [user, setUser] = useState<User>();
  const [shouts, setShouts] = useState<Shout[]>();
  const [images, setImages] = useState<Image[]>([]);
  const [hasError, setHasError] = useState(false);

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

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

    UserApi.getUserShouts(handle)
      .then(({ shouts, images }) => {
        setShouts(shouts);
        setImages(images);
      })
      .catch(() => setHasError(true));
  }, [handle]);

  ...

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

As you can see, all the code related to responses has vanished from the component. Not to forget that we got rid of the nasty `data` and `included` fields. 

At the same time, the only changes so far are

- we returned some nested fields instead of the complete response
- we renamed some fields.

Clearly things can be more complex. So let’s look at another example.

[![React Architecture Course Waitlist](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/39421fp4sx3wvy4l8888.png)](https://profy.dev/article/react-architecture-api-layer-and-data-transformations#newsletter-box)

## Bad Code - Example 2: Data transformations in UI code

Here’s another component of the example project: it’s a feed that shows the newest “Shouts” (aka Tweets) to a user.

```typescript
import FeedApi from "@/api/feed";
import { LoadingView } from "@/components/loading";
import { ShoutList } from "@/components/shout-list";
import { FeedResponse, Image, User } from "@/types";

export function Feed() {
  const [feed, setFeed] = useState<FeedResponse>();
  const [hasError, setHasError] = useState(false);

  useEffect(() => {
    FeedApi.getFeed()
      .then((feed) => setFeed(feed))
      .catch(() => setHasError(true));
  }, []);
  if (hasError) {
    return <div>An error occurred</div>;
  }
  if (!feed) {
    return <LoadingView />;
  }

  const users = feed.included.filter((u): u is User => u.type === "user");
  const images = feed.included.filter((i): i is Image => i.type === "image");
  return (
    <div className="w-full max-w-2xl mx-auto flex flex-col justify-center p-6 gap-6">
      <ShoutList shouts={feed.data} users={users} images={images} />
    </div>
  );
}
```

We can again see that the response is stored in the component. And as before the fetch function directly returns the API response data structure.

```typescript
async function getFeed() {
  const response = await apiClient.get<FeedResponse>("/feed");
  return response.data;
}
```

### What is the problem?

Ok, this looks pretty much like the same problem. Apart from these two lines of code:

```typescript
const users = feed.included.filter((u): u is User => u.type === "user");
const images = feed.included.filter((i): i is Image => i.type === "image");
```

Apparently, we again have to deal with an `included` field. But this time, that array not only contains images but also users. And the above two lines are data transformation logic that separate objects of type user and image. 

Meaning we have data transformation logic inside the component.

But the component 

	- shouldn’t care that the `included` field in the feed response contains users and images
	- shouldn’t need to know that it can distinguish the users and images by their `type` field.

> Note: You think the response data structure with the `data` field and the `included` field with mixed data types looks weird? You might not have encountered the popular [JSON:API standard](https://jsonapi.org/) yet.

### Solution: Move data transformation logic to the fetch function

Again, the goal is to remove this logic from the component. 

The solution is simple: we move the two lines to the fetch function.

```typescript
import { FeedResponse, Image, User } from "@/types";

import { apiClient } from "./client";

async function getFeed() {
  const response = await apiClient.get<FeedResponse>("/feed");
  const shouts = response.data.data;
  const users = response.data.included.filter(
    (u): u is User => u.type === "user"
  );
  const images = response.data.included.filter(
    (i): i is Image => i.type === "image"
  );
  return { shouts, users, images };
}

export default { getFeed };
```

Now the fetch function returns the actual data structures `User`, `Shout`, and `Image` and doesn’t expose API responses to the UI code anymore.

The component becomes a lot simpler:

```typescript
import { useEffect, useState } from "react";

import FeedApi from "@/api/feed";
import { LoadingView } from "@/components/loading";
import { ShoutList } from "@/components/shout-list";
import { Image, Shout, User } from "@/types";

export function Feed() {
  const [feed, setFeed] = useState<{
    shouts: Shout[];
    images: Image[];
    users: User[];
  }>();
  const [hasError, setHasError] = useState(false);

  useEffect(() => {
    FeedApi.getFeed()
      .then((feed) => setFeed(feed))
      .catch(() => setHasError(true));
  }, []);

  if (hasError) {
    return <div>An error occurred</div>;
  }

  if (!feed) {
    return <LoadingView />;
  }
  return (
    <div className="w-full max-w-2xl mx-auto flex flex-col justify-center p-6 gap-6">
      <ShoutList shouts={feed.shouts} users={feed.users} images={feed.images} />
    </div>
  );
}
```

Now the component directly gets the `users`, `shouts`, and `images`. It has no knowledge of the API responses anymore.

Great, our code becomes more streamlined. But so far we only focused on response data. What about input data like request bodies?

## Bad Code - Example 3: Transformation of input data

Here’s yet another component: A dialog that allows the user to reply to another user’s shout (aka tweet).

```typescript
import MediaApi from "@/api/media";
import ShoutApi from "@/api/shout";
import UserApi from "@/api/user";

...

export function ReplyDialog({ children, shoutId }: ReplyDialogProps) {
  const [open, setOpen] = useState(false);
  const [isLoading, setIsLoading] = useState(true);
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [hasError, setHasError] = useState(false);

  ...
  
  async function handleSubmit(event: React.FormEvent<ReplyForm>) {
    event.preventDefault();
    setIsLoading(true);
    try {
      const message = event.currentTarget.elements.message.value;
      const files = event.currentTarget.elements.image.files;
      let imageId = undefined;
      if (files?.length) {
        const formData = new FormData();
        formData.append("image", files[0]);
        const image = await MediaApi.uploadImage(formData);
        imageId = image.data.id;
      }
      const newShout = await ShoutApi.createShout({
        message,
        imageId,
      });
      await ShoutApi.createReply({
        shoutId,
        replyId: newShout.data.id,
      });
      setOpen(false);
    } catch (error) {
      console.error(error);
    } finally {
      setIsLoading(false);
    }
  }
  
  // a form using the handleSubmit function is rendered here
  return (
    ...
  );
}
```

The chained requests are a problem for the future. Here we focus on the preparation of the input data. Particularly, the image upload.

### What is the problem?

It appears that the `MediaApi` expects the image to be added to a `FormData` object. 

- But is this something the component should be concerned with?
- Why should it matter to the UI code whether the image is uploaded using form data or any other mechanism?

Let’s imagine we wanted to switch to another cloud storage like AWS S3? We’d most likely have to adjust the component code as well.

And since other places like the user profile are likely to upload images as well we’d have to adjust multiple parts of the codebase just because the underlying image storage changed.

We can do better.

### Solution: Move transformation of input data to API layer

Similar to the previous examples, let’s move the data transformation to the API layer. Here’s the adjusted `MediaApi.uploadImage()` function:

```typescript
import { Image } from "@/types";

import { apiClient } from "./client";

async function uploadImage(file: File) {
  const formData = new FormData();
  formData.append("image", file);

  const response = await apiClient.post<{ data: Image }>("/image", formData);
  const image = response.data.data;
  return image;
}

export default { uploadImage };
```

The fetch function in the API layer now expects a `File` as input. The exact upload mechanism is hidden from the component.

This simplifies the component code quite a bit:

```typescript
export function ReplyDialog({ children, shoutId }: ReplyDialogProps) {
  const [open, setOpen] = useState(false);
  const [isLoading, setIsLoading] = useState(true);
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  const [hasError, setHasError] = useState(false);

  ...
  
  async function handleSubmit(event: React.FormEvent<ReplyForm>) {
    event.preventDefault();
    setIsLoading(true);
    try {
      const message = event.currentTarget.elements.message.value;
      const files = event.currentTarget.elements.image.files;
      let image;
      if (files?.length) {
        image = await MediaApi.uploadImage(files[0]);
      }
      const newShout = await ShoutApi.createShout({
        message,
        imageId,
      });
      await ShoutApi.createReply({
        shoutId,
        replyId: newShout.data.id,
      });
      setOpen(false);
    } catch (error) {
      console.error(error);
    } finally {
      setIsLoading(false);
    }
  }
  
  // a form using the handleSubmit function is rendered here
  return (
    ...
  );
}
```

The component is still responsible getting the files from the event target and passing only the first one `files[0]` to the media API. The rest of the heavy lifting is handled by the API.

## Next refactoring steps

We took another step closer towards a cleaner React architecture. Data transformations are now handled by the API layer.

But there’s still room for improvement. For example, the fetch functions still pass the data structures `User`, `Shout`, or `Image` as they are returned from the API.

And look at this example. Ain’t pretty, right?

```typescript
{
    "id": "shout-1",
    "type": "shout",
    "createdAt": 1713455132695,
    "attributes": {
        "authorId": "user-1",
        "text": "The world sucks!!!!",
        "likes": 5,
        "reshouts": 0
    },
    "relationships": {
        "replies": [
            "shout-3"
        ]
    }
}
```

In the next article, we will introduce a domain layer that will help us get rid of this complexity.

[![React Architecture Course Waitlist](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/39421fp4sx3wvy4l8888.png)](https://profy.dev/article/react-architecture-api-layer-and-data-transformations#newsletter-box)