Leveling Up My GraphQL Skills: Real Time Subscriptions

John VesterJohn Vester
9 min read

For a few years now, I’ve tried to identify frameworks, products, and services that allow technologists to maintain their focus on extending the value of their intellectual property. This continues to be a wonderful journey for me, filled with unique learning opportunities.

The engineer in me recently wondered if there was a situation where I could find a secondary benefit for an existing concept that I’ve talked about before. In other words, could I identify another benefit with the same level of impact as the original parent solution previously recognized?

For this article, I wanted to dive deeper into GraphQL to see what I could find.

In my “When It’s Time to Give REST a Rest” article, I talked about how there are real-world scenarios when GraphQL is preferable to a RESTful service. We walked through how to build and deploy a GraphQL API using Apollo Server.

In this follow-up post, I plan to level up my knowledge of GraphQL by walking through subscriptions for real-time data retrieval. We’ll also build a WebSocket service to consume the subscriptions.

Recap: Customer 360 Use Case

My prior article centered around a Customer 360 use case, where patrons of my fictional business maintain the following data collections:

  • Customer information

  • Address information

  • Contact methods

  • Credit attributes

A huge win in using GraphQL is that a single GraphQL request can retrieve all the necessary data for a customer’s token (unique identity).

type Query {
    addresses: [Address]
    address(customer_token: String): Address
    contacts: [Contact]
    contact(customer_token: String): Contact
    customers: [Customer]
    customer(token: String): Customer
    credits: [Credit]
    credit(customer_token: String): Credit
}

Using a RESTful approach to retrieve the single (360) view of the customer would have required multiple requests and responses to be stitched together. GraphQL gives us a solution that performs much better.

Level Up Goals

In order to level up at any aspect of life, one has to achieve new goals. For my own goals here, this means:

  • Understanding and implementing the subscriptions value proposition within GraphQL.

  • Using a WebSocket implementation to consume a GraphQL subscription.

The idea of using subscriptions over queries and mutations within GraphQL is the preferred method when the following conditions are met:

  • Small, incremental changes to large objects

  • Low-latency, real-time updates (such as a chat application)

This is important, since implementing subscriptions inside GraphQL isn’t trivial. Not only will the underlying server need to be updated, but the consuming application will require some redesign as well.

Fortunately, the use case we’re pursuing with our Customer 360 example is a great fit for subscriptions. Also, we’ll be implementing a WebSocket approach to leveraging those subscriptions.

Like before, I’ll continue using Apollo going forward.

Leveling Up with Subscriptions Creds

First, we need to install the necessary libraries to support subscriptions with my Apollo GraphQL server:

npm install ws
npm install graphql-ws @graphql-tools/schema
npm install graphql-subscriptions

With those items installed, I focused on updating the index.ts from my original repository to extend the typedefs constant with the following:

type Subscription {
    creditUpdated: Credit
}

I also established a constant to house a new PubSub instance and created a sample subscription that we will use later:

const pubsub = new PubSub();

pubsub.publish('CREDIT_BALANCE_UPDATED', {
    creditUpdated: {
    }
});

I cleaned up the existing resolvers and added a new Subscription for this new use case:

const resolvers = {
    Query: {
        addresses: () => addresses,
        address: (parent, args) => {
            const customer_token = args.customer_token;
            return addresses.find(address => address.customer_token === customer_token);
        },
        contacts: () => contacts,
        contact: (parent, args) => {
            const customer_token = args.customer_token;
            return contacts.find(contact => contact.customer_token === customer_token);
        },
        customers: () => customers,
        customer: (parent, args) => {
            const token = args.token;
            return customers.find(customer => customer.token === token);
        },
        credits: () => credits,
        credit: (parent, args) => {
            const customer_token = args.customer_token;
            return credits.find(credit => credit.customer_token === customer_token);
        }
    },
    Subscription: {
        creditUpdated: {
            subscribe: () => pubsub.asyncIterator(['CREDIT_BALANCE_UPDATED']),
        }
    }
};

I then refactored the server configuration and introduced the subscription design:

const app = express();
const httpServer = createServer(app);
const wsServer = new WebSocketServer({
    server: httpServer,
    path: '/graphql'
});

