How to Add Custom GraphQL Query to Strapi V4
Have you ever had difficulties with setting up a new query, defining new types, or re-using already existing types from other content-types in Strapi V4? Did you feel confused about the available customizations when using GraphQL? Let us help you and show how we solved this in one of our projects!
While Strapi's own documentation is good for adding GraphQL support, it also confusing what customizations are available when somebody wants to use GraphQL. As our project is using Strapi v4, we had to learn how to create such queries, compared to v3.
The first example under the official GraphQL documentation's Customization section is a showcase of the capabilities without providing much context. That example - next to some ShadowCRUD settings - creates two object types (Book and Article), a custom Nexus plugin, overrides a query resolver for an existing content type (address) and disables authentication for the same content type (Query.address).
For me, it was hard to see how to set up a new query, define new types and how to re-use already existing types from other content-types. As our project is using Strapi v4, we had to learn how to create such queries, compared to v3.
Eventually, we could explore the documentation, the Strapi codebase and the examples of official plugins (like users and permissions plugin) and conclude a way to effectively add GraphQL customizations.
This post is showing the results of our findings and aims to help the developers who need to achieve similar goals with Strapi v4.
The domain for the example
Let's assume that we are developing an e-commerce site where we sell products. One day we would like to add a feature to track popularity of each product using a third party solution. To keep the API keys secret at the server side, we are planning to define a new GraphQL query to the existing GraphQL schema provided by Strapi.
This query will handle the integration to the 3rd party API and optionally return the product data if requested.
Initialize the project
For the sake of simplicity, we are setting up a new Strapi project for this example using the official guide, thus using npx from command line:
npx create-strapi-app@latest graphql-example --quickstart
After creating the initial administration account, we can use another terminal window to proceed.
Define the Product content type
In order to add a Product
content type with an API and a single attribute called name
, you can use the command line interface or the started admin web interface.
I will use the CLI so I can include its output.
npm run strapi generate content-type
? Content type display name Product ? Content type singular name product ? Content type plural name products ? Please choose the model type Collection Type ? Use draft and publish? No ? Do you want to add attributes? Yes ? Name of attribute name ? What type of attribute string ? Do you want to add another attribute? No ? Where do you want to add this model? Add model to new API ? Name of the new API? product ? Bootstrap API related files? Yes ✔ ++ /api/product/content-types/product/schema.json ✔ +- /api/product/content-types/product/schema.json ✔ ++ /api/product/controllers/product.js ✔ ++ /api/product/services/product.js ✔ ++ /api/product/routes/product.js
Add GraphQL plugin
The Strapi quick start project does not contain the GraphQL plugin, so we have to install it. You can follow the plugin's documentation, however, it is just as easy that running this command:
npm run strapi install graphql
When your Strapi application restarts, you can access the GraphQL playground at http://localhost:1337/graphql
Let's check the Product query which was created for the content type with the following query:
query Products { products { data { id attributes { name createdAt updatedAt } } } }
For the first try it fails because there is no permission set up for unauthenticated users.
After adding the find
and findOne
permissions of Product
content type to the Public
role in the admin Settings
/ Users & Permissions Plugin
/ Roles
/ Public
location in the admin web interface, the above query should return empty list.
If you would like to see some results, then add a new product manually or (after providing the create
permission for the Public
role) with this GraphQL mutation:
mutation CreateProduct { createProduct(data: { name: "The product" }) { data { id } } }
Prepare to add custom GraphQL query
We can add customizations to the GraphQL plugin in Strapi's register
lifecycle hook, which is in ./src/index.js
.
In a reasonably large project this file can contain lots of code, so to make it obvious where GraphQL related code is defined, we will create another file at ./src/graphql.js
. This will be responsible to collect the custom GraphQL queries and mutations and add them to Strapi.
After our changes the files look like this.
./src/index.js
'use strict'; const graphql = require('./graphql') module.exports = { register({ strapi }) { graphql(strapi) // externalize all graphql related code to ./src/graphql.js }, bootstrap(/*{ strapi }*/) {}, }
./src/graphql.js
'use strict'; const noop = (strapi) => ({nexus}) => ({}) const extensions = [noop] module.exports = (strapi) => { const extensionService = strapi.plugin('graphql').service('extension') for (const extension of extensions) { extensionService.use(extension(strapi)) } }
Later we will replace the noop extension with the popularity
extension, which will also be similar to noop
, ie: it will be a function taking strapi
as parameter, returns another function with a parameter which has a nexus
field and returns the extension definition.
This complicated function is helping in the long run, as strapi
is provided by our code, but nexus
is provided by the GraphQL plugin's extension
service. Both can be useful in our extension code to reach other Strapi services, plugins and use nexus api.
Use Nexus API
It is time to define our popularity
extension. I will copy the noop
extension into the .src/api/popularity/graphql.js
file and include this non-working definition to the extensions.
./src/api/popularity/graphql.js
'use strict'; module.exports = (strapi) => ({nexus}) => ({})
./src/graphql.js
'use strict'; const popularity = require('./api/popularity/graphql') const extensions = [popularity] module.exports = (strapi) => { const extensionService = strapi.plugin('graphql').service('extension') for (const extension of extensions) { extensionService.use(extension(strapi)) } }
As you can see I have chosen to import the new module and add it to the extensions list. The drawback is that whenever we add another GraphQL query, we have to manually add it to the extension list, otherwise it won't be recognized.
Let's continue with the actual extension, fill in the popularity
GraphQL definition. Our first approach is to use nexus api.
.src/api/popularity/graphql.js
'use strict'; module.exports = (strapi) => ({nexus}) => ({ types: [ nexus.extendType({ type: 'Query', // extending Query definition(t) { t.field('popularity', { // with this field type: 'PopularityResponse', // which has this custom type (defined below) args: { product: nexus.nonNull('ID') // and accepts a product id as argument }, resolve: async (parent, args, context) => ({ // and resolves to the 3rd party popularity service logic stars: Math.floor(Math.random() * 5) + 1, // a simple mocked response product: args.product }) }) } }), nexus.objectType({ name: 'PopularityResponse', // this is our custom object type definition(t) { t.nonNull.int('stars') // with a result of the 3rd party popularity service response t.field('product', { // and if requested, a field to query the product content type type: 'ProductEntityResponse', resolve: async (parent, args) => ({ // where we provide the resolver as Strapi does not know about relations of our new PopularityResponse type value: await strapi.entityService.findOne('api::product.product', parent.product, args) }) // but relations, components and dynamic zones of the Product will be handled by built-in resolvers }) } }), ], resolversConfig: { 'Query.popularity': { auth: { scope: ['api::product.product.findOne'] // we give permission to use the new query for those who has findOne permission of Product content type } } } })
I have included a mocked response for the 3rd party call, and let the reader elaborate on how to continue the implementation regarding the integration with any 3rd parties or custom logic.
Now you can check the results in the GraphQL Playground using the below query.
query GetPopularity { popularity(product: "1") { stars product { data { id attributes { name } } } } }
This gives the result:
{ "data": { "popularity": { "stars": 3, "product": { "data": { "id": "1", "attributes": { "name": "The product" } } } } } }
So our implementation works as we expect.
For us the hardest problem was to link the custom response type (PopularityResponse
) and the Product content type's resolvers. The final trick was to provide a resolver for the immediate relations (product
field in the example), and return the result in a format what Strapi's graphql plugin accepts (an object with value
field). This information came from the graphql plugin source code, which implies that using this trick is considered a dependency on graphql plugin internals and can break during upgrades.
Use GraphQL Schema
Since Strapi v4.0.8, the ProductResponseEntity
can be referenced from GraphQL SDL type definitions in Strapi. If you are interested, here is the same definition, but using GraphQL schema and avoiding nexus API.
.src/api/popularity/graphql.js
'use strict'; module.exports = (strapi) => ({nexus}) => ({ typeDefs: ` type PopularityResponse { stars: Int! product: ProductEntityResponse } extend type Query { popularity(product: ID!): PopularityResponse } `, resolvers: { Query: { popularity: { resolve: async (parent, args, context) => ({ stars: Math.floor(Math.random() * 5) + 1, product: args.product }) } }, PopularityResponse: { product: { resolve: async (parent, args) => ({ value: await strapi.entityService.findOne('api::product.product', parent.product, args) }) } } }, resolversConfig: { 'Query.popularity': { auth: { scope: ['api::product.product.findOne'] } } } })
Personally, I prefer the GraphQL SDL variant as it is more compact, and it is familiar to most developers. In case we make a mistake in the SDL definition, it is failing at starting up Strapi, which is usually the same when we use Nexus API.
For both cases the resolversConfig
can add configuration for authorization, middlewares and policies. While it is possible to override existing queries and mutation's resolversConfig
, you should keep in mind that the active resolversConfig
would be result of merging Strapi's default resolvers configuration and your settings. This makes the resolvers' configuration overrides dependent on the original settings and the merging process (which both can change with Strapi upgrades).
Conclusions
As we can see, it is not that hard to add customized queries to Strapi. However, there are major differences between Strapi v3 and v4 regarding GraphQL.
I hope the above example saves you the time we have spent to find out the ways to implement GraphQL queries in Strapi v4.