Skip to main content

Caution GraphQL! 4 most popular Headless CMS Examples and unexpected pitfalls in their GraphQL API. Part 1

let’s explore Headless CMS and its GraphQL API. We will discuss GraphQL API implementations in four popular headless CMSs: Contentful, Sanity, Storyblok, and Hygraph. Throughout the post, we will uncover the challenges, advantages, and disadvantages of each CMS, and examine how they handle various requirements. Get ready for an informative journey filled with humor, code snippets, and valuable insights into Headless CMS and GraphQL

Choosing best Headless CMS for you project could be a complicated task. You need to dit into different aspects and details about each system. Also it depends on your project stack, field, and details of what your’re creating. Especially it’s important to know how good your Headless CMS will appear when you are in the middle of development or when you need to grow your project. Shiny hope pages of different headless CMS attract you with promises to get the best experience. Even if you start learning and comparing details about provining features you still have just a shallow understanding what will you face after the project development is started.

One of the highly demanded feature for Headless CMS is GraphQL API. And most of modern CMS offer it even on free starter plan. But what does it means when you see a GraphQL feature enabled you the CMS of your choice? Not all GraphQL the same, believe me. I’ve learned this by my experience working with different CMS. And I want to share it with you now. This article will help you safely dive into GraphQL API implementations of four most popular headless CMS. I’ll guide you through pros and cons of them and show workarounds when it’s possible. You will see how different could be implemented similar functionality.

To show you these practical examples I’ve created four projects with the same content models and content in different CMS. They are:

  • Contentful
  • Sanity
  • Storyblok
  • Hygraph

So be ready to dive and take plague!

GraphQL requirements

to compare GraphQL API of these Headless CMSs I’ve created a check list with most required and reasonable features which I believe any of you would expect from your GraphQL API. Here they are:

  • representation of the content model with a separate access to all nested objects
  • Retrieval of a single document through a convenient handler
  • Filtering objects based on any field value, pagination, sorting
  • Filtering arrays, especially arrays of references, based on field values. Pagination, sorting
  • It should support arrays of objects of different types
  • Retrieval of a slice of documents corresponding to a specified environment, for example, production, draft
  • Existence of a reverse reference
  • Bonus: Retrieval of a slice of documents corresponding to a specific locale
  • Bonus: Filtering based on properties of fields that the references lead to
  • Bonus: GraphQL playground

We will pick them one by one and compare across all these CMSs. For each requirement I will give more details about use case and show usage examples.

Before we start we need to outline our content model. It should be the same for all systems and be able to reproduce all features.

Content model

Before we continue lets outline what content model we will use as an example. we'll look at a scheme where there are multiple content types and links between them. This will be an exaggerated version, because we want to use the simplest example reproducible in different CMS and use it to demonstrate how basic tasks are solved in different systems. Let’s consider a simple content model for a eCommerce shop. The visual scheme of that model is on the illustration below.

Content model for a eCommerce app

Content model for a eCommerce app

Let’s say we have a such hierarchy for representing a eCommerce part. We have a Brand type for representing different brands. Let’s say that Brand is a root level in our hierarchy. Each Brand represents their products not directly but with a list of collections. So Brand content type should have an array with references to a Collection documents. Collection already contain a list of Production of that collections. Yep I know in real life a product can be added to a different collections and collections shouldn’t belong to only one brand. But for simplicity let’s stick with exaggerated model.

Except eCommerce part we would like to have some landing pages with arbitrary content in our website. For that we will create a Landing content type which alongside obvious fields like title, slug and other SEO parameters will have an array of nested objects for content block. Content blocks is a generic name for several content types containing content of a specific type to be inserted on a page.

These can be blocks representing formatted text, text with a picture, quotes, cards with chips. In our case we will limit ourselves to the following content types:

  • block for inserting brand information. Will contain a link to this brand and formatted text to add context about that brand on a landing page. Do we need to have fields for logo and brand name in this type? No, because we can get it through a link from the brand object itself.
  • block for inserting information about the collection. Let's pretend that by design, in this block we display information about the name of the collection and the brand to which this collection belongs. We will also display the first three products from this collection. How many and what fields do you think we need to add to this type to be able to display all this information on the front end. The correct answer is one field with a link to the collection. Because following the principle of single source of truth all other information we can get from the collection object and its relationships.
  • block for the carousel with products. here we will have an array of links to different products.

