GraphQL Subscriptions using React and Node

vishal rana
11 min readFeb 10, 2023

I have been roaming my head around to find an article that can show me how i can setup subscription in my node server and react client , but was not able to find a solution. After spending hours into it, finally found a solution to it.

There are problems with Apollo documentation, as they get you confuse with the versions and socket libraries and there is no straight forward answer to this.

In this blog, I’ll cover the integration of GRAPQL subscription using apollo client library for React and apollo server for nodeJS.

Let’s jump into it.

Image from Google

What is GRAPHQL and what are GRAPHQL Subscription?

Graphql is a query language for your API. it gives full control to the client to fetch data that is required. Let say you have been working on a web app for courses, that exposes api to interact with your server, Basically a CRUD operation that create, read, update and delete courses. The generally approach is you need to expose 4 endpoints for this.

GET for read, PUT for update, DELETE for delete and POST for creating courses. This get complex as there are not going to be 4 endpoints for your application as it get bigger and more features are being introduce.

Another problem is you need to have different api that will do same things but return different output as in some apis more detail information is required and in some apis , little or less information is required.

The solution to these problems are GRAPHQL.

It gives you a single endpoint. It only uses POST method. so complexity of maintaining those different methods are gone and it gives you much more control over your data, as you can fetch only that values that you need.

GRAPQL have something called Query, Mutations. Query is basically you get method, and mutations are basically you POST or PUT you can say.

Mutation means you are making changes/ create new resources to resources on the server. As simple as that, no need to get confused.

WHAT ARE GRAPHQL SUBSCRIPTIONS?

GraphQL subscription are way for the server to push updates to the client. If you have worked with socket , you might know that socket are bidirectional, means they can be used to send information from client to server and from server to client.

GraphQL Subscriptions are used for same. They are used to send updated from server to Client. Now, you might think that where there is a need to send updated from server to client, should it be only client to server?

The answer is YES and NO.

Client make request to server in order to fetch data and server responds with that data to the client. Let’s suppose you are making a chat app, where you want to have your messages delivered to your friend in real time. One way is your client can poll server for any new messages and show them in your app. But this increase the overhead on the server as there is always a request that been made to the server and computation is used.

So in order to fix it, websocket were introduced that push message to the client as an event and client keep listening for those events.

To understand how subscriptions work, it’s important to first understand how a typical GraphQL query works. When a client sends a query to a GraphQL server, the server will execute the query and return the requested data to the client. This is a one-time operation, and the server will not send any further updates to the client unless the client makes another request.

With subscriptions, the client can subscribe to specific events on the server, and the server will push updates to the client whenever those events occur. This allows the client to receive real-time updates from the server without the need to constantly poll the server for new data

I hope you get the idea behind GRAPHQL subscriptions. Let’s setup our server for this.

NODEJS SETUP

  • Create a folder to setup our backend code and setup node project
// create directory for our backend code
mkdir backend

// switch to that directory
cd backend

// run npm init to setup node project
npm init
  • Install dependencies
npm i --save @apollo/server express bodyParser ws graphql-subscriptions graphql-ws

@apollo/server is for setting up graphql server for our backend.
express for setting up express app.
bodyParser for parsing our request body
ws for websocket
graphql-subscriptions for registering graphQL subscription as apollo/server don’t provide this feature.
graphql-ws for regsitering our websocket server and schema.

  • Let’s setup graphql server first
    Create a file to store this code server.js.we’ll setup a simple graphQL server that will expose an endpoint and allow the user to create new blog and fetch existing blogs

server.js

const { ApolloServer } = require("@apollo/server");
const { createServer } = require("http");
const { makeExecutableSchema } = require("@graphql-tools/schema");
const { expressMiddleware } = require("@apollo/server/express4");
const bodyParser = require("body-parser");
const express = require("express");
const cors = require("cors");
const {
ApolloServerPluginDrainHttpServer,
} = require("@apollo/server/plugin/drainHttpServer");

//define port for the graphql Server
const port = 4000;

// storing blogs in local
const blogs = [];

// schema defination for our grapql server
const typeDefs = `
type Blog {
id: ID!
content: String!
author: String!
}

type Query {
getBlogs: [Blog!]
}

type Mutation {
addNewBlog(content: String!, author: String!): Blog!
}

`;

// resolver handles all the incoming request to this server
const resolvers = {
// registering a mutation
Mutation: {
// add new blog mutation to create new blog
addNewBlog(_, { content, author }) {
const blog = {
id: blogs.length + 1,
content,
author,
};
// storing blog in the local blogs array
blogs.push(blog);
return blog;
},
},
// registering Qyery
Query: {
getBlogs() {
// return all blogs
return blogs;
},
},
};

