Leveling Up My GraphQL Skills: Real-Time Subscriptions
Dive a little deeper to explore real-time data subscriptions by using GraphQL to automatically receive server-side updates with help from a WebSocket consumer.
Join the DZone community and get the full member experience.
Join For FreeFor 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 in 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
As 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 side 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 are 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 receive 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!
Opinions expressed by DZone contributors are their own.
Comments