1. Code
  2. JavaScript
  3. Node

How to Make a Real-Time Sports Application Using Node.js

Scroll to top
Final product imageFinal product imageFinal product image
What You'll Be Creating

In today's article, I'm going to demonstrate how to make a web application that will display live game scores from the NHL. The scores will update automatically as the games progress.

This is a very exciting article for me, as it allows me the chance to bring two of my favorite passions together: development and sports.

The technologies that will be used to create the application are:

  1. Node.js
  2. Socket.io
  3. MySportsFeeds.com
  4. Preact (like React)
  5. HTM

If you don't have Node.js installed, visit their download page now and set it up before continuing.

What Is Socket.io?

Socket.io is a technology that connects a client to a server using WebSockets. In this example, the client is a web browser and the server is the Node.js application. The server can have multiple clients connected to it at any given time.

Once the connection has been established, the server can send messages to all of the clients or an individual client. In return, the client can send a message to the server, allowing for bi-directional real-time communication.

Before Socket.io, web applications would commonly use AJAX, and both the client and server would poll each other looking for events. For example, every 10 seconds an AJAX call would occur to see if there were any messages to handle.

Polling for messages caused a significant amount of overhead on both the client and server as it would be constantly looking for messages when there were none.

With Socket.io, messages are received instantaneously, without needing to look for messages, reducing the overhead.

Sample Socket.io Application

Before we consume the real-time sports data, let's create an example application to demonstrate how Socket.io works.

To begin, I am going to create a new Node.js application. Navigate to the folder you want your project in, create a new folder for the application, and create a new application:

1
cd ~/Documents/Nodejs
2
mkdir SocketExample
3
cd SocketExample
4
npm init

I used all the default settings.

Because we are making a web application, I'm going to use an NPM package called Express to simplify the setup. In a command prompt, install it as follows: npm install express --save

And of course we will need to install the Socket.io package: npm install socket.io --save

Let's begin by creating the web server. Create a new file called index.js and place the following code within it to create the web server using Express:

1
const app = require("express")();
2
const http = require("http").Server(app);
3
4
app.get("/", function (req, res) {
5
    res.sendFile(__dirname + "/index.html");
6
});
7
8
http.listen(3000, function () {
9
	console.log("HTTP server started on port 3000");
10
});

If you are not familiar with Express, the above code example includes the Express library and creates a new HTTP server. In this example, the HTTP server is listening on port 3000, e.g. https://localhost:3000. A route is created at the root of the site "/". The result of the route returns an HTML file: index.html.

Before we create the index.html file, let's finish the server by setting up Socket.io. Append the following to your index.js file to create the Socket server:

1
const io = require('socket.io')(http);
2
3
io.on('connection', function(socket){
4
    console.log('Client connection received');
5
});

Similar to Express, the code begins by importing the Socket.io library. This is stored in a variable called io. Next, using the io variable, an event handler is created with the on function. The event being listened for is connection. This event is called each time a client connects to the server.

Let's now create our very basic client. Create a new file called index.html and place the following code within:

1
<!DOCTYPE html>
2
<html>
3
    <head>
4
		<title>Socket.IO Example</title>
5
	</head>
6
	<body>
7
		<script src="/socket.io/socket.io.js"></script>
8
		<script type="module">
9
			const socket = io();
10
		</script>
11
	</body>
12
</html>

The HTML above loads the Socket.io client JavaScript and initializes a connection to the server. To see the example, start your Node application: node index.js

Then, in your browser, navigate to http://localhost:3000. Nothing will appear on the page; however, if you look at the console where the Node application is running, two messages are logged:

  1. HTTP server started on port 3000
  2. Client connection received

Now that we have a successful socket connection, let's put it to use. Let's begin by sending a message from the server to the client. Then, when the client receives the message, it can send a response back to the server.

Let's look at the abbreviated index.js file:

1
io.on("connection", function (socket) {
2
    console.log("Client connection received");
3
	socket.emit("sendToClient", { hello: "world" });
4
	socket.on("receivedFromClient", function (data) {
5
		console.log(data);
6
	});
7
});

The previous io.on function has been updated to include a few new lines of code. The first, socket.emit, sends the message to the client. The sendToClient is the name of the event. By naming events, you can send different types of messages so the client can interpret them differently. The second addition is the socket.on, which also contains an event name: receivedFromClient. This creates a function that accepts data from the client. In this case, the data is logged to the console window.

That completes the server-side amendments; it can now send and receive data from any connected clients.

Let's complete this example by updating the client to receive the sendToClient event. When it receives the event, it can respond with the receivedFromClient event back to the server.

This is accomplished in the JavaScript portion of the HTML, so in the index.html file, I have updated the JavaScript as follows:

1
const socket = io();
2
3
socket.on('sendToClient', function (data) {
4
    console.log(data);
5
    socket.emit('receivedFromClient', { my: 'data' });
6
});

Using the instantiated socket variable, we have very similar logic on the server with a socket.on function. For the client, it is listening to the sendToClient event. As soon as the client is connected, the server sends this message. When the client receives it, it is logged to the console in the browser. The client then uses the same socket.emit that the server used to send the original event. In this instance, the client sends back the receivedFromClient event to the server. When the server receives the message, it is logged to the console window.

Try it out for yourself. First, in a console, run your Node application: node index.js. Then load http://localhost:3000 in your browser.

Check the web browser console and you should see the following JSON data logged: {hello: "world"}

Then, in the command prompt where the Node application is running, you should see the following:

1
HTTP server started on port 3000
2
Client connection received
3
{ my: 'data' }

Both the client and server can use the JSON data received to perform specific tasks. We will learn more about that once we connect to the real-time sports data.

Sports Data

Now that we have mastered how to send and receive data to and from the client and server, this can be leveraged to provide real-time updates. I chose to use sports data, although the same theory is not limited to sports. Before I began this project, I researched different sports data. The one I settled on, because they offer free developer accounts, was MySportsFeeds (I am not affiliated with them in any way). To access the real-time data, I signed up for an account and then made a small donation. Donations start at $1 to have data updated every 10 minutes. This will be good for the example.

Once your account is set up, you can proceed to setting up access to their API. To assist with this, I am going to use their NPM package: npm install mysportsfeeds-node --save

After the package has been installed, API calls can be made as follows:

1
const MySportsFeeds = require("mysportsfeeds-node");
2
3
const msf = new MySportsFeeds("1.2", true);
4
msf.authenticate("********", "*********");
5
6
const today = new Date();
7
8
msf.getData('nhl', '2017-2018-regular', 'scoreboard', 'json', { 
9
    fordate: today.getFullYear() + 
10
    	('0' + parseInt(today.getMonth() + 1)).slice(-2) + 
11
		('0' + today.getDate()).slice(-2),
12
	force: true
13
});

In the example above, be sure to replace the call to the authenticate function with your username and password.

The following code executes an API call to get the NHL scoreboard for today. The fordate variable is what specifies today. I've also set force to true so that a response is always returned, even when the data has not changed.

With the current setup, the results of the API call get written to a text file. In the final example, this will be changed; however, for demonstration purposes, the results file can be reviewed in a text editor to understand the contents of the response. The results contain a scoreboard object. This object contains an array called gameScore. This object stores the result of each game. Each object contains a child object called game. This object provides the information about who is playing.

Outside of the game object, there are a handful of variables that provide the current state of the game. The data changes based on the state of the game. For example, when the game hasn't started, there are only a few variables that tell us the game is not in progress and has not started.

When the game is in progress, additional data is provided about the score, what period the game is in, and how much time is remaining. We will see this in action when we get to the HTML to show the game in the next section.

Real-Time Updates