// registering our type definations and resolver for schema that we need to
// use for creating our apollo server
const schema = makeExecutableSchema({ typeDefs, resolvers });
const app = express();

// for cross origin
app.use(cors());

// creating server instance
const httpServer = createServer(app);

// creating apollo server with schema and server instance
const apolloServer = new ApolloServer({
schema,
plugins: [
// Proper shutdown for the HTTP server.
ApolloServerPluginDrainHttpServer({ httpServer }),
],
});

(async function () {
// starting the apollo server to expose endoint to client
await apolloServer.start();
app.use("/graphql", bodyParser.json(), expressMiddleware(apolloServer));
})();

httpServer.listen(port, () => {
console.log(`🚀 Query endpoint ready at http://localhost:${port}/graphql`);
});

In the above code, we have One Query and a Mutation. This will expose an endpoint, you can now start the server using

node server.js

You can open the link in the browser

http://localhost:4000/graphql

You’ll see something like this:

This is your playground , where you can test your queries, like we used POSTMAN for rest api testing.

Create a query to call my getBlogs in the resolver and return me the id, content and author in return
I have called addNewBlog mutation to create new blog by providing content and author name as dynamic variables

You can fetch only the information that you require, in the above i requested for content, author and id.

Now, we’ll setup subscription in backend

const { ApolloServer } = require("@apollo/server");
const { createServer } = require("http");
const { makeExecutableSchema } = require("@graphql-tools/schema");
const { expressMiddleware } = require("@apollo/server/express4");
const bodyParser = require("body-parser");
const express = require("express");
const cors = require("cors");
const {
ApolloServerPluginDrainHttpServer,
} = require("@apollo/server/plugin/drainHttpServer");

const { WebSocketServer } = require("ws");
const { useServer } = require("graphql-ws/lib/use/ws");
const { PubSub } = require("graphql-subscriptions");

//define port for the graphql Server
const port = 4000;

// storing blogs in local
const blogs = [];

// event that the frontend will subscribe to
const blogs_created = "NEW_BLOG_CREATED";

// setting up publish/subscriber
const pubSub = new PubSub();

/**
* Function to push new blog event
* @param {*} content , content of the blog
* @param {*} author , author of the bog
* @param {*} id , id of the blog
*/
const publishNewBlogAdded = (content, author, id) => {
pubSub.publish(blogs_created, {
newBlog: { content, author, id },
});
};

// schema defination for our grapql server
const typeDefs = `
type Blog {
id: ID!
content: String!
author: String!
}

type Query {
getBlogs: [Blog!]
}

type Mutation {
addNewBlog(content: String!, author: String!): Blog!
}

type Subscription {
newBlog: Blog!
}
`;

// resolver handles all the incoming request to this server
const resolvers = {
// registering a mutation
Mutation: {
// add new blog mutation to create new blog
addNewBlog(_, { content, author }) {
const blog = {
id: blogs.length + 1,
content,
author,
};
// storing blog in the local blogs array
blogs.push(blog);

// once a blog is being created, we need to push the event so that frontend can catch it
publishNewBlogAdded(content, author, blog.id);
return blog;
},
},
// registering Qyery
Query: {
getBlogs() {
// return all blogs
return blogs;
},
},
// setting up subscription
Subscription: {
// newBlog subscription
newBlog: {
subscribe: () => pubSub.asyncIterator([blogs_created]),
},
},
};

// registering our type definations and resolver for schema that we need to
// use for creating our apollo server
const schema = makeExecutableSchema({ typeDefs, resolvers });
const app = express();

// for cross origin
app.use(cors());

// creating server instance
const httpServer = createServer(app);

// initializing websocket server
// as subscriptions make use of websockets to communicate
const wsServer = new WebSocketServer({
server: httpServer,
path: "/graphql",
});

const wsServerCleanup = useServer({ schema }, wsServer);

// creating apollo server with schema and server instance
const apolloServer = new ApolloServer({
schema,
plugins: [
// Proper shutdown for the HTTP server.
ApolloServerPluginDrainHttpServer({ httpServer }),

// Proper shutdown for the WebSocket server.
{
async serverWillStart() {
return {
async drainServer() {
await wsServerCleanup.dispose();
},
};
},
},
],
});

(async function () {
// starting the apollo server to expose endoint to client
await apolloServer.start();
app.use("/graphql", bodyParser.json(), expressMiddleware(apolloServer));
})();

httpServer.listen(port, () => {
console.log(`🚀 Query endpoint ready at http://localhost:${port}/graphql`);
console.log(
`🚀 Subscription endpoint ready at ws://localhost:${port}/graphql`
);
});