Why I want these content blocks to be a nested objects? Because it’s easier to work with them in this way. You can duplicate your landing page and get a new independent amount of blocks, so they can be edited safe without affecting other pages.

Generate Content

After I’ve spent pretty enough time creating identical content models for each Headless CMS I’d like to slightly speed up the process and ask ChatGPT to generate content for all fields of my content model. Damn! I event don’t know what exact products we going sell. Well let ChatGPT invent it for me as well. So… wait for a few seconds… and what we have now:

ChatGPT response:

Let's consider a thematic of "Handcrafted Artisan Chocolates" for your eCommerce webstore.

Brand:

TitleDescription
Artisan DelightsArtisan Delights offers an exquisite range of handcrafted artisan chocolates. Made with love and the finest ingredients, each piece is a journey of taste and delight.

Collections:

TitleBrand
Luxury TrufflesArtisan Delights
Vegan CollectionArtisan Delights

Products for Luxury Truffles Collection:

NameDescriptionCollectionSlug
French Silk TrufflesSmooth, creamy truffles with a velvety center, dusted lightly with cocoa.Luxury Trufflesfrench-silk-truffles
Hazelnut Heaven TrufflesDecadent truffles filled with a rich hazelnut ganache, coated in milk chocolate.Luxury Truffleshazelnut-heaven-truffles
Dark Desire TrufflesDark chocolate truffles with a bittersweet filling, for the lovers of intense flavors.Luxury Trufflesdark-desire-truffles

Products for Vegan Collection:

NameDescriptionCollectionSlug
Pure NoirLuxuriously dark vegan chocolates, offering a rich, indulgent experience.Vegan Collectionpure-noir
Almond BlissVegan chocolates filled with creamy almond butter for a delightful crunch.Vegan Collectionalmond-bliss
Berry BurstVegan chocolates with a tangy, fruity filling of mixed berries.Vegan Collectionberry-burst

I hope this meets your requirements. Feel free to ask for any modifications.

Well! That’s amazing, isn’t it? We have now all needed text fields for our eCommerce content model. This is great! I also want to generate some images. I’ll do it with DALL E 3 because it’s super good with texts and logos. What I have for a brand logo

Untitled