We have all the pieces to the puzzle, so it is now time to put the puzzle together to reveal the final picture. Currently, MySportsFeeds has limited support for pushing data to us, so we will have to poll the data from them. Luckily, we know the data only changes once every 10 minutes, so we don't need to add overhead by polling for changes too frequently. Once we poll the data from them, we can push those updates from the server to all clients connected.

To perform the polling, I will use the JavaScript setInterval function to call the API (in my case) every 10 minutes to look for updates. When the data is received, an event is sent to all of the connected clients. When the clients receive the event, the game scores will be updated with JavaScript in the web browser.

MySportsFeeds will also be called when the Node application first starts up. This data will be used for any clients who connect before the first 10-minute interval. This is stored in a global variable. This same global variable is updated as part of the interval polling. This will ensure that when any new clients connect after the polling, they will have the latest data.

To assist with some code cleanliness in the main index.js file, I have created a new file called data.js. This file will contain a function that is exported (available in the index.js file) that performs the previous call to the MySportsFeeds API. Here are the full contents of that file:

1
const MySportsFeeds = require("mysportsfeeds-node");
2
3
const msf = new MySportsFeeds("1.2", true, null);
4
msf.authenticate("*******", "******");
5
6
const today = new Date();
7
8
exports.getData = function () {
9
    return msf.getData("nhl", "2017-2018-regular", "scoreboard", "json", {
10
		fordate:
11
			today.getFullYear() +
12
			("0" + parseInt(today.getMonth() + 1)).slice(-2) +
13
			("0" + today.getDate()).slice(-2),
14
		force: true,
15
	});
16
};

A getData function is exported and returns the result of the call, which in this case is a Promise that will be resolved in the main application.

Now let's look at the final contents of the index.js file:

1
const app = require("express")();
2
const http = require("http").Server(app);
3
const io = require("socket.io")(http);
4
const data = require("./data.js");
5
6
// Global variable to store the latest NHL results

7
let latestData;
8
9
// Load the NHL data for when client's first connect

10
// This will be updated every 10 minutes

11
data.getData().then((result) => {
12
    latestData = result;
13
});
14
15
app.get("/", function (req, res) {
16
	res.sendFile(__dirname + "/index.html");
17
});
18
19
http.listen(3000, function () {
20
	console.log("HTTP server started on port 3000");
21
});
22
23
io.on("connection", function (socket) {
24
	// when clients connect, send the latest data

25
	socket.emit("data", latestData);
26
});
27
28
// refresh data

29
setInterval(function () {
30
	data.getData().then((result) => {
31
		// Update latest results for when new client's connect

32
		latestData = result;
33
34
		// send it to all connected clients

35
		io.emit("data", result);
36
37
		console.log("Last updated: " + new Date());
38
	});
39
}, 300000);

The first seven lines of code above instantiate the required libraries and the global latestData variable. The final list of libraries used are: Express, HTTP, Socket.io, and the aforementioned data.js file just created.

With the necessities taken care of, the application populates the latestData for clients who will connect when the server is first started:

1
// Global variable to store the latest NHL results

2
const latestData;
3
4
// Load the NHL data for when client's first connect

5
// This will be updated every 10 minutes

6
data.getData().then((result) => { 
7
    latestData = result;
8
});

