Extend the brX GraphQL Service Schema

Info: The features described on this page require a standard or premium Bloomreach Content license. Contact Bloomreach for details.

Overview

The brX GraphQL Service exposes a unified data graph for online shopping. Given the complexity of modern eCommerce, a single graph often cannot represent all required business domains.

The GraphQL ecosystem provides tools to extend and aggregate schemas. You can add new operation types, directives, or relationships between schemas as needed. This enables you to evolve your API without modifying the core service.

This page describes two common approaches for schema extension and aggregation:

  • Schema Merging: Combine type definitions and resolvers from multiple local schemas into a single executable schema. Use this when you need to add commerce functionality not available out-of-the-box, such as custom Commerce Backend API extensions.
  • Apollo Federation: Compose a single data graph from multiple sub-graphs. Use this to combine product data (commerce) with content data (CMS) or other domains.

The following sections provide implementation examples for both approaches.

Prerequisites

Schema Merging

Schema merging allows you to control the unified schema definition. This example demonstrates schema merging with executor functions, enabling schema delegation for all brX GraphQL Service operations. Schema extensions are resolved using the new types and resolvers you define.

Setup

Apply these changes to the GraphQL Service sample project. In your project directory, install the required NPM dependencies:

npm install graphql apollo-server node-jose @graphql-tools/wrap @graphql-tools/merge @graphql-tools/schema node-fetch 

Define the GraphQL Service Executor

Define an executor function to delegate GraphQL operations from the merged schema to the brX GraphQL Service sub-schema. The following example assumes the brX GraphQL Service is running locally on port 4000.

const gsExecutor = async ({ document, variables, context }) => { const query = print(document); let headers = { 'Content-Type': 'application/json', 'connector': 'brsm', }; if (context) { if (context.connector) { headers = { ...headers, 'connector': context.connector, } } if (context.authorization) { headers = { ...headers, 'authorization': context.authorization, } } } const fetchResult = await fetch('http://localhost:4000/graphql', { method: 'POST', headers, body: JSON.stringify({ query, variables }), }); return fetchResult.json(); };

Define Extension Types and Resolvers

Next, define new types and resolvers. This example introduces a CustomPayment type, allowing client applications to interact with a payment extension module in the Commerce Backend. The schema includes a payments query and a makePayment mutation. The resolvers translate these requests to the Commerce Backend and map the responses to the CustomPayment type.

const createServer = async (request) => { const typeDefs = ` type CustomPayment { id: String! } type Query { payments: [CustomPayment] } input PaymentInput { moneyAmount: Float } type Mutation { makePayment(paymentInput: PaymentInput): CustomPayment } `; const resolvers = { Query: { payments: async (parent, args, context) => { const accessData = await getAccessData(context.authorization); const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessData.accessToken.access_token}`, }; const fetchResult = await fetch('<COMMERCE_BACKEND_PAYMENT_API_URL>', { method: 'GET', headers, }); const { results } = await fetchResult.json(); return results; } }, Mutation: { makePayment: async (parent, args, context) => { const accessData = await getAccessData(context.authorization); const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessData.accessToken.access_token}`, }; const fetchResult = await fetch('<COMMERCE_BACKEND_PAYMENT_API_URL>', { method: 'POST', headers, body: JSON.stringify({ "amountPlanned": { "currencyCode": ..., "centAmount": ... } }), }); return fetchResult.json(); } }, CustomPayment: { id: payment => payment.id, }, }; ... }

The resolvers use the getAccessData helper function, described in the next section.

Merge Schemas

After defining the new types and resolvers, merge them with the brX GraphQL Service schema. Use the introspectSchema function from the graphql-tools library to retrieve the existing schema, and wrapSchema to delegate operations. Once merged, create an Apollo Server instance.

const createServer = async (request) => { ... const remoteSchema = wrapSchema({ schema: await introspectSchema(gsExecutor), executor: gsExecutor, }); const paymentSchema = makeExecutableSchema({ typeDefs, resolvers }); const mergedSchema = mergeSchemas({ schemas: [remoteSchema, paymentSchema] }); return new ApolloServer({ schema: mergedSchema, context: ({ req }) => ({ authorization: req.headers.authorization, connector: req.headers.connector }) }); }

