Extend the GraphQL schema
This guide explains how to customize the GraphQL schema to add your own domain objects
When developing an e-commerce store, you might at some point need to expose new data in your unified GraphQL schema to support new features and allow frontend developers to use them. Front-Commerce’s GraphQL modules is the mechanism allowing to extend and override any part of the schema defined by other modules.
The Front-Commerce core and platform integrations (such as Magento2) are implemented as GraphQL modules. They leverage features from the GraphQL Schema Definition Language (SDL).
This page will guide you through the process of exposing a new feature in your GraphQL schema. We will create an extension with a GraphQL module that allows to maintain a counter of clicks for a product.
Create an extension
To extend the GraphQL schema and implement your own logic, you first have to
create an extension. An extension can be created by adding a new folder under
extensions
and by creating a index.ts
file (the extension definition) with
the following content:
import { defineExtension } from "@front-commerce/core";
export default function clickCounter() {
return defineExtension({
name: "click-counter",
meta: import.meta,
});
}
Then you need to register the extension into your Front-Commerce project.
Add a GraphQL module within the extension
For now, the click-counter
extension does not provide any feature. To extend
the GraphQL schema and implement our own logic, we need to add a GraphQL module
to the extension containing a schema and some resolvers.
Add an empty GraphQL module
For that, you can create the GraphQL module definition in the file
example-extensions/click-counter/graphql/index.ts
with the following content:
import { createGraphQLModule } from "@front-commerce/core/graphql";
export default createGraphQLModule({
namespace: "ClickCounter",
});
This is a minimal GraphQL module definition which namespace is ClickCounter
and that does nothing for now.
Then, the extension needs to declare that it provides a GraphQL module by modifying the extension's index.ts:
import { defineExtension } from "@front-commerce/core";
import clickCounter from "./example-extensions/click-counter/graphql";
export default function clickCounter() {
return defineExtension({
name: "click-counter",
configuration: {
providers: [],
},
graphql: {
modules: [clickCounter],
},
});
}
At this point, your Front-Commerce project is configured with an extension
called click-counter
, this extension provides a GraphQL module which namespace
is Clickcounter
. The extension is now ready to bring changes to your Graph.
Extend the Graph
Front-Commerce lets you describe your schema using the expressive GraphQL Schema Definition Language (SDL).
We would like to:
- add a
clickCounter
field to the existingProduct
type - add an
incrementProductCounter
mutation
For that, you can add the type definitions as a typeDefs
value in your GraphQL
module definition.
import { createGraphQLModule } from "@front-commerce/core/graphql";
export default createGraphQLModule({
namespace: "ClickCounter",
typeDefs: /** GraphQL */ `
extend type Product {
clickCounter: Int
}
extend type Mutation {
incrementProductCounter(sku: String!, incrementValue: Int): MutationSuccess!
}
`,
});
The Product
type is part of Front-Commerce’s Front-Commerce/Product
module.
The MutationSuccess
type, used as the response for the mutation, is part of
Front-Commerce’s core.
At this point, you should be able to see in the GraphQL playground
http://localhost:4000/graphql
that the type Product
has a clickCounter
field and that a mutation incrementProductCounter
exists.
You can try to execute the query below in your GraphQL playground
http://localhost:4000/graphql
:
{
product(sku: "WH09") {
sku
clickCounter
}
}
Our GraphQL module does not yet have any code for sending content. This means
that even if you can request the clickCounter
field, it won't actually return
any data. So you should see the following response:
{
"data": {
"product": {
"sku": "WH09",
"clickCounter": null
}
}
}
Implement the logic
The latest step is to implement the logic, which we will refer to as the
GraphqlRuntime
, to retrieve the Product.clickCounter
value and behind the
incrementProductCounter
mutation. For that, you have to add resolvers for the
field and the mutation. This is where most of the real work is done, for
instance by fetching remote data sources or transforming data.
Resolvers are exposed using the resolvers
key of the GraphQL runtime
definition. It should be a
Resolver map: an object
where each key is a GraphQL type name, and values are mapping between field
names and resolver function. Resolver functions may return a
Promise
for asynchronous operations.
To learn more about resolvers and their internals, we recommend reading GraphQL Tools resolvers documentation.
First, let's update the module definition to register the runtime API:
import { createGraphQLModule } from "@front-commerce/core/graphql";
export default createGraphQLModule({
namespace: "ClickCounter",
loadRuntime: () => import("./runtime"), // as this only executes in during runtime, it needs to be an import statement.
});
And then let's create the GraphqlRuntime so that we can add the resolvers
map
as below:
import { createGraphQLRuntime } from "@front-commerce/core/graphql";
export default createGraphQLRuntime({
resolvers: {
Product: {
clickCounter: (product) => {
console.log(`click count retrieval for product ${product.sku}`);
// your own retrieval logic here
return 1;
},
},
Mutation: {
incrementProductCounter: (_root, params) => {
const { sku, incrementValue } = params;
console.log(`incrementing counter for product "${sku}"`);
console.log(`counter + ${incrementValue ?? 1}`);
// your own increment logic here
return {
success: true,
};
},
},
},
});
It brings a very basic implementation for both the Product.clickCounter
field
resolution and the incrementProductCounter
mutation. After restarting
Front-Commerce, the clickCounter
field of a product should now return 1
instead of null
. Likewise, the mutation can also be called from the
playground.
As stated above, a resolver function can also return a Promise and thus handle asynchronous result. A more real life resolver map would like look like:
export default createGraphQLRuntime({
resolvers: {
Product: {
clickCounter: (product) => {
const clickCountPromise = fetchClickCountForSku(product.sku);
return clickCountPromise;
},
},
Mutation: {
incrementProductCounter: (_root, params) => {
const { sku, incrementValue } = params;
try {
await incrementProductCount(sku, incrementValue);
return {
success: true,
};
} catch (error) {
return {
success: false,
errorMessage: error.message,
};
}
},
},
},
});