Skip to main content

Extending the GraphQL schema

Although the FastStore API provides a complete GraphQL schema for ecommerce, some stores may need to access other, more specific, information.

You should not add requests to other APIs to your storefront since this may impact site performance. For those cases, it is possible to extend the FastStore API schema, adding new data to the existing queries.

In this guide you will learn how to implement this in your project. You can also view a summary of the expected behavior and a complete code example.

info

Once you have implemented the schema extension in your code, you can run a local test with a GraphQL IDE.

Before starting​

Note that even though you can add information to the FastStore API schema, you must be careful not to over-fetch data on your pages. See the best practices for fetching data on your storefront.

Step by step​

To extend the GraphQL schema of your FastStore project, there are a few steps you must follow:

  1. Prepare files.
  2. Create type definitions.
  3. Create resolvers.
  4. Get FastStore API schema.
  5. Merge executable schemas.
  6. Integrate with the GraphQL layer.

Step 1 - Prepare files​

The FastStore executable schema is exported by the src/server/index.ts file of your project. This means you must edit that same file to merge the existing schema with the one you create.

However, you have the option of creating other folders and files to organize your new type definitions and resolvers. This may be a good idea, especially if you wish to add a large number of new fields to the schema.

Import tools​

Once you decided on your file structure, you can import the tools necessary to manipulate the schemas:

import { getSchema, getTypeDefs } from '@faststore/api'
import { makeExecutableSchema, mergeSchemas } from '@graphql-tools/schema'
import { mergeTypeDefs } from '@graphql-tools/merge'
import type { Options as APIOptions } from '@faststore/api'

Step 2 - Create type definitions​

Your new type definitions set the data structure for your new fields, extended from the existing FastStore GraphQL queries and types.

See the following code example of adding a new field called customField, which is a string, to the existing type StoreProduct.

const typeDefs = `
extend type StoreProduct {
customField: String
}
`

Step 3 - Create resolvers​

Resolvers are the functions that give meaning to the data you have structured in the type definitions. This means a resolver will be executed when the corresponding piece of information is queried.

A resolver can perform an operation on an existing field, get data from the query root, or fetch data from a third party API, for example.

You can create your resolvers like in the following code example.

const resolvers = {
StoreProduct: {
customField: async (root, args, context) => {
...
// Your code goes here
...
}
}
}

Using root object fields​

It is important to note that every resolver has implicit arguments aside from what you define when writing your function. This includes the root of the type, which means your resolver has access to all information in that type.

For instance, in the example above the resolver can use whatever information is contained in the existing StoreProduct type definition. All queries also return the root object, so you can also use any data available in this object. This minimizes the need for making additional API requests in your storefront, improving site performance.

See the example below to learn how to use the FastStore API root object fields in your custom resolvers:

const resolvers = {
StoreProduct: {
measurementUnit: async (root, args, context) => {
root.measurementUnit
}
}
}

In the example above, the resolver fetches the measurementUnit field, which is not available in the native FastStore API schema, but is in the product query root.

info

See the reference documentation of available root object fields.

Step 4 - Get FastStore API schema​

To get the existing FastStore API schema, use the imported getSchema function. This function takes arguments in the form of the imported APIOptions type.

It is likely that your src/server/index.ts file already has this implemented like in the example below.

import { getSchema } from '@faststore/api'
import type { Options as APIOptions } from '@faststore/api'

...

const apiOptions: APIOptions = {
platform: storeConfig.platform as APIOptions['platform'],
account: storeConfig.api.storeId,
environment: storeConfig.api.environment as APIOptions['environment'],
hideUnavailableItems: storeConfig.api.hideUnavailableItems,
channel: storeConfig.session.channel,
locale: storeConfig.session.locale,
flags: {
enableOrderFormSync: true,
},
}

export const apiSchema = getSchema(apiOptions)

Step 5 - Merge executable schemas​

Now it is time to make an executable schema from your newly created type definitions and resolvers and then merge that with the existing FastStore API schema.

To do this, use the imported functions makeExecutableSchema and mergeSchemas like in the example below.

