Your First Real-Time App in Node
Tags: javascript, node, tutorial, beginners
Published: 2019-12-05 18:05:04

Have you ever wondered how real-time apps like chat apps and online games are built? If you've never built one before, read this tutorial as I'm going to show you how to build a simple one using [socket.io](https://socket.io/).

## What are we going to build?

It's a simple app with one button and a label below it. The label displays "Likes: X" (where x is the current number of likes). When the user clicks on the button, the number of likes increases by one.

We're going to make it real time by showing users on the app how the number of likes increases as other users are clicking on the button. So you don't need to reload the page to see the latest value.

Here's how the app would look like:

![](https://thepracticaldev.s3.amazonaws.com/i/o29vbeqvo3nlg1uydwda.gif)

You can get the source code of this project on [GitHub](https://github.com/TahaSh/realtime-like-app).

## Creating a new project

In a new folder, add *package.json* using `npm init -y`, and then install these three packages:

```bash
npm install express ejs socket.io
```

We'll use [ejs](https://ejs.co/) as the templating engine, and *socket.io* for making our app a real-time app.

## Displaying a hello world page

As mentioned above, we'll use *ejs* for rendering our views. So create *index.ejs* and add the following:

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Realtime like app</title>
</head>
<body>
  Hello World!
</body>
</html>
```

Now let's create our node server and serve the above file as the homepage.

So create *node.js* and add this:

```js
const app = require('express')()
const path = require('path')

app.engine('html', require('ejs').renderFile)
app.set('view engine', 'html')

app.get('/', (req, res) => {
  res.render(path.join(__dirname + '/index.ejs'), null, (err, html) => {
    res.send(html)
  })
})

app.listen(3000, () => console.log('the app is running on localhost:3000'))
```

So we created a new server that runs on port `3000`. When the user hits *http://localhost:3000/* in the browser, we'll render *index.ejs* and display it.

If you run the app using `node index.js` (or using [nodemon](https://nodemon.io/) if you want the app to restart automatically on changes) and open *http://localhost:3000/*, you should see "Hello World!" displayed.

## Adding style.css

This isn't a CSS tutorial, so let's quickly add *style.css* in the root directory and fill it with this:

```css
body {
  background: hsl(0, 50%, 80%);
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  margin: 0;
  padding: 0;
}

button {
  background: hsl(0, 50%, 90%);
  border: none;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 150px;
  height: 150px;
  cursor: pointer;
  outline: none;
  box-shadow: 0 14px 28px hsla(0, 50%, 10%, 25%), 0 10px 10px hsla(0, 50%, 10%, 22%);
  transition: all 0.3s cubic-bezier(.25,.8,.25,1);
}

button:hover {
  box-shadow: 0 1px 3px hsla(0, 50%, 10%, 12%), 0 1px 2px hsla(0, 50%, 10%, 24%);
}

button:active {
  box-shadow: none;
}

svg path {
  fill: hsl(0, 30%, 30%);
}

.main {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.likes {
  margin-top: 20px;
  color: hsl(0, 60%, 20%);
  font-weight: bold;
  font-family: sans-serif;
  text-transform: uppercase;
  font-size: 20px;
}
```

Now let's tell our server about it so when we request it, it responds with this file.

Add this route in *index.js* (below the root route):

```js
app.get('/style.css', (req, res) => {
  res.sendFile(path.join(__dirname + '/style.css'))
})
```

And then let's use it in our *index.ejs* by adding this at the bottom of *<head>*:

```html
<link rel="stylesheet" href="./style.css">
```

## Displaying the button and the label

Open *index.ejs* and update it like this:

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Realtime like app</title>
  <link rel="stylesheet" href="./style.css">
</head>
<body>
  <div class="main">
    <button>
      <svg style="width:80px;height:80px" viewBox="0 0 24 24">
        <path fill="#000000" d="M12,21.35L10.55,20.03C5.4,15.36 2,12.27 2,8.5C2,5.41 4.42,3 7.5,3C9.24,3 10.91,3.81 12,5.08C13.09,3.81 14.76,3 16.5,3C19.58,3 22,5.41 22,8.5C22,12.27 18.6,15.36 13.45,20.03L12,21.35Z" />
      </svg>
    </button>
    <span class="likes">Likes: <%= likes %></span>
  </div>
</body>
</html>
```

For this to work, we have to pass `likes` from the server when rendering the template.

So open *index.js* and update the root route like this:

```js
let likes = 0

app.get('/', (req, res) => {
  res.render(path.join(__dirname + '/index.ejs'), { likes }, (err, html) => {
    res.send(html)
  })
})
```

Note how we defined *likes* above it.

To keep this example simple, we defined *likes* in the memory, which means its value will go back to 0 when the server restarts. Typically in real-world apps you'll have your data stored in the database.

## Incrementing likes by clicking on the button

To do so, we need to add a new route that increments likes and returns the new value. And then we'll make a request to this endpoint from *index.ejs*, when the user clicks on the button.

Let's define the route first, in *index.js*.

```js
app.post('/like', (req, res) => {
  likes++
  res.json({ likes })
})
```

So it's a POST endpoint at `/like`.

Now let's listen for the button's click event and send this request using the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch).

Add the following above `</body>`:

```js
<script>
  const btn = document.querySelector('button')
  const likesOutput = document.querySelector('.likes')
  btn.addEventListener('click', async () => {
    const response = await fetch('/like', { method: 'POST'})
    const data = await response.json()
    likesOutput.textContent = `Likes: ${data.likes}`
  })
</script>
```

The app is now ready to be used but without showing the updated value in realtime. So if you open the app in multiple browser windows and try to update one, you won't see the value updated in realtime on other browser windows until you reload them.

## Making it a real-time app

Socket.io isn't the only way to build real-time apps but it's the most popular one. Not only that, but it's also very good and easy to use.

We've already installed socket.io, so let's initialize it.

But before I show you how, note that socket.io is composed of two parts:

1. The server that we integrate with node http server.
2. The client library that we use on the browser to communicate with the server part.

To initialize the server part, open *index.js* and update the top part like this:

```js
const app = require('express')()
const http = require('http').createServer(app)
const path = require('path')
const io = require('socket.io')(http)
```

So we imported socket.io and passed it the http server object.

Now let's use the `http` object to run the server instead of `app`.

```js
http.listen(3000, () => console.log('the app is running on localhost:3000'))
```

With that, socket.io is initialized on the server!

When socket.io is initialized it exposes `/socket.io/socket.io.js` endpoint. This endpoint contains the JS file that we'll use in the browser to connect with socket.io. (So it's the socket.io's client library.)

Go to *index.ejs* and include that file above the `<script>` tag:

```html
<script src="/socket.io/socket.io.js"></script>
```

After you add this, you should have the `io` object exposed globally on the browser (check that from the console).

To connect the browser to the server, just call `io()` and store the returned socket in a variable.

So put the following at the top of your `<script>` section (above `const btn = document.querySelector('button')`).

```js
const socket = io()
```

Next, let's make the server listen for connections from clients.

Below all routes, add this into *index.js*:

```js
io.on('connection', socket => {
  console.log('a user connected!')
  socket.on('disconnect', () => {
    console.log('user disconnected!')
  })
})
```

To make sure it's working, open up the browser and check your terminal. When you open a new tab, you'll see *"a user connected!"*, and when you close it, you'll see *"user disconnected!"*.

The `socket` object is related to the connected user. Note how we listen for a disconnect event on that specific socket — not on all sockets.

Three steps left to complete this app:

1. When the user clicks on the like button, we should emit an event to the server about that.
2. When the server is notified about that, it re-emits that event with the new value of likes to all other users (excluding the user who clicked the button).
3. When other clients receives that event, they would update the likes label with the new value.

So step 1, let the server know that we updated the number of likes. To do this, open *index.ejs* and add this at the bottom of the button's click handler:

```js
socket.emit('likes:updated')
```

`likes:updated` is the name of the event — you can name it whatever you want.

Step 2, the server should listen to this event and re-emit it to other clients. So in *index.js* update your connection listener like this:

```js
io.on('connection', socket => {
  console.log('a user connected!')
  socket.on('likes:updated', () => {
    socket.broadcast.emit('likes:update', likes)
  })
  socket.on('disconnect', () => {
    console.log('user disconnected!')
  })
})
```

Using `socket.broadcast.emit` sends the event to all other users (excluding the sender).

Note that we're sending an event with a different name, `likes:update` (not `likes:updated`).

Our last step is to listen for this event on the client so we update the likes label.

So in *index.ejs*, before `</script>`, add this:

```js
socket.on('likes:update', likes => {
  likesOutput.textContent = `Likes: ${likes}`
})
```

That's it! Now our app is completely a real-time app!

If your code is not working, compare it with the source code of this demo on [GitHub](https://github.com/TahaSh/realtime-like-app) to make sure you haven't forgotten anything.