Frontend
.......................................................................................................
Signup to be notified when the Fluent React course
launches in MARCH 2025 .
(and get started with a 5-part free React course in the mean time.)
...........................................................................................

Intro

If your React app contains more than just static text, you will need to make API calls to a backend server to perform the CRUD (`CREATE`, `READ`, `UPDATE` and `DELETE`) operations/calls over HTTP for the data to render in your React components.
 
In this guide, we will take a look at 7 (SEVEN) best practices for making API calls from your React app.
 
Whether you use `Axios` or `Fetch()` or some other HTTP package or library, these best practices will still apply.
 
So, let's begin ...

 

1: Use an API-Specific Folder

Even if your React app is a simple one with just a few components, you should properly organize it so that each common piece of the app has its own folder and/or files. This applies to your app's API logic, too.
 
I have seen codebases (on some "inherited" project, at work, previously) where a developer creates a `components/` folder and then puts both the files for the components and the related API files in there. That's terrible! Keep your API files separate from your component files.
 
Create an `api/` (or similarly-named) folder inside your app's `src/` directory. Then, keep all your API files and code inside that `api/` folder.

This approach benefits you and your fellow developers in many ways, including:

  • Modularity: Even when working with "minimalist" web frameworks (without a rigid requirement for how to organize your app's folders and files) like ReactJS, you should adopt an approach that organizes your app's contents logically.

    You should have a `components/` folder (to hold your component code), a `hooks/` folder (to hold the code for your custom React hooks) and an `api/` folder (to hold the files and logic for your API calls.)

  • Code Clarity and Easy Reference: Even a newbie (or your Backend Developer) would know, just by looking, what parts of your React code go into the `api/` and `components/` folder. Aim for that level of clarity and easy reference.

  • Developer Sanity: Whether you are working with another developer on this React app or will "shelve" this and come back to it a few weeks later, having all your API-related files in one explicitly-named `api/` folder will save your and your colleagues (and anyone lucky to be assigned to work on the same codebase later on) a lot of frustration and drained developer time.

    So, aim for developer sanity, for yourself and for others.

  • Developer Collaboration: Whether you are the Tech Lead on a project or the newly-hired Junior Developer, having a properly-organized codebase will go a long way in making it easy to break down the app and assign different parts of it to different developers without unnecessarily tripping over each other's code.

    If you have a dedicated `api/` folder for your API calls, for instance, you can assign the `api/UserApi.js` to Dev Ali and `api/PostApi.js` to Dev Bob and have everything work out seamlessly (or as much as possible).

 

Create a Base API Object:

While creating your API-calling logic, you will need to pass in the base/root URL of the API backend's server (and port, if necessary). You may also need to pass in other header data such as the "Content-Type", API authentication token, CORS-related settings, etc.

You can easily create a Base API Object (what `axios-http` calls an "Axios Instance" in its docs) and pass in these configurations.

You can then use this custom instance to make your calls to the API backend without having to repeat passing the same settings for every API call.

An example of doing this (if you are using `axios`) goes like this:

import axios from 'axios'

const RequestHeaders = {
    'Access-Control-Allow-Origin': '*',
    'Content-Type': 'application/json',
}

const Axios = axios.create({
    baseURL: 'https://example.com/api/',
    headers: RequestHeaders,
})


export default Axios

The (Capitalized) `Axios` object we created and exported in this code sample is actually an instance of the default `axios` object that many developers just use 'as-is'. In our case, we have created a custom instance that takes into account both our API server's `baseURL` and any headers we need to make calls to the API servers.
 
If you use a different HTTP framework, please, see its docs for how to do this (if it is possible). Then, do it and make life (and code) reusable the next time you want to make an API call.
 

2: One Class and File per API Endpoint

Whether you are making HTTP calls to one API endpoint or a few dozen, you should apply some modularity here, too.
Keep to creating one JavaScript class for each API endpoint.
 
And, create only one API class in a file. Don't have two or more API classes in the same file, even if you are exporting each class separately.
 
For example, if you are making API calls to the `https://example.com/api/posts` endpoint, you should/could:
  • Create a `src/api/PostApi.js` file to hold the code for making API calls to that API endpoint.

  • Import the custom Base API Object (such as the custom `Axios` instance object we created in the previous section) into the `src/api/PostApi.js` file.

  • Create an appropriately-named JavaScript class (which we will call `PostApi`, in our case) inside the `src/api/PostApi.js` file.

  • Export the API class (for reuse elsewhere).
A simple example of doing this is:

import Axios from './BaseApi';

class PostApi {
    // To make API calls to the 'https://example.com/api/posts/' endpoint

}

export default PostApi;

 

3: One Method per API Query Functionality

Once you have created the API class, you should create one class method for each of the CRUD operations/calls you will be making against the API backend server. Each method should handle one and exactly one CRUD operation.
 
Further developing on our `PostApi` class above, we can implement this like this:

import Axios from './BaseApi';

class PostApi {
    // To make API calls to the 'https://example.com/api/posts/' endpoint

    static getAllPosts(){
        return Axios.get(`/posts/`);
    }

    static getOnePost(post_url){
        return Axios.get(`posts/${post_url}`);
    }

    static newPost(new_post_object){
        return Axios.post(`/posts`, new_post_object)
    }

    static getCommentsforPost(post_url){
        return Axios.get(`/posts/${post_url}/comments`);
    }

    static updatePost(post_url, updated_post){
        //Replace the existing post (in the API backend) with the new `updated_post` object
        return Axios.put(`/post/${post_url}`, updated_post)
    }

    static patchPost(post_url, updated_fields){
        return Axios.patch(`/posts/${post_url}`, updated_fields)
    }

    static deletePost(post_url){
        return Axios.delete(`/posts/${post_url}`);
    }

}

export default PostApi;
And, that's why we have the `getAllPosts()`, `getOnePost()`, `deletePost()` and other methods.
 

 

4: One Method per API Filter

Sometimes, you may need to query your API backend to return one or more items that satisfy some conditions/filters that you specify.
 
For example, you may want to get a list of only the blog posts that belong to a category or get the list of comments on a blog post.
 
In such a case, also create one class method for each of these filtering needs.
 
And, that's what the `getCommentsforPost()` method does in the example `PostApi` class we defined earlier.
 
So, you can have these two (clearly-named) methods in your API class, too:

static getCommentsforPost(post_url){
        return Axios.get(`/posts/${post_url}/comments`);
    }

static getDraftPosts(){
        return Axios.get(`/posts/?published=false`);
    }

And, for each filter method, return the HTTP response only. Any other work of code logic will go into your component files.

5: Handle Only API-Calling Logic in the API Object

Your API object (and its methods) such as our `PostApi` class above should only be concerned with making the API call and returning the HTTP response from the API call.
 
You shouldn't do anything else in the API object. Keep it clean and simple.
 
Unless it is absolutely necessary (and would be "cleaner" to do so in a special case you are handling), put all data preparation and manipulation logic inside your component files (or wherever your API objects will be consumed.)

6: Handle All Data-Wrangling in the Component Logic

If you need to perform any computations, do some data wrangling, do some JSONification or perform some data validation (such as from a user input in a form), you should do that inside your component's code.
 
So, for example, you may have a signup form in one of your components. Once the user enters their email and password, you should construct the `newUser` object from inside the component.
 
You should also "JSONify" (i.e convert to JSON) that object using the `JSON.stringify(newUser)` before making the API call to submit the new user's data.
 
And, once the API call returns the HTTP response (or error), you should handle unpacking and setting that data (to local component state and so on) from inside your React component's code and not inside the API class' method.
 
Your API object should only be concerned with:

1. Making the API call and;
2. Returning the HTTP response (containing the data or error) from the call.

7: Check for Empty Success Response before Setting Data or Error

When you make an API call to the API backend server, you will, generally speaking, receive one of two things back:

  • A success HTTP response data (that contains one item or an array of items). This is usually handled inside the `then()` block of the API object and is in the `response.data` object.

  • An error message with (some) details about the error (and what may have caused it.) This is usually handled inside the `catch()` block of the API object.
But depending on how the API backend is developed, you may sometimes receive an HTTP response that contains no data. From the frontend's perspective, that basically equals an error.

But from the backend's POV, there was no error; there just weren't any matching data to send back to the client/frontend. So, the API server sends you back an empty list of objects. Or, it may even send you an empty object (with no properties or values).

For example, let's say you make a call to the API backend server using our `getDraftPosts()` method (to get a back a list of the blog posts that haven't been published yet).