Looks good. Now I need to populate our CMSs with this content. Hey ChatGPT could you copy-paste everything where I need? Nope it can’t :( So I have to do it manually. Well there you go, AIs seem to be doing just fine with creative tasks, leaving humans to do the mundane chores.

Hey there, fellow developers and CMS enthusiasts! Today, I want to share with you an exciting journey into the world of Headless CMS and its GraphQL API. But, before we dive in, let me warn you - this adventure comes with unexpected pitfalls and a rollercoaster of emotions. So, buckle up and get ready for a wild ride!

So, you're on a quest to choose the perfect Headless CMS for your project. Trust me, I've been there too. It's not an easy task. You have to dig into various aspects, consider your project stack, field, and all the tiny details of what you're creating. And, let's not forget the crucial part - how good your Headless CMS will be during development and project growth. Those shiny hope pages and their promises can be quite tempting, right?

Now, let's talk about one of the highly demanded features - the GraphQL API. Almost every modern CMS offers it, even on the free starter plan. But here's the thing - not all GraphQL APIs are created equal. Believe me, I've learned this the hard way while working with different CMS. And now, I'm here to share my experiences with you. Buckle up, my friends, because we're about to explore the implementations of GraphQL APIs in four of the most popular headless CMS.

But first, let me introduce you to our cast of CMS superheroes:

  • Contentful
  • Sanity
  • Storyblok
  • Hygraph

Yes, they're the ones who will join us on this thrilling adventure. Now, hold on tight and get ready to be amazed!

The Quest for the Perfect GraphQL API

To compare the GraphQL APIs of these Headless CMSs, I've prepared a checklist of the most required and reasonable features. These are the features that any of you would expect from your GraphQL API. Here they are:

  • Representation of the content model with separate access to all nested objects
  • Retrieval of a single document through a convenient handler
  • Filtering objects based on any field value, pagination, and sorting
  • Filtering arrays, including arrays of references, based on field values, with pagination and sorting
  • Support for arrays of objects of different types
  • Retrieval of a slice of documents corresponding to a specified environment (e.g., production, draft)
  • Existence of a reverse reference
  • Bonus: Retrieval of a slice of documents corresponding to a specific locale
  • Bonus: Filtering based on properties of fields that the references lead to
  • Bonus: GraphQL playground

We will embark on this checklist, one requirement at a time, and compare how each of these CMSs performs. Throughout this journey, I'll provide you with detailed use cases, code snippets, and of course, a dash of humor to make your experience even more enjoyable.

But, before we set off, let's outline our content model. It will be the same for all the systems, allowing us to reproduce all the features and see how they handle our demands.

Our Content Model - The eCommerce Adventure

Imagine an eCommerce webstore specializing in handcrafted artisan chocolates. Exciting, right? Our content model will revolve around this theme, and I'll guide you through its structure step by step.

First, we have the Brand type, representing different brands of chocolates. For example, let's consider the brand "Artisan Delights." Here's a little description to get your taste buds tingling:

Artisan Delights offers an exquisite range of handcrafted artisan chocolates. Made with love and the finest ingredients, each piece is a journey of taste and delight.

Now, let's move on to the Collection type. Each brand represents its products through collections.

Now, let's move on to the Collection type. Each brand represents its products through collections. For our eCommerce adventure, let's create two collections: "Luxury Truffles" and "Vegan Collection."

In the "Luxury Truffles" collection, we have some mouthwatering products that will make chocolate lovers weak in the knees. Here are a few examples:

  • French Silk Truffles: Smooth, creamy truffles with a velvety center, dusted lightly with cocoa. Who can resist such indulgence?
  • Hazelnut Heaven Truffles: Decadent truffles filled with a rich hazelnut ganache, coated in milk chocolate. A heavenly combination of flavors!
  • Dark Desire Truffles: For those who crave the intense flavors of dark chocolate, these truffles with a bittersweet filling will satisfy your deepest desires.

Now, let's shift our attention to the "Vegan Collection." We believe that everyone should be able to enjoy delicious chocolates, regardless of dietary preferences. Here are some vegan delights to tantalize your taste buds:

  • Pure Noir: Luxuriously dark vegan chocolates, offering a rich, indulgent experience. Perfect for those who appreciate the depth of flavor in every bite.
  • Almond Bliss: Vegan chocolates filled with creamy almond butter for a delightful crunch. It's a blissful combination of smoothness and nuttiness.
  • Berry Burst: Vegan chocolates with a tangy, fruity filling of mixed berries. A burst of flavors that will leave you craving for more.

Now that you have a glimpse into our content model, it's time to see how these CMS superheroes handle our requirements. Will they rise to the occasion or stumble along the way? Get ready, my friends, because we're about to embark on a thrilling journey through the world of GraphQL APIs in Contentful, Sanity, Storyblok, and Hygraph.

But remember, even though this adventure may have its unexpected pitfalls, we'll tackle them with a sense of humor. After all, laughter is the best way to navigate through the ups and downs of technology.

So, fasten your seatbelts, grab your favorite chocolate treat, and let's dive into the wonderful world of Headless CMS and its GraphQL API. Get ready for an adventure like no other!

But before we proceed, I have a little surprise for you. I've enlisted the help of ChatGPT to generate some content for our CMSs. Yes, you heard it right. ChatGPT has helped us create the text fields for our eCommerce content model. It's like having an AI assistant to lighten our workload. Isn't that amazing?

Now, let's take a moment to appreciate the AI-generated content for our brand and collections. Feast your eyes on these delightful descriptions and let your imagination run wild:

ChatGPT response:

Let's consider a thematic of "Handcrafted Artisan Chocolates" for your eCommerce webstore.

Brand:

TitleDescription
Artisan DelightsArtisan Delights offers an exquisite range of handcrafted artisan chocolates. Made with love and the finest ingredients, each piece is a journey of taste and delight.

Collections:

TitleBrand
Luxury TrufflesArtisan Delights
Vegan CollectionArtisan Delights

Products for Luxury Truffles Collection:

NameDescriptionCollectionSlug
French Silk TrufflesSmooth, creamy truffles with a velvety center, dusted lightly with cocoa.Luxury Trufflesfrench-silk-truffles
Hazelnut Heaven TrufflesDecadent truffles filled with a rich hazelnut ganache, coated in milk chocolate.Luxury Truffleshazelnut-heaven-truffles
Dark Desire TrufflesDark chocolate truffles with a bittersweet filling, for the lovers of intense flavors.Luxury Trufflesdark-desire-truffles

Products for Vegan Collection:

NameDescriptionCollectionSlug
Pure NoirLuxuriously dark vegan chocolates, offering a rich, indulgent experience.Vegan Collectionpure-noir
Almond BlissVegan chocolates filled with creamy almond butter for a delightful crunch.Vegan Collectionalmond-bliss
Berry BurstVegan chocolates with a tangy, fruity filling of mixed berries.Vegan Collectionberry-burst

I hope this meets your requirements. Feel free to ask for any modifications.

Well! That’s amazing, isn’t it? We have now all the needed text fields for our eCommerce content model, thanks to the power of AI. But what about images? Don't worry, my friends. We'll add some visual appeal to our CMSs as well.

Now that we have our content ready, it's time to populate our CMSs with this delightful information. Unfortunately, ChatGPT can't copy-paste everything for us. So, we'll have to do it manually. But hey, at least AIs excel at creative tasks, leaving us humans to handle the mundane chores. It's a win-win situation, isn't it?

So, join me in the next chapter, where we'll dive into each CMS and explore their GraphQL APIs. Get ready to learn, laugh, and discover the wonders of Headless CMS in a way you've never experienced before. Let's make this adventure unforgettable!

GraphQL Playgrounds

Hey there! Now that everything is settled down, we're ready to dive into the exciting world of GraphQL testing. And what's the best way to do that? Utilizing online GraphQL Playgrounds, of course! These nifty tools allow us to test our GraphQL queries without even touching a line of code. How awesome is that?

Contentful

Let's start with Contentful. To enable the GraphQL playground in Contentful, we need to install it from their marketplace. Don't worry, I've got you covered with this handy link. Once installed, you can access the playground under the apps menu on your toolbar. Easy peasy, right?

!https://cdn.sanity.io/images/vftxng62/production/b0d83e771f2aea384301a124df35a26a35f00795-989x919.png

Now, there's a small inconvenience I should mention. If you close a tab in your browser, you'll lose all your queries. So, be careful and make sure to copy them somewhere safe if you need to.

!https://cdn.sanity.io/images/vftxng62/production/53835913ec89bb23313d2f46eb63116635841065-1611x1116.png

Other than that, it looks pretty reasonable. You can even open additional tabs to work with multiple queries. Talk about multitasking like a boss!

Sanity

Alright, let's move on to Sanity. In Sanity, we'll need to deploy our GraphQL endpoint via the command-line interface (CLI) from our project folder. This will give us a fancy URL to access our playground from the front store. Just open that URL in a browser, and voila! You've got a playground pointed directly to your dataset.

!https://prod-files-secure.s3.us-west-2.amazonaws.com/c03ac3e0-1f66-4ae8-8ab6-72d5b544b328/81cfa901-c271-4f12-96c1-43af59dce059/Untitled.png

The cool thing about Sanity is that you can deploy as many independent endpoints as you need. And guess what? Each endpoint comes bundled with a schema generated at the time of deployment. This is super useful because it allows you to keep versions history and ensures compatibility with your frontend apps. Especially in those big projects where CMS and frontend releases don't always go hand in hand.

!https://prod-files-secure.s3.us-west-2.amazonaws.com/c03ac3e0-1f66-4ae8-8ab6-72d5b544b328/3f70ee7e-7f79-402b-bc21-c38ca1009980/Untitled.png

Oh, did I mention that your queries collection will be saved even if you close the tab? That's right! You can continue to work with them anytime you want. Sanity's got your back!

Storyblok

Next up, Storyblok. Accessing the GraphQL playground in Storyblok is as simple as using a secret link. Ready for it? Here it is:

<https://gapi-browser.storyblok.com/?token=[your_token]>

Shhh, don't tell anyone! 😉 This approach, however, doesn't allow you to store your queries in the playground after closing a browser tab. And to make things even more interesting, you can only have one query on a page. But fear not! You can open more tabs in your browser and have as many queries as you need at the same time. It's like having a playground party!

!https://prod-files-secure.s3.us-west-2.amazonaws.com/c03ac3e0-1f66-4ae8-8ab6-72d5b544b328/e56df55a-0ad5-4f80-94b5-64861f372993/Untitled.png

Hygraph

Hold on to your seats, because Hygraph has something special for us. It comes with a built-in GraphQL playground right in your admin app. No need to go anywhere else! Just find it on the left-hand sidebar and let the fun begin.

!https://prod-files-secure.s3.us-west-2.amazonaws.com/c03ac3e0-1f66-4ae8-8ab6-72d5b544b328/f522de22-1b08-4b53-9036-509808942ebd/Untitled.png

But wait, there's more! Hygraph offers a handy way to open the playground directly from the document editor or the documents list. How convenient is that? Plus, Hygraph even writes a full query for you. It's like having a personal assistant for your GraphQL needs! Launch it or copy-paste it to your project, and you're good to go.

!https://prod-files-secure.s3.us-west-2.amazonaws.com/c03ac3e0-1f66-4ae8-8ab6-72d5b544b328/b0fcce92-7b19-4e21-aece-03ba88ccd3fb/Untitled.png

Scores

Alright, let's sum up our impressions in numbers. We'll rate each playground based on our experience. Drumroll, please!

PlaygroundRating
Contentful3/5
Sanity4.5/5
Storyblok2/5
Hygraph5/5

Looks like Hygraph takes the crown for the most convenient way of working with queries. But don't underestimate Sanity, which comes pretty close with its schema generation. Meanwhile, Contentful and Storyblok could use a little improvement when it comes to storing queries. Keep up the good work, playgrounds! 🎉

Representation of the Content Model with Separate Access to All Nested Objects

In this section, I want to discuss the representation of the content model with a separate access to all nested objects. This is an important aspect of GraphQL that allows us to precisely specify the shape of the response we need. By doing so, we can reduce the response size by avoiding redundant fields and retrieve all related information from referred and nested objects in a single query.

Let's imagine we need to render a landing page for our application. To do that, we might need to query the landing page itself, the nested objects (Content Blocks) to map them to appropriate React components, and the references from those Content Blocks.

Contentful

In Contentful, I wrote the following query to get the required information:

query GetDeepNestedFields {
landingPage(id: "A6Iaj1ZKsxXXP2UvwifNo") {
seoTItle
seoDescription
contentBlocksCollection(limit: 10) {
items {
... on CollectionBlock {
collection {
title
productsCollection {
items {
name
image {
url
}
}
}
}
}
}
}
}
}

With this query, I was able to retrieve the necessary fields and their values. The response provided the information I needed, including the landing page details, the collection titles, and the product names along with their respective image URLs.

It's worth mentioning that Contentful doesn't support nested objects directly. To work around this limitation, I implemented all Content Blocks with their own content types and added references to them from the landing page. Despite this, I was still able to obtain field values from the fourth level of nesting (landing page -> content block -> collection -> products), which satisfied my requirements.

However, it's important to note that Contentful has a complexity limitation for queries. When queries have multiple nested arrays like in our case, there is a risk of exceeding this limit. If the complexity limit is reached, you may receive an error message with the code TOO_COMPLEX_QUERY. To overcome this, you can utilize pagination on array fields and specify limit values to simplify the query.

Sanity

In Sanity, I used the following query to retrieve the necessary information:

query GetDeepNestedFields {
LandingPage(id: "542228fe-1ca9-4f13-81a8-9413954c4cac") {
seoTitle
seoDescription
contentBlocks {
... on CollectionBlock {
collection {
title
products {
name
image {
asset {
url
}
}
}
}
}
}
}
}

This query provided the expected results without any complications. The response included the SEO title, SEO description, and the required details of the content blocks, such as the collection titles and the product names along with their corresponding image URLs.

Storyblok

In Storyblok, the query looked like this:

query GetDeepNestedFields {
LandingpageItem(id: "393747412", resolve_relations: "Collection Block.Collection") {
content {
SEOTitle
ContentBlocks
}
}
}

With Storyblok, some additional considerations were necessary. The ContentBlocks field in the response had a type of BlockScalar, which meant that it returned a JSON string with all fields and their values. This approach provided all the nested objects expanded in a single JSON string, which may not align with the typical GraphQL approach.

Furthermore, to resolve references to the Collection and Products, I used the resolve_relations parameter in the query. This allowed me to retrieve the actual Collection object instead of just the references. However, Storyblok has limitations when it comes to resolving relationships, such as a maximum limit of 50 resolved items and a lack of support for nested resolvers.

To handle these limitations, I had to reorganize the content model or make additional requests to the Storyblok GraphQL API. Another option was to retrieve all existing products in the same query and process the result on the client side or during the build process.

Hygraph

In Hygraph, the query to get the required information looked like this:

query {
landingPage(where: {slug: "home-page"}) {
seoTItle
seoDescription
contentBlocks {
... on CollectionBlock {
collection {
title
products {
name
image {
url
}
}
}
}
}
}
}

This query worked seamlessly, providing the desired results without any issues.

It's worth noting that Hygraph has a default limit for the number of items returned from an array. By specifying the first or last parameter, you can control the number of items retrieved. Additionally, Hygraph operates with query complexity, so it's important to create queries wisely and understand the details of query complexity.

Scores

Let's summarize what we have learned in this section. Unlike REST, GraphQL allows us to create complex requests and retrieve all the necessary data in one go. With GraphQL, a single query can cover multiple REST requests, making it more efficient. However, it's important to keep in mind that utilizing third-party services and making API calls may come with associated costs.

In our exploration of different CMS platforms, we have seen various approaches to handling GraphQL query complexity and limitations. Each platform has its own strengths and considerations to take into account. Based on my experience, I would like to share my insights:

  • Contentful: While Contentful does have a complexity limit, it provides transparent notifications about it. It may require some extra attention to manage the complexity, but its transparency is commendable.
  • Sanity: In my opinion, Sanity handles nested queries exceptionally well. It allows for flexible and powerful querying, making it a reliable choice for handling complex data structures.
  • Storyblok: Although Storyblok's GraphQL API has some limitations, it does not provide a satisfactory result in terms of resolving nested references. Overcoming these limitations may require additional developer resources.
  • Hygraph: Hygraph's GraphQL API performed admirably during our exploration. It effectively handled nested queries and demonstrated excellent query performance.

When considering the best approach for your project, it's important to evaluate your specific requirements and consider factors such as transparency, complexity management, and resource utilization. By leveraging the strengths of each platform, you can efficiently navigate GraphQL query complexity and build a robust solution.

NestingContentfulSanityStoryblokHygraph
Score4.5525

Retrieval of a single document through a convenient handler

In practical cases you usually query for a list of objects or a single object from your Headless CMS. Good example here is a SSG based on the NextJS which is a first class framework in FocusReactive tech stack. When you rendering pages one by one in a build time you usually would like to query single pages by their slugs because this is natural to your site generation process. In general we’d like to be able to query documents by any unique parameter like id, slug and what ever else. Let’s see how our CMSs deal with this

Contentful

In Contentful we can only query a single page by its id field.

query GetDocumentByHandler {
landingPage(id: "A6Iaj1ZKsxXXP2UvwifNo") {
sys {
id
}
slug
seoTItle
}
}

It’s not super handy because if you have a dynamic routes you will have pages paths as a parameter. And likely your pages paths are not their ids. Can we solve this? Sure! The workaround is to query a list of pages but filter them by your unique value.

query GetDocumentByHandler {
landingPageCollection(where: {slug: "home-page"}) {
items {
sys {
id
}
slug
seoTItle
}
}
}

Now we get exactly what we need - a single page by it’s slug. A small tip. If you still need document’s id - in Contentful you can get it under sys object inside document.

Sanity

In this CMS it’s totally the same. You can query documents only by id. So we need the same workaround. I will show both requests in single query

query GetDocumentByHandler {
LandingPage(id: "542228fe-1ca9-4f13-81a8-9413954c4cac") {
_id
slug {
current
}
seoTitle
}
allLandingPage(where: { slug: {current: {eq: "home-page"}}}) {
_id
slug {
current
}
seoTitle
}
}

Storyblok

In Storyblok a document query accepts only id parameter. But documentation says that id could be Slug, ID or UUID of item. Let try!

query GetDocumentByHandler {
LandingpageItem(id: "home-page") {
id
slug
content {
SEOTitle
}
}
}

That’s funny but it’s works! Now Storyblok is a pretendent to be a winner in this section 🙂 But we have one more CMS left.

Hygraph

Hygraph allow to get a document either by slug of by id.

query GetDocumentByHandler {
landingPage(where: {slug: "home-page"}) {
id
slug
seoTItle
}
}

Looks great!

Scores

In this section Storyblok and Hygraph gives us the best results. I prefer more the Hygraph style as it most transparent of what exact parameter we’re passing. In Storyblok it could be not clear if you see the query for the first time.

Contentful and Sanity didn’t provide us with the same direct feature. But having a workaround makes it totally not a big problem.

Handling
ContentfulSanityStoryblokHygraph
334.55

Filtering objects based on any field value, pagination, sorting

Documents filtering is one of the most useful features in GraphQL and the most vital requirement in your project. This is where GraphQL plays good over Rest API. When documents are to much the pagination in turn plays well, providing ability to control the portions of your response. Finally sorting takes care of things that would otherwise have to be implemented manually in the application, but it is especially necessary when used with pagination, as it is the only way to specify which documents will arrive first. Let’s explore how filtering works in our Headless CMS.

Contentful

Let’s fetch our Products. I’ll write a query with filtering, sorting, and pagination.

query FilterPaginationSorting {
productCollection(
where: {
AND: [
{ OR: [{ rating_gte: 5 }, { rating_exists: false }] }
{ name_contains: "Truffles" }
]
}
order: [rating_DESC, sys_publishedAt_ASC]
limit: 1
skip: 1
) {
total
skip
limit
items {
name
rating
}
}
}

Looks complex right? Let’s dig into what’s here happening. First I’ve specified a filter with the where param. As you can see Contentful allows to write pretty complex filters in their GraphQL API. In our arsenal such logical functions as AND, OR, good set of string and number comparison functions. Specifically here I’m filtering to only Products which has a rating higher then 5 or didn’t get a rating at all, AND their name contains "Truffles". Result of the query with only filtering is only three Products of 6:

"items": [
{
"name": "Hazelnut Heaven Truffles",
"rating": 10
},
{
"name": "Dark Desire Truffles",
"rating": null
},
{
"name": "French Silk Truffles",
"rating": null
}
]

When I added sorting, the order of items was changed as I asked to start sorted by a rating value and if it’s equal by the published date.

Finally I’ve added a pagination. For that I specified an amount of receiving items by ones: limit: 1 and a skip parameter to shift my window through the items sequence. In my case skip should be be consistently set to 0, 1, and 3. Note: don’t set limit to 1 in your real projects 😂

Sanity

Here filtering has a bit another syntax and less possibilities. But still pretty enough for practical cases. In order to implement AND logic you can simple add fields into where params object. Here I filtered all objects with rating higher then 5 and with "Truffles" in the name.

query FilterPaginationSorting {
allProduct(where: { rating: { gte: 5 }, name: { matches: "Truffles" } }) {
name
rating
}
}

Unfortunately Sanity GraphQL API don’t provide OR logic in filtering. Except that it had reasonable range of numbers, string, and boolean comparison functions.

Let’s try sorting

query FilterPaginationSorting {
allProduct(sort: [{rating: DESC},{_createdAt: ASC}]) {
name
rating
}
}

I’ve added the same sorting properties here as for Contentful. But in Sanity I’ve faced one difference that is worth to mention. Please notice that we are filtering by rating value which is not specified for all Products. So some of them have numeric values and some are null . How would you expect the result of descedant sorting in such case? Think for a second and then check the result I receive form the Sanity API.

{
"data": {
"allProduct": [
{
"name": "Dark Desire Truffles",
"rating": null
},
{
"name": "French Silk Truffles",
"rating": null
},
{
"name": "Almond Bliss",
"rating": null
},
{
"name": "Hazelnut Heaven Truffles",
"rating": 10
},
{
"name": "Berry Burst",
"rating": 6
},
{
"name": "Pure Noir",
"rating": 4
}
]
}
}

The first three null values - is this what you wanted to receive in your project? Probably it depends. But I’d prefer when numeric values come first and then go null (despite if it ASC or DESC) - this is how it’s implemented in Contentful.

For pagination you have limit and offset params.

Storyblok

query looks like this. It has less comparison functions but note a search_term param, it helps to search across

query FilterPaginationSorting {
ProductItems(filter_query_v2: {rating: {gt_int: 5}}, search_term: "Truffles") {
items {
content {
Name
rating
}
}
}
}

now let’s try sorting

query FilterPaginationSorting {
ProductItems(sort_by: "content.rating:desc,first_published_at:asc") {
items {
content {
Name
rating
}
}
}
}

as a result I have such response

[
{
"content": {
"Name": "Berry Burst",
"rating": "6"
}
},
{
"content": {
"Name": "Pure Noir",
"rating": "4"
}
},
{
"content": {
"Name": "Hazelnut Heaven Truffles",
"rating": "10"
}
},
{
"content": {
"Name": "Dark Desire Truffles",
"rating": ""
}
},
{
"content": {
"Name": "French Silk Truffles",
"rating": ""
}
},
{
"content": {
"Name": "Almond Bliss",
"rating": ""
}
}
]

For pagination you have to pass per_page and page parameters. First is specified the amount of items in a response. The second defines a shift multiple of the first value. Example: per_page: 3, page: 1 to get first 3 items.

Hygraph

In Hygraph a where filter looks pretty the similar to Contentful one

query FilterPaginationSorting {
productsConnection(
where: {AND: [{OR: [{rating_gte: 5}, {rating: null}]}, {name_contains: "Truffles"}]}
orderBy: rating_DESC
first: 3
# skip: 1, <-- use `skip` param or `after`
after: "clo2w9v7000jv0ampv71t27l3"
) {
pageInfo {
hasNextPage
startCursor
endCursor
pageSize
}
aggregate {
count
}
edges {
cursor
node {
name
rating
}
}
}
}

I’d emphasize that it has NOT logical function in addition to AND and OR .

Surprisingly Hygraph allows me to specify only single parameter for sorting in their GraphQL API. And in a response I got nullish values in the top and then numbers of rating at the end of list.

But the most exited impression I got from the pagination feature. It contains a full set of utilities to handle pagination in your app. You can get information about the total count of items, details about you page current position. There are two ways of setting pagination. First you can utilize first / last parameters and skip to specify a shift. Another way you can provide the last fetched cursor to after parameter so you next fetch will be shifted automatically.

Please note that in Hygraph GraphQL there’re two ways to query multiple items. You can do it as products with a simple functionalify, and as productsConnection to have all that rich functionalify.

Scores

First of all I’d emphasize that filtering, sorting, and pagination are crucial for eCommerce project and probably not such critical for marketing landing pages. But in this section I am comparing the features of these Headless CMS from the position where they are most needed. So the question of which Headless CMS is the best for eCommerce projects is my priority in this section. Usually such projects provide users with a rich interface for filtering, searching and scrolling the products they sell. And just these options will be implemented with the help of GraphQL API features discussed here.

I’d like to start from some sort of fun. I want to share with your the results of sorting our six products by the single parameter: orderBy: rating_DESC . No filters, pagination, or other sorting except that one. I’ve put the results into a table for your convenience. Enjoy!

ContentfulSantiyStoryblokHygraph

| "name": "Hazelnut Heaven Truffles", "rating": 10 | "name": "French Silk Truffles", "rating": null | "Name": "Berry Burst", "rating": "6" | "name": "Almond Bliss", "rating": null | | "name": "Berry Burst", "rating": 6 | "name": "Almond Bliss", "rating": null | "Name": "Pure Noir", "rating": "4" | "name": "French Silk Truffles", "rating": null | | "name": "Pure Noir", "rating": 4 | "name": "Dark Desire Truffles", "rating": null | "Name": "Hazelnut Heaven Truffles", "rating": "10" | "name": "Dark Desire Truffles", "rating": null | | "name": "Dark Desire Truffles", "rating": null | "name": "Hazelnut Heaven Truffles", "rating": 10 | "Name": "Almond Bliss", "rating": "" | "name": "Hazelnut Heaven Truffles", "rating": 10 | | "name": "French Silk Truffles", "rating": null | "name": "Berry Burst", "rating": 6 | "Name": "French Silk Truffles", "rating": "" | "name": "Berry Burst", "rating": 6 | | "name": "Almond Bliss", "rating": null | "name": "Pure Noir", "rating": 4 | "Name": "Dark Desire Truffles", "rating": "" | "name": "Pure Noir", "rating": 4 |

I don’t have comments on that, please make your own decision 🙂 The only thing to clarify is that in Storyblok case I though that I made a mistake and set a string type for rating except the number. But no, there no mistakes. It’s a number.

Untitled

In the rest, two the best Headless CMS for eCommerce cases are Contentful and Hygraph. Sanity is also pretty close to them. Which one is the winner? To me, the best and most even coverage of the necessary features, starting with the most demanded and then in descending order is provided by Contentful. I like the behavior with null values in sorting and other functionality looks reasonable for me. Hygraph has some advantages over Contentful, but simplified sorting and nulls behaviour has more weight for me. Sanity also has pretty enough coverage of features, so also looks great. Storyblok upset me with her sorting, but notice that they have search_term for searching across all text fields, which is pretty nice feature for full text searching.

Queering items
ContentfulSanityStoryblokHygraph
5434.5

WRITTEN BY

Oleg Proskurin

Oleg Proskurin

TechLead at FocusReactive