Real time communication between a web app and a GraphQL API

Nov 10, 2021

Real time communication between a client and a server is a feature I find really cool. The idea of a client and a server communicating bilaterally is fascinating to me: the effort of keeping all clients up to date without polling an API endpoint unnecessarily.

While I find it fascinating, you might find it just "interesting". But I believe we agree that setting up such an architecture can be cumbersome. This blog post will aim to show you how easy you can set it up with your existing GraphQL API.

The example GraphQL API I'll show you is written in Typescript because it's my preference. It should work just as well with other stacks.

We'll be using Fastify, a Node.js framework to build a web server; and Mercurius, a GraphQL adapter for Fastify. With these tools we'll build a server to provide our client with a list of Movies with title and year. 

We'll start by defining our Movie type and its list query:

const schema: string = gql`
  type Movie {
    title: String!
    year: Int!
  }
  
  type Query {
    getMovies: [Movie!]!
  }
`;


For the list query we need to set our resolver. A resolver function is the function that generates the result for a GraphQL query, i.e. the code that needs to be executed to fulfil a given query. This is where we would fetch results from our database, but for simplicity we'll just return an Array<Movie>:

import { movies } from "./movies";

const resolvers: IResolvers = {
  Query: {
    getMovies(_, {}, ctx) {
      return movies;
    },
  },
};

We should now be able to set up our server with the following:

const server = fastify();

server.register(mercurius, {
  schema,
  resolvers,
});

server.listen(3000, (err, address) => {
  if (err) {
    console.error(err);
    process.exit(1);
  }
  console.log(`Server is now listening at ${address}`);
});

Now we can connect a GraphQL client (I'm using Altair) to our API and check its schema

Everything good so far, right? Now we need to allow our clients to create a movie in our database. For that we'll create a mutation

In REST, any request might end up causing some side-effects on the server, but by convention it's suggested that one doesn't use GET requests to modify data. GraphQL is similar - technically any query could be implemented to cause a data write. However, it's useful to establish a convention that any operations that cause writes should be sent explicitly via a mutation.

in Queries and Mutations | GraphQL

The setup for our Mutation is very similar to our list query, i.e. add it to our schema and add its respective resolver

type Mutation {
  createMovie(title: String!, year: Int!): Movie 
} 

const resolvers: IResolvers = {
  Query: {...},
  Mutation: createMovie(_, { title, year }, ctx) {
    const movie: Movie = { title, year };
    movies.push(movie);
    return movie;
  },
};

We should now see our Mutation appear on the Docs section on Altair

Great! We now have a way to list all our movies and to add a new movie. Our next step is to set up a Websocket server so we can connect in real time with our client app. And for that, we'll use Subscriptions. Subscriptions work like a regular GraphQL query, but instead of returning data immediately, they return data to subscribed clients when something specific, i.e. an event happens on the server.

Let's add a Subscription and its resolver to our schema:

type Subscription {
  createdMovie: Movie!
}
Subscription: {
  createdMovie: {
    subscribe: async (root, args, { pubsub }) => {
      return await pubsub.subscribe("MOVIE_CREATED");
    },
  },
},

and add support for Subscriptions on our Mercurius definition.

server.register(mercurius, {
  schema,
  resolvers,
  subscription: true, // added this line
});

Turning on support for subscriptions in Mercurius gives us access to a pubsub utility which will let us subscribe and publish to a specific topic (in this case, the topic is MOVIE_CREATED). This means that when a movie is added, we need to publish that movie to the topic. To do so we shall change our Mutation resolver:

Mutation: {
  createMovie(_, { title, year }, { pubsub }) {
    const movie: Movie = { title, year };
    movies.push(movie);

    pubsub.publish({
      topic: "MOVIE_CREATED",
      payload: {
        createdMovie: movie,
      },
    });

    return movie;
  },
},

Now in Altair, we can query the Subscription in one tab and add a new Movie in the other. If all goes well, a notification should pop up to tell us that the subscription query received some data.

As you can see, within our GraphQL API we have set up a Websocket server which our client app can connect to receive real time changes.

The code samples used to illustrate the points in this post are part of a bigger codebase that contains other interesting stuff (like Typescript code generation from GraphQL queries). If you wish to see the codebase for this server (and an example of a React client app), check here and here.


At Lavanda, we use modern technologies and grand software development practices to deliver an amazing product to our clients. If you think you'd be a great fit in our team, take a look at our open positions here.