Restart the server and go to the playground again,. now you can subscribe to the event.

Subscribing to the event
You’ll see in the bottom right that now we are listening to new events

When you try to create a new blog now, you’ll see that subscription event was captured with the changes.

We are able to capture this event because we have made changes to our addNewBlog function in the resolver. Check it.

Our subscription setup is done from backend. Now let’s move to the frontend.

React Setup

  • Create a new working directory with create-react-app
npx create-react-app subscription_demo

// change directory
cd subscription_demo
  • Install dependencies
npm i --save @apollo/client graphql-ws

@apollo/client for setting up apollo setup for using graphQL
graphql-ws for making websocket connection to be used for subscriptions

  • Setup apollo client
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import {
ApolloClient,
ApolloProvider,
HttpLink,
InMemoryCache,
split,
} from "@apollo/client";
import { createClient } from "graphql-ws";
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { getMainDefinition } from "@apollo/client/utilities";

// setting configuration for http connect for Query and Mutation
const httpLink = new HttpLink({
uri: "http://localhost:3000/graphql", //backend link, check backend console for link
});

// setting configuration for websocket connect for subscription
const wsLink = new GraphQLWsLink(
createClient({
url: "ws://localhost:3000/graphql", // backend link, check backend console for link
})
);

// The split function takes three parameters:
//
// * A function that's called for each operation to execute
// * The Link to use for an operation if the function returns a "truthy" value
// * The Link to use for an operation if the function returns a "falsy" value
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
wsLink, // web socket connection for subscriptions
httpLink // http connection for query and mutation
);

// setting up apollo client with the server http and websocket links
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(), // for in memory caching of data
});

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

we have used @apollo/client library to setup connection for our http request and graphql-ws for creating websocket client.

Now we have setup our apollo connection, let’s test out our query and mutations before we proceed with subscriptions.

As we are building a blog app, we’ll create one component,For viewing all the blogs.

Here is what my App.js looks like


import Home from "./home/home";
import { BrowserRouter, Routes, Route } from "react-router-dom";

function App() {
return (
<BrowserRouter>
<Routes>
<Route>
<Route index element={<Home />} />
</Route>
</Routes>
</BrowserRouter>
);
}

export default App;

My Folder structure looks like

- public
- src
- blog
- blog.js
- home
- home.js
- app.js
- index.js

blog.js

import { Card, CardContent, Typography } from "@mui/material";

export default function Blog({ content, sender, id }) {
return (
<div style={{ alignContent: "centre", margin: "5%", maxWidth: "200px" }}>
<Card sx={{ minWidth: 200 }}>
<CardContent>
<Typography variant="h5" component="div">
Blog Number {id}
</Typography>
<Typography sx={{ mb: 1.5 }} color="text.secondary">
Internal blogs
</Typography>
<Typography variant="body2">
{content}
<br />
by:{sender}
</Typography>
</CardContent>
{/* <CardActions>
<Button size="small">Learn More</Button>
</CardActions> */}
</Card>
</div>
);
}

home.js

import * as React from "react";
import Blog from "../blog/blog";
import { gql, useQuery, useSubscription } from "@apollo/client";

const GET_BLOGS = gql`
query GetBlogs {
getBlogs {
id
content
author
}
}
`;

const GET_LATEST_BLOG = gql`
subscription GetNewBlogs {
newBlog {
id
author
content
}
}
`;

export default function Home() {
// creating local state for blogs, so that once we get subscription event for
// new blog, we can update the state and see the latest blog on the UI
const [blogs, setBlogs] = React.useState([]);
// adding query to fetch all the blogs
const { data } = useQuery(GET_BLOGS);

React.useEffect(() => {
if (data?.getBlogs?.length > 0) {
setBlogs(data?.getBlogs);
}
}, [data]);

// Creating subscription for blogs //
useSubscription(GET_LATEST_BLOG, {
onSubscriptionData: (subscriptionData) => {
// This function will get triggered one a publish event is being initited by the server
// when new blog is being added
if (subscriptionData?.subscriptionData?.data?.newBlog) {
// we are updating the state of blogs
setBlogs([...blogs, subscriptionData?.subscriptionData?.data?.newBlog]);
}
},
});

return blogs.map(({ id, author, content }) => {
return <Blog key={id} content={content} sender={author} id={id}></Blog>;
});
}

You can start the server using

npm run start

and open http://localhost:3000 in the browser.

NOTE: Use chrome to test out this functionality as In most of the browsers like Safari and Brave they don’t allow for this unsecure communication and subscription won’t work

That is it, we have successfully integrated the subscriptions with node and react. if you are facing any issues, do let me know. you can checkout the code on github from there

backend-server

Frontend-client

--

--