This tutorial was updated on July 17, 2019 to reflect the latest versions of the technologies mentioned.
Typically when I’m working with RESTful APIs, the routes or endpoints return what I need, plus more, which would be too much data. However, recently I ended up working with an API where the responses were rather trim, resulting in the need to use many HTTP requests to various endpoints, rather than getting everything in a single request. To take it a step further, some of those HTTP requests depended on data from other requests creating a mess of asynchronous operations in JavaScript.
After consulting with my pal, Corbin Crutchley, we came up with a solution to what I needed, without creating chaos in my code. Remember, Corbin is a JavaScript professional, as demonstrated on the podcast we recorded together titled, Asynchronous JavaScript Development.
In this tutorial we’re going to see how to chain JavaScript promises, but also use data from parent links in the promise chain with child links in the same promise chain.
To get an idea of a plausible scenario, let’s think of the following. Let’s say we have the following API endpoints:
GET /people
GET /person/:id/address
From the above endpoints we can probably conclude that the first will get us multiple people, while the second will get us address information about a particular person. In many scenarios, this is fine, you may not need address information to always exist in your request for people. However, let’s say you will always need address information with your people information.
Let’s assume that the /people
endpoint returns the following:
[
{
"id": "1234",
"firstname": "Nic",
"lastname": "Raboy"
}
]
It is brief, but it serves our purpose. The /people
endpoint will return an array of objects. We could even go as far as to say that each of the objects includes a full route to the address information like so:
[
{
"id": "1234",
"firstname": "Nic",
"lastname": "Raboy",
"address": "http://localhost:3000/person/1234/address"
}
]
It doesn’t really matter if it has the address property because it has the id
. As of right now this is a theoretical example, so use your imagination.
Now let’s say the /address
endpoint might return the following:
{
"city": "Tracy",
"state": "CA"
}
Both of these endpoints by no means reflect how the database might be modeled. It isn’t relevant to us. What we’re concerned about is what endpoints we need to consume for our needs and the data that comes back.
Alright, so let’s start putting two and two together and getting our data in the following format:
[
{
"id": "1234",
"firstname": "Nic",
"lastname": "Raboy",
"address": {
"city": "Tracy",
"state": "California"
}
}
]
With that final object in mind, we can start making requests and chaining promises together.
When it comes to making HTTP requests with JavaScript, there are a lot of different ways. More if you’re using a framework like Angular, React, and Vue that might have their own modules for making requests. We’re going to keep things simple and make use of the fetch
command which is readily available in modern implementations of JavaScript.
To get people with the fetch
command, you might have a request that looks like the following:
fetch("http://localhost:3000/people")
.then(response => response.json())
.then(people => {
console.log(people);
});
The above code will get us to where we need to be with the first HTTP request. Now how do we customize that response with address information without losing track of which address goes with each person? Remember, you’re going to have an array of people, but in this example, only one address per person. Each HTTP request is an asynchronous operation.
Let’s take a look at the following modification to our code:
fetch("http://localhost:3000/people")
.then(response => response.json())
.then(people => {
return people.map(person => {
return fetch("http://localhost:3000/person" + person.id + "/address")
.then(response => response.json())
.then(address => {
person.address = address;
return person;
});
});
});
In the above example, after our request for people data has finished, we loop through each person in the array using the map
operation, where each item in the array is used in the next request for an address. When the address information is found for a particular person, the parent person
object is modified to now include that address data. Notice the return
statements in the above code. The fetch
operation is a promise and the return person;
line resolves that promise. Since we’re using a map
, we’re mapping the array of people object to now an array of people promises. Remember, the fetch
operation returns a promise.
So what does this mean for us?
Now that we have an array of promises as our final answer, we need to execute on that promise and do something with the result. One of the easiest solutions is to use a Promise.all
, but of course there are others as well. Take a look at the following code modification:
fetch("http://localhost:3000/people")
.then(response => response.json())
.then(people => {
return people.map(person => {
return fetch("http://localhost:3000/person" + person.id + "/address")
.then(response => response.json())
.then(address => {
person.address = address;
return person;
});
});
})
.then(people => {
Promise.all(people).then(result => {
console.log(result);
});
});
In the above code we are chaining our promise. We have the array of promises from the map
, so now we are running each of them. After all of the promises have resolved or rejected, we play around with the result, which in this case is an array of people with the address object included for each person.
Now let’s go even further, although not completely necessary. Let’s say you don’t want the final stage of the promise chain to be working with promises. We can do a slight modification to our code:
fetch("http://localhost:3000/people")
.then(response => response.json())
.then(people => {
let peoplePromises = people.map(person => {
return fetch("http://localhost:3000/person" + person.id + "/address")
.then(response => response.json())
.then(address => {
person.address = address;
return person;
});
});
return Promise.all(peoplePromises);
})
.then(people => {
console.log(people);
});
Notice this time, instead of returning the mapped array of promises, we store it as a variable and do a Promise.all
in the same link. We then return the result of that and at the end of our promise chain, we just print it out. If it worked correctly, people
in the final part of the promise chain should be an array of objects.
You just saw how to do promise chaining as well as use data from a parent link in the chain within a child link. In this case we saw how to use person data obtained in the first promise within the second promise which was in relation to address information. There probably aren’t too many instances where you’d need to do this because most APIs that I’ve seen return too much data or there is a GraphQL option. However, knowing how to accomplish this with promises is valuable in general.
Again, huge thanks to Corbin Crutchley. He helped fill in the gap on my promises. I use them every day, but still haven’t mastered them.