If all your posts have been published, your server may send back an empty array of blog posts.

Or, you may make an API call to get the 10th blog post using the `getOnePost(postId)` method. But, if there was no matching post (with an id of 10, for instance), your API server may (depending on how it is programmed) send you back any empty object.
 
In either of these two cases, the API Developer has decided to send you back an empty response: an empty array of posts or an empty post object.
 
So, how do you handle that?
 
Well, from inside your API object's `then()` block, you should check if the list of items returned are less than one (i.e empty) or greater than zero (i.e contains one or more matching posts).
 
If it is less than one (i.e empty), handle it, for example, as "No matching blog posts found" in the component's UI.
 
If the array contains one or more matching blog posts, set them to the component's `blog_posts` state (for example) and then render them inside that components's UI.
 
In the case of fetching a single blog post (using the `getOnePost()` method, in our case), you can check if the returned object contains a required field (such as the `title` or `body` fields, in this case).

If it does, set the object to a piece of the local component state (such as `blog_post`). If not, populate, for example, the `post_error` local state and render it in the component's UI as the error message you got back.

In either case, you can use either an `if...else` or a `switch` statement to implement the conditional logic to check for the validity of the object (or array of objects) sent back.

Of course, if your API backend sends back an actual error, you can handle that from inside the `catch()` block of your API object.

Custom Hooks vs Classes for APIs

Throughout this guide, we have worked within the premise that our API calls use a JavaScript class to encapsulate the API-calling logic. But there are situations where you may need or want to use a custom React hook to implement your API calling logic.

If you are using custom hooks, you should, ideally:
  • Create a `hooks/` or `api/` folder inside your React app's `src/` directory.

  • Create a base API object (and configure the headers, `baseURL`, authentication tokens, etc.) just like we did with the custom `Axios` object [earlier](#create-a-base-api-object).

  • Create, for example, a `src/api/PostApiHook.js` file to handle the logic/code for the custom API-calling hook.

  • Return back the success and error data from the API call. You can also (optionally) return the "worker function" (that is, the function that performs the API-calling "heavy lifting" inside the `src/api/PostApiHook.js` file).

  • Export the custom API hook from the `src/api/PostApiHook.js` file (for reuse in the React components and elsewhere in the app, as needed.)
Whether it is a matter of preference or need, compare the pros and cons of using either a JavaScript class or a custom React hook to handle the API-calling logic of your React app.

Outro (aka "What's Next?")

Beyond just the talk, go ahead and:
  • Create a new or (reuse an old) React app

  • Create one or more API objects (using either JavaScript classes or as custom React hooks)

  • Use the API object or hook to consume API data in your React components.

  • Rinse and repeat until you master the art of making API calls from your React app while following best practices.
Now, go out there into the world and build an app that makes the world better for all of us, developers and humans alike. :)

 


Technical Reference

 

Developer Guides Related to This One:

Please, share your insights, concerns and experiences about the topic and/or content of this article.