GraphQL plugin
By default Strapi createREST endpoints for each of your content-types. The GraphQL plugin adds a GraphQL endpoint to fetch and mutate your content. With the GraphQL plugin installed, you can use the Apollo Server-based GraphQL Sandbox to interactively build your queries and mutations and read documentation tailored to your content types.
Configured through both admin panel and server code, with different sets of options.
@strapi/plugin-graphql


Installation
To install the GraphQL plugin, run the following command in your terminal:
- Yarn
- NPM
yarnadd @strapi/plugin-graphql
npminstall @strapi/plugin-graphql
Once installed, the GraphQL sandbox is accessible at the/graphql
URL and can be used to interactively build your queries and mutations and read documentation tailored to your content-types.
Once the plugin is installed, theGraphQL Sandbox is accessible at the/graphql
route (e.g.,localhost:1337/graphql) when your Strapi application server is running.
Configuration
Most configuration options for the Documentation plugin are handled via your Strapi project's code, though the GraphQL playground also offers some non-specific Strapi settings.
Admin panel settings
The Strapi admin panel does not provide Strapi-specific settings for the GraphQL plugin. However, the GraphQL Playground accessible at the/graphql
route is an embedded Apollo Server playground, so it includes all configuration and settings available with such an instance. Please refer to the officialGraphQL playground documentation for details.
Code-based configuration
Plugins configuration are defined intheconfig/plugins.js
file. This configuration file can include agraphql.config
object to define specific configurations for the GraphQL plugin.
Available options
Apollo Server options can be passed directly to Apollo with thegraphql.config.apolloServer
configuration object. Apollo Server options can be used for instance to enable thetracing feature, which is supported by the GraphQL Sandbox to track the response time of each part of your query. TheApollo Server
default cache option iscache: 'bounded'
. You can change it in theapolloServer
configuration. For more information visitApollo Server Docs.
The GraphQL plugin has the following specific configuration options that should be declared in agraphql.config
object within theconfig/plugins
file. All parameters are optional:
Option | Type | Description | Default Value | Notes |
---|---|---|---|---|
endpoint | String | Sets the GraphQL endpoint path. | '/graphql' | Example:/custom-graphql |
shadowCRUD | Boolean | Enables or disables automatic schema generation for content types. | true | |
depthLimit | Number | Limits the depth of GraphQL queries to prevent excessive nesting. | 10 | Use this to mitigate potential DoS attacks. |
amountLimit | Number | Limits the maximum number of items returned in a single response. | 100 | Use cautiously to avoid performance issues. |
playgroundAlways | Boolean | [Deprecated] Enables GraphQL Playground in all environments (deprecated). | false | Prefer usinglandingPage instead. |
landingPage | Boolean | Function | Enables or disables the landing page for GraphQL. Accepts a boolean or a function returning a boolean or an ApolloServerPlugin implementingrenderLandingPage . | false in production,true in other environments | |
apolloServer | Object | Passes configuration options directly to Apollo Server. | {} | Example:{ tracing: true } |
The maximum number of items returned by the response is limited to 100 by default. This value can be changed using theamountLimit
configuration option, but should only be changed after careful consideration: a large query can cause a DDoS (Distributed Denial of Service) and may cause abnormal load on your Strapi server, as well as your database server.
The GraphQL Sandbox is enabled by default in all environments except production. Set thelandingPage
configuration option totrue
to also enable the GraphQL Sandbox in production environments.
The following is an example custom configuration:
- JavaScript
- TypeScript
module.exports={
graphql:{
config:{
endpoint:'/graphql',
shadowCRUD:true,
landingPage:false,// disable Sandbox everywhere
depthLimit:7,
amountLimit:100,
apolloServer:{
tracing:false,
},
},
},
};
exportdefault()=>({
graphql:{
config:{
endpoint:'/graphql',
shadowCRUD:true,
landingPage:false,// disable Sandbox everywhere
depthLimit:7,
amountLimit:100,
apolloServer:{
tracing:false,
},
},
},
})
Dynamically enable Apollo Sandbox
You can use a function to dynamically enable Apollo Sandbox depending on the environment:
- JavaScript
- TypeScript
module.exports=({ env})=>{
graphql:{
config:{
endpoint:'/graphql',
shadowCRUD:true,
landingPage:(strapi)=>{
if(env("NODE_ENV")!=="production"){
returntrue;
}else{
returnfalse;
}
},
},
},
};
exportdefault({ env})=>{
graphql:{
config:{
endpoint:'/graphql',
shadowCRUD:true,
landingPage:(strapi)=>{
if(env("NODE_ENV")!=="production"){
returntrue;
}else{
returnfalse;
}
},
},
},
};
CORS exceptions for Landing Page
If the landing page is enabled in production environments (which is not recommended), CORS headers for the Apollo Server landing page must be added manually.
To add them globally, you can merge the following into your middleware configuration:
{
name:"strapi::security",
config:{
contentSecurityPolicy:{
useDefaults:true,
directives:{
"connect-src":["'self'","https:","apollo-server-landing-page.cdn.apollographql.com"],
"img-src":["'self'","data:","blob:","apollo-server-landing-page.cdn.apollographql.com"],
"script-src":["'self'","'unsafe-inline'","apollo-server-landing-page.cdn.apollographql.com"],
"style-src":["'self'","'unsafe-inline'","apollo-server-landing-page.cdn.apollographql.com"],
"frame-src":["sandbox.embed.apollographql.com"]
}
}
}
}
To add these exceptions only for the/graphql
path (recommended), you can create a new middleware to handle it. For example:
- JavaScript
- TypeScript
module.exports=(config,{ strapi})=>{
returnasync(ctx, next)=>{
if(ctx.request.path==='/graphql'){
ctx.set('Content-Security-Policy',"default-src 'self'; script-src 'self' 'unsafe-inline' cdn.jsdelivr.net apollo-server-landing-page.cdn.apollographql.com; connect-src 'self' https:; img-src 'self' data: blob: apollo-server-landing-page.cdn.apollographql.com; media-src 'self' data: blob: apollo-server-landing-page.cdn.apollographql.com; frame-src sandbox.embed.apollographql.com; manifest-src apollo-server-landing-page.cdn.apollographql.com;");
}
awaitnext();
};
};
exportdefault(config,{ strapi})=>{
returnasync(ctx, next)=>{
if(ctx.request.path==='/graphql'){
ctx.set('Content-Security-Policy',"default-src 'self'; script-src 'self' 'unsafe-inline' cdn.jsdelivr.net apollo-server-landing-page.cdn.apollographql.com; connect-src 'self' https:; img-src 'self' data: blob: apollo-server-landing-page.cdn.apollographql.com; media-src 'self' data: blob: apollo-server-landing-page.cdn.apollographql.com; frame-src sandbox.embed.apollographql.com; manifest-src apollo-server-landing-page.cdn.apollographql.com;");
}
awaitnext();
};
};
Shadow CRUD
To simplify and automate the build of the GraphQL schema, we introduced the Shadow CRUD feature. It automatically generates the type definitions, queries, mutations and resolvers based on your models.
Example:
If you've generated an API calledDocument
usingthe interactivestrapi generate
CLI or the administration panel, your model looks like this:
{
"kind":"collectionType",
"collectionName":"documents",
"info":{
"singularName":"document",
"pluralName":"documents",
"displayName":"document",
"name":"document"
},
"options":{
"draftAndPublish":true
},
"pluginOptions":{},
"attributes":{
"name":{
"type":"string"
},
"description":{
"type":"richtext"
},
"locked":{
"type":"boolean"
}
}
}
Generated GraphQL type and queries
# Document's Type definition
inputDocumentFiltersInput{
name:StringFilterInput
description:StringFilterInput
locked:BooleanFilterInput
createdAt:DateTimeFilterInput
updatedAt:DateTimeFilterInput
publishedAt:DateTimeFilterInput
and:[DocumentFiltersInput]
or:[DocumentFiltersInput]
not:DocumentFiltersInput
}
inputDocumentInput{
name:String
description:String
locked:Boolean
createdAt:DateTime
updatedAt:DateTime
publishedAt:DateTime
}
typeDocument{
name:String
description:String
locked:Boolean
createdAt:DateTime
updatedAt:DateTime
publishedAt:DateTime
}
typeDocumentEntity{
id:ID
attributes:Document
}
typeDocumentEntityResponse{
data:DocumentEntity
}
typeDocumentEntityResponseCollection{
data:[DocumentEntity!]!
meta:ResponseCollectionMeta!
}
typeDocumentRelationResponseCollection{
data:[DocumentEntity!]!
}
# Queries to retrieve one or multiple restaurants.
typeQuery{
document(id:ID):DocumentEntityResponse
documents(
filters:DocumentFiltersInput
pagination:PaginationArg={}
sort:[String]=[]
publicationState:PublicationState=LIVE
):DocumentEntityResponseCollection
}
# Mutations to create, update or delete a restaurant.
typeMutation{
createDocument(data:DocumentInput!):DocumentEntityResponse
updateDocument(id:ID!,data:DocumentInput!):DocumentEntityResponse
deleteDocument(id:ID!):DocumentEntityResponse
}
Customization
Strapi provides a programmatic API to customize GraphQL, which allows:
- disabling some operations for theShadow CRUD
- using getters to return information about allowed operations
- registering and using an
extension
object toextend the existing schema (e.g. extend types or define custom resolvers, policies and middlewares)
Example of GraphQL customizations
- JavaScript
- TypeScript
module.exports={
/**
* 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.shadowCRUD('api::restaurant.restaurant').disable();
extensionService.shadowCRUD('api::category.category').disableQueries();
extensionService.shadowCRUD('api::address.address').disableMutations();
extensionService.shadowCRUD('api::document.document').field('locked').disable();
extensionService.shadowCRUD('api::like.like').disableActions(['create','update','delete']);
constextension=({ nexus})=>({
// Nexus
types:[
nexus.objectType({
name:'Book',
definition(t){
t.string('title');
},
}),
],
plugins:[
nexus.plugin({
name:'MyPlugin',
onAfterBuild(schema){
console.log(schema);
},
}),
],
// GraphQL SDL
typeDefs:`
type Article {
name: String
}
`,
resolvers:{
Query:{
address:{
resolve(){
return{value:{city:'Montpellier'}};
},
},
},
},
resolversConfig:{
'Query.address':{
auth:false,
},
},
});
extensionService.use(extension);
},
};
exportdefault{
/**
* 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.shadowCRUD('api::restaurant.restaurant').disable();
extensionService.shadowCRUD('api::category.category').disableQueries();
extensionService.shadowCRUD('api::address.address').disableMutations();
extensionService.shadowCRUD('api::document.document').field('locked').disable();
extensionService.shadowCRUD('api::like.like').disableActions(['create','update','delete']);
constextension=({ nexus})=>({
// Nexus
types:[
nexus.objectType({
name:'Book',
definition(t){
t.string('title');
},
}),
],
plugins:[
nexus.plugin({
name:'MyPlugin',
onAfterBuild(schema){
console.log(schema);
},
}),
],
// GraphQL SDL
typeDefs:`
type Article {
name: String
}
`,
resolvers:{
Query:{
address:{
resolve(){
return{ value:{ city:'Montpellier'}};
},
},
},
},
resolversConfig:{
'Query.address':{
auth:false,
},
},
});
extensionService.use(extension);
},
};
Disabling operations in the Shadow CRUD
Theextension
service provided with the GraphQL plugin exposes functions that can be used to disable operations on Content-Types:
Content-type function | Description | Argument type | Possible argument values |
---|---|---|---|
disable() | Fully disable the Content-Type | - | - |
disableQueries() | Only disable queries for the Content-Type | - | - |
disableMutations() | Only disable mutations for the Content-Type | - | - |
disableAction() | Disable a specific action for the Content-Type | String | One value from the list:
|
disableActions() | Disable specific actions for the Content-Type | Array of Strings | Multiple values from the list:
|
Actions can also be disabled at the field level, with the following functions:
Field function | Description |
---|---|
disable() | Fully disable the field |
disableOutput() | Disable the output on a field |
disableInput() | Disable the input on a field |
disableFilters() | Disable filters input on a field |
Examples:
// Disable the 'find' operation on the 'restaurant' content-type in the 'restaurant' API
strapi
.plugin('graphql')
.service('extension')
.shadowCRUD('api::restaurant.restaurant')
.disableAction('find')
// Disable the 'name' field on the 'document' content-type in the 'document' API
strapi
.plugin('graphql')
.service('extension')
.shadowCRUD('api::document.document')
.field('name')
.disable()
Using getters
The following getters can be used to retrieve information about operations allowed on content-types:
Content-type getter | Description | Argument type | Possible argument values |
---|---|---|---|
isEnabled() | Returns whether a content-type is enabled | - | - |
isDisabled() | Returns whether a content-type is disabled | - | - |
areQueriesEnabled() | Returns whether queries are enabled on a content-type | - | - |
areQueriesDisabled() | Returns whether queries are disabled on a content-type | - | - |
areMutationsEnabled() | Returns whether mutations are enabled on a content-type | - | - |
areMutationsDisabled() | Returns whether mutations are disabled on a content-type | - | - |
isActionEnabled(action) | Returns whether the passedaction is enabled on a content-type | String | One value from the list:
|
isActionDisabled(action) | Returns whether the passedaction is disabled on a content-type | String | One value from the list:
|
The following getters can be used to retrieve information about operations allowed on fields:
Field getter | Description |
---|---|
isEnabled() | Returns whether a field is enabled |
isDisabled() | Returns whether a field is disabled |
hasInputEnabled() | Returns whether a field has input enabled |
hasOutputEnabled() | Returns whether a field has output enabled |
hasFiltersEnabled() | Returns whether a field has filtering enabled |
Extending the schema
The schema generated by the Content API can be extended by registering an extension.
This extension, defined either as an object or a function returning an object, will be used by theuse()
function exposed by theextension
service provided with the GraphQL plugin.
The object describing the extension accepts the following parameters:
Parameter | Type | Description |
---|---|---|
types | Array | Allows extending the schema types usingNexus-based type definitions |
typeDefs | String | Allows extending the schema types usingGraphQL SDL |
plugins | Array | Allows extending the schema using Nexusplugins |
resolvers | Object | Defines custom resolvers |
resolversConfig | Object | Definesconfiguration options for the resolvers, such asauthorization,policies andmiddlewares |
Thetypes
andplugins
parameters are based onNexus. To use them, register the extension as a function that takesnexus
as a parameter:
Example:
- JavaScript
- TypeScript
module.exports={
register({ strapi}){
constextension=({ nexus})=>({
types:[
nexus.objectType({
…
}),
],
plugins:[
nexus.plugin({
…
})
]
})
strapi.plugin('graphql').service('extension').use(extension)
}
}
exportdefault{
register({ strapi}){
constextension=({ nexus})=>({
types:[
nexus.objectType({
…
}),
],
plugins:[
nexus.plugin({
…
})
]
})
strapi.plugin('graphql').service('extension').use(extension)
}
}
Custom configuration for resolvers
A resolver is a GraphQL query or mutation handler (i.e. a function, or a collection of functions, that generate(s) a response for a GraphQL query or mutation). Each field has a default resolver.
Whenextending the GraphQL schema, theresolversConfig
key can be used to define a custom configuration for a resolver, which can include:
- authorization configuration with the
auth
key - policies with the
policies
key - andmiddlewares with the
middlewares
key
Authorization configuration
By default, the authorization of a GraphQL request is handled by the registered authorization strategy that can be eitherAPI token or through theUsers & Permissions plugin. The Users & Permissions plugin offers a more granular control.
Authorization with the Users & Permissions plugin
With the Users & Permissions plugin, a GraphQL request is allowed if the appropriate permissions are given.
For instance, if a 'Category' content-type exists and is queried through GraphQL with theQuery.categories
handler, the request is allowed if the appropriatefind
permission for the 'Categories' content-type is given.
To query a single category, which is done with theQuery.category
handler, the request is allowed if the thefindOne
permission is given.
Please refer to the user guide on how todefine permissions with the Users & Permissions plugin.
To change how the authorization is configured, use the resolver configuration defined atresolversConfig.[MyResolverName]
. The authorization can be configured:
- either with
auth: false
to fully bypass the authorization system and allow all requests, - or with a
scope
attribute that accepts an array of strings to define the permissions required to authorize the request.
Examples of authorization configuration
- JavaScript
- TypeScript
module.exports={
register({ strapi}){
const extensionService= strapi.plugin('graphql').service('extension');
extensionService.use({
resolversConfig:{
'Query.categories':{
/**
* Querying the Categories content-type
* bypasses the authorization system.
*/
auth:false
},
'Query.restaurants':{
/**
* Querying the Restaurants content-type
* requires the find permission
* on the 'Address' content-type
* of the 'Address' API
*/
auth:{
scope:['api::address.address.find']
}
},
}
})
}
}
exportdefault{
register({ strapi}){
const extensionService= strapi.plugin('graphql').service('extension');
extensionService.use({
resolversConfig:{
'Query.categories':{
/**
* Querying the Categories content-type
* bypasses the authorization system.
*/
auth:false
},
'Query.restaurants':{
/**
* Querying the Restaurants content-type
* requires the find permission
* on the 'Address' content-type
* of the 'Address' API
*/
auth:{
scope:['api::address.address.find']
}
},
}
})
}
}
Policies
Policies can be applied to a GraphQL resolver through theresolversConfig.[MyResolverName].policies
key.
Thepolicies
key is an array accepting a list of policies, each item in this list being either a reference to an already registered policy or an implementation that is passed directly (seepolicies configuration documentation).
Policies directly implemented inresolversConfig
are functions that take acontext
object and thestrapi
instance as arguments.Thecontext
object gives access to:
- the
parent
,args
,context
andinfo
arguments of the GraphQL resolver, - Koa'scontext with
context.http
andstate withcontext.state
.
Example of GraphQL policies applied to resolvers
- JavaScript
- TypeScript
module.exports={
register({ strapi}){
const extensionService= strapi.plugin('graphql').service('extension');
extensionService.use({
resolversConfig:{
'Query.categories':{
policies:[
(context,{ strapi})=>{
console.log('hello', context.parent)
/**
* If 'categories' have a parent, the function returns true,
* so the request won't be blocked by the policy.
*/
return context.parent!==undefined;
}
/**
* Uses a policy already created in Strapi.
*/
"api::model.policy-name",
/**
* Uses a policy already created in Strapi with a custom configuration
*/
{name:"api::model.policy-name",config:{/* all config values I want to pass to the strapi policy */}},
],
auth:false,
},
}
})
}
}
exportdefault{
register({ strapi}){
const extensionService= strapi.plugin('graphql').service('extension');
extensionService.use({
resolversConfig:{
'Query.categories':{
policies:[
(context,{ strapi})=>{
console.log('hello', context.parent)
/**
* If 'categories' have a parent, the function returns true,
* so the request won't be blocked by the policy.
*/
return context.parent!==undefined;
}
/**
* Uses a policy already created in Strapi.
*/
"api::model.policy-name",
/**
* Uses a policy already created in Strapi with a custom configuration
*/
{name:"api::model.policy-name", config:{/* all the configuration values to pass to the strapi policy */}},
],
auth:false,
},
}
})
}
}
Middlewares
Middlewares can be applied to a GraphQL resolver through theresolversConfig.[MyResolverName].middlewares
key. The only difference between the GraphQL and REST implementations is that theconfig
key becomesoptions
.
Themiddlewares
key is an array accepting a list of middlewares, each item in this list being either a reference to an already registered middleware or an implementation that is passed directly (seemiddlewares configuration documentation).
Middlewares directly implemented inresolversConfig
can take the GraphQL resolver's`parent`, `args`, `context` and `info` objects as arguments.
Middlewares with GraphQL can even act on nested resolvers, which offer a more granular control than with REST.
Examples of GraphQL middlewares applied to a resolver
- JavaScript
- TypeScript
module.exports={
register({ strapi}){
const extensionService= strapi.plugin('graphql').service('extension');
extensionService.use({
resolversConfig:{
'Query.categories':{
middlewares:[
/**
* Basic middleware example #1
* Log resolving time in console
*/
async(next, parent, args, context, info)=>{
console.time('Resolving categories');
// call the next resolver
const res=awaitnext(parent, args, context, info);
console.timeEnd('Resolving categories');
return res;
},
/**
* Basic middleware example #2
* Enable server-side shared caching
*/
async(next, parent, args, context, info)=>{
info.cacheControl.setCacheHint({maxAge:60,scope:"PUBLIC"});
returnnext(parent, args, context, info);
},
/**
* Basic middleware example #3
* change the 'name' attribute of parent with id 1 to 'foobar'
*/
(resolve, parent,...rest)=>{
if(parent.id===1){
returnresolve({...parent,name:'foobar'},...rest);
}
returnresolve(parent,...rest);
}
/**
* Basic middleware example #4
* Uses a middleware already created in Strapi.
*/
"api::model.middleware-name",
/**
* Basic middleware example #5
* Uses a middleware already created in Strapi with a custom configuration
*/
{name:"api::model.middleware-name",options:{/* all config values I want to pass to the strapi middleware */}},
],
auth:false,
},
}
})
}
}
exportdefault{
register({ strapi}){
const extensionService= strapi.plugin('graphql').service('extension');
extensionService.use({
resolversConfig:{
'Query.categories':{
middlewares:[
/**
* Basic middleware example #1
* Log resolving time in console
*/
async(next, parent, args, context, info)=>{
console.time('Resolving categories');
// call the next resolver
const res=awaitnext(parent, args, context, info);
console.timeEnd('Resolving categories');
return res;
},
/**
* Basic middleware example #2
* Enable server-side shared caching
*/
async(next, parent, args, context, info)=>{
info.cacheControl.setCacheHint({ maxAge:60, scope:"PUBLIC"});
returnnext(parent, args, context, info);
},
/**
* Basic middleware example #3
* change the 'name' attribute of parent with id 1 to 'foobar'
*/
(resolve, parent,...rest)=>{
if(parent.id===1){
returnresolve({...parent, name:'foobar'},...rest);
}
returnresolve(parent,...rest);
}
/**
* Basic middleware example #4
* Uses a middleware already created in Strapi.
*/
"api::model.middleware-name",
/**
* Basic middleware example #5
* Uses a middleware already created in Strapi with a custom configuration
*/
{name:"api::model.middleware-name", options:{/* all the configuration values to pass to the middleware */}},
],
auth:false,
},
}
})
}
}
Security
GraphQL is a query language allowing users to use a broader panel of inputs than traditional REST APIs. GraphQL APIs are inherently prone to security risks, such as credential leakage and denial of service attacks, that can be reduced by taking appropriate precautions.
Disable introspection and Sandbox in production
In production environments, disabling the GraphQL Sandbox and the introspection query is strongly recommended.If you haven't edited theconfiguration file, it is already disabled in production by default.
Limit max depth and complexity
A malicious user could send a query with a very high depth, which could overload your server. Use thedepthLimit
configuration parameter to limit the maximum number of nested fields that can be queried in a single request. By default,depthLimit
is set to 10 but can be set to a higher value during testing and development.
To increase GraphQL security even further, 3rd-party tools can be used. See the guide aboutusing GraphQL Armor with Strapi on the forum.
Usage
The GraphQL plugin adds a GraphQL endpoint accessible and provides access to a GraphQL playground, accessing at the/graphql
route of the Strapi admin panel, to interactively build your queries and mutations and read documentation tailored to your content types. For detailed instructions on how to use the GraphQL Playground, please refer to the officialApollo Server documentation.
Usage with the Users & Permissions feature
TheUsers & Permissions feature allows protecting the API with a full authentication process.
Registration
Usually you need to sign up or register before being recognized as a user then perform authorized requests.
mutation{
register(input:{username:"username",email:"email",password:"password"}){
jwt
user{
username
email
}
}
}
You should see a new user is created in theUsers
collection type in your Strapi admin panel.
Authentication
To perform authorized requests, you must first get a JWT:
mutation{
login(input:{identifier:"email",password:"password"}){
jwt
}
}
Then on each request, send along anAuthorization
header in the form of{ "Authorization": "Bearer YOUR_JWT_GOES_HERE" }
. This can be set in the HTTP Headers section of your GraphQL Sandbox.
Usage with API tokens
To use API tokens for authentication, pass the token in theAuthorization
header using the formatBearer your-api-token
.
Using API tokens in the the GraphQL Sandbox requires adding the authorization header with your token in theHTTP HEADERS
tab:
{
"Authorization" : "Bearer <TOKEN>"
}
Replace<TOKEN>
with your API token generated in the Strapi Admin panel.
GraphQL API
The GraphQL plugin adds a GraphQL endpoint that can accessed through Strapi's GraphQL API: