Skip to content
Code with Hou

Introduction to Node.js Part VI - Building a Live Trivia Game with Socket.IO

Node.js, JavaScript, Socket.io, Handlebars, intermediate, tutorial, intro-to-node-js11 min read

Introduction

Welcome to the final part of the six-part Introduction to Node.js tutorial series. By the end of this tutorial, you will be able to:

  • create real-time, bi-directional, and event-based communication using a Socket.IO-powered Express server
  • make HTTP requests from an Express server using the built-in http Node.js module and with node-fetch

Tutorial Outline

  • Socket.IO Overview

  • Client-Server Communication Protocols

  • Building a Live Multi-Player Programming Trivia Game

    • Complete Example
    • Trivia Game Features
    • Task 1: Installing Express & Socket.IO
    • Task 2: Create a Basic Express Server
    • Task 3: Connect Socket.IO to Express Server
    • Task 4: Set Up Client-Side Files
    • Task 5: Create the Welcome Header
    • Task 6: Working with Socket.IO Events
      • Sending Events with emit
      • Add, Retrieve, and Remove Players from the Game
      • Receiving Events with on
    • Task 7: Letting Players Know a New Player Has Joined (socket.broadcast.to)
    • Task 8: Display the Game Info (io.in.emit)
    • Task 9 (CHALLENGE): Handle Player Disconnect
    • Task 10: Create Chat Functionality
    • Task 11: Display a Trivia Question for All Players
      • CHALLENGE: Update the Trivia Section with the Question Prompt
      • CHALLENGE: Refactor your HTTP Request to Use Node-Fetch
    • Task 12 (CHALLENGE): Allow Players to Submit an Answer to the Trivia
    • Task 13 (CHALLENGE): Allow Players to Reveal the Answer
    • CHALLENGE: Taking Your Game Further
  • Additional Resources

  • Review

  • Key Takeaways

  • What's next

Socket.IO Overview

From the Socket.IO website:

"Socket.IO enables real-time, bidirectional and event-based communication. It works on every platform, browser or device, focusing equally on reliability and speed."

Socket.IO can be used to build a variety of applications that have real-time communication capabilities, such as streaming applications, document collaboration, and instant messaging.

In this tutorial, we will be building a Socket.IO-powered multi-player trivia game.

Client-Server Communication Protocols

AJAX polling, long polling, and WebSockets are popular communication protocols between a client (e.g., browser) and a web server.

Let's review each protocol type briefly, as it's important for understanding how Socket.IO works.

AJAX Polling

  • The client connects with the server and sends repeated HTTP requests at regular intervals (e.g., every second)
    • significant HTTP overhead!
AJAX Polling NotesAJAX polling is a technique applied in AJAX applications where the client opens a connection with the server, and then repeatedly sends HTTP requests to a server at regular intervals (e.g., every 1 second) to get any new information that the server has.

After sending each request, the client waits for the server to respond, either with the data or with an empty response (if no data is available).

This technique significantly increases the HTTP overhead (e.g., creating HTTP headers, establishing and closing connections, generating a response, etc.) because the client has to keep bugging the server for new pieces of data, most of which are just empty responses.

Long Polling

  • The client opens a connection with the server, sends an initial HTTP request, and waits for a response from the server.
Long Polling NotesIn long polling, the client opens a connection with the server and makes an initial HTTP request but does not expect the server to respond immediately.

Instead of sending an empty response when information is not available, the server "hangs" or waits until data becomes available to push a response to the client or a timeout threshold is reached.

The client then immediately sends another HTTP request to the server, thereby ensuring that the server would always have a request waiting for it to respond to with any new data. This process is repeated as many times as needed.

The client may have to reconnect to the server periodically after the connection is closed due to timeouts.

WebSockets

"WebSocket is a computer communications protocol, providing full-duplex communication channels over a single Transmission Control Protocol connection" - Wikipedia

  • The browser establishes a persistent session with the server via a WebSocket handshake.
  • Socket.IO uses WebSocket as the transport mechanism if it's available in the browser, falling back on HTTP long polling if it's not.
WebSockets NotesWebSocket creates a persistent, bi-directional communication session between the user's browser and a web server.

To establish a WebSocket connection, the browser contacts the server and asks for a WebSocket a connection (a process known as the WebSocket handshake).

If the handshake succeeds, then the server and client can pass data back and forth at any time, as long as the connection remains open. Responses are event-driven, so there's no need to continually poll the server.

Socket.IO uses WebSocket as a transport when the WebSocket API is available in the browser, falling back on HTTP long polling if it's not. As of July 2020, 97% of all browsers support WebSocket.

The Socket.IO client is a wrapper around the WebSocket API, whereas the Socket.IO server exposes a socket object that extends the Node.js EventEmitter class.

Take a look at this article if you're interested in a deep dive into WebSockets!