The next few lines set up a route for the root page of the website (http://localhost:3000/) and start the HTTP server to listen on port 3000.

Next, the Socket.io is set up to look for connections. When a new connection is received, the server emits an event called data with the contents of the latestData variable.

And finally, the final chunk of code creates the polling interval. When the interval occurs, the latestData variable is updated with the results of the API call. This data then emits the same data event to all clients.

1
// refresh data

2
setInterval(function() {
3
    data.getData().then((result) => { 
4
		// Update latest results for when new client's connect

5
		latestData = result; 
6
	
7
		// send it to all connected clients

8
		io.emit('data', result);
9
		
10
		console.log('Last updated: ' + new Date());
11
	});
12
}, 300000);

You may notice that when the client connects and an event is emitted, it is emitting the event with the socket variable. This approach will send the event to that connected client only. Inside the interval, the global io is used to emit the event. This will send the event to all clients.

That completes the server. Let's work on the client front-end. In an earlier example, I created a basic index.html file that set up the client connection that would log events from the server and send one back. I am going to extend that file to contain the completed example.

Because the server is sending us a JSON object, I am going to use Preact, which is like an optimized version of React (if you are not familiar with React, that is fine). Additionally, I will use HTM. HTM will allow me to use syntax like React's JSX without the build tools. Additionally, it includes integration with Preact.

First, I will need to create a div with an id of games

1
<div id="games"></div>

Then, I will create the template. Here is the full script for the template (you will need to put this in the primary HTML script):

1
import { html, render } from "https://esm.sh/htm/preact";
2
import { signal } from "https://esm.sh/@preact/signals";
3
const games = signal([]);
4
const socket = io();
5
socket.on("data", function (data) {
6
    games.value = data;
7
});
8
9
function ordinalSuffix(input) {
10
	const tenRemainder = input % 10,
11
		hundredRemainer = input % 100;
12
	if (tenRemainder == 1 && hundredRemainer != 11) {
13
		return input + "st";
14
	}
15
	if (tenRemainder == 2 && hundredRemainer != 12) {
16
		return input + "nd";
17
	}
18
	if (tenRemainder == 3 && hundredRemainer != 13) {
19
		return input + "rd";
20
	}
21
	return input + "th";
22
}
23
function timeLeft(time) {
24
	const minutes = Math.floor(time / 60);
25
	const seconds = time - minutes * 60;
26
27
	return minutes + ":" + ("0" + seconds).slice(-2);
28
}
29
function stats() {
30
	return html`${games.value.forEach(
31
		(game) =>
32
			html`<div class="game">

33
				<div>

34
					${game.game.awayTeam.City} ${game.game.awayTeam.Name} at at

35
					${game.game.homeTeam.City} ${game.game.homeTeam.Name}

36
				</div>

37
				<div>

38
					${(() => {
39
						if (game.isUnplayed) {
40
							return `Game Starts at ${game.game.time}`;
41
						} else if (game.isCompleted === "false") {
42
							return html`<div>

43
									Current Score: ${game.awayScore} - ${game.homeScore}

44
								</div>

45
								<div>

46
									${(() => {
47
										if (game.currentIntermission) {
48
											return `${ordinalPrefix(
49
												game.currentIntermission
50
											)} Intermission`;
51
										} else if (game.currentPeriod) {
52
											return html`${ordinalPrefix(
53
													game.currentPeriod
54
												)}<br />${timeLeft(
55
													game.currentPeriodSecondsRemaining
56
												)}`;
57
										} else {
58
											return `1st`;
59
										}
60
									})()}

61
								</div>`;
62
						} else {
63
							return `Final Score: ${game.awayScore} - ${game.homeScore}`;
64
						}
65
					})()}

66
				</div>

67
			</div>`
68
	)}`;
69
}
70
render(stats, document.getElementById("games"));

That is a lot! Let's go through this step by step. First, we import Preact, HTM, and something called Preact Signals. We will talk more about that later.

Next, we establish the WebSocket connection. This code is the same as the code we had earlier, except for the difference in event name and what we do with the data. You might notice that the object we assign the data to is a signal. This is a fast way of managing state in Preact. You can read more about it on the page for Preact Signals.

Next, we have some helper functions, which we will use later in the actual template. After that, we have the template component. In the first bit, we iterate through all of the games and return markup for each game.

The first part of the markup shows the teams. Then, we get into the main part of the game data.

In the next section, we first check if the game has started yet. If not, we show when the game will start. If it has started, we show the current score, as well as the current period and time left. This is where the helper functions are used.

Finally, if the game has ended, we just show the final score. The final line of the script is just to render the template in the div we created earlier.

Here is an example of what it looks like when there is a mix of finished games, games in progress, and games that have not started yet. I'm not a very good designer, so it looks as you would expect when a developer makes their own user interface. If you want, you can create your own CSS styles.

An example of finished games

Here is the HTML and JavaScript together.

1
<!DOCTYPE html>
2
<html>
3
    <head>
4
		<title>Socket.IO Example</title>
5
	</head>
6
	<body>
7
		<script src="/socket.io/socket.io.js"></script>
8
		<script type="module">
9
			import { html, render } from "https://esm.sh/htm/preact";
10
			import { signal } from "https://esm.sh/@preact/signals";
11
			const games = signal([]);
12
			const socket = io();
13
			socket.on("data", function (data) {
14
				games.value = data;
15
			});
16
17
			function ordinalSuffix(input) {
18
				const tenRemainder = input % 10,
19
					hundredRemainer = input % 100;
20
				if (tenRemainder == 1 && hundredRemainer != 11) {
21
					return input + "st";
22
				}
23
				if (tenRemainder == 2 && hundredRemainer != 12) {
24
					return input + "nd";
25
				}
26
				if (tenRemainder == 3 && hundredRemainer != 13) {
27
					return input + "rd";
28
				}
29
				return input + "th";
30
			}
31
			function timeLeft(time) {
32
				const minutes = Math.floor(time / 60);
33
				const seconds = time - minutes * 60;
34
35
				return minutes + ":" + ("0" + seconds).slice(-2);
36
			}
37
			function stats() {
38
				return html`${games.value.forEach(
39
					(game) =>
40
						html`<div class="game">

41
							<div>

42
								${game.game.awayTeam.City} ${game.game.awayTeam.Name} at

43
								${game.game.homeTeam.City} ${game.game.homeTeam.Name}

44
							</div>

45
							<div>

46
								${(() => {
47
									if (game.isUnplayed) {
48
										return `Game Starts at ${game.game.time}`;
49
									} else if (game.isCompleted === "false") {
50
										return html`<div>

51
												Current Score: ${game.awayScore} - ${game.homeScore}

52
											</div>

53
											<div>

54
												${(() => {
55
													if (game.currentIntermission) {
56
														return `${ordinalPrefix(
57
															game.currentIntermission
58
														)} Intermission`;
59
													} else if (game.currentPeriod) {
60
														return html`${ordinalPrefix(
61
																game.currentPeriod
62
															)}<br />${timeLeft(
63
																game.currentPeriodSecondsRemaining
64
															)}`;
65
													} else {
66
														return `1st`;
67
													}
68
												})()}

69
											</div>`;
70
									} else {
71
										return `Final Score: ${game.awayScore} - ${game.homeScore}`;
72
									}
73
								})()}

74
							</div>

75
						</div>`
76
				)}`;
77
			}
78
			render(stats, document.getElementById("games"));
79
		</script>
80
		<div id="games"></div>
81
	</body>
82
</html>

Start the Node application and browse to http://localhost:3000 to see the results for yourself!

Every X minutes, the server will send an event to the client. The client will redraw the game elements with the updated data. So when you leave the site open and periodically look at it, you will see the game data refresh when games are in progress.

Conclusion

The final product uses Socket.io to create a server that clients connect to. The server fetches data and sends it to the client. When the client receives the data, it can seamlessly update the display. This reduces load on the server because the client only performs work when it receives an event from the server.

Sockets are not limited to one direction; the client can also send messages to the server. When the server receives the message, it can perform some processing.

Chat applications would commonly work this way. The server would receive a message from the client and then broadcast to all connected clients to show that someone has sent a new message.

Hopefully you enjoyed this article as I had a blast creating this real-time sports application for one of my favorite sports!

This post has been updated with contributions from Jacob Jackson. Jacob is a web developer, technical writer, freelancer, and open-source contributor.

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.