Andrew Welch · Insights · #graphql #craft-3 #frontend

Published , updated · 5 min read ·


Please consider 🎗 sponsoring me 🎗 to keep writing articles like this.

Using the Craft CMS “headless” with the GraphQL API

Craft CMS 3.3 added a GraphQL lay­er that gives your web­site a for­mal­ized, struc­tured API out of the box. Here’s how to use GraphQL + Craft CMS as a head­less” CMS

Link GraphQL & Craft CMS

Relat­ed talk: Solv­ing Prob­lems with Mod­ern Tooling

This arti­cle explores cod­ing pat­terns you might find use­ful when work­ing with Craft CMS’s GraphQL API via real-world exam­ples, and also why you might want to use GraphQL to begin with.

This arti­cle describes the tech­ni­cal imple­men­ta­tion of this project; if you want to get into the high­er-lev­el deci­sions, check out the Post-Mortem: Out­break Data­base article.

GraphQL describes itself with the fol­low­ing pithy tagline:

A query language for your API

And that’s exact­ly what it is. It’s a neu­tral lay­er that sits on top of what­ev­er API lay­er you have, from data­bas­es like MySQL, Post­gres, Mon­go, etc. to cus­tom built solutions.

In August 2019, Pix­el & Ton­ic added GraphQL to Craft CMS 3.3, and vast­ly improved it in Craft CMS 3.4.

Pri­or to this, we relied on Mark Huot’s CraftQL plu­g­in for GraphQL, as dis­cussed in the Using Vue­JS + GraphQL to make Prac­ti­cal Mag­ic article.

Should you use Mark’s CraftQL plu­g­in or should you use Craft CMS’s first-par­ty GraphQL implementation?

Unless you need muta­tions (the abil­i­ty to change data in the Craft CMS back­end via GraphQL), using the Craft CMS first-par­ty GraphQL imple­men­ta­tion in Craft 3.3 or lat­er is the way to go.

Craft CMS’s first-par­ty GraphQL API does­n’t cur­rent­ly sup­port muta­tions, but it does offer a robust, per­for­mant data source for head­less Craft CMS.

Link Why use GraphQL?

As a web devel­op­er, you’d use GraphQL if you were writ­ing a fron­tend that is sep­a­rate from the back­end. You’d be using Craft CMS as a head­less” CMS for its excel­lent con­tent author­ing experience.

Per­haps you’re writ­ing the fron­tend in a frame­work like React, Vue.js, Svelte, or one of the frame­works that lay­ers on top of them like Next.js, Nuxt.js, Gats­by, Grid­some, or Sap­per.

Or per­haps you’re jump­ing on the JAM­stack band­wag­on, and just need a self-host­ed CMS like Craft CMS to man­age your content.

If you need a way to get data out of Craft CMS, GraphQL is a great way to do it

Or maybe the project has an iPhone app that needs to com­mu­ni­cate with a con­tent man­age­ment sys­tem on the back­end for data.

In those cas­es, you don’t have access to Twig or the Ele­ment Queries to inter­act with Craft CMS and your data, but you still want to reap the ben­e­fits of Craft CMS’s won­der­ful & flex­i­ble con­tent author­ing experience.

You could write a cus­tom API for Craft CMS using the Ele­ment API, but that can be a sig­nif­i­cant amount of work, and you’ll end up with some­thing bou­tique, rather than an indus­try standard.

Link Enter the GraphQL

This is where GraphQL for Craft CMS excels. By virtue of cre­at­ing your con­tent mod­els in Craft CMS, you auto­mat­i­cal­ly get a GraphQL API to access it, with no extra work on your part.

It’s also self-doc­u­ment­ing, and strict­ly typed. You don’t need to define the GraphQL schemas, because you’ve already implic­it­ly done that in Craft.

It just works.

You just need to be using Craft CMS Pro” edi­tion, and you’ll see GraphQL in your CP, and you can imme­di­ate­ly start explor­ing the GraphQL API using the explor­er (which is pro­vid­ed by a fron­tend tool called GraphiQL):

Craft CMS GraphQL Explorer