Building a Live Multi-Player Programming Trivia Game

Let's build a live multi-player trivia game using Socket.IO! We'll learn about Socket.IO's various API methods along the way.

Trivia Game Features

The following features will be part of the game:

  • Each player is required to enter a name and a room to join a game
  • Multiple players can join a single channel/room. Multi-channel support is not part of MVP, so only one game can happen at a time
  • Within the game channel/room,
    • players can chat with each other in real-time
    • players can view who else are in the room as well as the room they're currently in
    • any player can initiate a game
    • each player is allowed to submit only one response per player
    • each player must submit a response before the answer can be revealed

Here's the deployed game for your reference.

If you ever feel lost as you progress through the tutorial, you can always reference the commit link provided at the end of each task to compare your code against the completed version!

You can find the code for the completed project here.

Starter Code

There's no starter code for this tutorial. You will follow the steps described below to build the project from scratch.

Complete Example

A Github repo with the complete code is available here.

Task 1: Installing Express & Socket.IO

Here's a commit showing the changes for task 1.

  1. From your terminal, change into your course directory: $ cd /path/to/your/course/directory

  2. Create a new directory: $ mkdir trivia-game.

  3. Change into your /trivia-game directory: $ cd trivia-game.

  4. Turn your directory into an npm package and use the information extracted from the current directory to configure your package.json:

    1$ npm init -yes

    Running this command creates a package.json file in your current directory.

  5. Install the required dependencies:

    1$ npm install express socket.io

    Running this command creates a new folder called node_modules in your current directory.

  6. Add a .gitignore file to your current directory:

    1$ touch .gitignore

    Add node_modules to the first line of the .gitignore file to avoid committing node_modules folder to Git.

Task 2: Create a Basic Express Server

Here's a commit showing the changes for task 2.

  1. Create a folder for storing our server-side code: $ mkdir src.

  2. Create a file for configuring and starting our server: $ touch src/index.js.

  3. Set up a basic Express server:

    src/index.js
    1const express = require("express");
    2const app = express();
    3
    4const port = process.env.PORT || 8080;
    5app.listen(port, () => {
    6 console.log(`Server is up on port ${port}.`);
    7});
  4. Start up your application server: $ nodemon src/index.js. Your console should tell you that your server is up and running!

    task 1 Server Running on localhost:8080

  5. Create a public/ folder in project folder: $ mkdir public.

  6. Set up our server to serve static assets (e.g., HTML, CSS, and scripts for our game pages) to the client.:

    src/index.js
    1const express = require('express');
    2+ const path = require('path');
    3
    4const port = process.env.PORT || 8080;
    5
    6const app = express();
    7
    8+ const publicDirectoryPath = path.join(__dirname, '../public');
    9+ app.use(express.static(publicDirectoryPath));
    10+
    11app.listen(port, () => {
    12 console.log(`Server is up on port ${port}.`);
    13});

Task 3: Connect Socket.IO to Express Server

