How to create a simple and beautiful chat with MongoDB, Express, React and Node.js (MERN stack)

armelpingault

Armel

Posted on December 30, 2019

How to create a simple and beautiful chat with MongoDB, Express, React and Node.js (MERN stack)

Recently, I worked on an interesting project called SpeedBoard which is a real-time board for Agile and Scrum retrospectives. It's the kind of tool we use at work after our Scrum Sprint review to easily share our feedback about the last Sprint.

Since it was a very enriching experience, I thought that I would do a quick tutorial on how to set up a simple chat with the same technology stack which includes: MongoDB, Express, React, Node.js and is also called the MERN stack. I am also using Socket.IO for the real-time engine and Material-UI which is a UI framework for React based on Material Design.

If you don't want to wait until the end of this tutorial, you can already check a preview of the final result, and also check the Github repository if you want to fork it and start to improve it ;)

Prerequisites

In this tutorial, we will use Heroku for hosting our live project and Github for hosting our code and deploying it to Heroku, so make sure you already have an account with them, they both provide a free sign up.

Structure

Before we start, let's have a quick look at the structure of our project. Inside our root folder, we will have 2 subfolders: one called client which contains the React app and one called server with our Node.js server:

speedchatapp/
├── client/
├── server/
Enter fullscreen mode Exit fullscreen mode

Let's open our Terminal and create our project folder:

mkdir speedchatapp
cd speedchatapp/
Enter fullscreen mode Exit fullscreen mode

Set up the client

On the client-side, we will use the Create React App (CRA) which provides a very easy way to start building any React SPA.

CRA provides a very simple command to install the app, but first, let's ensure that npx is using the latest version if you used create-react-app in the past:

npm uninstall -g create-react-app
Enter fullscreen mode Exit fullscreen mode

Now, let's create our app in our client folder with this simple command:

npx create-react-app client
Enter fullscreen mode Exit fullscreen mode

This might take a couple of minutes to install all the dependencies, and once you are done, try:

cd client/
npm start
Enter fullscreen mode Exit fullscreen mode

You should now be able to access your app at http://localhost:3000/

Create React App

That was quick and simple : ) But still pretty far from our final result! We'll come back a little bit later to our React app once the server-side of our project is ready.

Set up the server

Now that we have the skeleton of our client ready, let's have a look at the backend side.

First, let's create our server folder at the root of our project and initialize our package.json file:

mkdir server
cd server/
npm init
Enter fullscreen mode Exit fullscreen mode

A utility will take you through the configuration of the file but you can type Enter for all options for this tutorial.

Now, we will install all the dependencies required for our server (Express, Mongoose and Socket.IO) with the following command:

npm install express mongoose socket.io --save
Enter fullscreen mode Exit fullscreen mode

Then, copy the .gitignore file from the client folder to the server folder to prevent some files and folders to be pushed to our GitHub repository (e.g. /node_modules folder):

cp ../client/.gitignore ./
Enter fullscreen mode Exit fullscreen mode

We will create the 2 files necessary for our server to work. The first one (Message.js) is the schema of the documents we will keep in our database. We will need 3 information: the name of the user who is posting a message in the chat, the content of its message and a timestamp to know when he posted his message.

server/Message.js

const mongoose = require('mongoose');

const messageSchema = new mongoose.Schema({
  content: String,
  name: String,
}, {
  timestamps: true,
});

module.exports = mongoose.model('Message', messageSchema);
Enter fullscreen mode Exit fullscreen mode

The second one (index.js) is our main file, I won't go too much into details because that would make this tutorial a bit too long, but feel free to ask any question in the comments, I'll be glad to answer them or improve the comments directly in the code if necessary.

server/index.js

const express = require('express');
const app = express();
const http = require('http').Server(app);
const path = require('path');
const io = require('socket.io')(http);

const uri = process.env.MONGODB_URI;
const port = process.env.PORT || 5000;

const Message = require('./Message');
const mongoose = require('mongoose');

mongoose.connect(uri, {
  useUnifiedTopology: true,
  useNewUrlParser: true,
});

app.use(express.static(path.join(__dirname, '..', 'client', 'build')));