import { getTypeDefs } from '@faststore/api'
import { makeExecutableSchema, mergeSchemas } from '@graphql-tools/schema'
import { mergeTypeDefs } from '@graphql-tools/merge'

...

// Merging your custom type definitions with the ones from @faststore/api
const mergedTypeDefs = mergeTypeDefs([getTypeDefs(), typeDefs])

const getMergedSchemas = async () =>
mergeSchemas({
schemas: [
await apiSchema,
makeExecutableSchema({
resolvers,
typeDefs: mergedTypeDefs,
}),
],
})

// Merging schemas into a final schema
export const finalSchema = getMergedSchemas()
caution

Note that the final merged schema in the example above has a different name than the existing exported one. You must make sure that all instances in which it is used have the correct name. For instance, you can change the name of the unextended native schema to nativeApiSchema and the final one to apiSchema which is already used in the rest of the project. An example of this is in the complete example below.

Step 6 - Integrate with the GraphQL layer​

If you are already using the FastStore API with its native schema, it is likely that this is already coded on your src/server/index.ts file. The main goal at this point is to make sure you are passing the extended schema, not the native one.

The example below is based on the Base store starters which use Envelop.

const getEnvelop = async () =>
envelop({
plugins: [
useAsyncSchema(apiSchema), // Pass extended schema name here.
useExtendContext(apiContextFactory),
useMaskedErrors({ formatError }),
useGraphQlJit(),
useValidationCache(),
useParserCache(),
],
})

However, there are different ways to enrich your GraphQL execution layer, such as using Apollo instead of Envelop, each with its implementation specification. Whatever technology you decide to use, ensure you are passing the extended schema.

Complete code example​

The example below contains all code described in the sections above as it might be implemented in a single file, src/server/index.ts. It covers the consolidated code you have seen above, which are modifications and additions to the existing code, not the complete resulting file.

info

As indicated above, this example contains adjusted schema names, so as not to break anything that was already using the exported schema.

import { getSchema, getTypeDefs } from '@faststore/api'
import { makeExecutableSchema, mergeSchemas } from '@graphql-tools/schema'
import { mergeTypeDefs } from '@graphql-tools/merge'
import type { Options as APIOptions } from '@faststore/api'

...

// Creating type definitions
const typeDefs = `
extend type StoreProduct {
customField: String
}
`

// Creating resolvers
const resolvers = {
StoreProduct: {
customField: async () => {
...
// Your code goes here
...
}
}
}

// Getting existing FastStore API schema
const apiOptions: APIOptions = {
platform: storeConfig.platform as APIOptions['platform'],
account: storeConfig.api.storeId,
environment: storeConfig.api.environment as APIOptions['environment'],
hideUnavailableItems: storeConfig.api.hideUnavailableItems,
channel: storeConfig.session.channel,
locale: storeConfig.session.locale,
flags: {
enableOrderFormSync: true,
},
}

const nativeApiSchema = getSchema(apiOptions)

// Merging your custom type definitions with the ones from @faststore/api
const mergedTypeDefs = mergeTypeDefs([getTypeDefs(), typeDefs])

const getMergedSchemas = async () =>
mergeSchemas({
schemas: [
await nativeApiSchema,
makeExecutableSchema({
resolvers,
typeDefs: mergedTypeDefs,
}),
],
})

// Merging schemas into a final schema
export const apiSchema = getMergedSchemas()

...

// Integrating schema with the GraphQL execution layer
const getEnvelop = async () =>
envelop({
plugins: [
useAsyncSchema(apiSchema), // Pass extended schema name here.
useExtendContext(apiContextFactory),
useMaskedErrors({ formatError }),
useGraphQlJit(),
useValidationCache(),
useParserCache(),
],
})

...

Summary​

After completing the steps above, the FastStore GraphQL API schema of your project will be extended and you will be able to use your custom API fields in your store. See these other guides to learn how to test your schema using a GraphQL IDE and fetch API information in your storefront.

Didn't find your answers? Ask the Community. For documentation suggestions, submit your feedback.

JOIN THE COMMUNITY