CDN
TakeShape offers a GraphQL CDN out-of-the-box and has several features to help you optimize your API by making queries cacheable at the CDN level. This improves the performance of your API and conserves your rate limit. This is most helpful for read-focused applications where the content is more stable. This guide will teach you how to take advantage of these features.
This builds on the powerful caching features TakeShape offers at the platform level. Learn more about those in our API Indexing Guide.
Using the TakeShape CDN
The GraphQL CDN is available on paid plans. The CDN URL can be found on the dashboard when you log in. It looks like this:
https://cdn.takeshape.io/project/YOUR_PROJECT_ID/production/graphql
The path part of this URL has the same structure as the origin API endpoint, including the branch name. The only difference is that the domain is cdn.takeshape.io
instead of api.takeshape.io
.
Your project's CDN endpoint can handle GraphQL queries using both GET and POST. As you will see below, you will want to use GET as often as possible for cacheability.
Make your queries more cacheable
Let's look at an example TakeShape project schema and walk through how to query it optimally and add caching hints to the schema.
Starting Point
Let's say you have a schema that is already set up to index assets from Contentful. You've even extended the Contentful_Asset
shape to query Shopify product data based on an ID stored on the asset. Here is an example of how that could be accomplished:
View example schema
{
"defaultLocale": "en-us",
"locales": ["en-us"],
"queries": {
"getIndexedContentfulAssetList": {
"shape": "PaginatedList<Contentful_Asset>",
"resolver": {
"shapeName": "Contentful_Asset",
"name": "takeshape:queryApiIndex",
"service": "takeshape",
"options": { "indexedShape": "Contentful_Asset" }
},
"description": "Fetch Conftentful_Asset data from the API Index.",
"args": "TSListArgs<Contentful_Asset>"
},
"Contentful_assetCollection": {
"resolver": {
"name": "graphql:query",
"service": "contentful",
"fieldName": "assetCollection"
},
"args": {
"type": "object",
"properties": {
"skip": { "type": "integer", "default": 0 },
"limit": { "type": "integer", "default": 100 },
"preview": { "type": "boolean" },
"locale": { "type": "string" },
"where": { "@ref": "contentful:AssetFilter" },
"order": { "type": "array", "items": { "@ref": "contentful:AssetOrder" } }
}
},
"shape": "Contentful_AssetCollection"
},
"Shopify_product": {
"resolver": {
"name": "graphql:query",
"service": "shopify",
"fieldName": "product"
},
"args": {
"type": "object",
"properties": {
"id": {
"type": "string",
"@tag": "id",
"description": "The ID of the Product to return."
}
},
"required": ["id"]
},
"shape": "Shopify_Product"
}
},
"mutations": {},
"shapes": {
"Contentful_Asset": {
"id": "Contentful_Asset",
"name": "Contentful_Asset",
"title": "Contentful Asset",
"cache": {
"enabled": true,
"triggers": [{ "type": "schedule", "loader": "list", "interval": 1440 }],
"idField": "url"
},
"loaders": {
"list": {
"query": "contentful:Query.assetCollection",
"pagination": {
"type": "offset",
"offsetArg": "skip",
"pageSizeArg": "limit",
"itemsPath": "items",
"itemTotalPath": "total"
}
}
},
"schema": {
"extends": [
{ "@ref": "contentful:Asset" },
{
"type": "object",
"properties": {
"shopifyProduct": {
"@ref": "shopify:Product",
"@resolver": {
"name": "delegate",
"to": "shopify:Query.product",
"args": { "ops": [{ "path": "id", "mapping": "$source.title" }] }
},
"title": "Shopify Product"
}
}
}
]
}
},
"Shopify_Product": {
"id": "Shopify_Product",
"name": "Shopify_Product",
"title": "Shopify Product",
"schema": { "extends": [{ "@ref": "shopify:Product" }] },
"interfaces": [
"shopify:HasMetafieldDefinitions",
"shopify:HasMetafields",
"shopify:HasPublishedTranslations",
"shopify:LegacyInteroperability",
"shopify:Navigable",
"shopify:Node",
"shopify:OnlineStorePreviewable",
"shopify:Publishable"
],
"cache": { "enabled": true }
}
},
"workflows": {},
"forms": {},
"schemaVersion": "3.42.0",
"apiVersion": "2",
"services": {
"contentful": {
"id": "contentful",
"title": "Contentful",
"namespace": "Contentful",
"provider": "contentful",
"serviceType": "graphql",
"authenticationType": "bearer",
"options": {
"endpoint": "https://graphql.contentful.com/content/v1/spaces/84ssynxl6f3z",
"space": "84ssynxl6f3z",
"introspectedAt": "2024-06-06T19:50:17.873Z"
}
},
"shopify": {
"id": "shopify",
"title": "Shopify",
"namespace": "Shopify",
"provider": "shopify",
"serviceType": "graphql",
"authenticationType": "custom",
"options": {
"shop": "gregs-takeshape-store",
"endpoint": "https://gregs-takeshape-store.myshopify.com/admin/api/2023-04/graphql.json",
"introspectedAt": "2024-06-06T19:35:40.908Z"
}
}
}
}
Typically you might query your API using POST like this:
export GRAPHQL_API_KEY="xxxx"
export GRAPHQL_ENDPOINT="xxxx"
curl -g \
-X POST \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $GRAPHQL_API_KEY" \
-d '{"query":"query {getIndexedContentfulAssetList { items { title url shopifyProduct { title createdAt } } total }}"}' \
$GRAPHQL_ENDPOINT
This will work, but the CDN will not cache POST requests.
Using GET
Here is an example GET request. Note that the GraphQL query is in the URL.
export GRAPHQL_API_KEY="xxxx"
export GRAPHQL_ENDPOINT="xxxx"
curl -G \
-H "Authorization: Bearer $GRAPHQL_API_KEY" \
--data-urlencode 'query={"query":"query {getIndexedContentfulAssetList { items { title url shopifyProduct { title createdAt } } total }}"}' \
$GRAPHQL_ENDPOINT
This example works for short queries, but one limitation of GET
is the URL length. GraphQL queries get complicated and can easily bump your URL above 8,192 bytes, which is the URL length limit for the TakeShape GraphQL CDN (and is around the typical limit for other popular CDNs as well). You can surpass this limit using Automatic Persisted Queries.
Automatic Persisted Queries
TakeShape's Automatic Persisted Queries feature will persist your GraphQL query string so that you don't need to send it every time you want to execute that query. This is supported out of the box with popular clients such as Apollo and urql.
To implement it yourself, include a SHA256 hash of your query (along with any variables) instead of the query itself, like this:
export GRAPHQL_API_KEY="xxxx"
export GRAPHQL_ENDPOINT="xxxx"
export QUERY='{"query":"query {getIndexedContentfulAssetList { items { title url shopifyProduct { title createdAt } } total }}"}'
export QUERY_HASH=$(echo -n "$QUERY" | openssl sha256 | awk '{print $2}')
curl -G \
-H "Authorization: Bearer $GRAPHQL_API_KEY" \
--data-urlencode "extensions={\"persistedQuery\":{\"version\": 1, \"sha256Hash\": \"$QUERY_HASH\"}}" \
$GRAPHQL_ENDPOINT
If the query isn't currently persisted, the API responds with an error to let us know:
{ "errors": [{ "message": "Persisted Query Not Found", "extensions": { "code": "PERSISTED_QUERY_NOT_FOUND" } }] }
Now let's POST our query (to avoid the URL length limit) with the hash in the query parameters so that we get a response and also persist the query at the same time:
export GRAPHQL_API_KEY="xxxx"
export GRAPHQL_ENDPOINT="xxxx"
export QUERY='{"query":"query {getIndexedContentfulAssetList { items { title url shopifyProduct { title createdAt } } total }}"}'
export QUERY_HASH=$(echo -n "$QUERY" | openssl sha256 | awk '{print $2}')
curl -g \
-X POST \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $GRAPHQL_API_KEY" \
-d "$QUERY" \
"$GRAPHQL_ENDPOINT?extensions=%7B%22persistedQuery%22%3A%7B%22version%22%3A%201%2C%20%22sha256Hash%22%3A%20%22$QUERY_HASH%22%7D%7D"
You'll get your query's response back as usual. Now if you execute the previous GET request again (with just the hash and the query string not specified) you'll get data back from that too because the query was persisted.
Add Cache Hints
Now that you are sending requests that your CDN can cache, you'll want to add cache hints to your schema. This will allow TakeShape to calculate Cache-Control
headers that the CDN can use.
Looking at our example schema from before, there are 2 shapes that we will add cache hints to. When doing this, think about how long content for each shape should be cached. In this schema, the Contentful_Asset
shape is indexed on a schedule with an interval of 1440 minutes (daily). But what if you know that your Contentful assets do not update very often and you're comfortable caching them for longer than that? Let's add a maxAge
to the Contentful_Asset
cache configuration with the larger value of 259200 seconds (3 days)
"Contentful_Asset": {
"id": "Contentful_Asset",
"name": "Contentful_Asset",
"title": "Contentful Asset",
"cache": {
"enabled": true,
"triggers": [{"type": "schedule", "loader": "list", "interval": 1440}],
"idField": "url",
"maxAge": 259200
},
...
}
Now let's assume that our Shopify products do update frequently, so we'll set the maxAge
for that shape to 86400 seconds (1 day).
"Shopify_Product": {
"id": "Shopify_Product",
"name": "Shopify_Product",
"title": "Shopify Product",
"cache": {
"enabled": true,
"maxAge": 86400
},
...
}
Now that these cache hints have been added, you'll see that TakeShape sets the cache-control
header based on which shapes are used in the query.
For example:
{
getIndexedContentfulAssetList {
items {
title
url
shopifyProduct {
title
createdAt
}
}
total
}
}
This query includes data from both Contentful_Asset
and Shopify_Product
. In this case, the lowest maxAge
is used; that is, the maxAge
of Shopify_Product
. The cache-control
header will have the value max-age=86400
.
Another example:
{
getIndexedContentfulAssetList {
items {
title
url
}
total
}
}
This time we are not selecting any Shopify_Product
data. In this case, the maxAge
from Contentful_Asset
will be used and the cache-control
header will have the value max-age=259200
.
Conclusion
This guide demonstrated the tools TakeShape offers to cache your GraphQL API at the CDN level. The particulars of how to use those tools will always be a little different for every application. If you have any questions about how to use these features in your project, feel free to get in touch using the live chat in the help menu.