How to use the Dynamic Content delivery APIs to expose content in your GraphQL server

  • Engineering
  • Guides
10 mins reading time
Darren Lee | August 28, 2020

GraphQL is rapidly becoming the go to pattern for exposing and aggregating data for modern frontend apps. GraphQL helps your app cut down the number of requests it needs to make, avoids under and over fetching of data, and reduces latency and payload sizes. Not to mention the productivity improvements it offers to developers.

To get the most out of GraphQL, it's best to include all of the data sources your app might need in your schema. Headless CMS tools like Dynamic Content make it possible to include content in that mix as we represent content as data which allows it to be sliced and diced in a GraphQL layer. This is an advantage over traditional web CMS solutions as it's much harder to query a fragment of HTML in a similar way.

This article will show how you can consume the Dynamic Content delivery APIs to expose content in your GraphQL server. Doing this will allow your frontends to request content alongside every other data source in your architecture.

In this walkthrough, we are using Apollo, however the same principles apply to other GraphQL implementations.

Check List

Before jumping in, take a moment to make sure you have everything you need to follow along.

GraphQL Concepts

This article assumes you are familiar with the core GraphQL concepts. If you need to brush up, I recommend going through the official introduction and the “Get started with Apollo Server” tutorial.

Content and Content Types

You will also need some content types and content setup in Dynamic Content that we can expose in the GraphQL server. This article will be using the content types from one of the Dynamic Content react samples but you can use any content type you wish.

You can find the code used in this blog on GitHub.

Let’s Go!

Boilerplate

First, we need to create a project and install the dependencies:

1npm init
1npm install --save apollo-server graphql apollo-datasource-rest

Then, create an index.js file which will act as the entry point for the server:

1const { ApolloServer, gql } = require('apollo-server');
2 
3const typeDefs = gql`
4 
5 `;
6 
7const resolvers = {
8  
9};
10 
11const server = new ApolloServer({ typeDefs, resolvers });
12 
13server.listen().then(({ url }) => {
14 console.log(`🚀 Server ready at ${url}`);
15});

Note: If you try to run this you will get an error, don’t worry, you didn’t make a mistake… Apollo requires a valid schema before it will run that we will create in the next section.

Define the GraphQL Schema

GraphQL uses a schema to define the structures and operations the server knows about. Think of this as your API design. Clients will only be able to access the types, fields, queries and mutations you add to your schema.

Dynamic Content also uses a schema to define content types. If we want to expose a content type in our GraphQL server, we need to create a type for it in the GraphQL schema. In most cases, the GraphQL type will very closely resemble the Content Type.

Doing this is relatively easy, you simply need to look at the properties that make up your content type and map them to the equivalent structure in GraphQL. Typically objects will map to a “type” and properties of those objects will map to a “field”.

Keep in mind that the type system in GraphQL is very strict. If a field can be null then don't add the non-null operator "!". This is important to remember as you evolve your content types and add new fields which may not be set in pre-existing content items.

I am going to use this “Navigation” content type, which converts into the following GraphQL schema:

1const typeDefs = gql`
2 type NavigationLink {
3     title: String
4     href: String
5 }
6 
7 type Navigation {
8   links: [NavigationLink!]
9 }
10`;

Notice that we had to create two types. That is because this particular content type contains a nested object.

Finally, we need to add navigation to the Query type. This is the entry point for fetching the navigation.

1type Query {
2     navigation: Navigation
3 }

Building a Data Source

Next, we need a data access layer that will make API calls to the content delivery API to fetch content. Apollo has some useful “DataSource” classes that you can extend which make it easy to connect to the most common types of data sources such as a REST API or a database.

1const { RESTDataSource } = require('apollo-datasource-rest');
2 
3class ContentAPI extends RESTDataSource {
4   constructor() {
5       super();
6       // Replace "labs" with your hub name
7       this.baseURL = 'https://labs.cdn.content.amplience.net/';
8   }
9 
10   async getContentByKey(key) {
11       const response = await this.get(`/content/key/${key}`, {depth: 'root', format: 'inlined'});
12       return response.content;
13   }
14}

This data source sets the base URL in the constructor (update this to use your base url) and then exposes a method to lookup content by its delivery key.

Typically, when I use the Content Delivery API, I set the depth query parameter to “all” which tells the API to return any nested content items too, however since this is GraphQL we only need the root item to avoid over fetching. Nested content will be fetched by another resolver (more on this later).

Finally, update your code so that the ApolloServer knows about this new data source.

1const server = new ApolloServer({
2   typeDefs,
3   resolvers,
4   dataSources: () => {
5       return {
6           contentAPI: new ContentAPI()
7       }
8   }
9});

Resolving Content

Now we have a datasource, we can implement the first resolver function for the “navigation” query. Resolvers tell the server how to fetch the data associated with a particular field.

1const resolvers = {
2   Query: {
3       navigation: (parent, args, { dataSources }) => {
4           return dataSources.contentAPI.getContentByKey('component/navigation');
5       }
6   },
7};

You can see that resolvers have access to the data source we created earlier. This resolver simply fetches the navigation using the delivery key I assigned to the content item in Dynamic Content. A more complicated resolver might take in the key as a parameter.

Querying Content

We are now ready to start the server and perform our first query 🎉.

1node index.js

This will start up your server at http://localhost:4000 where you will be greeted by the GraphQL playground which lets you perform GraphQL queries and explore your schema.

1query {
2   navigation {
3     links {
4       title
5       href
6     }
7   }
8 }

For my example, I can run the query above and see the content being returned.

Resolving Nested Content

This works well for resolving a simple content type, but it is quite common to build more complicated structures by nesting content items together. The code we have so far would only fetch the root item, returning null for any nested items.

