GraphQL API Customizations Explained: Fine-Tuning Your Strapi Experience

Enhance your Strapi project with this guide on optimizing the GraphQL schema. Discover practical steps for schema customization and advanced API usage.

Strapi
Strapi
7 min readDec 20, 2023

--

When managing a growing Strapi project, you’ll likely encounter the need to customize its GraphQL API for better performance and functionality. This article delves into the intricacies of Strapi’s GraphQL schema and provides practical guidance on enhancing it.

By the end of this read, you’ll have a clearer picture of how GraphQL functions within Strapi and the steps to tailor it for specific project needs. We’ll explore adding a new custom type and expanding an existing one with additional fields in Strapi’s GraphQL schema. For hands-on insights, the entire project and the core extension code are available on GitHub.

Prerequisites

Prerequisites for this tutorial include:

  • Experience with Strapi-based projects.
  • Knowledge of GraphQL APIs, including schema and resolver concepts.
  • Understanding of SQL queries, like INNER JOINS](https://www.w3schools.com/sql/sql_join_inner.asp) and aggregations (helpful but not mandatory).

Integrating External Datasets into Strapi’s API

Imagine we’re tasked with creating an API for a ‘Politician Trust Meter’. This tool aims to quantify a politician’s trustworthiness based on their historical statements. Our primary resource is the LIAR dataset from Hugging Face, a collection designed for detecting fake news.

The LIAR dataset comprises 12.8K human-annotated statements from politifact.com. Each statement has been assessed for truthfulness by a politifact.com editor. The distribution of labels in the LIAR dataset is relatively well-balanced: except for 1,050 pants-fire cases, the instances for all other labels range from 2,063 to 2,638.

After some cosmetic merging and processing, we load the dataset into Strapi. We end up with the following database schema:

Politician trust meter database

We can now run queries to get a specific politician alongside some additional data. We can also find statements associated with a given politician. Front end folks come to us with the following sketch of the interface:

Politician card UI sketch

How can we tweak our current schema to include stats for every politician based on their statements? What if we could extend Politician object to include dynamically calculated stats without changing the underlying data structure and keep things nice and normalized. Our goal is to produce a GraphQL schema that would look like this:

type PoliticianHonestyStat {
label: ENUM_STATEMENT_LABEL!
count: Int!
}

type Politician {
name: String!
job: String
state: String
party: String
createdAt: DateTime
updatedAt: DateTime
stats: [PoliticianHonestyStat!]
}

Turns out we can do it in Strapi! But before we jump in, let’s look under the hood and see how Strapi handles GraphQL.

GraphQL in Strapi: a look under the hood

Once you add graphql plugin to your Strapi project, you ✨ automagically ✨ get all your content APIs exposed via /grapqhl endpoint - you get types, queries, mutations. Strapi does all the heavy lifting behind the scenes using GraphQL Nexus.

In GraphQL Nexus, you define your GraphQL schema in code using js/ts as opposed to using GraphQL SDL. Here's an example:

import { objectType } from "nexus";

export const Post = objectType({
name: "Post", // <- Name of your type
definition(t) {
t.int("id"); // <- Field named `id` of type `Int`
t.string("title"); // <- Field named `title` of type `String`
t.string("body"); // <- Field named `body` of type `String`
t.boolean("published"); // <- Field named `published` of type `Boolean`
},
});

As you can see, writing these schemas is a pretty tedious task. Fortunately, Strapi simplifies this process significantly. However, the real power lies in the additional APIs provided by the GraphQL plugin. These APIs grant access to the underlying Nexus framework, enabling deep customizations of the application’s GraphQL schema. Let’s see how!

Customizing the GraphQL Schema in Strapi

Let’s get back to our app. Here are the tasks we need to go through to roll the new schema:

  • Define PoliticianHonestyStat: This new type will encapsulate aggregated stats for each politician.
  • Extend the Politician object: We'll add a field to the Politician type that lists PoliticianHonestyStat entries.
  • Implement resolver logic: This logic will fetch the relevant stats for a politician from the database.

But first, how do we get hold of Nexus inside Strapi? We do so using extension service exposed by graphql plugin:

const extensionService = strapi.plugin("graphql").service("extension");

extensionService.use(({ nexus, strapi }: { nexus: Nexus; strapi: Strapi }) => {
return {
types: [
// we will return new types
// and extend existing types here
],
};
});

We will begin by declaring a new type called PoliticianHonestyStat containing 2 fields: label and count. Notice how label is typed as ENUM_STATEMENT_LABEL, which was generated by Strapi for Enum field belonging to Statement content type. Our definition looks as follows:

nexus.objectType({
name: "PoliticianHonestyStat",
definition(t) {
t.nonNull.field("label", {
type: "ENUM_STATEMENT_LABEL",
});
t.nonNull.int("count");
},
}),

Next up, extending Politician object type to include a list of our newly crafted PoliticianHonestyStat:

nexus.extendType({
type: "Politician",
definition(t) {
t.list.field("stats", {
type: nonNull("PoliticianHonestyStat"),
resolve: async (parent) => {
// XX: shortcut!!!
// let's leave this empty for now,
// we'll get back here in a minute
return [];
},
});
},
}),

If we now inspect GraphQL schema generated by our app (you can use your local GraphiQL instance at http://localhost:1337/graphql), we will be able to locate two following definitions:

type PoliticianHonestyStat {
label: ENUM_STATEMENT_LABEL!
count: Int!
}

type Politician {
name: String!
job: String
state: String
party: String
createdAt: DateTime
updatedAt: DateTime
stats: [PoliticianHonestyStat!]
}

We can even go ahead and run a query to test things out:

query {
politicians {
data {
id
attributes {
name
party
stats {
label
count
}
}
}
}
}

Which renders this outpout:

{
"data": {
"politicians": {
"data": [
{
"id": "1",
"attributes": {
"name": "rick-perry",
"party": "republican",
"stats": []
}
},
{
"id": "2",
"attributes": {
"name": "katrina-shankland",
"party": "democrat",
"stats": []
}
},
{
"id": "3",
"attributes": {
"name": "donald-trump",
"party": "republican",
"stats": []
}
}
/* more stuff here*/
]
}
}
}

So this kinda works but the stats are just not there. Remember that little shortcut from above? Well, it’s time to fix it:

nexus.extendType({
type: "Politician",
definition(t) {
t.list.field("stats", {
type: nonNull("PoliticianHonestyStat"),
resolve: async (parent) => {
// XX: shortcut!!!
return [];
},
});
},
}),

There are at least a couple of ways to handle this. We could use Strapi’s entity service API to pull stats for a given politician and then do some math adding things up, OR we could leverage Strapi’s raw database handle and write some sweet sweet SQL to count things for us:

nexus.extendType({
type: "Politician",
definition(t) {
t.list.field("stats", {
type: nonNull("PoliticianHonestyStat"),
resolve: async (parent) => {
// parent points to the instance of the Politician entity
const { id } = parent;

return strapi.db.connection.raw(`
SELECT COUNT(statements.id) as "count", statements.label
FROM politicians
INNER JOIN statements_politician_links ON statements_politician_links.politician_id = politicians.id
INNER JOIN statements ON statements.id = statements_politician_links.statement_id
WHERE politicians.id = ${id}
GROUP BY statements_politician_links.politician_id, statements.label
`);
},
});
},
});

This little bit of SQL exposes some other Strapi’s internals around database schema. Without getting into too many details that are beyond the scope of this article, let’s take a quick look at what is happening.

You can look at the database for your local app by opening data.db inside .tmp directory in the root of the project using your favorite SQLite client

There are 3 tables of interest for us here: politicians, statements and statements_politician_links. The first two are straightforward: they map directly to the collections we have defined in our app. The third table, statements_politician_links, connects Politicians and Statements collection together, it has two fields (excluding the primary key ID); one of them points to the politician table while the other one points to statement telling us which statement belongs to which politician.

Given this schema and some INNER JOIN and GROUP BY kung fu, we are able to pull all statements for a given politician, group them by label and then count how many statements per label we have.

Putting it all together

Let’s head to index.ts in /src and put the whole thing together:

import type { Strapi } from "@strapi/types";
import type * as Nexus from "nexus";
import { nonNull } from "nexus";

type Nexus = typeof Nexus;

export default {
/**
* An asynchronous register function that runs before
* your application is initialized.
*
* This gives you an opportunity to extend code.
*/
register({ strapi }) {
const extensionService = strapi.plugin("graphql").service("extension");
extensionService.use(
({ nexus, strapi }: { nexus: any; strapi: Strapi }) => {
return {
types: [
nexus.extendType({
type: "Politician",
definition(t) {
t.list.field("stats", {
type: nonNull("PoliticianHonestyStat"),
resolve: async (parent) => {
const { id } = parent;

return strapi.db.connection
.raw(`SELECT COUNT(statements.id) as "count", statements.label
FROM politicians
INNER JOIN statements_politician_links ON statements_politician_links.politician_id = politicians.id
INNER JOIN statements ON statements.id = statements_politician_links.statement_id
WHERE politicians.id = ${id}
GROUP BY statements_politician_links.politician_id, statements.label`);
},
});
},
}),
nexus.objectType({
name: "PoliticianHonestyStat",
definition(t) {
t.nonNull.field("label", {
type: "ENUM_STATEMENT_LABEL",
});
t.nonNull.int("count");
},
}),
],
};
}
);
},

async bootstrap({ strapi }) {
// some stuff here
},
};

Let’s go ahead and test this using America’s 45th president as an example:

query {
politicians(filters: { name: { eq: "donald-trump" } }) {
data {
id
attributes {
name
party
stats {
label
count
}
}
}
}
}

Which gives:

{
"data": {
"politicians": {
"data": [
{
"id": "3",
"attributes": {
"name": "donald-trump",
"party": "republican",
"stats": [
{
"label": "barely_true",
"count": 63
},
{
"label": "half_true",
"count": 51
},
{
"label": "lie",
"count": 117
},
{
"label": "mostly_true",
"count": 37
},
{
"label": "pants_fire",
"count": 61
},
{
"label": "truth",
"count": 14
}
]
}
}
]
}
}
}

Conclusion

In this post, we presented a use case for extending existing GraphQL schema that you might encounter in your Strapi projects. We discovered how Strapi integrates GraphQL in its stack and how we can work with it to modify data layout to suit our use case. You can find the codebase for the project on Github and take it for a spin locally.

--

--

Strapi
Strapi

The open source Headless CMS Front-End Developers love.