Clean API Architecture for React Project

Bernard Bado
January 05, 2022
Clean API Architecture for React Project
When you use affiliate links in this article, We earn a small comission. Thank you!

Did you ever feel like your API calls are not organized properly? Or while working on your React project, did you feel like your API architecture needs a bit of improvement?

If you answered "Yes" to at least one of the questions above, this article is exactly for you!

A project structure is one of the most important aspects of the React development environment.

tip

Good project structure makes it easy to organize different modules in a well-organized hierarchy. This will make it super easy to recognize and group project dependencies.

Different people may have different approaches when it comes to structuring their React projects. After all, React it's unopinionated, which means there is no one way to do a certain thing.

However, there are some common approaches that can help you improve React's project structure, and achieve clean architecture.

In the next section, we'll take a closer look at them.

File Structure in React

The file structure is not a new concept to developers. React official documentation is specifically saying it's not important what file structure you choose for your project.

React doesn’t have opinions on how you put files into folders. However, there are common approaches you should consider. (source: React)

Nonetheless, there are 2 file structure concepts you should consider:

  • Grouping by feature
  • Grouping by file type

Grouping Files by Feature

In a file structure when files are grouped by feature, all the files of a specific feature or a page are grouped together. This means all the .js, .jsx, .css and other file types reside in the same directory.

Using this technique, it's easier and faster for developers to find the files they want.

This is how the example project structure would look like.

src/
  common/
    Info.js
    Info.css
    InfoAPI.js
    Info.test.js
  dashboard/
    index.js
    Dashboard.js
    Dashboard.css
    Dashboard.test.js
    DashboardAPI.js
  profile/
    index.js
    Profile.js
    Profile.css
caution

The problems with this approach may arise when we need to reuse some code between 2 or multiple features. When handling such a scenario, we can use grouping by file type.

Grouping Files by Type

Using this technique, files are grouped by their purpose. All the components are located in the components directory. CSS files are located in the styles directory. Basically, all the files that belong to a certain part of the application are located together.

This technique makes it easy to find necessary files. On top of that, it makes reusing certain pieces of code a breeze. However, one can argue it's not as clean as grouping by feature.

Using type grouping, we can achieve the following file structure.

src/
  components/
    common/
      Info.js
    dashboard/
      index.js
      Dashboard.js
    profile/
      index.js
      Profile.js
  styles/
    Info.css
    Dashboard.css
    Profile.css
  api/
    DashboardAPI.js
    InfoAPI.js
  testing/
    Info.test.js
    Dashboard.test.js
info

Both of these projects structures are valid and can be used without any worries. You can choose the folder structure that you feel comfortable using. And also, you should choose a folder structure that suits your project.

How to Call API from React

Before we dive deeper into the React architecture and project overview. Let's quickly discuss how to actually call API in React.

note

If you're already familiar with API calls in React, you can skip to the next section.

For the purpose of this article, we'll use Start Wars Open API. This is a completely open API, which means no authentication is necessary.

We can access Start Wars API using the following endpoint.

https://swapi.dev/api

To make this request inside React app, we have multiple options. We can use 3rd party libraries or plain fetch.

From 3rd-party libraries, there are 2 options you should consider.

tip

It's entirely up to you to choose the method of data fetching. However, we suggest using any library based on Fetch API. For example, ky.

The next code snippet shows how to fetch the same piece of data using the following libraries:

  • Axios
  • Ky
  • Plain Fetch
import axios from "axios";
import ky from "ky";

const baseUrl = "https://swapi.dev/api";

const fetchDataWithAxios = async () => {
  const { data } = await axios.get(baseUrl + "/people");
  console.log(data);
};

const fetchDataWithKy = async () => {
  const data = await ky.get(baseUrl + "/people").json();
  console.log(data);
};

const fetchData = async () => {
  const res = await fetch(baseUrl + "/people");
  const data = await res.json();
  console.log(data);
};

How to Structure API Calls in React

Now that we covered all the base information. Let's combine them and see how to structure API calls in React.

