We have learned about GraphQL Queries and Mutations in the previous articles of this series. Queries and Mutations are request-response based and the HTTP / HTTPS protocol is used. In this case, the network connection is made and then broken for every request that the client needs to send. This is not ideal for real-time communication use cases like chat apps, online-offline status, and updating scores in real-time. For these kinds of use cases, we need to use the WebSocket protocol and GraphQL supports this with the help of GraphQL Subscriptions. In subscriptions, the connection between the client and server is made just once, after which they use a publish-subscribe model to communicate effortlessly without sending a new request for each update they want to make.
A subscription looks similar to a GraphQL query or mutation, it is preceded by the operation type subscription
. All GraphQL features (variables, named subscriptions, fragments, etc) that we saw in the Query (Link Article) and Mutation (Link Article) articles are applicable to subscriptions as well.
# Schematype UserStatus {userId: String!isOnline: Boolean!}type Subscription {userStatusUpdated: UserStatus!}# Usagesubscription getUserStatusUpdates {userStatusUpdated {userIdisOnline}}
In this example, we have a subscription getUserStatusUpdates
that returns the changes in the user’s online status. Consider this scenario, you are a workspace user and want to see the online-offline status for all members of your workspace. In this case, when you log in, your client app will trigger this subscription, the server will then keep publishing all real-time updates of all workspace members to you and your client will show you the online green dot accordingly.
Building A GraphQL Server
Let us understand how to build a very basic GraphQL API from scratch then we will expand on how to build and use subscriptions in a GraphQL API. We previously learned how to use ready queries and mutations provided by HyGraph, but writing the server-side code is altogether another thing. We will build a very basic GraphQL server to query dummy data and give us the results.
First, initialize the repository, install the packages - @apollo/server
, graphql
, and graphql-tag
, and then create a file index.js
. Add the imports and the dummy data. We will use a dummy Product variable to avoid the complexity of a database at the moment
import { ApolloServer } from '@apollo/server';import { startStandaloneServer } from '@apollo/server/standalone'import { gql } from 'graphql-tag';const products = [{id: '1',name: 'Daikin 1.5T',category: 'Air Conditioners',},{id: '2',name: 'God Of War',category: 'Gaming',}];
Schema
We need to define the schema which includes all our types and basic info about all available operations like queries, mutations, and subscriptions.
Create the GraphQL type definitions, we have a Product type, and two queries that getAllProducts
returns all products and getProductById
returns a single product by Id.
const typeDefs = gql`type Product {id: ID!name: Stringcategory: String}type Query {getProductById(id: ID!): ProductgetAllProducts: [Product]}`;
Resolvers
We need need to write our resolvers
for each operation. Resolvers are written on the server-side code and basically, they are functions that determine how to fetch or update the data for a specific query, mutation, or subscription. Resolvers will contain the actual code to make a connection with the actual data source, construct and fire the query to your data source and then get the data back and resolve it for the client. We have two queries defined in our GraphQL schema, so we need to write two resolvers for it, in our case we won’t be querying the database but the dummy variable we created earlier.
const resolvers = {Query: {getAllProducts: () => products,getProductById: (parent, args, contextValue, info) => products.find((product) => args.id === product.id)},};
Create an Apollo Server Instance and start the server
const server = new ApolloServer({typeDefs: schema,resolvers,});startStandaloneServer(server, { listen: { port: 4000 } }).then(({ url }) => {console.log(`🚀 Apollo Server listening at: ${url}`);});
Here is the complete file:
import { ApolloServer } from '@apollo/server';import { startStandaloneServer } from '@apollo/server/standalone'import { gql } from 'graphql-tag';const products = [{id: '1',name: 'Daikin 1.5T',category: 'Air Conditioners',},{id: '2',name: 'God Of War',category: 'Gaming',}];const typeDefs = gql`type Product {id: ID!name: Stringcategory: String}type Query {getProductById(id: ID!): ProductgetAllProducts: [Product]}`;const resolvers = {Query: {getAllProducts: () => products,getProductById: (parent, args, contextValue, info) => products.find((product) => args.id === product.id)},};const server = new ApolloServer({typeDefs,resolvers,});startStandaloneServer(server, { listen: { port: 4000 } }).then(({ url }) => {console.log(`🚀 Apollo Server listening at: ${url}`);});
That's it, run this file and go to the API playground running on your local machine to fire some queries.
Now we know a little more about the internal working of a GraphQL server. We understand Schema, Query, and Resolvers, and how they’re tied together.
Setting Up A Subscription Server
After having cleared the basics of creating a GraphQL server, we can now move towards building a more advanced subscription server. We will set up one subscription, wherein the server publishes changes in a user’s online-offline status in real-time.
To get started let us install some new dependencies:
graphql-subscriptions
graphql-ws
ws
@graphql-tools/schema
apollo-server-express
express
body-parser
cors
Let us create a new file named wsIndex.js
and add our imports.
import { ApolloServer } from '@apollo/server';import { expressMiddleware } from '@apollo/server/express4';import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';import { createServer } from 'http';import { makeExecutableSchema } from '@graphql-tools/schema';import { WebSocketServer } from 'ws';import { useServer } from 'graphql-ws/lib/use/ws';import { PubSub } from 'graphql-subscriptions';import { gql } from 'graphql-tag';import express from 'express';import bodyParser from 'body-parser';import cors from 'cors';async function startServer() {// main implementation goes here}startServer()
We used Apollo Server’s startStandaloneServer
in the previous example, but that does not support WebSocket and subscriptions by default. Hence, we will create an express app
, a separate WebSocket
server for subscriptions, and then we'll create an http.Server
instance that will wrap the express app and the WebSocket server and will be our main listener.
Recommended reading
Let's start writing the startServer
function piece by piece.
We will first define our schema type definitions.
const typeDefs = gql`type UserStatus {userId: String!isOnline: Boolean!}type Query {userStatus: UserStatus!}type Subscription {userStatusUpdated: UserStatus!}`;
Create a new PubSub instance for the publish-subscribe mechanism and define resolver functions for the query and subscription defined in the schema above.
const pubsub = new PubSub();const resolvers = {Query: {userStatus() {return userStatus;},},Subscription: {userStatusUpdated: {subscribe: () => pubsub.asyncIterator(['USER_STATUS_UPDATED']),},},};
Create the GraphQL schema from TypeDefs and Resolvers:
const schema = makeExecutableSchema({ typeDefs, resolvers });
Create an express app and an HTTP Server for our HTTP requests:
const app = express();const httpServer = createServer(app);
Create a Websocket Server and its cleanup to support our Websocket operations:
const wsServer = new WebSocketServer({server: httpServer,path: '/',});const serverCleanup = useServer({ schema }, wsServer);
Create an ApolloServer instance, and add plugins for the proper shutdown of HTTP and WebSocket instances respectively.
const server = new ApolloServer({schema,plugins: [ApolloServerPluginDrainHttpServer({ httpServer }),{async serverWillStart() {return {async drainServer() {await serverCleanup.dispose();},};},},],});await server.start();app.use('/', cors(), bodyParser.json(), expressMiddleware(server));
Start listening with the help of our HTTP server:
const PORT = 4000;httpServer.listen(PORT, () => {console.log(`🚀 GraphQL Server ready at http://localhost:${PORT}/`);console.log(`🚀 GraphQL WS ready at ws://localhost:${PORT}/`);});
Since we do not have anything to actually publish a change, we will mimic it with the help of a toggleUserStatus function. This function will toggle the user’s online status every 5 seconds.
let userStatus = {userId: "ceqak08y3sjkbasdfausjg",isOnline: true};function toggleUserStatus() {userStatus.isOnline = !userStatus.isOnline;pubsub.publish('USER_STATUS_UPDATED', { userStatusUpdated: userStatus });setTimeout(toggleUserStatus, 5000);}toggleUserStatus()
That’s it. Here’s the complete file wsIndex.js
.
import { ApolloServer } from '@apollo/server';import { expressMiddleware } from '@apollo/server/express4';import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';import { createServer } from 'http';import { makeExecutableSchema } from '@graphql-tools/schema';import { WebSocketServer } from 'ws';import { useServer } from 'graphql-ws/lib/use/ws';import { PubSub } from 'graphql-subscriptions';import { gql } from 'graphql-tag';import express from 'express';import bodyParser from 'body-parser';import cors from 'cors';async function startServer() {const typeDefs = gql`type UserStatus {userId: String!isOnline: Boolean!}type Query {userStatus: UserStatus!}type Subscription {userStatusUpdated: UserStatus!}`;const pubsub = new PubSub();const resolvers = {Query: {userStatus() {return userStatus;},},Subscription: {userStatusUpdated: {subscribe: () => pubsub.asyncIterator(['USER_STATUS_UPDATED']),},},};const schema = makeExecutableSchema({ typeDefs, resolvers });const app = express();const httpServer = createServer(app);const wsServer = new WebSocketServer({server: httpServer,path: '/',});const serverCleanup = useServer({ schema }, wsServer);const server = new ApolloServer({schema,plugins: [ApolloServerPluginDrainHttpServer({ httpServer }),{async serverWillStart() {return {async drainServer() {await serverCleanup.dispose();},};},},],});await server.start();app.use('/', cors(), bodyParser.json(), expressMiddleware(server));const PORT = 4000;httpServer.listen(PORT, () => {console.log(`🚀 GraphQL Server ready at http://localhost:${PORT}/`);console.log(`🚀 GraphQL WS ready at ws://localhost:${PORT}/`);});let userStatus = {userId: "ceqak08y3sjkbasdfausjg",isOnline: true};function toggleUserStatus() {userStatus.isOnline = !userStatus.isOnline;pubsub.publish('USER_STATUS_UPDATED', { userStatusUpdated: userStatus });setTimeout(toggleUserStatus, 5000);}toggleUserStatus()}startServer()
Start the servers using node wsIndex.js
and here we go!
As you can see in this video, the updates automatically get published to the API playground by the GraphQL server. Now using these subscription updates, the client can toggle the online status of the user on the frontend.
Conclusion
In conclusion, subscriptions are the way to support real-time websocket-based use cases in a GraphQL server. They are ideal where bi-directional communication is needed, use cases where latency is important and the payload size is minimal. We learned how to build a basic GraphQL server, write resolver functions to support data fetching for our query and mutations, and we also went through an in-depth advanced example that demonstrates how to set up and use GraphQL subscriptions.