Here's a commit showing the changes for task 3.

  1. Connect Socket.IO to our Express server:

    src/index.js
    1const express = require('express');
    2+ const http = require('http');
    3const path = require('path');
    4+ const socketio = require('socket.io');
    5
    6const port = process.env.PORT || 8080;
    7
    8const app = express();
    9+ const server = http.createServer(app); // create the HTTP server using the Express app created on the previous line
    10+ const io = socketio(server); // connect Socket.IO to the HTTP server
    11
    12const publicDirectoryPath = path.join(__dirname, '../public');
    13app.use(express.static(publicDirectoryPath));
    14
    15+ io.on('connection', () => { // listen for new connections to Socket.IO
    16+ console.log('A new player just connected');
    17+ })
    18
    19- app.listen(port, () => {
    20+ server.listen(port, () => {
    21 console.log(`Server is up on port ${port}.`);
    22});

Task 4: Set Up Client-Side Files

Here's a commit showing the changes for task 4.

Let's create the files that the client will need to render the game in the browser.

  1. Create folders for our JavaScript files and CSS in /public: $ mkdir public/js public/css

  2. Create the client-side files: $ touch public/index.html public/trivia.html public/js/trivia.js public/css/styles.css

    Your public/ folder should look like this:

    Public Folder

  3. Since HTML & CSS are not the primary focus of the course, we will not create these from scratch! Just copy and paste the HTML (i.e., public/index.html, public/trivia.html) and CSS code (i.e., public/css/styles.js) from this commit into your own files. That said, feel free to tweak the styling to your liking!

    public/index.html and public/trivia.html contain the HTML code for the registration and game pages respectively.

    public/trivia.html includes a few essential third-party scripts for our game:

    public/trivia.html
    1<!-- Handlebars: our client-side templating engine -->
    2<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.7.6/handlebars.min.js"></script>
    3
    4<!-- MomentJS: a library for formatting timestamps -->
    5<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.min.js"></script>
    6
    7<!-- Client-Side Socket IO -->
    8<script src="/socket.io/socket.io.js"></script>
    9
    10<!-- Custom JavaScript file where we write our game-related logic -->
    11<script src="/js/trivia.js"></script>

    Socket.IO automatically serves up /socket.io/socket.io.js which contains the client-side Socket.IO code.

    http://localhost:8080 should now display a registration page that includes a form which allows a player to input their name and room.

    Registration Page

  4. Fill out the form and hit Join. You should be taken to the game page.

    Game Page

    The app does not work yet because we haven't added any of the game logic!

Task 5: Create the Welcome Header

Here's a commit showing the changes for task 5.

The welcome header is currently missing, so let's add that.

Notice in the completed game that the header greets the player by the name that they provided on the registration page. We can use Handlebars to generate the header dynamically in the client!

  1. Create the welcome header:

    public/js/trivia.js
    1// Extract the playerName from the url search params, which the player provided on the registration page
    2const urlSearchParams = new URLSearchParams(window.location.search);
    3const playerName = urlSearchParams.get("playerName");
    4
    5// We'll follow the basic tasks below whenever we need to update the page with Handlebars.
    6// Target the template which is embedded as a script tag in the `public/trivia.html` file
    7const mainHeadingTemplate = document.querySelector(
    8 "#main-heading-template"
    9).innerHTML;
    10
    11// Compile the template into HTML by calling Handlebars.compile(), which returns a function
    12const welcomeHeadingHTML = Handlebars.compile(mainHeadingTemplate);
    13// Insert the welcomeHeadingHTML right after the opening <main> tag
    14document.querySelector("main").insertAdjacentHTML(
    15 "afterBegin",
    16 // Invoke the welcomeHeadingHTML function, passing in the data that will be used to render the heading
    17 welcomeHeadingHTML({
    18 playerName,
    19 })
    20);

Task 6: Working with Socket.IO Events

Here's a commit showing the changes for task 6.

Socket.IO uses events facilitate data transfer between the client and the server.

Every event consists of two sides: the sender and the receiver.

If the server is the sender, then the client is the receiver. If the client is the sender, then the server is the receiver.

Sending Events with socket.emit

Events can be sent from the sender using the socket.emit function. Let's send an event from the client to notify the server that a player has joined the game:

public/js/trivia.js
1+ // io is provided by the client-side Socket.IO library that was loaded in public/trivia.html
2+ const socket = io();
3
4const urlSearchParams = new URLSearchParams(window.location.search);
5const playerName = urlSearchParams.get('playerName');
6+ // Extract the `room` value from the url, as we will need to use it below
7+ const room = urlSearchParams.get('room');
8.
9.
10.
11+ // Call `socket.emit` to send an event to the server.
12+ // The first argument describes the name of the event (i.e., `join`).
13+ // The second argument contains the data (i.e., `playerName` and `room`) to be sent to the server along with the event.
14+ // The third argument is a callback function that is invoked when the server acknowledges the event.
15+ // If there's an error, the player will see an alert and be redirected back to the registration page
16+
17+ socket.emit('join', { playerName, room }, error => {
18+ if (error) {
19+ alert(error);
20+ location.href = '/';
21+ }
22+ });

Navigate to http://localhost:8080 on a few separate browser tabs. In your Node.js console, you should see a console message A new player just connected for each browser tab opened.

Socket.IO Running

Add, Retrieve, and Remove Players from the Game

To keep our code modular and maintain separation of concerns, let's store all the code related to adding, retrieving, and removing players from a game in a separate file in the server.

Go ahead and create a new folder called /utils and a file called /players.js inside that folder:

src/utils/players.js
1const players = [];
2
3// Add a new player to the game
4const addPlayer = ({ id, playerName, room }) => {
5 if (!playerName || !room) {
6 return {
7 error: new Error("Please enter a player name and room!"),
8 };
9 }
10
11 // clean the player registration data
12 playerName = playerName.trim().toLowerCase();
13 room = room.trim().toLowerCase();
14
15 const existingPlayer = players.find((player) => {
16 return player.room === room && player.playerName === playerName;
17 });
18
19 if (existingPlayer) {
20 return {
21 error: new Error("Player name is in use!"),
22 };
23 }
24
25 const newPlayer = { id, playerName, room };
26 players.push(newPlayer);
27
28 return { newPlayer };
29};
30
31// Get a player by id
32const getPlayer = (id) => {
33 const player = players.find((player) => player.id === id);
34
35 if (!player) {
36 return {
37 error: new Error("Player not found!"),
38 };
39 }
40
41 return { player };
42};
43
44// Get all players in the room
45const getAllPlayers = (room) => {
46 return players.filter((player) => player.room === room);
47};
48
49// Remove a player by id
50const removePlayer = (id) => {
51 return players.find((player, index) => {
52 if (player.id === id) {
53 return players.splice(index, 1)[0];
54 }
55 return false;
56 });
57};
58
59// Export our helper methods
60module.exports = {
61 addPlayer,
62 getPlayer,
63 getAllPlayers,
64 removePlayer,
65};

DISCUSS: In your breakout group, discuss what the helper functions in src/utils/players.js do.

Let's also create a function to format game messages so that they include timestamps which we will display along with the trivia answers and chat messages in the game.

src/utils/formatMessage.js
1module.exports = (playerName, text) => {
2 return {
3 playerName,
4 text,
5 createdAt: new Date().getTime(),
6 };
7};

Don't forget to import these helper functions in the main server file so we can use them later on:

src/index.js
1const formatMessage = require("./utils/formatMessage.js");
2
3const {
4 addPlayer,
5 getAllPlayers,
6 getPlayer,
7 removePlayer,
8} = require("./utils/players.js");

Receiving Events with socket.on

Events can be received by the receiver using socket.on.

In the server, listen for the join event within the connection event handler:

src/index.js
1- io.on('connection', () => {
2+ io.on('connection', socket => {
3 console.log('A new player just connected');
4
5+ // Call `socket.on` to listen to an event.
6+ // The first argument describes the name of the event to listen to (i.e., `join`).
7+ // The second argument is a callback that runs on receiving the event. This callback has access to two parameters.
8+ // The first parameter represents a data object sent from the client along with the event.
9+ // The second parameter is a callback function that can be called on the server to trigger the acknowledgement function on the client, a process called Event Acknowledgments
10+
11+ socket.on('join', ({ playerName, room }, callback) => {
12+ const { error, newPlayer } = addPlayer({ id: socket.id, playerName, room });
13+
14+ // In this example, the callback is called with an error message
15+ // if faulty regisration information was detected.
16+ // The argument would get passed to the client where the error could be shown.
17+ if (error) return callback(error.message);
18+ callback(); // The callback can be called without data.
19+
20+ // The server is responsible for adding and removing players from a room.
21+ // Call `socket.join` to subscribe the socket to a given room.
22+ socket.join(newPlayer.room);
23+
24+ // Call `socket.emit` to send the new player a welcome message when the player joins a room
25+ // We'll write code in the client later on to listen for the `message` event
26+ // Call `formatMessage` to add format our message with timestamps
27+ socket.emit('message', formatMessage('Admin', 'Welcome!'));
28+ });
29});

The client should listen for the message event and update the chat section accordingly whenever the message event is received:

public/js/trivia.js
1socket.on("message", ({ playerName, text, createdAt }) => {
2 // target the container in the DOM where we'll attach the new message to
3 const chatMessages = document.querySelector(".chat__messages");
4
5 // target the Handlebars template we need to create a message
6 const messageTemplate = document.querySelector("#message-template").innerHTML;
7
8 // Compile the template into HTML by calling Handlebars.compile(),
9 // which returns a function
10 const template = Handlebars.compile(messageTemplate);
11
12 const html = template({
13 playerName,
14 text,
15 createdAt: moment(createdAt).format("h:mm a"),
16 });
17
18 // Insert the new html right at the beginning of chatMessages container
19 chatMessages.insertAdjacentHTML("afterBegin", html);
20});

Test your game page and make sure the Welcome message is displaying in the chat box at this point!

task 6 Welcome Chat Message

Task 7: Letting Players Know a New Player Has Joined (socket.broadcast.to)

Here's a commit showing the changes for task 7.

Next, use socket.broadcast.to to send a message event to let all other players in the room know when a new player has joined the game.

src/index.js
1io.on('connection', () => {
2 console.log('A new player just connected');
3
4 socket.on('join', ({ playerName, room }, callback) => {
5 const { error, newPlayer } = addPlayer({ id: socket.id, playerName, room });
6
7 if (error) return callback(error.message);
8
9 socket.join(newPlayer.room);
10
11 socket.emit('message', formatMessage('Admin', 'Welcome!'));
12
13+ socket.broadcast
14+ .to(newPlayer.room)
15+ .emit(
16+ 'message',
17+ formatMessage('Admin', `${newPlayer.playerName} has joined the game!`)
18+ );
19+
20+ });
21
22});

Navigate to http://localhost:8080 and join the same room from several browser tabs. You should see the chat box update with a new message about the player who has just joined every time a new socket connects to the server.

task 7 Players Join Game

Task 8: Display the Game Info (io.in.emit)

Here's a commit showing the changes for task 8.

The Game Info section displays the player's current room and the players who are active in that room. We can use the io.in.emit function to send an event (i.e., room) to all players in the room:

What is the difference between `socket.broadcast.to` and `io.in.emit`?

socket.broadcast.to sends an event to all clients in the room except the sender whereas io.in.emit sends an event to all clients in the room

NOTE: io.in.emit and io.to.emit methods do the same thing!

src/index.js
1io.on('connection', () => {
2 console.log('A new player just connected');
3
4socket.on('join', ({ playerName, room }, callback) => {
5 const { error, newPlayer } = addPlayer({ id: socket.id, playerName, room });
6
7 if (error) return callback(error.message);
8
9 socket.join(newPlayer.room);
10
11 socket.emit('message', formatMessage('Admin', 'Welcome!'));
12
13 socket.broadcast
14 .to(newPlayer.room)
15 .emit(
16 'message',
17 formatMessage('Admin', `${newPlayer.playerName} has joined the game!`)
18 );
19
20+ // Emit a "room" event to all players to update their Game Info sections
21+ io.in(newPlayer.room).emit('room', {
22+ room: newPlayer.room,
23+ players: getAllPlayers(newPlayer.room),
24+ });
25 });
26});

We have to set up the client to listen for the room event and update the page (i.e., the Game Info section) accordingly on receiving the event:

public/js/trivia.js
1socket.on("room", ({ room, players }) => {
2 // target the container where we'll attach the info to
3 const gameInfo = document.querySelector(".game-info");
4
5 // target the Handlebars template we'll use to format the game info
6 const sidebarTemplate = document.querySelector(
7 "#game-info-template"
8 ).innerHTML;
9
10 // Compile the template into HTML by calling Handlebars.compile(), which returns a function
11 const template = Handlebars.compile(sidebarTemplate);
12
13 const html = template({
14 room,
15 players,
16 });
17
18 // set gameInfo container's html content to the new html
19 gameInfo.innerHTML = html;
20});

task 8 Game Info

Task 9 (CHALLENGE): Handle Player Disconnect

Here's a commit showing the changes for task 9.

Sometimes, players get frustrated and quit the game without saying a word!

Your challenge here is to write code to remove the player from the game, send a message to all other players in the room to let them know of the player's departure, and also update the players list in the Game Info section.

Use the Socket.IO docs to help you.

HINT: What event gets fired when a player disconnects from the game?

SOLUTION

Listen for the disconnect event on the server. This event is automatically fired whenever a socket is disconnected.

src/index.js
1socket.on("disconnect", () => {
2 console.log("A player disconnected.");
3
4 const disconnectedPlayer = removePlayer(socket.id);
5
6 if (disconnectedPlayer) {
7 const { playerName, room } = disconnectedPlayer;
8 io.in(room).emit(
9 "message",
10 formatMessage("Admin", `${playerName} has left!`)
11 );
12
13 io.in(room).emit("room", {
14 room,
15 players: getAllPlayers(room),
16 });
17 }
18});

Task 10: Create Chat Functionality

Here's a commit showing the changes for task 10.

Let's allow players to communicate with each other in the chat box! In the client, add an event listener on the chat form:

public/js/trivia.js
1// First, target the chat form in the DOM
2const chatForm = document.querySelector(".chat__form");
3
4// Second, add an event listener on the chat form
5// The first argument is the event to listen for (i.e., 'submit')
6// The second argument is the callback function that is called when the event is triggered
7chatForm.addEventListener("submit", (event) => {
8 event.preventDefault();
9
10 const chatFormInput = chatForm.querySelector(".chat__message");
11 const chatFormButton = chatForm.querySelector(".chat__submit-btn");
12
13 // Disable the chat form submit button to prevent the player from being able to submit multiple messages simultaneously
14 chatFormButton.setAttribute("disabled", "disabled");
15
16 // Extract the message that the player typed into the message box
17 const message = event.target.elements.message.value;
18
19 // Send an event to the server along with the player's chat message
20 socket.emit("sendMessage", message, (error) => {
21 // On event acknowledgement by the server, reset the chat form button
22 chatFormButton.removeAttribute("disabled");
23 chatFormInput.value = "";
24 chatFormInput.focus();
25
26 // Alert an error if an error is received
27 if (error) return alert(error);
28 });
29});

In the server, listen for the sendMessage event and emit the message to all players:

src/index.js
1socket.on("sendMessage", (message, callback) => {
2 const { error, player } = getPlayer(socket.id);
3
4 if (error) return callback(error.message);
5
6 if (player) {
7 io.to(player.room).emit(
8 "message",
9 formatMessage(player.playerName, message)
10 );
11 callback(); // invoke the callback to trigger event acknowledgment
12 }
13});

Task 11: Display a Trivia Question for All Players

Here's a commit showing the changes for task 11.

Next, when any of the player clicks on the Get Question button, the game should begin for all players in the room. The same trivia question should display in the "Trivia" section for all players.

Where should we write the code for making the API call to get a trivia question from our Trivia API?In the server! Because we want the same question to be displayed for all players.

In the client, send an event called getQuestion to the server when any player clicks on the Get Question button:

public/js/trivia.js
1const triviaQuestionButton = document.querySelector(".trivia__question-btn");
2triviaQuestionButton.addEventListener("click", () => {
3 // pass null as the second argument because we're not sending any data to the server
4 // alert the error if the server sends back an error
5 socket.emit("getQuestion", null, (error) => {
6 if (error) return alert(error);
7 });
8});

You will be using trivia questions from the Open Trivia Database API. Here's an example of a JSON response from the API.

As the players interact with the game, we'd have to keep track of different pieces of data, including storing the current question prompt and correct answer, as well as keeping track of how many answers have been submitted.

Let's create helper functions that allow us to manage and update our game data on the server:

src/utils/game.js
1const https = require("https");
2const { getAllPlayers } = require("./players.js");
3
4// An object containing data for the current game
5const game = {
6 // prompt keeps track of the current question and answer choices
7 // to be displayed to the user
8 prompt: {
9 answers: "",
10 question: "",
11 createdAt: "",
12 },
13 status: {
14 submissions: {}, // submissions keeps track of players' answer submissions
15 correctAnswer: "", // the correct answer to the current question
16 isRoundOver: false, // determines whether or not the answer can be revealed
17 },
18};
19
20// Get information about the game
21const getGameStatus = ({ event }) => {
22 const { correctAnswer, isRoundOver } = game.status;
23
24 if (event === "getAnswer" && isRoundOver) {
25 return { correctAnswer };
26 }
27};
28
29// Update the game status
30const setGameStatus = ({ event, playerId, answer, room }) => {
31 if (event === "sendAnswer") {
32 const { submissions } = game.status;
33
34 // Store only one response per player per round
35 if (!submissions[`${playerId}`]) {
36 submissions[`${playerId}`] = answer;
37 }
38
39 // Set isRoundOver to true when the number of submissions matches the total number of players.
40 // Otherwise, set it to false
41 game.status.isRoundOver =
42 Object.keys(submissions).length === getAllPlayers(room).length;
43 }
44
45 return game.status;
46};
47
48// Calls the opentdb API to get a random programming trivia question
49const setGame = (callback) => {
50 // configure the url query params to get one question (i.e., amount=1)
51 // in the programming category (i.e., category=18)
52 const url = "https://opentdb.com/api.php?amount=1&category=18";
53 let data = "";
54
55 // Here, we use the built-in `https` module to make https requests.
56 // We need to pass a callback that handles data received in chunks and ends the request.
57 const request = https.request(url, (response) => {
58 // Listen to the readable stream 'data' event and and process incoming chunks of data
59 response.on("data", (chunk) => {
60 data = data + chunk.toString();
61 });
62
63 // When the data stream ends, the stream 'end' event is called once
64 response.on("end", () => {
65 const { correct_answer, createdAt, incorrect_answers, question } =
66 JSON.parse(data).results[0];
67
68 game.status.submissions = {};
69 game.status.correctAnswer = correct_answer;
70 game.prompt = {
71 answers: shuffle([correct_answer, ...incorrect_answers]),
72 question,
73 };
74
75 // wrap the data in a callback for asynchronous processing
76 callback(game);
77 });
78 });
79
80 // Handle error by logging any error to the server console
81 request.on("error", (error) => {
82 console.error("An error", error);
83 });
84 request.end();
85};
86
87// Shuffles an array. Source: https://javascript.info/task/shuffle
88const shuffle = (array) => {
89 for (let end = array.length - 1; end > 0; end--) {
90 let random = Math.floor(Math.random() * (end + 1));
91 [array[end], array[random]] = [array[random], array[end]];
92 }
93 return array;
94};
95
96module.exports = {
97 getGameStatus,
98 setGameStatus,
99 setGame,
100};

DISCUSS: In your breakout group, discuss what the helper functions in src/utils/game.js do.

The server will have to listen for the getQuestion event from the client and invoke setGame to get a programming trivia question and send it to all players in the room:

src/index.js
1socket.on("getQuestion", (data, callback) => {
2 const { error, player } = getPlayer(socket.id);
3
4 if (error) return callback(error.message);
5
6 if (player) {
7 // Pass in a callback function to handle the promise that's returned from the API call
8 setGame((game) => {
9 // Emit the "question" event to all players in the room
10 io.to(player.room).emit("question", {
11 playerName: player.playerName,
12 ...game.prompt,
13 });
14 });
15 }
16});

CHALLENGE: Update the Trivia Section with the Question Prompt

Make the needed changes in the client to update the Trivia Section with the question prompt. Make sure the Get Question button is disabled after a question is displayed, as questions cannot be skipped by trying to get a new question.

SOLUTION

In the client, listen for the question event and update the page accordingly with the question prompt.

public/js/trivia.js
1// We'll use this helper function to decode any HTML-encoded
2// strings in the trivia questions
3// e.g., "According to DeMorgan&#039;s Theorem, the Boolean expression (AB)&#039; is equivalent to:"
4const decodeHTMLEntities = (text) => {
5 const textArea = document.createElement("textarea");
6 textArea.innerHTML = text;
7 return textArea.value;
8};
9
10socket.on("question", ({ answers, createdAt, playerName, question }) => {
11 const triviaForm = document.querySelector(".trivia__form");
12 const triviaQuestion = document.querySelector(".trivia__question");
13 const triviaAnswers = document.querySelector(".trivia__answers");
14 const triviaQuestionButton = document.querySelector(".trivia__question-btn");
15 const triviaFormSubmitButton = triviaForm.querySelector(
16 ".trivia__submit-btn"
17 );
18
19 const questionTemplate = document.querySelector(
20 "#trivia-question-template"
21 ).innerHTML;
22
23 // Clear out any question and answers from the previous round
24 triviaQuestion.innerHTML = "";
25 triviaAnswers.innerHTML = "";
26
27 // Disable the Get Question button to prevent the player from trying to skip a question
28 triviaQuestionButton.setAttribute("disabled", "disabled");
29
30 // Enable the submit button to allow the player to submit an answer
31 triviaFormSubmitButton.removeAttribute("disabled");
32
33 const template = Handlebars.compile(questionTemplate);
34
35 const html = template({
36 playerName,
37 createdAt: moment(createdAt).format("h:mm a"),
38 question: decodeHTMLEntities(question),
39 answers,
40 });
41
42 triviaQuestion.insertAdjacentHTML("beforeend", html);
43});

Try clicking on the Get Question button on any of the players' tab. The same trivia question should appear in all tabs! You won't be able to submit an answer just yet.

CHALLENGE: Refactor your HTTP Request to Use Node-Fetch

Here's a commit showing the changes for this refactoring exercise.

As you might have noticed, using the https module requires a significant amount of code to get it working.

node-fetch is a Node.js module that allows the server to use the Fetch API. Its syntax is much simpler. Let's refactor the setGame function in src/utils/game.js to use node-fetch.

Here's the documentation for your reference. Don't forget to install and save the dependency to your package.json file!

1$ npm install --save node-fetch
SOLUTION

Note how much cleaner the syntax is!

Task 12 (CHALLENGE): Allow Players to Submit an Answer to the Trivia

Here's a commit showing the changes for task 12.

In the client, the trivia form should listen for a submit event. On submit, send the player's answer to the server.

Don't forget to disable the button after the player has submitted an answer, since we don't want to allow players to submit multiple answers.

SOLUTION
public/js/trivia.js
1const triviaForm = document.querySelector(".trivia__form");
2triviaForm.addEventListener("submit", (event) => {
3 event.preventDefault();
4
5 const triviaFormSubmitButton = triviaForm.querySelector(
6 ".trivia__submit-btn"
7 );
8 const triviaFormInputAnswer = triviaForm.querySelector(".trivia__answer");
9
10 triviaFormSubmitButton.setAttribute("disabled", "disabled");
11
12 const answer = event.target.elements.answer.value;
13 socket.emit("sendAnswer", answer, (error) => {
14 triviaFormInputAnswer.value = "";
15 triviaFormInputAnswer.focus();
16
17 if (error) return alert(error.message);
18 });
19});

In the server, listen for and handle the sendAnswer event:

src/index.js
1// import setGameStatus function
2const { setGame, setGameStatus } = require("./utils/game.js");
3
4socket.on("sendAnswer", (answer, callback) => {
5 const { error, player } = getPlayer(socket.id);
6
7 if (error) return callback(error.message);
8
9 if (player) {
10 const { isRoundOver } = setGameStatus({
11 event: "sendAnswer",
12 playerId: player.id,
13 room: player.room,
14 });
15
16 // Since we want to show the player's submission to the rest of the players,
17 // we have to emit an event (`answer`) to all the players in the room along
18 // with the player's answer and `isRoundOver`.
19 io.to(player.room).emit("answer", {
20 ...formatMessage(player.playerName, answer),
21 isRoundOver,
22 });
23
24 callback();
25 }
26});

In the client, listen for the answer event and update the Trivia section with the data from the server:

public/js/trivia.js
1socket.on("answer", ({ playerName, isRoundOver, createdAt, text }) => {
2 const triviaAnswers = document.querySelector(".trivia__answers");
3 const triviaRevealAnswerButton = document.querySelector(
4 ".trivia__answer-btn"
5 );
6
7 const template = Handlebars.compile(messageTemplate);
8
9 const html = template({
10 playerName: playerName,
11 text,
12 createdAt: moment(createdAt).format("h:mm a"),
13 });
14
15 triviaAnswers.insertAdjacentHTML("afterBegin", html);
16
17 // If isRoundOver is set to true, activate the reveal answer button
18 if (isRoundOver) {
19 triviaRevealAnswerButton.removeAttribute("disabled");
20 }
21});

Task 13 (CHALLENGE): Allow Players to Reveal the Answer

Here's a commit showing the changes for task 13.

Go ahead and allow any of the players to reveal the answer when the round is over (i.e., when each player has submitted an answer). Use correctAnswer as your event name.

SOLUTION

In the client, emit a getAnswer event to the server:

public/js/trivia.js
1const triviaRevealAnswerButton = document.querySelector(".trivia__answer-btn");
2triviaRevealAnswerButton.addEventListener("click", () => {
3 socket.emit("getAnswer", null, (error) => {
4 if (error) return alert(error);
5 });
6});

In the server, emit correctAnswer to all players along with the correct answer:

src/index.js
1socket.on("getAnswer", (data, callback) => {
2 const { error, player } = getPlayer(socket.id);
3
4 if (error) return callback(error.message);
5
6 if (player) {
7 const { correctAnswer } = getGameStatus({
8 event: "getAnswer",
9 });
10 io.to(player.room).emit(
11 "correctAnswer",
12 formatMessage(player.playerName, correctAnswer)
13 );
14 }
15});

Back in the client, listen for the correctAnswer event and display the correct answer in the trivia section.

public/js/trivia.js
1socket.on("correctAnswer", ({ text }) => {
2 const triviaAnswers = document.querySelector(".trivia__answers");
3 const triviaQuestionButton = document.querySelector(".trivia__question-btn");
4 const triviaRevealAnswerButton = document.querySelector(
5 ".trivia__answer-btn"
6 );
7 const triviaFormSubmitButton = triviaForm.querySelector(
8 ".trivia__submit-btn"
9 );
10
11 const answerTemplate = document.querySelector(
12 "#trivia-answer-template"
13 ).innerHTML;
14 const template = Handlebars.compile(answerTemplate);
15
16 const html = template({
17 text,
18 });
19
20 triviaAnswers.insertAdjacentHTML("afterBegin", html);
21
22 triviaQuestionButton.removeAttribute("disabled");
23 triviaRevealAnswerButton.setAttribute("disabled", "disabled");
24 triviaFormSubmitButton.removeAttribute("disabled");
25});

And there you have it! Your trivia game should now be fully functional. Congratulations!

CHALLENGE: Taking Your Game Further

Did you finish the tutorial early? Here are a few suggestions on how to take your game to the next level!

  • Implement support for multiple, concurrent channels. Our game logic currently only works properly for one active channel at a time, even though the players can join multiple channels.

  • Implement an automated scoring system. Create a scoreboard that keeps track of players' scores. Declare a winner or tie after a certain number of rounds or score is reached in the game.

  • Set a time limit within which players must submit an answer or they forfeit their round.

  • Allow players to select rather than type an answer. Validate the answer prior to scoring.

  • Improve and centralize error handling within the application.

  • Generalize the game by allowing the players to select a different trivia category besides programming.

  • Deploy your game to Heroku to play with friends and family!

  • Add a database to save game sessions and scores!

  • Anything else you can think of!

Review

Take a moment to reflect on what you’ve learned in this tutorial and answer the following questions:

  • Explain the differences between AJAX Polling, Long Polling, and WebSockets.

  • What is Socket.IO? Which of the above communication protocols does Socket.IO rely on?

  • Explain how events work in Socket.IO.

Key takeaways

  • AJAX polling is a technique applied in AJAX applications where the client opens a connection with the server, and then repeatedly sends HTTP requests to a server at regular intervals (e.g., every 1 second) to get any new information that the server has.

  • In long polling, the client opens a connection with the server and makes an initial HTTP request but does not expect the server to respond immediately.

  • WebSocket creates a persistent, bi-directional communication session between the user's browser and a web server.

  • Socket.IO can be used to build a variety of applications that have real-time communication capabilities, such as streaming applications, document collaboration, and instant messaging. Socket.IO uses WebSocket as a transport when the WebSocket API is available in the browser, falling back on HTTP long polling if it's not.

  • Socket.IO uses events facilitate data transfer between the client and the server. Every event consists of two sides: the sender and the receiver. If the server is the sender, then the client is the receiver. If the client is the sender, then the server is the receiver.

You did it!

Congratulations, you've reached the end of the Introduction to Node.js tutorial series!

Want more content like this? Subscribe to get the latest updates in your inbox

Share your feedback

What did you like or didn't like about this post? Let me know what worked well and what can be improved. Your feedback is much appreciated!