Query and chain multiple requests with Contentful SDK

In a modern app, you'll be making queries to an API and expecting the response to include all the data you need. Hoping that you can avoid looping asynchronous calls and getting lost in Promise hell.

But the reality is that this problem is really prevalent in REST APIs by design and is actually what GraphQL sets out to solve.

No need to fear though, we're here to learn how to chain queries elegantly using plain ES6 with a real example from Contentful. A pretty modern API, with both REST and GraphQL support.

Introduction

Although it may seem straight forward to just nest your queries in the callback of a promise but you'll quickly venture into Promise Hell and may not find a way back if your queries get any more complicated.

The aim by the end of reading this is to provide an alternative, and hopefully a more elegant way, of chaining your queries using my favourite trio: async/await, map and Promise.all.

So before carrying on, there are some fairly radical assumptions that I am going to make. That you are somewhat familiar with:

  • HTTP clients like Axios or fetch API
  • Contentful or knowledge basic data structures
  • ES6 syntax and modules

Not a big problem if you aren't familiar, maybe you're just after the code to solve your urgent issue and move on. So here is a direct link to the final code for you to share.

The classic Photo Album problem

So on that note, let's dive right in and use an the photo gallery on from my website as the example. In this example I needed to display Photos from a Gallery entry referenced inside a Travel Post.

The problem was that when I queried for a travelPost entry, it had provided me with a gallery object but the images inside the gallery were missing key bits of information like url and description.

The photo object only contained a Contentful sys.id - which is a unique identifier for every piece of content which we can use to make individual queries.

Let's tackle this problem from very the beginning by creating a connection to Contentful.

Creating a Contentful client

Fortunately, Contentful have an SDK to help connect with spaces and environments - but most importantly it provides a simple and well documented API.

If are unfamiliar on where to acquire your authentication values for your account, you can find out more here in the Contentful Docs.

So with your authentication values on hand - let's install the Contentful SDK.

CLI

yarn add contentful

After that, we will create a module for our Contentful JS client which defines a basic connection with a Contentful environment so we can make queries to.

We import the contentful module in a client.js script which we will use as an entry file. We then create our own module with an export default which contains credentials for our Contentful CDA. This will be passed into a Contentful's createClient API.

client.js

import contentful from 'contentful

export default () => {
    const config = {
        space: [SPACE_ID],
        environment: [ENVIRONMENT_ID],
        accessToken: [ACCESS_TOKEN]
    }
    
    const client = contentful.createClient(config)
}

Requesting entries

So now that we have a Contentful client connected with an environment, we can write our first query to fetch a Travel Post by it's slug.

Since we are using await - our function needs to include async before the function keyword. This is async/await 101. When the await keyword is executed in JavaScript - any following code will be only be executed once a response has returned.

async function getGalleryBySlug(slug) {
    const entries = await client.getEntries({
      'fields.slug': slug,
      content_type: 'travelPost'
    })
    
    if (!entries.items) {
      console.log('☹️Could not find any TravelPost entries with this slug')
      return
    }
    
    return entries
}

So this small function will return all entries that match the arguments defined in the getEntries parameter. By definition, a slug is unique - meaning that this query will always return an Array with one result.

Below is a condensed response of what we expect to see.

[
  {
    items: [
      {
        fields: {
          body: { ... },
          country: 'Morocco',
          gallery: {
            fields: {
              images: [
                {
                  sys: {
                    id: "5BO0FkE2qOfVjZx0D1H50I",
                    linkType: "Entry",
                    type: "Link"
                  }
                },
                {
                  sys: {
                    id: "5IyGz2PsbHmH4xuRujvwtA",
                    linkType: "Entry",
                    type: "Link"
                  }
                },
                {
                  sys: {
                    id: "CRfRsN5eE62zYRu05Neda",
                    linkType: "Entry",
                    type: "Link"
                  }
                }
              ]
            }
          }
        }
      }
    ]
  }
]

Based on this response, you can only see the sys.id of the image object, and not the the data we need like url or title.

Queries using a value from another response

To get the full data from the images object, we now move onto the most important piece. This is the outline of what we will write to get a response:

  • Error handling for when there is no gallery attached to the Travel Post entry
  • Assign the images Array to a variable for simpler referencing
  • Create a new object that only contains the ids of the photo entries within the gallery object
  • Create an Array of asynchronous calls to Contentful to fetch the entry of each individual image id
  • Execute this Array of async calls inside a Promise.all function. This which will run each query one by one, and return the responses.
if (!entries.items[0].fields.gallery) {
  console.log('Could not find gallery from post')
  return
}

const gallery = entries.items[0].fields.gallery.fields.images
const ids = gallery.map((id) => id.sys.id)
const queries = ids.map((id) => client.getEntry(id))

return Promise.all(queries)

Wrapping up

That's it! So the full function looks like this.

import contentful from 'contentful

export default () => {
    const config = {
        space: [SPACE_ID],
        environment: [ENVIRONMENT_ID],
        accessToken: [ACCESS_TOKEN]
    }
    
    const client = contentful.createClient(config)
    
    /**
     * Get all photos from a gallery by slug.
     * @param {String} slug - Post slug.
     */
    async function getGalleryBySlug(slug) {
        const entries = await client.getEntries({
          'fields.slug': slug,
          content_type: 'travelPost'
        })
        
        if (!entries.items) {
          console.log('Could not find any TravelPost entries with this slug')
          return
        }
        
        if (!entries.items[0].fields.gallery) {
          console.log('Could not find gallery from post')
          return
        }
        
        const gallery = entries.items[0].fields.gallery.fields.images
        const ids = gallery.map((id) => id.sys.id)
        const queries = ids.map((id) => client.getEntry(id))
        
        return Promise.all(queries)
    }
}

You can even write some more error handling into the Promise.all in case you run into issues with query limits. But as you see, there is no deep nesting or callback hell summoned in our calls.

Just plain old ES6 and an easy to follow trail of what is happening in this async function.