Dynamic Content has two types of nested content fields, a “content link” and a “content reference”, which would be resolved in the same way.

To demonstrate how to handle this, we will add another type to the schema called “NavigationSlot”. This Content Type acts as a container for the navigation. It has a field called “navigation” which uses a content link to reference the “Navigation” itself.

First, we need to update the GraphQL schema so that the server knows about the new type and to add a new query for fetching the navigation slot.

1type NavigationSlot {
2     navigation: Navigation!
3 }
4 
5 type Query {
6     navigation: Navigation!
7     navigationSlot: NavigationSlot!
8 }

Next, we need a new function in the datasource to load content by id. This is because the parent content item will contain the id of the nested content that we want to fetch.

1async getContentById(id) {
2       const response = await this.get(`/content/id/${id}`, {depth: 'root', format: 'inlined'});
3       return response.content;
4   }

The final step is to create two new resolver functions.

1const resolvers = {
2   Query: {
3       navigation: (parent, args, { dataSources }) => {
4           return dataSources.contentAPI.getContentByKey('component/navigation');
5       },
6       navigationSlot: (parent, args, { dataSources }) => {
7           return dataSources.contentAPI.getContentByKey('slots/navigation');
8       }
9   },
10   NavigationSlot: {
11       navigation: (parent, args, { dataSources }) => {
12           const id = parent.navigation.id;
13           return dataSources.contentAPI.getContentById(id);
14       }
15   }
16};

As well as adding a new query resolver, we also added a resolver for the field “navigation” on the type “NavigationSlot”. Whenever a query requests the navigation field on a navigation slot, this resolver will be invoked to fetch the nested content.

Apollo gives you access to the “parent” object, in this case the NavigationSlot, which allows us to read the content link id and proceed to fetch it.

1query {
2 navigationSlot {
3   navigation {
4     links {
5       title
6       href
7     }
8   }
9 }
10}

After you restart the server, you will be able to query against these new fields.

Handling Union Types

A "NavigationSlot" only ever contains "Navigation" items, but how could we model a Page with lot's a different content?

Let's say we have "Page" that can contain a "HeroBannerBlock", a "GalleryBlock", or an "EditorialBlock". GraphQL can model this with a union type, which allows a single field to have one of a list of possible types.

Step 1: Define the schema

First, we need to model the 3 nested types.

1type ImageLink {
2     name: String!
3     endpoint: String!
4     defaultHost: String!
5 }
6 
7 type EditorialBlock {
8     title: String
9     description: String
10 }
11 
12 type GalleryBlockItem {
13     image: ImageLink
14     callToAction: String
15     callToActionHref: String
16 }
17 
18 type GalleryBlock {
19     title: String
20     items: [GalleryBlockItem]
21 }
22 
23 type HeroBannerBlock {
24     image: ImageLink
25     title: String
26     description: String
27     callToAction: String
28     callToActionHref: String
29 }

Next, create a union type that groups these together.

1union PageBlock = EditorialBlock | GalleryBlock | HeroBannerBlock

Finally, we can define the Page type and add a query field to fetch a page.

1type Page {
2   components: [PageBlock]
3 }
4
5type Query {
6     navigation: Navigation!
7     navigationSlot: NavigationSlot!
8     homepage: Page
9 }

Step 2: Define the resolvers

To resolve a “Page” content type, we need to fetch the page itself and each of the nested components. Instead of lazy loading the nested content like we did previously, I am going to pre-fetch it by setting the content API depth parameter to “all” when loading the page. This would be wasteful if most queries did not ask for the nested content, but in my case, I know the app will always want the nested content when rendering the page.

Update the datasource to take an additional parameter called “depth” that we can pass to the API.

1async getContentByKey(key, depth) {
2       const response = await this.get(`/content/key/${key}`, {depth: depth || 'root', format: 'inlined'});
3       return response.content;
4   }

Then add a query resolver for the homepage, setting depth to “all”.

1homepage: (parent, args, { dataSources }) => {
2   return dataSources.contentAPI.getContentByKey('slots/homepage', 'all');
3}

Since the data will be pre-loaded, we don’t need to add a resolver for the “components” field in Page.

Step 3: Resolve the typename

One last hurdle remains. We have all the nested content but the Apollo server has no idea what type each one is. To fix this, we can implement a “resolveType” function.

The resolver below looks at the Content Type of the object and maps it to the appropriate GraphQL type.

1PageBlock: {
2       __resolveType: (obj, context, info) => {
3           switch (obj._meta.schema) {
4               case 'https://anyafinn.dev/component-hero-banner-block.json':
5                   return 'HeroBannerBlock';
6               case 'https://anyafinn.dev/component-editorial-block.json':
7                   return 'EditorialBlock';
8               case 'https://anyafinn.dev/component-gallery-block.json':
9                   return 'GalleryBlock'
10           }
11       }
12   }

Another approach is to simply add the type as a constant in your Content Type.

1"__typename": {
2   "type": "string",
3   "const": "EditorialBlock"
4}

In my content type, each page block already has a constant field called “component” that I can repurpose:

1PageBlock: {
2       __resolveType: (obj, context, info) => {
3           return obj.component;
4       }
5   }

Step 4: Query

Once everything is set up, you will be able to perform queries to pull back the page, its content and specify the list of fields you want for each type individually.

1query {
2   homepage {
3     components {
4       __typename
5       ... on EditorialBlock {
6         title
7       }
8       ... on HeroBannerBlock {
9         title
10       }
11       ... on GalleryBlock {
12         title
13       }
14     }
15   }
16 }

You can also reuse these lists of fields with the Fragments feature.

Next Mission 🚀

Congratulations, you now have all the fundamentals covered! In the next article, we will look at ways to optimize the server and solve the N+1 problem, reducing the number of network calls we need to make to process a complex query.