The Apollo Server context stores the authorization and connector request header values. The executor uses these values when delegating requests to the underlying service.

Implement Access Data Helper

Define the getAccessData helper to decrypt the authorization header and extract access data (such as the access token).
As described in the Access Management documentation, the brX GraphQL Service stores the access data in the authorization header. If your schema extension interacts with the same commerce backend, you can reuse the access token for those requests.

const getAccessData = async(authorization) => { const encryptedAccessData = authorization?.substring('Bearer '.length); let accessData; if (encryptedAccessData) { try { const jwk = await jose.JWK.asKeyStore(JSON.parse(process.env.JWK_KEYSTORE)); const { payload } = await jose.JWE.createDecrypt(jwk).decrypt(encryptedAccessData); return JSON.parse(payload.toString()); } catch (e) { throw new Error(`Invalid authorization: ${e}`); } } } async function startApolloServer() { const server = await createServer(); // The `listen` method launches a web server. server.listen({ port: 4100 }).then(({ url }) => { console.log(`� Server ready at ${url}`); }); } startApolloServer();

The startApolloServer function starts the Apollo Server on port 4100.

Complete Example

Your index.js file should look similar to the following. Ensure you include all required require statements.

const graphqlService = require("@bloomreach/graphql-commerce-connector-service"); const { print } = require('graphql'); const { ApolloServer } = require('apollo-server'); const jose = require('node-jose'); const { introspectSchema, wrapSchema } = require('@graphql-tools/wrap'); const { mergeSchemas } = require('@graphql-tools/merge'); const { makeExecutableSchema } = require('@graphql-tools/schema'); const fetch = require("node-fetch"); const gsExecutor = async ({ document, variables, context }) => { const query = print(document); let headers = { 'Content-Type': 'application/json', 'connector': 'brsm', }; if (context) { if (context.connector) { headers = { ...headers, 'connector': context.connector, } } if (context.authorization) { headers = { ...headers, 'authorization': context.authorization, } } } const fetchResult = await fetch('http://localhost:4000/graphql', { method: 'POST', headers, body: JSON.stringify({ query, variables }), }); return fetchResult.json(); }; const createServer = async (request) => { const typeDefs = ` type CustomPayment { id: String! } type Query { payments: [CustomPayment] } input PaymentInput { moneyAmount: Float } type Mutation { makePayment(paymentInput: PaymentInput): CustomPayment } `; const resolvers = { Query: { payments: async (parent, args, context) => { const accessData = await getAccessData(context.authorization); const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessData.accessToken.access_token}`, }; const fetchResult = await fetch('<COMMERCE_BACKEND_PAYMENT_API_URL>', { method: 'GET', headers, }); const { results } = await fetchResult.json(); return results; } }, Mutation: { makePayment: async (parent, args, context) => { const accessData = await getAccessData(context.authorization); const headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessData.accessToken.access_token}`, }; const fetchResult = await fetch('<COMMERCE_BACKEND_PAYMENT_API_URL>', { method: 'POST', headers, body: JSON.stringify({ "amountPlanned": { "currencyCode": ..., "centAmount": ... } }), }); return fetchResult.json(); } }, CustomPayment: { id: payment => payment.id, }, }; const remoteSchema = wrapSchema({ schema: await introspectSchema(gsExecutor), executor: gsExecutor, }); const paymentSchema = makeExecutableSchema({ typeDefs, resolvers }); const mergedSchema = mergeSchemas({ schemas: [remoteSchema, paymentSchema] }); return new ApolloServer({ schema: mergedSchema, context: ({ req }) => ({ authorization: req.headers.authorization, connector: req.headers.connector }) }); } const getAccessData = async(authorization) => { const encryptedAccessData = authorization?.substring('Bearer '.length); let accessData; if (encryptedAccessData) { try { const jwk = await jose.JWK.asKeyStore(JSON.parse(process.env.JWK_KEYSTORE)); const { payload } = await jose.JWE.createDecrypt(jwk).decrypt(encryptedAccessData); return JSON.parse(payload.toString()); } catch (e) { throw new Error(`Invalid authorization: ${e}`); } } } async function startApolloServer() { const server = await createServer(); // The `listen` method launches a web server. server.listen({ port: 4100 }).then(({ url }) => { console.log(`� Server ready at ${url}`); }); } startApolloServer();

Replace all placeholder values, such as <COMMERCE_BACKEND_PAYMENT_API_URL>, with the correct values for your environment.

Run the Project

Start the project locally:

node --require dotenv/config index.js

Authenticate and retrieve an authorization token:

curl --location --request POST 'http://localhost:4000/signin' \--header 'connector: <commerce_backend>' \--header 'Content-Type: application/json' \--data-raw '{}'

Verify the merged schema by sending a findItemsByKeyword query:

curl --location --request POST 'http://localhost:4100/' \
--header 'Content-Type: application/json'  --header 'connector: brsm' --header 'authorization: Bearer $TOKEN' \
--data-raw '{"query":"query{\n  findItemsByKeyword(\n    text:\"\", limit: 1, offset: 0, \n  ) {\n    items{\n      displayName\n      itemId{\n        id,\n        code\n      }\n      description\n      customAttrs {\n        name, values\n      }\n    }\n  }\n}\n","variables":{}}'

Test the makePayment mutation:

curl --location --request POST 'http://localhost:4100/' \
--header 'Content-Type: application/json'  --header 'connector: <commerce_backend>' --header 'authorization: Bearer $TOKEN' \
--data-raw '{"query":"mutation { makePayment(paymentInput: { moneyAmount: 12 }) { id } }","variables":{}}' 

Retrieve the newly created payment:

curl --location --request POST 'http://localhost:4100/' \
--header 'Content-Type: application/json'  --header 'connector: <commerce_backend>' --header 'authorization: Bearer $TOKEN' \
--data-raw '{"query":"query { payments { id } }","variables":{}}'

If you see the payment entry, the schema extension is working as expected.

Apollo Federation

The brX GraphQL Service supports extension using Apollo Federation. This section demonstrates a basic example of graph federation, combining the brX GraphQL Service with an additional Article Service. The example uses the Apollo platform.

Setup

Create a new directory (for example, graphql-service-federation). Initialize a new NPM project:

npm init -y 

Install the required dependencies:

npm install @apollo/gateway apollo-server apollo-server-express express graphql

Implementing Services

This example uses two services:

  1. brX GraphQL Service: Provides commerce functionality. It should be running at http://localhost:4000/graphql.
  2. Article Service: Provides product-related articles.

The Article Service is a simple GraphQL server that resolves articles with a single description field. You can extend this service to connect to other APIs as needed.

Create an index.js file and add the following code:

const { ApolloServer, gql } = require('apollo-server'); const { buildFederatedSchema } = require('@apollo/federation'); const typeDefs = gql` type Article { description: String! } type ItemId { id: String! code: String } extend type Item @key(fields: "itemId") { "The ItemId of a product item" itemId: ItemId! @external "Related article" article: Article } `; const resolvers = { Article: { description() { return 'Hey, this is an article description'; } }, Item: { article() { return {}; } } } const articleService = new ApolloServer({ schema: buildFederatedSchema([{ typeDefs, resolvers }]), }); articleService.listen({ port: 4001}, () => { console.log(`Article Service ready at http://localhost:4001`); });

This schema extends the Item type from the brX GraphQL Service, linking each product to a related article. You can now query both product and article data in a single request.

Configure the Gateway

After both services are running, set up the Apollo Gateway to compose the federated schema. The gateway enables clients to query across both services.

Append the following code to your index.js file:

const { ApolloGateway, RemoteGraphQLDataSource } = require('@apollo/gateway'); class CommerceConnectorDataSource extends RemoteGraphQLDataSource { willSendRequest({ request, context }) { if (context.connector) { request.http.headers.set('connector', context.connector); } if (context.authorization) { request.http.headers.set('authorization', context.authorization); } } } const gateway = new ApolloGateway({ serviceList: [ { name: 'graphql-service', url: 'http://localhost:4000/graphql' }, { name: 'article-service', url: 'http://localhost:4001' }, ], buildService({ name, url }) { return new CommerceConnectorDataSource({ url }); }, });

By default, Apollo Gateway does not forward incoming HTTP headers to implementing services. The CommerceConnectorDataSource class ensures that the authorization and connector headers are forwarded as needed.

Expose Authentication Endpoints

The brX GraphQL Service provides authentication endpoints. To support authentication through the gateway, re-expose the /signin and /signout endpoints at the gateway level.

Append the following code to your index.js file:

const express = require('express'); const { ApolloServer: ApolloServerExpress } = require('apollo-server-express'); const server = new ApolloServerExpress({ gateway, // Disable subscriptions (not currently supported with ApolloGateway) subscriptions: false, context: ({ req }) => { return { connector: req.headers.connector, authorization: req.headers.authorization, }; }, }); const app = express(); app.post('/signin', function(req, res) { res.redirect(307, 'http://localhost:4000/signin'); }); app.post('/signout', function(req, res) { res.redirect(307, 'http://localhost:4000/signout'); }); server.applyMiddleware({ app }); app.listen({ port: 4999 }, () => { console.log(`🚀 Gateway ready at http://localhost:4999`); });

Sign-in and sign-out requests sent to the gateway are redirected to the brX GraphQL Service. The server context includes the authorization and connector headers for downstream services.

Complete Example

Your index.js file should look like this:

const { ApolloServer, gql } = require('apollo-server'); const { buildFederatedSchema } = require('@apollo/federation'); const { ApolloGateway, RemoteGraphQLDataSource } = require('@apollo/gateway'); const express = require('express'); const { ApolloServer: ApolloServerExpress } = require('apollo-server-express'); const typeDefs = gql` type Article { description: String! } type ItemId { id: String! code: String } extend type Item @key(fields: "itemId") { "The ItemId of a product item" itemId: ItemId! @external "Related article" article: Article } `; const resolvers = { Article: { description() { return 'Hey, this is an article description'; } }, Item: { article() { return {}; } } } const articleService = new ApolloServer({ schema: buildFederatedSchema([{ typeDefs, resolvers }]), }); articleService.listen({ port: 4001}, () => { console.log(`Article Service ready at http://localhost:4001`); }); class CommerceConnectorDataSource extends RemoteGraphQLDataSource { willSendRequest({ request, context }) { if (context.connector) { request.http.headers.set('connector', context.connector); } if (context.authorization) { request.http.headers.set('authorization', context.authorization); } } } const gateway = new ApolloGateway({ serviceList: [ { name: 'graphql-service', url: 'http://localhost:4000/graphql' }, { name: 'article-service', url: 'http://localhost:4001' }, ], buildService({ name, url }) { return new CommerceConnectorDataSource({ url }); }, }); const server = new ApolloServerExpress({ gateway, // Disable subscriptions (not currently supported with ApolloGateway) subscriptions: false, context: ({ req }) => { return { connector: req.headers.connector, authorization: req.headers.authorization, }; }, }); const app = express(); app.post('/signin', function(req, res) { res.redirect(307, 'http://localhost:4000/signin'); }); app.post('/signout', function(req, res) { res.redirect(307, 'http://localhost:4000/signout'); }); server.applyMiddleware({ app }); app.listen({ port: 4999 }, () => { console.log(`🚀 Gateway ready at http://localhost:4999`); });

Start the application:

node index.js 

You should see output similar to:

Article Service ready at http://localhost:4001
🚀 Gateway ready at http://localhost:4999 

The GraphQL playground is available at http://localhost:4999/graphql.

Query Example

You can now execute GraphQL queries against the federated schema:

query findItemsByKeyword {
  findItemsByKeyword(text: "", offset: 0, limit: 5) {
    items {
      itemId {
        id
        code
      }
      displayName
      # ...additional fields
    }
  }
}

This query returns product data from the brX GraphQL Service. You can also query for related article data using the federated schema.


For more details on schema extension, delegation, and federation, refer to the official Apollo Federation documentation and GraphQL Tools documentation.

Share Feedback
Page: /build/commerce-backend/how-to-extend-the-graphql-service-using-apollo-federation
Section: Build
Category *
Extend the brX GraphQL Service schema | Bloomreach Content Documentation