io.on('connection', (socket) => {

  // Get the last 10 messages from the database.
  Message.find().sort({createdAt: -1}).limit(10).exec((err, messages) => {
    if (err) return console.error(err);

    // Send the last messages to the user.
    socket.emit('init', messages);
  });

  // Listen to connected users for a new message.
  socket.on('message', (msg) => {
    // Create a message with the content and the name of the user.
    const message = new Message({
      content: msg.content,
      name: msg.name,
    });

    // Save the message to the database.
    message.save((err) => {
      if (err) return console.error(err);
    });

    // Notify all other users about a new message.
    socket.broadcast.emit('push', msg);
  });
});

http.listen(port, () => {
  console.log('listening on *:' + port);
});
Enter fullscreen mode Exit fullscreen mode

The structure of your project should now look like this:

speedchatapp/
├── client/
│   └── (Several files and folders)
└── server/
    ├── node_modules/
    ├── .gitignore
    ├── index.js
    ├── Message.js
    ├── package-lock.json (auto-generated)
    └── package.json
Enter fullscreen mode Exit fullscreen mode

Before coming back to our React app to finish our project, let's set up our Heroku hosting and link it to our Github repository to make sure the deployment works fine.

Set up our Heroku hosting

Let's download and install the Heroku CLI to set up everything from our Terminal.

Once downloaded and installed, let's go back to our Terminal and login to our Heroku account:

heroku login
Enter fullscreen mode Exit fullscreen mode

It will open a new tab in your browser and once you are logged in, you can close the browser tab and go back to your Terminal.

Now let's create our new app that will host our project:

heroku create
Enter fullscreen mode Exit fullscreen mode

It will automatically generate an identifier with a URL where you can access your app, it should look like this:

https://sleepy-meadow-81798.herokuapp.com/

Heroku App

You can rename your app if you want something a little bit easier to remember, you can then use it for the rest of this tutorial:

Rename the app in Heroku UI

Alright, now we need our MongoDB database to store the chat messages from the users. Let's add the mongolab addon to our app:

heroku addons:create mongolab --app speedchatapp
Enter fullscreen mode Exit fullscreen mode

I used speedchatapp in the previous command because I renamed my application but you should use the one provided when you created it if you didn't rename it, for example, sleepy-meadow-81798.

Once created it will show you the name of a variable in green, i.e MONGODB_URI. Now let's get the configuration URI of our newly created database:

heroku config:get MONGODB_URI
Enter fullscreen mode Exit fullscreen mode

You should see something like this:

mongodb://heroku_123abc:abc123@ds141188.mlab.com:41188/heroku_123abc
Enter fullscreen mode Exit fullscreen mode

Copy this URI, and create a file at the root of your project called .env with the following content [VARIABLE_IN_GREEN]=[URI]. It should look like this:

MONGODB_URI=mongodb://heroku_123abc:abc123@ds141188.mlab.com:41188/heroku_123abc
Enter fullscreen mode Exit fullscreen mode

Let's copy one more time the .gitignore and add the .env file at the end of it to avoid pushing the credentials of our database to GitHub:

cp server/.gitignore ./
echo '.env' >> .gitignore
Enter fullscreen mode Exit fullscreen mode

During the deployment of our app, we need to tell Heroku how to start our server. It can be done by using a Procfile that we will put at the root of our project. So let's create it and add the command line that will start our server:

echo 'web: node server/index.js' > Procfile
Enter fullscreen mode Exit fullscreen mode

Now let's initialize another package.json at the root of our project. Same as before, don't worry about all the options, for now, just type Enter at all prompts:

npm init
Enter fullscreen mode Exit fullscreen mode

One last thing we want to do here is to install the npm package called Concurrently that will allow us to run both the server and the client in a single command line during our development mode:

npm install --save-dev concurrently
Enter fullscreen mode Exit fullscreen mode

And finally, in our newly created package.json at the root of the project, we will add 2 lines in the scripts section:

"scripts": {
    "dev": "concurrently --kill-others \"heroku local\" \"npm run start --prefix ./client\"",
    "postinstall": "npm install --prefix ./server && npm install --prefix ./client && npm run build --prefix ./client",
}
Enter fullscreen mode Exit fullscreen mode

The postinstall command, as you can guess, will be executed after Heroku has finished running the npm install command at the root of our folder. It's telling Heroku to also run the npm install command inside our client and server folder and will also build our React app for production.

Now, it's time to test it, go to the root of your project and type:

npm run dev
Enter fullscreen mode Exit fullscreen mode

This will launch the server and our React app in development mode, and it should open a window in your browser with the previous landing page of our React app.

In your terminal, you should see something like this:

> concurrently --kill-others "heroku local" "npm run start --prefix ./client"

[1] 
[1] > react-scripts start
[1] 
[0] [OKAY] Loaded ENV .env File as KEY=VALUE Format
[0] 12:16:15 PM web.1 |  listening on *:5000
[1] Starting the development server...
[1] 
[1] Compiled successfully!
[1] 
[1] You can now view client in the browser.
[1] 
[1]   Local:            http://localhost:3000/
[1]   On Your Network:  http://192.168.0.10:3000/
[1] 
[1] Note that the development build is not optimized.
[1] To create a production build, use npm run build.
Enter fullscreen mode Exit fullscreen mode

Note: we are using the same database for both Dev and Live mode, if you want to use a different database, you can always create another one in Heroku like we have seen before and update your .env file with the credentials of your new database to make sure it won't interfere with the one in production.

Set up GitHub and link to Heroku

Now, we are going create a new repository on GitHub, and we are going to connect it to Heroku so every time we will merge a Pull Request on the master branch, it will automatically deploy it on Heroku.

Let's create our repository on GitHub. Go to https://github.com/new:

Create a new repository on Github

Write down the repository URL that we will use in the next step. Back to our Terminal, in the root folder of our project:

// Initialize the root folder as a Git repository
git init 

// Add all the files for the initial commit
git add .

// Commit staged files
git commit -m "Initial commit"

// Set the GitHub remote repository
git remote add origin <repository url>

// Push the local changes to GitHub
git push origin master
Enter fullscreen mode Exit fullscreen mode

Now our code is on GitHub, let's link this repository to our Heroku app.

From the Heroku UI, select your app and click on the Deploy tab. In the Deployment method, click on Github, type your repository name and click on Connect:

Connect to GitHub

Also, make sure that the "Enable Automatic Deploys" on the master branch is activated:

Enable Automatic Deploys

It should now look like this:

Automatic Deploys Enabled

Now let's trigger a first manual deployment to check that everything is fine. Click on the Deploy Branch and wait until you see you see Your app was successfully deployed.

Finally, after clicking on the Open App button at the top right of the page, you should see the React app on your Heroku hosting.

From now on, after pushing any update to your GitHub repository, you should see the deployment triggered automatically in your Heroku UI:

Deployment triggered

Finishing the client

Now that the architecture of our project is ready, let's finish our clientReact app.

The first thing we will need here is to install our frontend dependencies in the client folder: Socket.IO for client, Material-UI core and icons:

cd client/
npm install socket.io-client @material-ui/core @material-ui/icons --save
Enter fullscreen mode Exit fullscreen mode

Now in the client/package.json, add the following proxy field at the end of the file:

"proxy": "http://localhost:5000"
Enter fullscreen mode Exit fullscreen mode

It will tell the development server to proxy any unknown requests to your server in development. Check the official documentation for more information.

Next, we'll create a config.js file to tell our app to switch endpoints in case we are on our local machine or live hosting:

client/src/config.js

import pkg from '../package.json';

export default {
  development: {
    endpoint: pkg.proxy
  },
  production: {
    endpoint: window.location.hostname
  }
}
Enter fullscreen mode Exit fullscreen mode

Okay now let's start our local development environment from our root folder:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Last steps

For the last step, either create or update each file below manually or go directly to the GitHub repository to check out the project.

Replace client/src/App.css:

body {
  background: #f5f5f5;
  padding: 16px;
}

#chat {
  max-height: calc(100vh - 128px);
  overflow: scroll;
  padding: 16px;
}

.name {
  color: rgba(0, 0, 0, 0.54);
}

.content {
  margin-bottom: 8px;
}
Enter fullscreen mode Exit fullscreen mode

Replace client/src/App.js:

import React from 'react';
import config from './config';
import io from 'socket.io-client';

import Paper from '@material-ui/core/Paper';
import Typography from '@material-ui/core/Typography';

import BottomBar from './BottomBar';
import './App.css';

