Using Node.js, GraphQL and React to build a real-time chat app - Part 1
In this tutorial, we're going to generate a useful app while compiling a few wonderful technology tools - and having some fun with development along the way.
To build our new real-time chat application we'll be using GraphQL, MongoDB, Node.js, and most importantly ReactHooks. This app will offer users the ability to view all of the other online users once they have logged in, so they can chat with anyone on the platform. There will also be a feature to notify users when new members log in or if the person they're chatting with is actively typing a response. Sound like a good plan? Awesome, let's get going!
During my early days as a web developer, I stumbled upon GraphQL. I was searching for a tutorial on a full-stack development tool, and the most recently posted video happened to be about GraphQL. I pushed play and watched the tutorial, without any real background knowledge of GraphQL, how it worked, or what the options were for other similar programs. I probably don't need to tell you that this method of learning came with a few bumps in the road. I worked my way through by looking over available explanations of GraphQL online and pouring over the few available answers to questions posted on StackOverflow regarding GraphQL. It surprised me that there weren't more resources available about GraphQL, especially considering how useful it can be. Putting out this article is my way of giving back to the dev community I'm lucky to be a part of, so that you can maybe learn faster than I did.
This is the beginning of a two part tutorial in which I will discuss designing the server. I am not going to go into great detail on the tech stack, because I do feel that there are plenty of resources out there that could do a better job. The next installment of our two part tutorial will take you through building the frontend of the app.
So, let's get to the fun stuff. I've posted the entire code for the app on Github, so if you're interested feel free to check out the code by cloning the repo:
Because this is such a simple app, we're going to use a simple file structure to house it. Once we're done, the server file structure should look like this:
For this configuration we'll be using GraphQL-yoga. GraphQL is an easy to set up fully-featured server. It was created by the developers at Facebook as an alternative API to REST. It also functions as a runtime used to fulfill queries. The APIs that are coded using GraphQL, as opposed to REST architecture, provide clients the function of asking the server for specifically what they need from the database. As a result, they get what they want and nothing more. Isn't that sweet?
We'll also be using mLab as well as the Mongoose library. If you prefer to use a local instance, you can install it on your local machine by referencing the MongoDB official docs. I plan to use mLab instead since the app will be hosted on Heroku. mLab functions as a database-as-a-service provider for MongoDB. Using mLab you can access free MongoDB databases through the internet. If you need help setting up mLab, look here.
Now, in order to install the dependencies we'll need for the app you should initialize npm.
Import the following dependencies into the index.js file.
We'll be using GraphQLServer, a core primitive provided by graphql-yoga, to make an instance of the server where all the building will take place. The server created will automatically be configured with all related features of GraphQLschema along with many basic server configurations. In order to make our app real-time, we will be activating withFilter and PubSub as subscriptions within GraphQL. We'll talk more about this later.
The next step in our chat app is to connect your database to Mongoose. If you aren't successful within the first two arguments you will get depreciation warnings from Mongoose.
Next, you should pass your schema into the new Mongoose models you create. Because the app we're building is relatively simple, you'll only need User and Message models. Go ahead and include these in the models.js file:
We'll be using email addresses to identify our users, so each email input must be unique. Also, we need to make sure that both name and email fields are required.
There are a couple of ways we can input schema into our GraphQL server; we can input schema manually or we could use typeDefs and resolvers. We're going to go with the latter for this tutorial. TypeDefs is a program to define the schema models we want included and specify what the contents of each model should be. Along with defining the structure of the GraphQL API (written in GraphQL SDL, Schema Definition Language), they also dictate the GraphQL operation types we would like to have. There are three operation types available in TypeDef - queries, mutations, and subscriptions. The Queries operation type is used for requesting data through the R-Read in the CRUD functions. Mutations allow us to make changes, or mutate, data in our models. They account for the C-Create, U-Update and D-Delete CRUD functions. Both Queries and Mutations have specific entry points into the schema. Subscriptions are used for creation and maintenance of the constant connection with the server. If you'd like more information, check out this source and this source. Go ahead and make a typeDefs.js file and insert the following code (don't forget to include the backticks):
Now we've successfully defined Message and User types along with the operation types we discussed earlier: query, mutation, and subscription. These are each considered object types and as such are the simplest components of GraphQL schema. Using these, we'll be able to define what the program should fetch from the server. Note that these functions are not limited to a Node.js backend because they are language agnostic.
Pay particular attention to User - you should notice that it contains a field for messages. This allows us many message options depending on the specific user we query. The message field also includes a reference to the user it was created by so that we can connect the messages created to the user who sent them.
We included mutations that would dictate which fields we need and what information we want returned from those fields. For example, with the createUser mutation, we would like the user to include a name in string form. By including the exclamation mark (!) we're signifying that this field cannot be left blank and is required in order for a new user to be created. Likewise, a new user must include an email that is a string. Only if these two operations are successfully completed, will the created user be returned. The user! outside the parentheses is signifying these values returned as a result of the mutation. You can use this same logic for additional mutations. Although we didn't make an id for this particular mutation, it is required for some mutations because oftentimes an id is automatically created each time a new model is created.
An interesting feature we're planning on including in this app is a notification to users when the person they are chatting with is typing, isn't that cool! The userTyping mutation will allow us to listen in to a user with a subscription. In order for the subscription model to work, we need to be able to identify the user typing as well as the user the message is intended for. That's the reason for the inclusion of email and receiverMail in the userTyping mutation.
Because we don't want every user to receive messages from all users on the platform, we need to specify messages only from people we are chatting with using a newMessage subscription. We will do this by instructing the server to only send messages from the person with the email we provide. This is the same procedure that we followed with the userTyping subscription. When combining these two subscription features, we should be able to see when a person we're chatting with sends a new message as well as when they are actively typing. The difference is that for userTyping we will only return an email for identification but with newMessage we want to return the message that was created.
With a newUser subscription, we would like to return new user information each time a new account is created. This subscription feature will allow users to see any new users on the app as soon as they join. By contrast, the oldUser subscription will notify anyone using the app that someone has been deleted from the platform. We didn't require any parameters when coding these subscriptions because we want all users notified in either case.
Let's move on to our resolvers. Begin by making a resolvers.js and insert this:
Remember earlier when we discussed the fact that typeDefs defines the structure of the GraphQL API we're working on? The functions for each of the object types defined under typeDefs are called resolvers and are used to implement the API. We'll be defining the resolvers for each of the object types specified by typedefs. In the queries dictated above, we're returning all of the items searched for in the query category. For example, if you run a query for Message or User, the program should return all the database entries for each of these parameters.
When we run a query for either User or Message, the system should find and return any related content. Because we want to make sure that the messages created by any given user are attached to that particular user we want the User resolver to search all messages and return those that have the same email address as senderMail. We would like to have a similar process for the Message query so that we can find the user who sent each message on the platform. Remember that these functions should still be the result of resolvers, like the Query field written in the early section. Include this code for these features:
Moving on to mutation resolvers. For our purposes, we need to use the values we requested in typeDefs to either make a new object, modify an existing object, or delete an object. Because we want to be careful to not create any blocking I/O calls, we're going to be using asynchronous functions in our database interactions. The coding of the async functions is for future projects, and while we could use callback functions down the line for certain features, looking ahead is way cooler, don't you think? Our current app won't use these functions, but they will remain in the backend as landmarks for subsequent builds. Generally, resolvers can accept four arguments: obj,args,context and info. For this app we will only be using args- argument.
Remember that some of the features we have specified for this app are the ability to notify users when a new user joins or an existing user leaves, when a user has a new message, and when a user is typing a new message. These features are the reason we will include the pubsub.publish() function to act on our mutations. This code allows us to include publish and subscribe API that will in turn allow us to include additions to the field and push those to active users. Anytime new resolvers for the mutations we have included are published, pubsub is called and publishes new features to the users. Another feature we will include is the deletion of all messages sent by a user once they delete their account. The Promise.all() function will perform this deletion.
Our subscriptions will be powered by WebSocket, a computer communications tool similar to HTTP that allows for full-duplex communication channels. WebSocket allows for messaging between client and server. In HTTP these types of messages always need to be initiated by the client so it is unidirectional while WebSocket is bi-directional. We are using the latter because it will be perfect for finding new messages or users in the system and sending information about these to the client.
For the subscription we are interested in, we're going to use pubsub.asyncIterator() to map an event by passing in the subscription name as an argument. This allows the GraphQL engine to recognize the subscription. Moving forward each time pubsub.publish() is used within the subscription, for example when a resolver we defined pubsub.publish() is called, it will automatically be published for us. Yay! We will also be working with the withFilter API within our newMessage and userTyping subscriptions because it allows us to use a filter function to decide which subscriptions we want to be published. When we are only interested in publishing a feature to specific clients, we will pass the argument that determines which users will get the message. We want to make sure that all users can see the filters.
In conclusion, when finishing the coding for the backend, make sure that you include an instance of PubSub, then create and start your server. We'll be passing pubsub to the GraphQLServer as context so that the server can access pubsub in the resolvers we included.
Run your server with the following command:
I personally prefer to run nodemon so that the server doesn't need to be restarted after every change. The nodemon program keeps track of any changes made in the index.js file, and any time there is a modification it automatically restarts the server. If you would like to install nodemon as a dev dependency so that it only runs when you're in development mode and not once the app is in production, use this code:
Next, insert it into the start script for your package.json:
You should now be able to run npm start to start the server and from here on out nodemon will restart the server when it detects changes.
If you would like to confirm anything, follow the link to the code:
Now, go ahead and go to localhost:4000 on your browser. GraphQL-yoga will allow you to use GraphQL Playground which is an IDE for working with GraphQL APIs. You can even test out subscriptions through this system, so you should be able to test all components of everything you've written so far. You now have a real time chat app that you built! Congrats!
Thanks so much for joining me on this journey.
Stay tuned for part two in next week's blog, where we tackle the front end!
If you found Chukwualuka's tutorial useful, check out our other technical blogs for more essential tips and insights.
Are you a developer interested in growing your software engineering career? Apply to join the Andela Talent Network today.
Your career is a journey, not just a job. Taking ownership of your career development and actively seeking out opportunities for advancement can not only spark career growth, but also increase your enthusiasm for your work. Read our seven tips to accelerating your work ambitions!
With technology advancing faster than ever before, tech skills are always in demand. These are the top six right now: Core engineering, Cloud API, database expertise, data analytics, communications, and Devops methodology.