If you're getting into JavaScript and frontend development, you've probably come across many use cases where you'll need to fetch data from a remote service endpoint. Depending on the type of application you're working on, this data can come in many different shapes. For single-page applications, it's common to handle remote data in the JSON format and fetch it through HTTP requests. In this article, we want to guide you through some common patterns when handling such requests and share other tips we think are helpful.
AJAX​
AJAX stands for Asynchronous JavaScript And XML. This concept embraces the use of specific tools to achieve the following:
- Allow you to make requests from the browser to the server without refreshing the page
- Receive and work with data from the server You will find Ajax associated with the term XMLHttpRequest, the API that specifies requests and the data exchanged between client and server. Ajax is a broader term that can sometimes be misplaced. Now you'll hopefully associate this immediately with client-side HTTP requests. Now let's look at the APIs that allow us to perform the said HTTP request.
XMLHttpRequest & fetch​
XMLHttpRequest and fetch are the two most common browser APIs to perform HTTP requests. Both APIs are related to the term Ajax in the sense that they both. Opposed to XMLHttpRequest, fetch is Promise-based. A Promise is just another browser API; it's an evolutionary step from JavaScript callbacks to simplify the way we handle asynchronous code. Another big difference is that fetch has a slightly less broad browser support compared XMLHttpRequest; this is an extremely relevant detail if your product needs to support older browser vendors
At the end of the day, both the APIs allow you receive or send data to a remote server!
Here are two simple examples of using XMLHttpRequest and fetch respectively.
// copy & paste it in your console to try it out
const request = new XMLHttpRequest();
request.onload = () => {
// I'll run when the request completes
const data = JSON.parse(request.response);
window.alert(`${data.name} is a ${data.gender} with a probability of ${data.probability}`);
};
request.open('GET', 'https://api.genderize.io?name=peter');
request.send();
// copy & paste it in your console to try it out
fetch('https://api.genderize.io?name=peter')
.then(response => response.json())
.then(data => {
window.alert(`${data.name} is a ${data.gender} with a probability of ${data.probability}`);
});
As of today, the difference in caniuse.com/fetch seems to be ~2% in terms of global usage.
Common patterns and practices​
In this section we want to cover a few pertinent topics for you to consider when performing HTTP requests.
Use of abstractions​
If if you browse the web, you'll encounter numerous JavaScript libraries that wrap the XMLHttpRequest and fetch APIs to either:
- Provide additional functionalities that are not shipped by browser vendors.
- Create wrappers around the server response objects to adapt the response into some more modern API such as Promises (mainly the case for XMLHttpRequest). For example, the library axios, one of the most popular HTTP clients, does precisely that.
Why would you pick a wrapper library?​
We think these are three good reasons:
- Portability - not only cross-browser but also cross-environment (browser vs. server).
- Additional Features - extra things you can't achieve with native APIs.
- Consistency of use (developer productivity) - same API, less friction (closely related with point 1.).
This answer will wildly vary from use case to use case. However, if you're writing code across the stack (meaning in the browser and in the server with Node.js), it would be a big win to standardize how HTTP requests are handled. This would make the transition between working on backend or frontend components seamless as the knowledge of the API would be transferable. This cannot be achievable with native APIs because although Node.js allows you to run JavaScript. It does not have its APIs aligned with the browser vendor APIs because server and browser are entirely different environments. Another relevant consideration is browser support, which was already mentioned above.
Separation of concerns​
Last but not least would like to alert you to a common beginner mistake when handling remote data in a single-page application.
If you have no prior experience with JavaScript, like many others, you'll settle for the following approach: You perform the HTTP request to get some data. You store that data in some local state to display directly in the UI.
For simple projects, this might be fine, but be aware that REST APIs and their data contracts are constantly changing! In our experience, for larger projects, it justifies introducing a mapping function between steps 1 and 2. That function aims to transform the HTTP response data into a different data model (let's call it UI model or view model) before being used in the application.
This way, you ensure that data changes on a remote endpoint response won't be propagated throughout several visual components and the business logic of your application. By defining this layer, you'll take control of what data flows through your application.
This can be achieved with a pure JavaScript function that takes in the HTTP response and outputs the formatted data.
// copy & paste it in your console to try it out
// this is an exaggeration to make a point
function mapRemoteData(data) {
const requestedName = data.name;
const resultGender = data.gender;
const resultProbability = data.probability;
return {
requestedName,
resultGender,
resultProbability,
}
}
fetch('https://api.genderize.io?name=peter')
.then(response => response.json())
.then(data => mapRemoteData(data))
.then(data => {
// this data fields are controlled by you now!
window.alert(`${data.requestedName} is a ${data.resultGender} with a probability of ${data.resultProbability}`);
});
We cannot emphasize enough how relevant this pattern is and how much cleaner and maintainable your code will be in the long term. If you want to read more on this, we suggest looking for references for the Model–ViewModel (MVVM) pattern.
Error handling​
We believe that are two essential aspects to error handling:
- Don't crash.
- Retry failed requests within a reasonable time frame.
Following the above two principles will undoubtedly enhance the resilience of your application.
Handling errors is of utmost importance. You should never crash your application, no matter the remote server's response, or even in the absence of it! The front end is at the frontline of your project. It should have fallbacks to handle any kind of remote failures gracefully to provide a great user experience.
Here are some small snippets for error handling with both XMLHttpRequest and fetch respectively
// copy & paste it in your console to try it out
const request = new XMLHttpRequest();
request.onerror = e => console.error('Oops! something went wrong', e);
request.open('GET', 'https://badurl/api');
request.send();
// copy & paste it in your console to try it out
fetch('https://badurl/api')
.catch(e => console.error('Oops! something went wrong', e));
HTTP errors are not always straightforward to simulate. For that, we recommend you to check out this article.
It's a good practice to avoid console.log
in production environments, you can wrap this calls with an environment check such as if (process.env.NODE_ENV === 'development') { ... }
. Ideally you can use some bundler (e.g. webpack) to strip those calls when you build your production version of the application.
Pagination​
Like big websites with lots of content have a paginated UI, sometimes APIs have to spend vast amounts of data on the client. For performance reasons, many REST APIs choose to paginate their results. This means that instead of getting all the data with a single request, you'll have to perform many similar HTTP requests to retrieve the entire dataset.
The same way you can start reading a book from a given page, many REST APIs will allow you to specify the "page" or segment of data you want to fetch. The term "cursor" is often used to name a pointer/identifier that keeps track of the current position you're reading on the remote server. This way, the server-side application knows how to interpret the cursor, knowing what chunk of data to send to the client.
How would this look in practical terms? Here's a good example.
And that's all. Just to summarize, here's a small list of the topics covered in this article:
- Ajax requests
- XMLHttpRequest and fetch
- Patterns and common practices
- Using wrappers or libraries to enhance portability and create reusable functionalities on top of the XMLHttpRequest and fetch APIs
- Error handling and 2 sound principles to get you started
- A bit about handling pagination
- Handling remote data for long term maintainability
If you liked this article, consider sharing (tweeting) it to your followers.