There is a high chance we'll need to reuse API calls. Because of this fact, it makes sense to put all API calls inside the same directory. We'll call this directory api and the structure will look as follows.

note

We will keep using Star Wars API for the purpose of this example.

src
└── api
    ├── index.js
    ├── client.js
    └── services
        ├── People.js
        ├── Films.js
        ├── Starships.js
        ├── Vehicles.js
        ├── Species.js
        └── Planets.js
    

All the necessary configuration will be handled in the client.js. We can specify what is the URL to call API or specify logic that should be used reused for every API call. The file can look something like this.

client.js
import ky from "ky";

const client = ky.extend({
  prefixUrl: "https://swapi.dev/api",
  hooks: {
    beforeRequest: [
      (request) => {
        // Do something before every request
        // This is a good place to authorize request if needed
      }
    ],
    afterResponse: [
      (response) => {
        // Do something after every response
        // For example, check status code etc...
      }
    ]
  }
});

export default client;

Now that we have API client defined. We can use it in one of the API services. Service usually represents one piece of data. In our example, it can be People, Films, Starships, etc.

To implement People service, we only need 2 methods:

  • Fetch all the people
  • Fetch one person based on id

The implementation for this will look as follows.

import client from "./client";

export default {
  // returns list of all the people
  list: () => client.get("people"),
  // return one specific person
  detail: (id) => client.get(`people/${id}`)
};

And to get the list of all people, we just need to call the function.

  const fetchPeople = async () => {
    try {
      const data = await People.list().json();
      // Do something with returned data
      console.log(data);
    } catch (error) {
      // Handle API errors
      console.error(error.code);
    }
  };

It's a simple implementation, but it's quite effective. The beauty of this approach is that you only need to define API call once. If the API changes somehow, you can quickly adapt the API definition without touching any parts of the codebase.

Using React Query for Data Fetching

In the previous section, we implemented a simple service that can fetch data. However, in your application, you'll most likely need to store this data somewhere. Maybe even change it.

To cover all these use cases, we can use an awesome library that was built for this purpose.

note

React Query is a handy library for managing data fetching and data management. However, to cover it in depth is beyond the scope of this article. If you want to learn more, follow the official documentation by clicking this link.

React Query helps to fetch, cache, and update data in your React and React Native applications all without touching any "global state". (source: React Query)

In this section, we'll modify our previous example to use React Query library.

The API client doesn't require any changes. All we need to do is rewrite a service to use react-query. And along with that, we need to change the way we call the service.

People.js
import { useQuery } from "react-query";

import client from "./client";

export default {
  // returns list of all the people
  useList: () =>
    useQuery("peopleList", async () => {
      try {
        return await client.get("people").json();
      } catch (error) {
        return Promise.reject(error);
      }
    }),
  // return one specific person
  useDetail: (id) =>
    useQuery(["peopleDetail", id], async () => {
      try {
        return await client.get(`people/${id}`).json();
      } catch (error) {
        return Promise.reject(error);
      }
    })
};
caution

React Query is based on React hooks, which means the library can only be used inside functional components.

In order to fetch all people from the API, we only need to use the hook inside any component.

App.jsx
import People from "./People";

const App = () => {
  const { data } = People.useList();

  return (
    <ul>
      {data.results.map((person) => (
        <li key={person.name}>{person.name}</li>
      ))}
    </ul>
  );
}

export default App;

Concluding Thoughts

API calls and data fetching are essential parts of any web application.

In React, we have many different ways to fetch data. We also have a lot of ways to structure these API calls.

In this article, we discussed why good project architecture is important. And we showed 2 ways you should consider when choosing file structure for your project.

We also showed how to structure API calls with both of these approaches.

tip

If you’re just starting a project, you shouldn't spend much time overthinking this. After all, at the beginning of a project, we don't get the perfect picture of what our codebase will look like.

After doing some actual coding, you'll start to get an idea of what is the right file structure for you. And you'll know how to organize API calls in your React project.