const schema = makeExecutableSchema({ typeDefs, resolvers });
const serverCleanup = useServer({ schema }, wsServer);

const server = new ApolloServer({
    schema,
    plugins: [
        ApolloServerPluginDrainHttpServer({ httpServer }),
        {
            async serverWillStart() {
                return {
                    async drainServer() {
                        serverCleanup.dispose();
                    }
                };
            }
        }
    ],
});

await server.start();

app.use('/graphql', cors(), express.json(), expressMiddleware(server, {
    context: async () => ({ pubsub })
}));

const PORT = Number.parseInt(process.env.PORT) || 4000;
httpServer.listen(PORT, () => {
    console.log(`Server is now running on http://localhost:${PORT}/graphql`);
    console.log(`Subscription is now running on ws://localhost:${PORT}/graphql`);
});

To simulate customer-driven updates, I created the following method to increase the credit balance by $50 every five seconds while the service is running. Once the balance reaches (or exceeds) the credit limit of $10,000, I reset the balance back to $2,500, simulating a balance payment being made.

function incrementCreditBalance() {
    if (credits[0].balance >= credits[0].credit_limit) {
        credits[0].balance = 0.00;
        console.log(`Credit balance reset to ${credits[0].balance}`);

    } else {
        credits[0].balance += 50.00;
        console.log(`Credit balance updated to ${credits[0].balance}`);
    }

    pubsub.publish('CREDIT_BALANCE_UPDATED', { creditUpdated: credits[0] });
    setTimeout(incrementCreditBalance, 5000);
}

incrementCreditBalance();

The full index.ts file can be found here.

Deploy to Heroku

With the service ready, it’s time for us to deploy the service so we can interact with it. Since Heroku worked out great last time (and it’s easy for me to use), let’s stick with that approach.

To get started, I needed to run the following Heroku CLI commands:

$ heroku login
$ heroku create jvc-graphql-server-sub

Creating ⬢ jvc-graphql-server-sub... done
https://jvc-graphql-server-sub-1ec2e6406a82.herokuapp.com/ | https://git.heroku.com/jvc-graphql-server-sub.git

The command also automatically added the repository used by Heroku as a remote:

$ git remote
heroku
origin

Like I noted in my prior article, Apollo Server disables Apollo Explorer in production environments. To keep Apollo Explorer available for our needs, I needed to set the NODE_ENV environment variable to development. I set that with the following CLI command:

$ heroku config:set NODE_ENV=development

Setting NODE_ENV and restarting ⬢ jvc-graphql-server-sub... done, v3
NODE_ENV: development

I was ready to deploy my code to Heroku:

$ git commit --allow-empty -m 'Deploy to Heroku'
$ git push heroku

A quick view of the Heroku Dashboard showed my Apollo Server running without any issues:

In the Settings section, I found the Heroku app URL for this service instance:

https://jvc-graphql-server-sub-1ec2e6406a82.herokuapp.com/

Please note – this link will no longer be in service by the time this article is published.

For the time being, I could append graphql to this URL to launch Apollo Server Studio. This let me see the subscriptions working as expected:

Notice the Subscription responses on the right-hand size of the screen.

Leveling Up with WebSocket Skillz

We can leverage WebSocket support and Heroku’s capabilities to create an implementation that consumes the subscription we’ve created.

In my case, I created an index.js file with the following contents. Basically, this created a WebSocket client and also established a dummy HTTP service that I could use to validate the client was running:

import { createClient } from "graphql-ws";
import { WebSocket } from "ws";
import http from "http";

// Create a dummy HTTP server to bind to Heroku's $PORT
const PORT = process.env.PORT || 3000;
http.createServer((req, res) => res.end('Server is running')).listen(PORT, () => {
  console.log(`HTTP server running on port ${PORT}`);
});

const host_url = process.env.GRAPHQL_SUBSCRIPTION_HOST || 'ws://localhost:4000/graphql';

const client = createClient({
  url: host_url,
  webSocketImpl: WebSocket
});

const query = `subscription {
  creditUpdated {
    token
    customer_token
    credit_limit
    balance
    credit_score
  }
}`;

function handleCreditUpdated(data) {
  console.log('Received credit update:', data);
}

// Subscribe to the creditUpdated subscription
client.subscribe(
  {
    query,
  },
  {
    next: (data) => handleCreditUpdated(data.data.creditUpdated),
    error: (err) => console.error('Subscription error:', err),
    complete: () => console.log('Subscription complete'),
  }
);

The full index.js file can be found here.

We can deploy this simple Node.js application to Heroku too, making sure to set the GRAPHQL_SUBSCRIPTION_HOST environment variable to the Heroku app URL we used earlier.

I also created the following Procfile to tell Heroku how to start up my app:

web: node src/index.js

Next I created a new Heroku app:

$ heroku create jvc-websocket-example

Creating ⬢ jvc-websocket-example... done
https://jvc-websocket-example-62824c0b1df4.herokuapp.com/ | https://git.heroku.com/jvc-websocket-example.git

Then, I set the the GRAPHQL_SUBSCRIPTION_HOST environment variable to point to my running GraphQL server:

$ heroku --app jvc-websocket-example \
    config:set \
GRAPHQL_SUBSCRIPTION_HOST=ws://jvc-graphql-server-sub-1ec2e6406a82.herokuapp.com/graphql

At this point, we are ready to deploy our code to Heroku:

$ git commit --allow-empty -m 'Deploy to Heroku'
$ git push heroku

Once the WebSocket client starts, we can see its status in the Heroku Dashboard:

By viewing the logs within the Heroku Dashboard for jvc-websocket-example instance, we can see the multiple updates to the balance property of the jvc-graphql-server-sub service. In my demo, I was even able to capture the use case where the balance was reduced to zero, simulating that a payment was made:

In the terminal, we can access those same logs with the CLI command heroku logs.

2024-08-28T12:14:48.463846+00:00 app[web.1]: Received credit update: {
2024-08-28T12:14:48.463874+00:00 app[web.1]:   token: 'credit-token-1',
2024-08-28T12:14:48.463875+00:00 app[web.1]:   customer_token: 'customer-token-1',
2024-08-28T12:14:48.463875+00:00 app[web.1]:   credit_limit: 10000,
2024-08-28T12:14:48.463875+00:00 app[web.1]:   balance: 9950,
2024-08-28T12:14:48.463876+00:00 app[web.1]:   credit_score: 750
2024-08-28T12:14:48.463876+00:00 app[web.1]: }

Not only do we have a GraphQL service with a subscription implementation running, but we now have a WebSocket client consuming those updates.

Conclusion

My readers may recall my personal mission statement, which I feel can apply to any IT professional:

“Focus your time on delivering features/functionality that extends the value of your intellectual property. Leverage frameworks, products, and services for everything else.”

— J. Vester

In this deep-dive into GraphQL subscriptions, we’ve successfully consumed updates from an Apollo Server running on Heroku by using another service also running on Heroku—a Node.js-based application that uses WebSockets. By leveraging lightweight subscriptions, we avoided sending queries for unchanging data, but simply subscribed to receive credit balance updates as they occurred.

In the introduction, I mentioned looking for an additional value principle inside a topic I’ve written about before. GraphQL subscriptions is an excellent example of what I had in mind because it allows consumers to receive updates immediately, without needing to make queries against the source data. This will make consumers of the Customer 360 data very excited, knowing that they can live updates as they happen.

Heroku is another example that continues to adhere to my mission statement by offering a platform that enables me to quickly prototype solutions using a CLI and standard Git commands. This not only gives me an easy way to showcase my subscriptions use case but to implement a consumer using WebSockets too.

If you’re interested in the source code for this article, check out my repositories on GitLab:

I feel confident when I say that I’ve successfully leveled up my GraphQL skills with this effort. This journey was new and challenging for me … and also a lot of fun!

I plan to dive into authentication next, which hopefully provides another opportunity to level up with GraphQL and Apollo Server. Stay tuned!

Have a really great day!

0
Subscribe to my newsletter

Read articles from John Vester directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

John Vester
John Vester

Information Technology professional with 30+ years expertise in application design and architecture, feature development, project management, system administration and team supervision. Currently focusing on enterprise architecture/application design utilizing object-oriented programming languages and frameworks.