DEV Community

Cover image for The 5 pillars of every HTTP request
Amin
Amin

Posted on

The 5 pillars of every HTTP request

Request

A request, or an HTTP request, is an action that is sent to a server in order to get something, or to send some information. This includes the URL of the server, the headers and the body of the request.

Most of what will be explained will be important for requesting some information, but can also be applied when sending information as well.

Loading

Displaying a loading information for your users is an important step of a request because we never know what could happen on the network, maybe the connection is slow, maybe the server is slowed down because of the numerous requests.

Showing a loader, or a text indicating that the request is still being made is an additional step that can make your application look more professional and is more user friendly than thinking that everyone has a fast internet connection.

You can simulate slowed down request from the Developer Console in your favorite browser such as Firefox or Google Chrome.

Error

Things happen in the network, and we can't control everything beside what is happening inside our code.

The network might be shut down momentarily, or the user has activated airplane mode, or the server has been down for some time. We never know what kind of problem there might be, or when it can happen but we know that there might be some problem and we must account for that.

It is a good practice to account for these thing in the code, especially in JavaScript since sending a request often involves using a Promise, and a promise might be in a rejected state.

You can also simulate an offline connection in your browser from the Developer Console.

Cancelable

If you plan on giving your users data from a remote API, at least provide a way of canceling these requests.

This is a good practice and an added user experience bit in any application since getting a huge payload from a remote server might be costy for users that are on data plans and having the choice is a good way of showing your users that you are considering everyone, even those that cannot afford much data transfer.

Using JavaScript and the Web API Fetch, you can use a signal along with an Abort Controller in order to provide a way of canceling a request.

Validation

Finally, you have sent a request, everything goes according to plan and you receive a successful response. Or is it?

How can you be sure that the server won't change its response in a day, or a week, or a year? Your application might work for a while, but if anyone decide to send an object with a property instead of an array as usual, you might get into trouble because you'll try to iterate over an object instead of an array which is not possible out-of-the-box in JavaScript.

Data validation is an important step, and might as well be mandatory in some case because even if you know what you are doing today and you are the only developer for a frontend & backend application, you might not be alone in a year and people might join the fight and help you.

If you go back from a long vacation and the API has change, at least with data validation you know that this is a case you accounted for and your application won't crash suddenly (and you might even get better errors that will lead you to resolve this error quicker than without data validation).

Also, with data validation, you can rely on languages that are strongly typed like TypeScript to ensure that once this data has been parsed and validated, you are 100% sure you can iterate over it instead of being afraid it might change in a near future.

Example

Here is what a beginner application might look like in React for the example.

import React, { useEffect, useState } from "react";

export const App = () => {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/users").then(response => {
      return response.json();
    }).then(newUsers => {
      setUsers(newUsers);
    });
  }, []);

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.username}</li>
      ))}
    </ul>
  );
};
Enter fullscreen mode Exit fullscreen mode

As you can see, no loading state, no cancelable request, no accounting for errors nor data validation.

Here is what it might look like with all these things added.

import React, { Fragment, useRef, useEffect, useState, useCallback } from "react";

const isValidUser = input => {
  return typeof input === "object"
    && input !== null
    && typeof input.id === "number"
    && typeof input.username === "string";
}

const isValidUsers = users => {
  if (!Array.isArray(users)) {
    return false;
  }

  if (!users.every(user => isValidUser(user))) {
    return false;
  }

  return true;
}

export const App = () => {
  const [users, setUsers] = useState([]);
  const abortController = useRef(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  const cancel = useCallback(() => {
    abortController?.current?.abort();
  }, [abortController]);

  useEffect(() => {
    abortController.current = new AbortController();
    const { signal } = abortController.current;

    setError(null);
    setLoading(true);

    fetch("https://jsonplaceholder.typicode.com/users", {
      signal
    }).then(response => {
      if (response.ok) {
        return response.json();
      }

      return Promise.reject(new Error("Something went wrong"));
    }).then(newUsers => {
      if (!isValidUsers(newUsers)) {
        throw new Error("Wrong response from the server");
      }

      setUsers(newUsers);
    }).catch(error => {
      setError(error);
    }).finally(() => {
      setLoading(false);
    });
  }, []);

  return (
    <Fragment>
      {loading && (
        <small>Loading, please wait...</small>
      )}
      {error && (
        <small>{error.message}</small>
      )}
      <button onClick={cancel}>Cancel</button>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.username}</li>
        ))}
      </ul>
    </Fragment>
  );
};
Enter fullscreen mode Exit fullscreen mode

