Path To A Clean(er) React Architecture - Domain Entities & DTOs
Tags: react, javascript, webdev, frontend
Published: 2024-05-24 11:00:00

---
description: Passing data structures of API responses directly to the UI code often isn’t ideal. In this article, we isolate the React components from the server by separating data transfer objects (DTOs) and domain entities.
---

_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 fourth 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) and [data transformations](https://profy.dev/article/react-architecture-api-layer-and-data-transformations). 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 prevent raw response data structures from entering UI code by introducing a domain layer and DTOs. Another step towards a cleaner architecture.

{% embed https://youtu.be/-ojYfV4NSUQ %}

## Table Of Contents

1. [Bad Code: Data Structures Of API Responses In UI Code](#bad-code-data-structures-of-api-responses-in-ui-code)
2. [What is the problem?](#what-is-the-problem)
    1. [We use data structures defined by the API](#we-use-data-structures-defined-by-the-api)
    2. [Tight coupling of UI code to server API](#tight-coupling-of-ui-code-to-server-api)
3. [Solution: Introducing domain entities and DTOs](#solution-introducing-domain-entities-and-dtos)
    1. [The domain model](#the-domain-model)
    2. [DTOs and data transformations](#dtos-and-data-transformations)
4. [The good and the bad](#the-good-and-the-bad)
    1. [Pros: Cleaner architecture, more resilience, better separation](#pros-cleaner-architecture-more-resilience-better-separation)
    2. [Cons: More code, more complexity](#cons-more-code-more-complexity)
    3. [Conventions and automation](#conventions-and-automation)
5. [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-domain-entities-and-dtos#newsletter-box)

## Bad Code: Data Structures Of API Responses In UI Code

Let’s have a look at a problematic code example. Here is a user profile component that fetches data from an API and renders it.

```typescript
// src/pages/user-profile.tsx

import UserApi from "@/api/user";
import { User } from "@/types";

interface UserProfileProps {
  handle: string;
}

export function UserProfile({ handle }: UserProfileProps) {
  const [user, setUser] = useState<User>();

  useEffect(() => {
    UserApi.getUser(handle)
      .then((user) => setUser(user));
  }, [handle]);
  
  return (
    <section>
      <img
        src={user.attributes.avatar}
        alt={`${user.attributes.handle}'s avatar`}
      />
      <h1>@{user.attributes.handle}</h1>
      <p>({user.relationships.followerIds.length} follower)</p>
      <p>{user.attributes.info || "Shush! I'm a ghost."}</p>
    </section>
  );
}
```

> If you’re thinking we should use a library for server state management like react-query instead of manually fetching data in a useEffect: You’re right. But for now let’s stay tool-agnostic. We’ll get to react-query in a future article.

## What is the problem?

The UI code uses an unnecessarily nested data structure for the `user` object. We have

- `user.attributes.xyz` or
- `user.relationships.followerIds`

multiple times. Here’s the TypeScript interface:

```typescript
// src/types/index.ts

export interface User {
  id: string;
  type: "user";
  attributes: {
    handle: string;
    avatar: string;
    info?: string;
  };
  relationships: {
    followerIds: string[];
  };
}
```

Similar data structures can be found all over the project even inside components deep inside the component tree.

But what’s the reason for this nesting?

### We use data structures defined by the API

Turns out the `user` data is directly returned from the [fetch function we created in an earlier article](https://profy.dev/article/react-architecture-api-layer-and-fetch-functions):

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

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

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

export default { getUser };
```

And inspecting the network requests we can see that this data structure is defined by the server.

![Screenshot of the API response data showing a nested data structure.](https://media.graphassets.com/L3P2GnLOTj62815ZMnNA)

The server seems to be using the popular [JSON:API standard](http://jsonapi.org/) which is a great way to build APIs. But should we really use these data structures in the frontend?

### Tight coupling of UI code to server API

The problem is: By passing the data coming from the API directly to the components we tightly couple the UI to the server. The response data doesn’t reflect the data structure of the (user) entity but is merely a data-transfer object (DTO).

DTO is a useful pattern as you can model the data according to the needs of the consumer. You can include additional data that isn’t included in the original database model like `relationships.followerIds`.

But exposing the DTO to the UI components leads to unnecessary complexity by accessing fields via `user.attributes.xyz` or object destructuring that bloats the code base like

```typescript
const {
  handle,
  avatar,
} = user.attributes;
const {
	followerIds
} = user.relationships;
```

Apart from the immediate extra complexity we also make the complete UI code dependent on changes of the server API.

Imagine the backend folks decide to ditch the JSON:API standard. Suddenly there are no `user.attributes` anymore. But we sprinkled them all over the UI code. So just because the server API changed we now have to adjust like 90% of the frontend code as well.

## Solution: Introducing domain entities and DTOs

### The domain model

We start by creating a new TypeScript interface that simplifies the `User` . In this case we just flatten the API data structure.

```typescript
// src/domain/index.ts

export interface User {
  id: string;
  handle: string;
  avatar: string;
  info?: string;
  followerIds: string[];
}
```

_Side note: Here we introduced a new folder on the root level called_ _`domain`__. The domain is a concept reflecting the core of the business used e.g. in the Clean Architecture or Domain Driven Design (DDD)._

Instead of the data structures returned by the API let’s use the domain model in our component.

```typescript
// src/pages/user-profile/user-info.tsx

import UserApi from "@/api/user";
import { User } from "@/domain";

interface UserProfileProps {
  handle: string;
}

export function UserProfile({ handle }: UserProfileProps) {
  const [user, setUser] = useState<User>();

  useEffect(() => {
    UserApi.getUser(handle)
      .then((user) => setUser(user));
  }, [handle]);
  
  return (
    <section>
      <img
        src={user.avatar}
        alt={`${user.handle}'s avatar`}
      />
      <h1>@{user.handle}</h1>
      <p>({user.followerIds.length} follower)</p>
      <p>{user.info || "Shush! I'm a ghost."}</p>
    </section>
  );
}
```

Great, we got rid of the annoying `user.attributes` and `user.relationships` simplifying our code a bit.

But more importantly, we decoupled our UI code from the server making it more resilient against external changes.

Great improvement, but we’re not done yet. `UserApi.getUser(handle)` still returns the nested data structure coming from the API.

### DTOs and data transformations

The current fetch function in our API layer simply returns the API data.

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

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

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

export default { getUser };
```

With our components expecting the domain model we need some changes here as well.

We start by defining the model of the data transfer object (DTO) in the API layer.

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

export interface UserDto {
  id: string;
  type: "user";
  attributes: {
    handle: string;
    avatar: string;
    info?: string;
  };
  relationships: {
    followerIds: string[];
  };
}
```

From now on this data structure is only used inside the API layer and is not to be exposed to the rest of the application code.

Thus we need to transform the API response data to the domain model. A simple function will do the job.

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

import { User } from "@/domain";
import { UserDto } from "./dto";

export function dtoToUser(dto: UserDto): User {
  return {
    id: dto.id,
    avatar: dto.attributes.avatar,
    handle: dto.attributes.handle,
    info: dto.attributes.info,
    followerIds: dto.relationships.followerIds,
  };
}
```

Next, we apply this function to the response data before returning it to the component.

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

import { apiClient } from "../client";
import { UserDto } from "./dto";
import { dtoToUser } from "./transform";

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

export default { getUser };
```

## The good and the bad

### Pros: Cleaner architecture, more resilience, better separation

From an architectural perspective the new code is a lot cleaner:

- The data structures coming from the API are only used in the API layer as DTOs.
- The UI code is effectively isolated from the server’s REST API.
- Most of the application code uses shared domain models.

This way the code is more resilient and concerns are separated.

### Cons: More code, more complexity

But this decoupling also comes with a cost: Boilerplate.

For each entity we need to add one domain model, at least one DTO, and one transformer function. We have more code and a lot more files in our code base.

Also the level of complexity increased. At least for developers who aren’t familiar with the code base or its architecture.

### Conventions and automation

Once you get used to the architecture everything is quite clear though thanks to the conventions and consistency.

- The transformer files contain data transformation functions.
- The API files handle the requests.
- The domain layer contains the application wide data structures.

And while the boilerplate problem exists you can also automate the creation of some of these files depending on your tech stack. 

For example:

- For fullstack TypeScript applications you can use shared domain models on the server and the client e.g. in a monorepo or by moving them in a separate npm package.
- If you have a REST API that is well-documented with OpenAPI you can automatically generate your domain models or DTOs.
- Depending on the data structures in the API responses you might not need DTOs and data transformations. You can always introduce them later if the REST API changes.

## Next refactoring steps

You may have realized that we’re slowly adding responsibilities to the API fetch functions, like `getUser`. It’s not only responsible for sending API requests but also for transforming response data.

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

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

export default { getUser };
```

Here’s another example from the code base:

![Screenshot showing API code that also transforms input and output data.](https://media.graphassets.com/JmJ4ZIvGSVuzcDj46T6e)

In the next article of this series we will use the repository pattern to move some of these responsibilities away from the fetch functions.

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