<Quick.Webdev />

be in touch with web development

How to cope with frontend-backend communication using React, Express and REST API?

Reading time: 6 minutes

In the world of Javascript, there are plenty of frameworks and libraries for building web applications. The most popular frontend technology is React and on the backend side - Express, Koa, or Nest.js. When building a full-stack app, it’s super important to plan the architecture, directory structure, and the way the data flow through a code. In this article, I’ll depict my solution to the problem of communication using the traditional REST API.

As an example, I would like to show different parts of my quiz application which I’ve been creating for the last months. Click to go the project here!

Dashboard component

In a dashboard component, we have a list of quizzes created by users. In order to get them from our database (MongoDB), we have to make a request to our API. That’s where the communication starts.

Go here => /src/pages/Dashboard/index.js

const Dashboard = () => {
  const { data: quizzes, loading, error } = useFetching(getQuizzes);

  if (loading) return <Loader />;
  if (error) return <ErrorPage msg={error} />;

  return (
    <Layout>
...

Frontend API

In line 2, I’m calling a custom hook (useFetching) with a getQuizzes argument. This argument is the reference to a function from the /src/api.index.js file. So let’s see how this function looks like and then, I’ll explain to you what happens in the useFetching hook.

export const getQuizzes = async () => {
  const { data } = await instance.get('/api/quizzes').catch(({ response }) => response);
  return data;
};

This is a simple asynchronous function that sends a request to /api/quizzes using the axios package. I’m also using the catch method to gather potential errors. All the returned information is in a data property so I grab it using ES6 destructuring.

Custom useFetching hook

Go here => /src/hooks/useFetching.js

export const useFetching = (method, ...methodArgs) => {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [data, setData] = useState(undefined);

  useEffect(() => {
    const cancelToken = axios.CancelToken;
    const source = cancelToken.source();

    (async () => {
      const { type, data, msg } = await method([...methodArgs]);

      if (type === responseTypes.success) {
        setData(data);
      } else {
        setError(msg);
      }

      setLoading(false);
    })();

    return () => source.cancel();
  }, []);

  return { data, loading, error };
};

This function excepts the method to call and the array of the method arguments (if they exist). Below I declare state (loading, error, and data). In line 10, I’m calling the passed method with the potential arguments using the spread operator and then, destructuring the type of the response, data, and an error message (here it’s a msg value). After that, if the request finished successfully, I set the data, the error, and finally set the loading variable to false in order to inform that the request had been finished and a loader component can disappear. In the end, I return the object with all the necessary data. Don’t remember to close all subscriptions and asynchronous tasks in a cleanup function (here I’m using axios.CancelToken). Now, back to the first code snippet and look once again.

  const { data: quizzes, loading, error } = useFetching(getQuizzes);

Here, I’m destructuring the object returned from the hook and now, I can use that data to visualize the result:

  if (loading) return <Loader />;
  if (error) return <ErrorPage msg={error} />;

And finally, list through the quizzes array.

Express.js Backend

Now, let’s head over to the backend to show how the request is processed there.

Backend here

Go to => /routes/api.js

router.get("/quizzes", verify, quizzesController.getQuizzes);

In this line, I created a GET /quizzes route that handles the request. This request is now forwarding to the dedicated quizzesController. Let’s take a look there.

Controller

The role of the controller is to:

  • Obtain the request
  • Call an appropriate service (if needed)
  • Return a data or an error

And that’s what happens in a /controllers/quizzes.js file

module.exports.quizzesController = {
  getQuizzes: async (req, res) => {
    if (req.user) {
      const { type, data, msg } = await QuizzesService.getQuizzes();

      if (type === responseTypes.success) {
        res.status(200).json({ type, data });
      } else {
        res.status(401).json({ type, msg });
      }
    }
  },
...

Here we have the getQuizzes method that meets the aforementioned requirements:

  • Obtain the request (line 2)
  • Call an appropriate service (line 4)
  • Return the data or error in according to thr returned type value (lines 6-
The first and the third step is quite simple but the most interesting thing is in line 4. Here, I’m calling the quizzes service.

Service

The role of the service is to handle more complex tasks, for instance, making database queries and preparing the data to pass to our controller.

class QuizzesService {
  async getQuizzes() {
    try {
      const quizzes = await Quiz.find({}).sort({ creationDate: -1 });

      if (quizzes.length > 0) {
        return {
          type: responseTypes.success,
          data: quizzes,
        };
      } else throw messages.QUIZZES_NOT_FOUND;
    } catch (e) {
      return makeResponse(e, responseTypes.error);
    }
  }
...

In the last step, I’m doing the query to MongoDB. I’m getting all quizzes, sorting them by the publish date. If the quizzes exist, I return them but if not, I catch an error and send them back.

I recommend you to use try catch syntax to make the code more readable and easy to debug.

Voila! The whole communication ends right here. Now, the data returned from this service are coming back to the controller, then to the frontend side (/src/api/index.js), next to the useFetching hook, and in the end to the appropriate React component (Dashboard) from which we’ve been started our long but cool trip! 😎

To sum up, the communication step by step looks like this:

React component -> useFetching hook -> API method -> backend route -> controller -> service (we have the data) -> controller -> API method -> useFetching hook -> React component (we use the data).

Other articles:

Krzysztof Szczepański - All rights reserved 2021