Skip to main content
Version: 3.x

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:

example-extensions/click-counter/index.ts
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:

example-extensions/click-counter/graphql/index.ts
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:

Changes to example-extensions/click-counter/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:

  1. add a clickCounter field to the existing Product type
  2. add an incrementProductCounter mutation

For that, you can add the type definitions as a typeDefs value in your GraphQL module definition.

example-extensions/click-counter/graphql/index.ts
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!
}
`,
});
info

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:

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.

note

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:

Changes to example-extensions/click-counter/graphql/index.ts
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:

example-extensions/click-counter/graphql/runtime.ts
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:

example-extensions/click-counter/graphql/runtime.ts
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,
};
}
},
},
},
});