Of course no styling has been applied so it looks awful but at least you get the idea.

We added a way of canceling the request, a loading indication to reassure the user, a display of any error and a basic data validation mechanism in order to ensure the data we get is not corrupted or has been changed.

Conclusion

We saw that in order to build reliable applications, there were 5 steps that we must follow whenever we make a request to a server:

  • Send a proper request
  • Display a loading state
  • Display errors if any
  • Make the request cancelable
  • Validate the data we receive

If you manage to follow these steps, you'll build highly reliable applications that are time-tested and sturdy.

This will instantly make your application way better in the eyes of your users.

These concepts are not tied to JavaScript nor React and can be applied to pretty much any language or any framework & library out there as long as you follow these steps.

Top comments (9)

Collapse
 
joshuaamaju profile image
Joshua Amaju • Edited

You should change this line

const [abortController, setAbortController] = useState(new AbortController());
Enter fullscreen mode Exit fullscreen mode

to

export const App = () => {
  const [users, setUsers] = useState([]);
  const abortController = useRef(); // to this
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  const cancel = useCallback(() => {
    abortController.current?.abort(); // and here
  }, []);

  useEffect(() => {
    const newAbortController = new AbortController();
    const { signal } = newAbortController;

    abortController.current = newAbortController; // and lastly, here

    setError(null);
    setLoading(true);

    fetch("https://jsonplaceholder.typicode.com/users", {
      signal
    }).then(response => {
      if (response.ok) {
        return response.json();
      }

      return Promise.reject(new Error("Something went wrong"));
    }).then(newUsers => {
      if (!isValidUsers(newUsers)) {
        throw new Error("Wrong response from the server");
      }

      setUsers(newUsers);
    }).catch(error => {
      setError(error);
    }).finally(() => {
      setLoading(false);
    });
  }, []);

...
...


Enter fullscreen mode Exit fullscreen mode

because your initial example causes a re-render each time you update the abort controller, and there's no apparent reason why that's neccessary.

Collapse
 
aminnairi profile image
Amin

Hi Joshua and thank you for your comment.

You are absolutely correct, I'll update the exemple to reflect the change.

Thank you for your contribution!

Collapse
 
alais29dev profile image
Alfonsina Lizardo

Great article! I never thought about making the request cancelable and validating the data received, but you make an excellent point on why we should do both.

Collapse
 
aminnairi profile image
Amin

Hi Alfonsina and thank you for your comment.

Glad you find this article useful. I wish you to make wonderful applications with all those tips!

Collapse
 
coolcucumbercat profile image
coolCucumber-cat

I would change the isValidUsers function to:

return Array.isArray(users) && users.every(isValidUser)
Enter fullscreen mode Exit fullscreen mode

user => isValidUser(user) checks if a user is valid, so does isValidUser, so it's unneeded.

And I would change the fetch to this:

try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users', {signal})

    if(!response.ok) throw new Error('Something went wrong')

    const newUsers = await response.json()

    if(!isValidUsers(newUsers)) throw new Error('Wrong response from the server')

    setUsers(newUsers)
} catch (error) {
    setError(error)
} finally {
    setLoading(false)
}
Enter fullscreen mode Exit fullscreen mode

This flows much better and doesn't need to be in a .then.

Collapse
 
aminnairi profile image
Amin • Edited

Hi coolCucumber-cat and thank you for your answer!

You are very correct, I could have used the function isValidUser as a higher-order function and pass it directly to the Array.prototype.every method.

For this time, I'm not going to update my article with your changes as I want the readers that are interested in these tips to find them out in the comments.

In a real-world situation where I know my colleagues are comfortable with higher-order functions and functional programming in general (and I cannot install a third-party library such as Zod) I would totally go for it and I'm 100% agreeing with you!

Thank you for your contribution!

Collapse
 
sohang3112 profile image
Sohang Chopra

isValidUsers can be simplified to: const isValidUsers = users => Array.isArray(users) && users.every(isValidUser);

Collapse
 
windyaaa profile image
Windya Madhushani

Great article.

Collapse
 
aminnairi profile image
Amin

Thank you @Windya, glad you liked it.

I hope it serves you well in your future work!