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 for this tutorial include:
schema
and resolver
concepts.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:
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:
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:
1type PoliticianHonestyStat {
2 label: ENUM_STATEMENT_LABEL!
3 count: Int!
4}
5
6type Politician {
7 name: String!
8 job: String
9 state: String
10 party: String
11 createdAt: DateTime
12 updatedAt: DateTime
13 stats: [PoliticianHonestyStat!]
14}
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.
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:
1import { objectType } from "nexus";
2
3export const Post = objectType({
4 name: "Post", // <- Name of your type
5 definition(t) {
6 t.int("id"); // <- Field named `id` of type `Int`
7 t.string("title"); // <- Field named `title` of type `String`
8 t.string("body"); // <- Field named `body` of type `String`
9 t.boolean("published"); // <- Field named `published` of type `Boolean`
10 },
11});
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!
Let's get back to our app. Here are the tasks we need to go through to roll the new schema:
PoliticianHonestyStat
: This new type will encapsulate aggregated stats for each politician.Politician
object: We'll add a field to the Politician
type that lists PoliticianHonestyStat
entries.But first, how do we get hold of Nexus inside Strapi? We do so using extension
service exposed by graphql
plugin:
1const extensionService = strapi.plugin("graphql").service("extension");
2
3extensionService.use(({ nexus, strapi }: { nexus: Nexus; strapi: Strapi }) => {
4 return {
5 types: [
6 // we will return new types
7 // and extend existing types here
8 ],
9 };
10});
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:
1nexus.objectType({
2 name: "PoliticianHonestyStat",
3 definition(t) {
4 t.nonNull.field("label", {
5 type: "ENUM_STATEMENT_LABEL",
6 });
7 t.nonNull.int("count");
8 },
9}),
Next up, extending Politician
object type to include a list of our newly crafted PoliticianHonestyStat
:
1nexus.extendType({
2 type: "Politician",
3 definition(t) {
4 t.list.field("stats", {
5 type: nonNull("PoliticianHonestyStat"),
6 resolve: async (parent) => {
7 // XX: shortcut!!!
8 // let's leave this empty for now,
9 // we'll get back here in a minute
10 return [];
11 },
12 });
13 },
14}),
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:
1type PoliticianHonestyStat {
2 label: ENUM_STATEMENT_LABEL!
3 count: Int!
4}
5
6type Politician {
7 name: String!
8 job: String
9 state: String
10 party: String
11 createdAt: DateTime
12 updatedAt: DateTime
13 stats: [PoliticianHonestyStat!]
14}
We can even go ahead and run a query to test things out:
1query {
2 politicians {
3 data {
4 id
5 attributes {
6 name
7 party
8 stats {
9 label
10 count
11 }
12 }
13 }
14 }
15}
Which renders this outpout:
1{
2 "data": {
3 "politicians": {
4 "data": [
5 {
6 "id": "1",
7 "attributes": {
8 "name": "rick-perry",
9 "party": "republican",
10 "stats": []
11 }
12 },
13 {
14 "id": "2",
15 "attributes": {
16 "name": "katrina-shankland",
17 "party": "democrat",
18 "stats": []
19 }
20 },
21 {
22 "id": "3",
23 "attributes": {
24 "name": "donald-trump",
25 "party": "republican",
26 "stats": []
27 }
28 }
29 /* more stuff here*/
30 ]
31 }
32 }
33}
So this kinda works but the stats are just not there. Remember that little shortcut from above? Well, it's time to fix it:
1nexus.extendType({
2 type: "Politician",
3 definition(t) {
4 t.list.field("stats", {
5 type: nonNull("PoliticianHonestyStat"),
6 resolve: async (parent) => {
7 // XX: shortcut!!!
8 return [];
9 },
10 });
11 },
12}),
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:
1nexus.extendType({
2 type: "Politician",
3 definition(t) {
4 t.list.field("stats", {
5 type: nonNull("PoliticianHonestyStat"),
6 resolve: async (parent) => {
7 // parent points to the instance of the Politician entity
8 const { id } = parent;
9
10 return strapi.db.connection.raw(`
11 SELECT COUNT(statements.id) as "count", statements.label
12 FROM politicians
13 INNER JOIN statements_politician_links ON statements_politician_links.politician_id = politicians.id
14 INNER JOIN statements ON statements.id = statements_politician_links.statement_id
15 WHERE politicians.id = ${id}
16 GROUP BY statements_politician_links.politician_id, statements.label
17 `);
18 },
19 });
20 },
21});
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.
Let's head to index.ts
in /src
and put the whole thing together:
1import type { Strapi } from "@strapi/types";
2import type * as Nexus from "nexus";
3import { nonNull } from "nexus";
4
5type Nexus = typeof Nexus;
6
7export default {
8 /**
9 * An asynchronous register function that runs before
10 * your application is initialized.
11 *
12 * This gives you an opportunity to extend code.
13 */
14 register({ strapi }) {
15 const extensionService = strapi.plugin("graphql").service("extension");
16 extensionService.use(
17 ({ nexus, strapi }: { nexus: any; strapi: Strapi }) => {
18 return {
19 types: [
20 nexus.extendType({
21 type: "Politician",
22 definition(t) {
23 t.list.field("stats", {
24 type: nonNull("PoliticianHonestyStat"),
25 resolve: async (parent) => {
26 const { id } = parent;
27
28 return strapi.db.connection
29 .raw(`SELECT COUNT(statements.id) as "count", statements.label
30 FROM politicians
31 INNER JOIN statements_politician_links ON statements_politician_links.politician_id = politicians.id
32 INNER JOIN statements ON statements.id = statements_politician_links.statement_id
33 WHERE politicians.id = ${id}
34 GROUP BY statements_politician_links.politician_id, statements.label`);
35 },
36 });
37 },
38 }),
39 nexus.objectType({
40 name: "PoliticianHonestyStat",
41 definition(t) {
42 t.nonNull.field("label", {
43 type: "ENUM_STATEMENT_LABEL",
44 });
45 t.nonNull.int("count");
46 },
47 }),
48 ],
49 };
50 }
51 );
52 },
53
54 async bootstrap({ strapi }) {
55 // some stuff here
56 },
57};
Let's go ahead and test this using America's 45th president as an example:
1query {
2 politicians(filters: { name: { eq: "donald-trump" } }) {
3 data {
4 id
5 attributes {
6 name
7 party
8 stats {
9 label
10 count
11 }
12 }
13 }
14 }
15}
Which gives:
1{
2 "data": {
3 "politicians": {
4 "data": [
5 {
6 "id": "3",
7 "attributes": {
8 "name": "donald-trump",
9 "party": "republican",
10 "stats": [
11 {
12 "label": "barely_true",
13 "count": 63
14 },
15 {
16 "label": "half_true",
17 "count": 51
18 },
19 {
20 "label": "lie",
21 "count": 117
22 },
23 {
24 "label": "mostly_true",
25 "count": 37
26 },
27 {
28 "label": "pants_fire",
29 "count": 61
30 },
31 {
32 "label": "truth",
33 "count": 14
34 }
35 ]
36 }
37 }
38 ]
39 }
40 }
41}
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.
Philip Nuzhnyi