Comments (9)
Hello @callmephilip, This topic idea sounds interesting, you can go ahead. :)
If you need help on anything, feel free to reach out to me here on or Discord.
from community-content.
@hanakhelifa sounds good! i'll get initial draft ready and share it here to review
from community-content.
@hanakhelifa here's the first version of the article. looking forward to your feedback
Fine-Tuning Your Strapi Experience: GraphQL API Customizations Explained
Table of Contents
- TLDR;
- Prereqs and assumptions
- So what's the problem?
- Sidebar: GraphqQL in Strapi: a look under the hood
- Customize application GraphQL schema
- Putting it all together
- Conclusion
TLDR;
In this post we will modify the GraphQL schema that Strapi generates to include a new custom type and extend the existing type to include a new field. The whole project is on Github; the main extension code is here
Prereqs and assumptions
- you have worked on a Strapi based project before
- you have used GraphQL APIs and understand terms like
schema
,resolver
- (kind of optional but helpful) you have written SQL queries before and are comfortable with
INNER JOINS
andaggregations
So what's the problem?
Let's say we are building an API for Politician Trust Meter, giving us stats on how trustworthy a given politician is based on his/her previous track record. We got our hands on LIAR dataset hosted on Hugging Face. Here's the dataset summary:
LIAR is a dataset for fake news detection with 12.8K human labeled short statements from politifact.com's API, and each statement is evaluated by a politifact.com editor for its truthfulness. 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. In each case, the labeler provides a lengthy analysis report to ground each judgment.
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 smth 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.
GraphqQL 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, so it's great that Strapi takes care of it. However, graphql
plugin does expose additional APIs allowing us to tap into the underlying Nexus machine to customize application GraphQL schema. Let's see how!
Customize application GraphQL schema
Let's get back to our app. Here are the tasks we need to go through to roll a new schema:
- define
PoliticianHonestyStat
that includes aggregated stats - extend
Politician
object to include a list of stats of typePoliticianHonestyStat
- define resolver logic to pull stats for a given 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 2 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 gives back smth like this:
{
"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 2 are pretty straightforward, as they map directly to the collections we have defined in our app. The 3rd table, statements_politician_links
, connects Politicians and Statements collection together, it has 2 fields (not counting its 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.
from community-content.
Hi @callmephilip,
I absolutely loved your article: the copy is quite proximate to the reader, with a funny tone, while providing solid technical knowledge. I especially liked the fact that you took time to doodle some skecthes–that's awesome!
To add some suggestions, I copied the text in a google doc right here: https://docs.google.com/document/d/1_8Cp_tt9tvLomO57CAv2relT5lELVFSdJK86ri8j-4Y/edit?usp=sharing (you should have access to it).
I mainly changed the introduction, to set the context a bit more using the brief you sent previously–the rest was awesome.
Let me know what you think of the suggestions, and once it's done I'd be happy to have you as a guest author on Strapi's blog and have it published once we're done
from community-content.
@hanakhelifa thank you for your kind words. gonna review your comments later today and get the new version ready 🙌
from community-content.
v2 below ⬇️ thank you for the feedback @hanakhelifa
Fine-Tuning Your Strapi Experience: GraphQL API Customizations Explained
Table of Contents
- Introduction
- Prereqs and assumptions
- So what's the problem?
- Sidebar: GraphqQL in Strapi: a look under the hood
- Customizing the GraphQL Schema in Strapi
- Putting it all together
- Conclusion
Introduction
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.
Prereqs and assumptions
- Prior experience with a Strapi-based project.
- Familiarity with GraphQL APIs, including concepts like
schema
andresolver
. - Experience with SQL queries, specifically INNER JOINS and aggregations (beneficial, but not essential).
So what's the problem?
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 smth 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 thePolitician
type that listsPoliticianHonestyStat
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 2 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 gives back smth like this:
{
"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 2 are pretty straightforward, as they map directly to the collections we have defined in our app. The 3rd table, statements_politician_links
, connects Politicians and Statements collection together, it has 2 fields (not counting its 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.
from community-content.
Looks perfect to me, thank you!
I'd say it's good to be published on Strapi's blog 🎉
from community-content.
@hanakhelifa cool! what do I need to do next then?
from community-content.
Related Issues (20)
- [SUBMIT] API data validation in Strapi HOT 13
- [SUBMIT] How to create an Educational Flashcard app with Strapi, Ionic and Vue.js HOT 3
- [SUBMIT] Understanding the Strapi Request Chain: from KOA to Modern Middleware Architecture HOT 10
- Best practice usage of Strapi’s entityService vs Query Engine vs Direct Knex queries HOT 6
- Have questions? Stop by for Open Office Hours (open to all) Mon - Friday 6AM CST and 12:30 PM CST
- [SUBMIT] How to Build a Multilingual Blog with Strapi Headless CMS, Vue.js and GraphQL HOT 1
- Test submission [SUBMIT] HOT 2
- [SUBMIT] STRAPI FOR NON-DEVELOPERS HOT 4
- [SUBMIT] Strapi Cron Jobs Best Practices HOT 1
- [ISSUE] Building an E-commerce Storefront with Strapi-Nuxt and Nuxt 3 HOT 1
- [UPDATE OUTDATED CONTENT] How to Build a Podcast app using Strapi and Nextjs HOT 1
- [SUBMIT] Introducing refine's `strapi-v4` data provider - how to quickly build a React CRUD app HOT 5
- [SUBMIT] Custom Soft Delete with Strapi and NextJs 14: Building a Recycle Bin HOT 9
- [SUBMIT] How to Build a Multilingual Blog with Strapi Headless CMS, Vue.js and GraphQL HOT 10
- [TRANSLATE] Building a Complete Food Mobile App with React Native and Strapi - (FR) HOT 1
- How to send a custom newsletter to users in Strapi[ISSUE]
- [SUBMIT]
- How to send a custom newsletter to users in Strapi [SUBMIT] HOT 7
- How to Build a chat app with React, Strapi & Firebase[SUBMIT] HOT 12
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from community-content.