While it’s beyond the scope of this arti­cle to teach you GraphQL (there are many great resources online for that), there are a few things to note here:

  • In the upper-left pane, is the GraphQL query spec­i­fy­ing what we’re searching
  • In the low­er-left pane are vari­ables we pass into the query, in JSON format
  • The mid­dle pane is the data returned from the GraphQL end­point as a result of our query
  • To the right pane is a search­able, doc­u­ment­ed schema reference

The fun thing about using this GraphQL explor­er is that you can learn by just pok­ing around.

You can press Option-Space­bar at any time for a list of auto-com­plet­ed items that can appear in any giv­en con­text. You can also use the Doc­u­men­ta­tion Explor­er to see your entire schema.

Com­bined with the offi­cial Craft CMS GraphQL API doc­u­men­ta­tion, you can get pret­ty far just by explor­ing around in the GraphQL Explorer.

Link The Way of the GraphQL

Before we dive into some queries in JavaScript, I have a few tips I’d like to pass along to you to make your life with GraphQL easier.

One nice fea­ture of the Craft CMS imple­men­ta­tion of GraphQL is that it has a caching lay­er for your queries. While this is nor­mal­ly great for per­for­mance rea­sons, when you’re devel­op­ing a site, you real­ly want it off to avoid poten­tial­ly deal­ing with cached results.

You can do this via the enableGraphQlCaching set­ting in your config/general.php file:

<?php
/**
 * General Configuration
 *
 * All of your system's general configuration settings go in here. You can see a
 * list of the available settings in vendor/craftcms/cms/src/config/GeneralConfig.php.
 *
 * @see \craft\config\GeneralConfig
 */

return [
    // Craft config settings from .env variables
    'enableGraphQlCaching' => (bool)getenv('ENABLE_GQL_CACHING'),
];

Here I set it to an Envi­ron­ment Vari­able, but you can set it to true or false direct­ly if you prefer.

Craft CMS also has a headless­Mode set­ting. This is intend­ed for sit­u­a­tions where Craft CMS will nev­er be used to ren­der any con­tent on the frontend.

Due to lim­i­ta­tions in how this works in Craft CMS 3.3, I rec­om­mend leav­ing it set to false, since ele­ments lack URIs in the 3.3 imple­men­ta­tion of headlessMode. This caus­es many plu­g­ins to not work prop­er­ly, and oth­er unde­sired behavior.

How­ev­er, Pix­el & Ton­ic has made improve­ments to how the headlessMode set­ting works in Craft CMS 3.4, so you can set it to true if you want Craft CMS to nev­er ren­der con­tent, and sim­ply be a data/​API provider via GraphQL.

Some­thing many peo­ple aren’t aware of is that you can use GraphQL in Twig if you want to:

{# Set the GraphQL query #}
{% set query %}
query organismsQuery($needle: String!)
{
    entries(section: "organisms", search: $needle, orderBy: "title") {
        title,
        slug,
        id
    }
}
{% endset %}


{# Set the variables to pass into the query #}
{% set variables = {
    'needle': 'Botulism'
} %}

{# Query the data via Twig! #}
{% set data = gql(query, variables) %}

{% dd data %}

The gql() Twig fil­ter was added in Craft CMS 3.3.12. Why would you ever want to do such a thing?

Usu­al­ly, you would­n’t. You’d just use Ele­ment Queries instead… but it can be a nice way to exper­i­ment and play around with queries in a familiar/​comfortable environment.

In fact, it ham­mers home a very impor­tant con­cep­tu­al point with GraphQL in Craft CMS:

In Craft CMS, GraphQL queries map to Element Queries

GraphQL is a thin-ish lay­er in Craft CMS that han­dles map­ping from a GraphQL queries to Ele­ment Queries. It works in both directions:

  • The GraphQL schema is derived from the under­ly­ing Craft CMS Sec­tions, Ele­ment con­tent mod­els, etc.
  • GraphQL queries are inter­nal­ly mapped to Ele­ment Queries, which are then executed

The rea­son this is an impor­tant con­cept is that if you want to fig­ure out how to do some­thing in GraphQL, fig­ure out how to do it via an Ele­ment Query first, and then work your way back­wards.

So it can then be handy to use gql() in Twig, because you can write your Ele­ment Query as you nor­mal­ly would. Get it work­ing the way you want it to, and then con­struct & test the anal­o­gous GraphQL query in the same place.

Final­ly, if you’re using Php­Storm, check out the GraphQL Schema Auto-Com­ple­tion with Php­Storm arti­cle for bliss­ful auto-com­plete query writ­ing in your code.

Link Controller of Fury

I men­tioned ear­li­er that you get a GraphQL API with­out hav­ing to do any work with Craft CMS. While this is true, hav­ing a lit­tle cus­tom con­troller as part of your site mod­ule can be helpful.

I always have a site mod­ule as part of my Craft CMS setups, as dis­cussed in the Enhanc­ing a Craft CMS 3 Web­site with a Cus­tom Mod­ule article.

You always start with no cus­tom code, but inevitably you end up need­ing a lit­tle piece here or there. And if you have a mod­ule in place already, it makes adding this in real­ly easy.

In our case, we want a cus­tom con­troller that our fron­tend can ping to:

  • actionGetCsrf() — get the cur­rent CSRF token to val­i­date POST request submissions
  • actionGetGqlToken() — get a par­tic­u­lar GraphQL token for access permissions
  • actionGetFieldOptions() — get the options in a Drop­down field in Craft CMS

POST requests are used to send data from the web­site along to an end­point. While we could use GET for sim­ple requests like actionGetGqlToken(), we need to send data to oth­er end­points like actionGetFieldOptions(), so we might as well use POST for all requests.

Here’s what our cus­tom con­troller looks like:

<?php
/**
 * Site module for Craft CMS 3.x
 *
 * An example module for Craft CMS 3 that lets you enhance your websites with a
 * custom site module
 *
 * @link      https://nystudio107.com/
 * @copyright Copyright (c) 2019 nystudio107
 */

namespace modules\sitemodule\controllers;

use Craft;
use craft\web\Controller;

use yii\web\Response;

/**
 * @author    nystudio107
 * @package   SiteModule
 * @since     1.0.0
 */
class SiteController extends Controller
{
    // Constants
    // =========================================================================

    const GQL_TOKEN_NAME = 'Site GQL Token';

    // Protected Properties
    // =========================================================================

    protected $allowAnonymous = [
        'get-csrf',
        'get-gql-token',
        'get-field-options',
    ];

    // Public Methods
    // =========================================================================

    /**
     * @inheritdoc
     */
    public function beforeAction($action): bool
    {
        // Disable CSRF validation for get-csrf POST requests
        if ($action->id === 'get-csrf') {
            $this->enableCsrfValidation = false;
        }

        return parent::beforeAction($action);
    }

    /**
     * @return Response
     */
    public function actionGetCsrf(): Response
    {
        return $this->asJson([
            'name' => Craft::$app->getConfig()->getGeneral()->csrfTokenName,
            'value' => Craft::$app->getRequest()->getCsrfToken(),
        ]);
    }

    /**
     * @return Response
     */
    public function actionGetGqlToken(): Response
    {
        $result = null;
        $tokens = Craft::$app->getGql()->getTokens();
        foreach ($tokens as $token) {
            if ($token->name === self::GQL_TOKEN_NAME) {
                $result = $token->accessToken;
            }
        }

        return $this->asJson([
            'token' => $result,
        ]);
    }

    /**
     * Return all of the field options from the passed in array of $fieldHandles
     *
     * @return Response
     */
    public function actionGetFieldOptions(): Response
    {
        $result = [];
        $request = Craft::$app->getRequest();
        $fieldHandles = $request->getBodyParam('fieldHandles');
        foreach ($fieldHandles as $fieldHandle) {
            $field = Craft::$app->getFields()->getFieldByHandle($fieldHandle);
            if ($field) {
                $result[$fieldHandle] = $field->options;
            }
        }

        return $this->asJson($result);
    }
}

While this cus­tom con­troller isn’t nec­es­sary for using your GraphQL end­point, it does make the expe­ri­ence a bit nicer. We grab the CSRF token, and send that CSRF token along with fur­ther requests, to allow Yii2 to val­i­date the data submissions.

Link Schemas & Tokens

GraphQL in Craft CMS has a con­cept of Schemas and Tokens:

  • Schemas — schemas you can think of these as per­mis­sions, in a way, in that they define what parts of the under­ly­ing CraftCMS con­tent you want exposed

Craft CMS GraphQL Schemas

  • Tokens — tokens are they keys” that you pass along with your GraphQL queries, and they link to a schema

Craft CMS GraphQL Tokens

So what I do is:

  1. Get the CSRF token for the cur­rent session
  2. Use that CSRF to obtain a spe­cif­ic GraphQL token used for API access
  3. Use that GraphQL token in all GraphQL request to the endpoint

In many cas­es, you won’t need to do this because you’ll just have one Pub­lic Schema that defines your GraphQL API. But if you want to poten­tial­ly have vary­ing lev­els of access, you’d cre­at­ed mul­ti­ple tokens and use them to what schema you can use.

If you just want to use the spe­cial Pub­lic Schema, you don’t need any token at all. Just don’t send a bear­er token down with your GraphQL queries, and it’ll just use what­ev­er schema is defined in Pub­lic Schema.

N.B.: In our case, we want pub­lic access to do the data. The CRSF and GraphQL tokens are just for light­weight auth, it is not treat­ed as a locked down secret” for auth’ing. For that you’d want some­thing like JSON Web Tokens (JWT), check out the Craft JWT Auth plugin.

Link Controller & GraphQL XHRs

Let’s get into some JavaScript that we use to com­mu­ni­cate with our cus­tom con­troller end­points, and also the Craft CMS GraphQL endpoint.

First here is a lit­tle xhr.js helper to con­fig­ure and exe­cute gener­ic XHRs:

// Configure the XHR api endpoint
export const configureXhrApi = (url) => ({
    baseURL: url,
    headers: {
        'X-Requested-With': 'XMLHttpRequest'
    }
});

// Execute an XHR to our api endpoint
export const executeXhr = async(api, variables, callback) => {
    // Execute the XHR
    try {
        const response = await api.post('', variables);
        if (response.data) {
            callback(response.data);
        }
    } catch (error) {
        console.error(error);
    }
};

The X-Requested-With: XMLHttpRequest head­er lets Craft CMS know that this is an AJAX” request, which can affect how the request is processed.

Here’s how we get the CSRF from our cus­tom con­troller endpoint:

import axios from 'axios';
import { configureXhrApi, executeXhr } from '../utils/xhr.js';

const CSRF_ENDPOINT = '/actions/site-module/site/get-csrf';

// Fetch & commit the CSRF token
export const getCsrf = async({commit, state}) => {
    const api = axios.create(configureXhrApi(CSRF_ENDPOINT));
    let variables = {
    };
    // Execute the XHR
    await executeXhr(api, variables, (data) => {
        commit('setCsrf', data);
    });
};

The above func­tion is a Vuex Action, but Vuex is just used as a con­ve­nient place to store data. It’s not impor­tant that you use Vuex your­self to get some­thing out of the exam­ples pre­sent­ed here.

The code is just cre­at­ing an Axios instance, exe­cut­ing the XHR, and stash­ing the returned CSRF data for future use.

We just use Axios here as opposed to some­thing like Apol­lo Client just because all we real­ly need is a light­weight way to com­mu­ni­cate with­out GraphQL end­point. We could also use the built-in Fetch API, but Axios han­dles some edge-case sit­u­a­tions and brows­er com­pat­i­bil­i­ty for us.

Then this is how we get the GraphQL token:

import axios from 'axios';
import { configureGqlApi, executeGqlQuery } from '../utils/gql.js';

// Fetch & commit the GraphQL token
export const getGqlToken = async({commit, state}) => {
    const api = axios.create(configureXhrApi(TOKEN_ENDPOINT));
    let variables = {
        ...(state.csrf && { [state.csrf.name]: state.csrf.value }),
    };
    // Execute the XHR
    await executeXhr(api, variables, (data) => {
        commit('setGqlToken', data);
    });
};

Once again, this is a Vuex Action, but it does­n’t have to be. The impor­tant part is that it’s using the CSRF we obtained ear­li­er, and send­ing that along with a request for our GraphQL token, which it then saves for future use.

The funky ...(state.csrf && { [state.csrf.name]: state.csrf.value }), line deserves some expla­na­tion. It’s using the JavaScript Spread Syn­tax to add a key/​value pair to the variables object.

But it’s doing it in a con­di­tion­al way, using the behav­ior of the JavaScript && Log­i­cal Oper­a­tor to only spread the { [state.csrf.name]: state.csrf.value } object into variables only if state.csrf is truthy.

This works because JavaScript will return the object if state.csrf is truthy, and if not, it’ll return state.csrf itself, and the spread syn­tax is smart enough to know not to spread null, so it spread noth­ing into variables.

Final­ly we have some helper JavaScript in gql.js (very sim­i­lar to the xhr.js above) which con­fig­ures and cre­ates an Axios instance for query­ing Craft’s GraphQL endpoint:

// Configure the GraphQL api endpoint
export const configureGqlApi = (url, token) => ({
    baseURL: url,
    headers: {
        'X-Requested-With': 'XMLHttpRequest',
        ...(token && { 'Authorization': `Bearer ${token}` }),
    }
});

// Execute a GraphQL query by sending an XHR to our api endpoint
export const executeGqlQuery = async(api, query, variables, callback) => {
    // Execute the GQL query
    try {
        const response = await api.post('', {
            query: query,
            variables: variables
        });
        if (callback && response.data.data) {
            callback(response.data.data);
        }
        // Log any errors
        if (response.data.errors) {
            console.error(response.data.errors);
        }
    } catch (error) {
        console.error(error);
    }
};

This just adds an addi­tion­al a Bear­er Token to the head­er which Craft CMS will use to link to the schema that the request will have per­mis­sion to access, and it adds some error log­ging spe­cif­ic to the GraphQL implementation.

Link A Simple Query

Let’s look at a sim­ple query that uses the infra­struc­ture we’ve described above:

export const organismsQuery =
    `
query organismsQuery($needle: String!)
{
    entries(section: "organisms", search: $needle, orderBy: "title") {
        title,
        slug,
        id
    }
}
`;

This is the same query we looked at in the GraphQL Explor­er pre­vi­ous­ly, which is exe­cut­ed as follows:

import axios from 'axios';
import { configureGqlApi, executeGqlQuery } from '../utils/gql.js';

const GRAPHQL_ENDPOINT = '/api';

// Fetch & commit the Organisms array from a GraphQL query
export const getOrganisms = async({commit, state}) => {
    const token = state.gqlToken ? state.gqlToken.token : null;
    const api = axios.create(configureGqlApi(GRAPHQL_ENDPOINT, token));
    let variables = {
        needle: 'useInSearchForm:*'
    };
    // Execute the GQL query
    await executeGqlQuery(api, organismsQuery, variables, (data) => {
        if (data.entries) {
            commit('setOrganisms', data.entries);
        }
    });
};

This is once again a Vuex Action, which uses the GraphQL token we’ve already obtained, and pass­es in useInSearchForm:* as a search para­me­ter in needle.

This is look­ing for a lightswitch field in Craft CMS with the han­dle useInSearchForm, so that it’ll only return entries from the organisms sec­tion that have that lightswitch on.

This returns data that looks like this:

[
    {
        "title": "Bacillus cereus",
        "slug": "bacillus-cereus",
        "id": "10226"
    },
    {
        "title": "Bacterial toxin",
        "slug": "bacterial-toxin",
        "id": "14472"
    },
]

Link Querying a Single Thing

Nor­mal­ly all of your GraphQL queries will return an array of data, even if you’re only ask­ing for one thing. While this con­sis­ten­cy is nice, there are times when you real­ly only ever want one thing returned.

In Craft CMS 3.4, sin­gu­lar ver­sions of all of the queries were made available:

  • assets()asset()
  • categories()category()
  • entries()entry()
  • globalSets()globalSet()
  • tags()tag()
  • users()user()

This is anal­o­gous to the .all().one() meth­ods used on Ele­ment Queries.

So we can lever­age this for our queries where we only ever want one thing returned, so we don’t have to do ugly [0] array syntax:

export const outbreakDetailQuery =
    `
query outbreakDetailQuery($slug: [String])
{
    entry(section: "outbreaks", slug: $slug) {
        title,
        url,
        slug,
        ...on outbreaks_outbreaks_Entry {
            summary,
            beginningDate,
            states,
            country,
            hasTest,
            testResults,
            productSubjectToRecall,
            totalIll,
            numberIllByCaseDefinitionKnown,
            probableIll,
            possibleIll,
            confirmedIll,
            hospitalized,
            numberHospitalized,
            anyDeaths,
            numberOfDeaths,
            recallLinks {
                col1
                url
            },
            reportReferenceLinks {
                col1
                url
            },
            brands {
                title
            },
            locations {
                title
            },
            organisms {
                title
            },
            tags {
                title
            },
            vehicles {
                title
            }
        }
    }
}
`;

This rather large query exem­pli­fies the GraphQL mind­set, which is that you need to spec­i­fy all of the data that you want returned. This is unlike Ele­ment Queries, where by default you get all of the data back.

Here’s what it looks like exe­cut­ing this query:

import axios from 'axios';
import { configureGqlApi, executeGqlQuery } from '../utils/gql.js';

const GRAPHQL_ENDPOINT = '/api';

// Fetch & commit an Outbreak detail from a GraphQL query
export const getOutbreakDetail = async({commit, state}) => {
    const token = state.gqlToken ? state.gqlToken.token : null;
    const outbreakSlug = state.outbreakSlug || null;
    const api = axios.create(configureGqlApi(GRAPHQL_ENDPOINT, token));
    let variables = {
        slug: outbreakSlug
    };
    // Execute the GQL query
    await executeGqlQuery(api, outbreakDetailQuery, variables, (data) => {
        if (data.entry) {
            commit('setOutbreakDetail', data.entry);
        }
    });
};

This code is almost exact­ly iden­ti­cal to the code we used to for getOrganisms(), except that we pass in a slug to look for in our entry() query rather than a search parameter.

Here’s what the data returned from this query looks like:

{
  "title": "2017 Multistate Outbreak of Salmonella Infantis Linked Mangoes (Suspected)",
  "url": "http://outbreakdatabase.test/outbreaks/2017-multistate-outbreak-of-salmonella-infantis-linked-mangoes-suspected",
  "slug": "2017-multistate-outbreak-of-salmonella-infantis-linked-mangoes-suspected",
  "summary": "In the summer of 2017 federal, state and local health officials investigated an outbreak of Salmonella Infantis. The suspected vehicle of transmission was mangoes. Forty eight outbreak associated cases were reported by 14 states. Reported and estimated illness onset dates ranged from June 30, 2017 to August 31, 2017. Among 42 cases with available information, there were 15 reported hospitalizations. No deaths were reported. \nThe cluster investigation was assigned CDC 1708MLJFX-1.",
  "beginningDate": "2017-06-01T07:00:00+00:00",
  "states": [
    "CA",
    "IL",
    "IN",
    "MI",
    "MN",
    "NJ",
    "NY",
    "NC",
    "OH",
    "OK",
    "TX",
    "WA",
    "WI"
  ],
  "country": "us",
  "hasTest": "yes",
  "testResults": "JFXX01.0010",
  "productSubjectToRecall": "yes",
  "totalIll": 48,
  "numberIllByCaseDefinitionKnown": "yes",
  "probableIll": null,
  "possibleIll": null,
  "confirmedIll": 48,
  "hospitalized": "yes",
  "numberHospitalized": 15,
  "anyDeaths": "",
  "numberOfDeaths": null,
  "recallLinks": [
    {
      "col1": "",
      "url": ""
    }
  ],
  "reportReferenceLinks": [
    {
      "col1": "",
      "url": ""
    }
  ],
  "brands": [],
  "locations": [
    {
      "title": "Retail"
    }
  ],
  "organisms": [
    {
      "title": "Salmonella"
    }
  ],
  "tags": [
    {
      "title": "salmonellosis"
    },
    {
      "title": "salmonella"
    },
    {
      "title": "mangoes"
    },
    {
      "title": "infantis"
    }
  ],
  "vehicles": [
    {
      "title": "Mangoes"
    }
  ]
}

Link Dynamic Parameter Queries

But what if we’re doing some­thing more com­pli­cat­ed, like a faceted search, where we want to nar­row down the search cri­te­ria based on a series of option­al search para­me­ters from the user such as shown in this video:

Pri­or to Craft CMS 3.4, you could­n’t do this via the GraphQL API. But Craft CMS 3.4 added two key fea­tures to make it possible

  • It’s now pos­si­ble to query for ele­ments by their cus­tom field val­ues via GraphQL. (#5208)
  • It’s now pos­si­ble to fil­ter ele­ment query results by their relat­ed ele­ments using rela­tion­al fields’ ele­ment query params (e.g. publisher(100) rather than relatedTo({targetElement: 100, field: 'publisher'})). (#5200)

These are both huge in terms of mak­ing the GraphQL API flex­i­ble, so hat’s off to Andris for mak­ing it happen.

This allows us to con­struct a dynam­ic query like this:

export const outbreaksQuery = (additionalParams) => {
    let queryAdditionalParams = '';
    let entryAdditionalParams = '';
    additionalParams.forEach((item) => {
        queryAdditionalParams += `, $${item.fieldName}: [QueryArgument]`;
        entryAdditionalParams += `, ${item.fieldName}: $${item.fieldName}`;
    });
    return `
    query outbreaksQuery($needle: String!${queryAdditionalParams})
    {
        entries(section: "outbreaks", search: $needle${entryAdditionalParams}) {
            title,
            url,
            slug,
            ...on outbreaks_outbreaks_Entry {
                summary,
                beginningDate,
                vehicles {
                    title
                },
                organisms {
                    title
                },
                tags {
                    title
                }
            }
        }
    }
    `;
};

Note that this is a func­tion, not a sim­ple returned tem­plate lit­er­al as in the pre­vi­ous examples.

This allows us to dynam­i­cal­ly add para­me­ters to our query based on what­ev­er has been pushed into the additionalParams array.

So the base query looks like this:

query outbreaksQuery($needle: String!)
{
    entries(section: "outbreaks", search: $needle) {
        title,
        url,
        slug,
        ...on outbreaks_outbreaks_Entry {
            summary,
            beginningDate,
            vehicles {
                title
            },
            organisms {
                title
            },
            tags {
                title
            }
        }
    }
}

…and the vari­ables we pass in look like this:

{
    needle: "vomit"
}

If the user then also adds in an organ­ism to search on (which in our case is a relat­ed entry in the Craft CMS back­end), the query will look like this:

query outbreaksQuery($needle: String!, $organisms: [QueryArgument])
{
    entries(section: "outbreaks", search: $needle, organisms: $organisms) {
        title,
        url,
        slug,
        ...on outbreaks_outbreaks_Entry {
            summary,
            beginningDate,
            vehicles {
                title
            },
            organisms {
                title
            },
            tags {
                title
            }
        }
    }
}

…and the vari­ables we pass in look like this:

{
    needle: "vomit",
    organisms: "4425"
}

The 4425 num­ber is the ele­ment ID of the relat­ed organ­ism entry in the Organ­isms sec­tion. It could also be an array of IDs, or any­thing else you’d nor­mal­ly pass in as a rela­tion­al field­’s ele­ment query params.

This tech­nique works for an arbi­trary num­ber of addi­tion­al faceted search cri­te­ria, which gives the user quite a bit of pow­er in the search form.

Here’s the Vuex Action code that imple­ments it:

import axios from 'axios';
import { configureGqlApi, executeGqlQuery } from '../utils/gql.js';

const GRAPHQL_ENDPOINT = '/api';

// Push additional params
const pushAdditionalParam = (state, dataFieldName, dbFieldName, additionalParams) => {
    let fieldValue = state.searchForm ? state.searchForm[dataFieldName] || '' : '';
    if (fieldValue.length) {
        // Special case for fields
        if (dbFieldName === 'states') {
            // As per https://docs.craftcms.com/v3/checkboxes-fields.html#querying-elements-with-checkboxes-fields
            fieldValue = `*"${fieldValue}"*`
        }
        additionalParams.push({
            fieldName: dbFieldName,
            fieldValue: fieldValue,
        });
    }
};

// Fetch & commit the Outbreaks array from a GraphQL query
export const getOutbreaks = async({commit, state}) => {
    const token = state.gqlToken ? state.gqlToken.token : null;
    const keywords = state.searchForm ? state.searchForm.keywords || '*' : '*';
    // Construct the additional parameters
    let additionalParams = [];
    for (const [key, value] of Object.entries(additionalParamFields)) {
        pushAdditionalParam(state, key, value, additionalParams);
    }
    // Configure out API endpoint
    const api = axios.create(configureGqlApi(GRAPHQL_ENDPOINT, token))
    // Construct the variables object
    let variables = {
        needle: keywords,
    };
    additionalParams.forEach((item) => {
        variables[item.fieldName] = item.fieldValue;
    });
    // Execute the GQL query
    await executeGqlQuery(api, outbreaksQuery(additionalParams), variables, (data) => {
        if (data.entries) {
            commit('setOutbreaks', data.entries);
        }
    });
};

This pushAdditionalParam() func­tion just does a lit­tle mag­ic to map from the name of a prop­er­ty in our Vue com­po­nent to the name of the field in the database.

Then it spe­cial-cas­es for the states field, which is Check­box­es field in Craft CMS. To query for an item in a Check­box­es field, you need to use the for­mat '*"foo"*'.

As per what we dis­cussed pre­vi­ous­ly, we fig­ured this out by look­ing up how to do it in an Ele­ment Query, and then using that in our GraphQL query.

Then the getOutbreaks() func­tion then just dynam­i­cal­ly builds the additionalParams array and variables object, which then is passed along to our query.

The data returned from this query looks like this:

[
  {
    "title": "1997 Botulism After Consumption of Home-Pickled Eggs, Illinois",
    "url": "http://outbreakdatabase.test/outbreaks/1997-botulism-after-consumption-of-home-pickled-eggs-illinois",
    "slug": "1997-botulism-after-consumption-of-home-pickled-eggs-illinois",
    "summary": "On November 23, 1997, a previously healthy man became nauseated, vomited, and complained of abdominal pain. During the next 2 days, he developed double vision, difficult movement of joints, and respiratory impairment. He was hospitalized and placed on mechanical ventilation. His physical examination confirmed multiple cranial nerve abnormalities, including extraocular motor palsy and diffuse flaccid paralysis. Botulism was diagnosed, and antibotulinum toxin was administered. His blood serum demonstrated the presence of type B botulinum toxin. A food history revealed no exposures to home-canned products; however, the patient had eaten pickled eggs that he had prepared seven days before his symptoms began. The patient recovered after a prolonged period of supportive care. \n\nThe pickled eggs were prepared using a recipe that consisted of hard-boiled eggs, commercially prepared beets and hot peppers, and vinegar. The intact hard-boiled eggs were peeled and punctured with toothpicks then combined with the other ingredients in a glass jar that closed with a metal screw-on lid. The mixture was stored at room temperature and occasionally was exposed to sunlight. \n\nCultures revealed Clostridium botulinum type B, and type B toxin was detected in samples of the pickled egg mixture, the pickling liquid, beets, and egg yolk. The commercially sold peppers contained no detectable toxin or Clostridium botulinum bacteria. Beets from the original commercial containers were not available. The pH of the pickling liquid was 3.5 (i.e., adequate to prevent C. botulinum germination and toxin formation, however, the pH of egg yolk, although not tested for the investigation, is normally 6.8., conducive for bacteria growth and toxin production. Inadequate acidification and lack of refrigeration enhanced the risk of bacterial contamination of this home-prepared food.",
    "beginningDate": "1997-11-01T08:00:00+00:00",
    "vehicles": [
      {
        "title": "Eggs, Pickled eggs"
      }
    ],
    "organisms": [
      {
        "title": "Botulism"
      }
    ],
    "tags": [
      {
        "title": "c.+botulinum"
      },
      {
        "title": "clostridium+botulinum"
      },
      {
        "title": "c.+bot."
      }
    ]
  }
]

Link Tapping Out

Hope­ful­ly these real-world exam­ples of using Craft CMS’s GraphQL API in head­less” CMS setups has been help­ful to you!

Craft CMS is an excel­lent choice if you want to com­bine a great con­tent author­ing expe­ri­ence on the back­end with a head­less” CMS that serves up data to a fron­tend via a GraphQL API.

If you’re a plu­g­in or cus­tom mod­ule devel­op­er, there are also some oth­er nice new fea­tures in Craft CMS 3.4 for GraphQL:

  • Plu­g­ins can now mod­i­fy the GraphQL schema via craft\gql\TypeManager::EVENT_DEFINE_GQL_TYPE_FIELDS
  • Plu­g­ins can now mod­i­fy the GraphQL per­mis­sions via craft\services\Gql::EVENT_REGISTER_GQL_PERMISSIONS

This allows for great flex­i­bil­i­ty in terms of extend­ing the exist­ing Craft CMS GraphQL API.

Hap­py querying!