class App extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      chat: [],
      content: '',
      name: '',
    };
  }

  componentDidMount() {
    this.socket = io(config[process.env.NODE_ENV].endpoint);

    // Load the last 10 messages in the window.
    this.socket.on('init', (msg) => {
      let msgReversed = msg.reverse();
      this.setState((state) => ({
        chat: [...state.chat, ...msgReversed],
      }), this.scrollToBottom);
    });

    // Update the chat if a new message is broadcasted.
    this.socket.on('push', (msg) => {
      this.setState((state) => ({
        chat: [...state.chat, msg],
      }), this.scrollToBottom);
    });
  }

  // Save the message the user is typing in the input field.
  handleContent(event) {
    this.setState({
      content: event.target.value,
    });
  }

  //
  handleName(event) {
    this.setState({
      name: event.target.value,
    });
  }

  handleSubmit(event) {
    // Prevent the form to reload the current page.
    event.preventDefault();

    // Send the new message to the server.
    this.socket.emit('message', {
      name: this.state.name,
      content: this.state.content,
    });

    this.setState((state) => {
      // Update the chat with the user's message and remove the current message.
      return {
        chat: [...state.chat, {
          name: state.name,
          content: state.content,
        }],
        content: '',
      };
    }, this.scrollToBottom);
  }

  // Always make sure the window is scrolled down to the last message.
  scrollToBottom() {
    const chat = document.getElementById('chat');
    chat.scrollTop = chat.scrollHeight;
  }

  render() {
    return (
      <div className="App">
        <Paper id="chat" elevation={3}>
          {this.state.chat.map((el, index) => {
            return (
              <div key={index}>
                <Typography variant="caption" className="name">
                  {el.name}
                </Typography>
                <Typography variant="body1" className="content">
                  {el.content}
                </Typography>
              </div>
            );
          })}
        </Paper>
        <BottomBar
          content={this.state.content}
          handleContent={this.handleContent.bind(this)}
          handleName={this.handleName.bind(this)}
          handleSubmit={this.handleSubmit.bind(this)}
          name={this.state.name}
        />
      </div>
    );
  }
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Create client/src/BottomBar.js:

import React from 'react';

import { fade, makeStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import InputBase from '@material-ui/core/InputBase';
import Toolbar from '@material-ui/core/Toolbar';

import ChatIcon from '@material-ui/icons/Chat';
import FaceIcon from '@material-ui/icons/Face';

const useStyles = makeStyles(theme => ({
  appBar: {
    bottom: 0,
    top: 'auto',
  },
  inputContainer: {
    backgroundColor: fade(theme.palette.common.white, 0.15),
    '&:hover': {
      backgroundColor: fade(theme.palette.common.white, 0.25),
    },
    borderRadius: theme.shape.borderRadius,
    marginLeft: theme.spacing(1),
    position: 'relative',
    width: '100%',
  },
  icon: {
    width: theme.spacing(7),
    height: '100%',
    position: 'absolute',
    pointerEvents: 'none',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
  },
  inputRoot: {
    color: 'inherit',
  },
  inputInput: {
    padding: theme.spacing(1, 1, 1, 7),
    width: '100%',
  },
}));

export default function BottomBar(props) {
  const classes = useStyles();

  return (
    <AppBar position="fixed" className={classes.appBar}>
      <Toolbar>
        <div className={classes.inputContainer} style={{maxWidth: '200px'}}>
          <div className={classes.icon}>
            <FaceIcon />
          </div>
          <InputBase
            onChange={props.handleName}
            value={props.name}
            placeholder="Name"
            classes={{
              root: classes.inputRoot,
              input: classes.inputInput,
            }}
            inputProps={{ 'aria-label': 'name' }}
          />
        </div>
        <div className={classes.inputContainer}>
          <form onSubmit={props.handleSubmit}>
            <div className={classes.icon}>
              <ChatIcon />
            </div>
            <InputBase
              onChange={props.handleContent}
              value={props.content}
              placeholder="Type your message..."
              classes={{
                root: classes.inputRoot,
                input: classes.inputInput,
              }}
              inputProps={{ 'aria-label': 'content' }}
            />
          </form>
        </div>
      </Toolbar>
    </AppBar>
  );
}
Enter fullscreen mode Exit fullscreen mode

Every time you update the code, you should see the project at http://localhost:3000 automatically reload with the last changes.

Finally, let's push our latest update to GitHub to trigger a new deployment on our live project:

git add .
git commit -m "Final update"
git push origin master
Enter fullscreen mode Exit fullscreen mode

Et voilà, Bob's your uncle! Our chat is now finished and ready: https://speedchatapp.herokuapp.com/

If you have any question, feel free to ask in the comments, I'll be glad to answer it and improve this tutorial. And feel free to fork the project to improve it ;)

💖 💪 🙅 🚩
armelpingault
Armel

Posted on December 30, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related