Extend using Apollo Federation

Introduction

The GraphQL Commerce provides a single data graph modeling online shopping. Considering the complexity of eCommerce nowadays, it is challenging to represent several facets with a single graph. The GraphQL Commerce is ready to be extended according to the Apollo Federation principles.

As an example, retailers may be interested in combining product data - powered by the GraphQL Commerce - with additional content data: in particular, marketers may want to provide extensive articles explaining how to get the most out of that product.

The next paragraphs describe a very basic example of graph federation in action, mostly related to the use-case provided above. The example is based on the Apollo platform.

Prerequisites

Make sure that you have access to the GraphQL Commerce running in your Bloomreach Content environment.

Setup

Create a new separate folder (e.g. graphql-service-federation), open a shell/terminal session on that folder and initialize an empty NPM project:

npm init -y

A new package.json file is created with the default configurations. Let's install the required dependencies:

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

The implementing services

This basic example is composed of two implementing services:

  1. The GraphQL Commerce, exposing commerce functionalities. It should be already up and running (e.g. http://localhost:4000/graphql);
  2. The Article Service, providing extensive information (e.g. how-to, guides) regarding products.

For the sake of simplicity, let's assume that the Article Service is a GraphQL Server responsible for resolving very basic articles, containing just one _description _field. You may want to increase the complexity of this service later on, maybe connecting to an external REST API. 

Create a new index.js file and paste the following snippet:

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`);
});

The Article Service graph definition extends the Item type, already defined in the GraphQL Commerce. This new definition links the Item to the related Article: it would be possible to "navigate" from a product to the related article description, using the same GraphQL request.

The gateway

Once the implementing services are ready to be federated, you can set up the gateway. This layer is responsible for composing the individual schema in a single - federated - graph. Once the gateway is up and running, client applications can execute GraphQL operations against this new instance: it would be possible to execute GraphQL requests on the composed schema.

For simplicity, you can - properly - append the following snippet to the existing 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 });
  },
});

The gateway instantiation is straightforward, except for the buildService option. By default, an Apollo Gateway instance does not forward incoming HTTP headers to the implementing services. This behavior could be problematic for the GraphQL Commerce, since it may require the authorization and connector headers.

The Apollo Gateway provides the buildService function: if specified, it is invoked once for each implementing service. In the example above, the CommerceConnectorDataSource class forwards both the authorization and connector headers: it reads the values from the context and, if those exist, set them accordingly as part of the outgoing HTTP request. 

Additional changes

The GraphQL Commerce is a full-fledged application: next to the Apollo Server, the service provides an Access Management layer enabling client authentication/authorization. Considering that the underlying service only accepts authorized requests, it may be an option to re-expose the sign-in and sign-out endpoints at gateway level. You can - properly - append the following snippet to the existing 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`);
});

If a client triggers a sign-in request against the gateway, it is simply redirected to the related GraphQL Commerce endpoint.
Additionally, as discussed in the previous paragraph, the server application is responsible for initializing the context with the expected properties: in the example above both the authorization and connector headers are added.

Run

If everything goes well, you should have an index.js file like the following:

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`);
});

Execute the application above with the following command:

node index.js

Once the application has started, you should see the following logs:

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

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

Query example

Now you are able to execute a GraphQL request against the federated schema:

query findItemsByKeyword {
  findItemsByKeyword(text: "", offset: 0, limit: 5) {
    items {
      itemId {
        id
        code
      }
      displayName
      article {
        description
      }
    }
    total
  }
}

Do not forget to include the following HTTP headers:

​{"connector": "<CONNECTOR_ID>", "authorization": "Bearer <AUTHORIZATION_TOKEN>"}