Showing posts with label express. Show all posts
Showing posts with label express. Show all posts

Sunday, March 6, 2022

Build a socket.io chat web app with node.js

Socket.IO is a JavaScript library for real-time web applications. It enables real-time, bi-directional communication between web clients and servers. 

This article addresses the very basic building blocks to creating a socket.io  application in Node.js. 

Source code: https://github.com/medhatelmasry/socket-chat.git

Pre-requisites

The following pre-requisites are assumed:
  • The latest versions of Node.js & npm are installed on your computer
  • You have some basic knowledge of Node.js and Express

Getting started

In a working directory, create a folder for our web app named socket-chat then change into that directory with the following commands:

mkdir socket-chat
cd socket-chat

Initialize a Node.js application with the following command:

npm init -y

Install packages ejsexpress, nodemon and  socket.io with the following command:

npm install ejs express nodemon socket.io

We need to create our server. To accomplish this, create a file in the root of your app named index.js with the the following content:

const express = require("express");
const app = express();
const server = require("http").createServer(app);
const io = require("socket.io")(server, { cors: { origin: "*" } }); 

app.set("view engine", "ejs");

app.get("/", (req, res) => {
  res.render("index", {title: "Socket.io chat example"});
}); 

const port = process.env.PORT || 3030;
server.listen(port, () => {
  console.log(`Server started and listening on port ${port}`);
});

// emit & receive messages
io.on('connection', (socket) => {
  console.log(`Someone connected with socket id: ${socket.id}`);
});

What does the above code do?

  • we initialize express, server, and io 
  • EJS is declared as our view engine
  • the / route points to a view page named index. This will be created later.
  • the server will be started on port 3030
  • whenever someone connects, the unique socket id is displayed in the server console

Edit file package.json and add the following to the "scripts" section:

"dev": "nodemon index.js"

The above allows us to run the web app using command "npm run dev". The advantage of nodemon is that it watches for any changes in the file system and restarts the server whenever a file changes.
 
Create a folder named views and add to it a file named index.ejs with the following content:

<!DOCTYPE html>
<html lang="en">
<head>
    <title><%- title %></title>
    <script src="https://cdn.socket.io/socket.io-3.0.1.min.js"></script>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"/>
</head>
<body>
    <div class="container">
        <h3><%- title %></h3>
    </div>
</body>
</html>

Also in views/index.ejs, add the following JavaScript code below the closing </div> tag:

<script>
    const socket = io('http://localhost:3030');
    socket.on("connection", () => {});
</script>

The above code does a web socket hand-shake with the server. Start your server, if it is not started already, then point your browser to http://localhost:3030. The page looks like this:


More interesting is that your console window reveals information about socket IDs:


This proves that we have established a socket.io connection between the server and clients. Let us write some code that sends messages from one client to all the others. To do that, we will create a simple input field and a button in our frontend view. Add the following markup in views/index.ejs right under " <h3><%- title %></h3>":

<input type="text" id="msg" />
<button onclick="sendMessage()" class="btn btn-success btn-sm">
   Send Message
</button>
<hr />
<ul id="msgList" class="list-group"></ul>

In our backend index.js file,  add the following just under the console.log(. . .) statement:

socket.on('chat', (data) => {
   console.log(data);
   // broadcast the same message to all except the sender
   socket.broadcast.emit('chat', data);
});

The above code indicates that when a message named "chat" is received, it will broadcast that same message to all other clients except the sender. The variable data represents the actual message.

Now, back in our views/index.ejs frontend, we will actually emit (or send) a message. Type the following code right under "socket.on("connection", () => {});":

const sendMessage = () => {
   const msgInput = document.querySelector("#msg");
   socket.emit("chat", msgInput.value);
   document.querySelector("#msg").value = "";
}

The above is the sendMessage() handler for the button click. It sends the text entered by user to the message event named chat and, subsequently, clears the input field.

To actually see the message that is being passed around, we need to add some code to the frontend that will append each chat message received to the <ul> . . . </ul> unordered list with id msgList. Add this code below the "const sendMessage = () => { . . . . });" block: 

socket.on("chat", (data) => {
  const msg = data
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
  var li = document.createElement("li");
  li.classList.add("list-group-item", "list-group-item-warning");
  li.textContent = msg;
  document.querySelector("#msgList").appendChild(li);
});

What does the above code do?

  • data is sanitized such that & is replaced by &amp; and < is replaced by &lt; and > is replaced by &gt;
  • an li element is created and given bootstrap styling
  • data is assigned as content to the li element
  • finally, the li item is appended to the unordered list with id msgList

Testing our chat app

Point two (or more) browser windows to http://localhost:3030. In the first window enter a message then click on "Send message". You will notice the second window will pickup the same message:






This is a very simple socket.io app. I hope you will use this starting point to do much more useful and interesting applications.

Sunday, February 20, 2022

Node.js express, SQLite, CSV and Google Charts

In this article, I will develop a Node.js Express application that imports data from a CSV file into a SQLite database and render a pie chart using Google Charts. The purpose of this post is to familiarize the reader with Node Express, importing a CSV file into SQLite and, subsequently, rendering a pie chart with the data.

SQLite is the most used database engine in the world. SQLite is built into all mobile phones and most computers. Even though it is not customary to use SQLite with MongoDB apps, it is worth exploring as this database engine is self-contained and does not need a database server.

Source code: https://github.com/medhatelmasry/xpress-sqlite-chart

The following is assumed about your environment:

  • Node.js & NPM are installed on your computer
  • You have installed nodemon
  • You already have some familiarity with Node.js

Initializing our node express app

Let us first create a skeleton template of a simple Node.js Express application. This application will focus of APIs, so there will be no need to use any view engines. 

In a working directory, create a subdirectory named xpress-sqlite-chart for our new Node.js application with:

mkdir xpress-sqlite-chart

cd xpress-sqlite-chart

Let us initialize a new node app with:

npm init --yes

This creates a package.json that looks like this:

{
  "name": "xpress-sqlite-chart",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Edit the package.json as follows:

1) Change the value for main: from index.js to server.js:

2) add another script >> "start": "node server.js" 

The resulting package.json will look like this, with changes highlighted:

{
  "name": "xpress-sqlite-chart",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

We need to add some packages with the following npm command:

npm install express csvtojson sqlite3

The three above packages are self-explanatory for those familiar with Node.js.

Add folders configdata, database, public, and routes as shown below:



Sample data

Inside the data folder, create a text file named students.csv with the following content:

 

FirstName,LastName,School
Ann,Fay,Mining
Joe,Sun,Nursing
Ben,Ray,Mining
Zoe,Cox,Business
Sam,Ray,Mining
Dan,Ash,Medicine
Pat,Lee,Computing
Kim,Day,Nursing
Tim,Rex,Computing
Rob,Ram,Business
Jan,Fry,Mining
Jim,Tex,Nursing
Ben,Kid,Business
Mia,Chu,Medicine
Ted,Tao,Computing
Amy,Day,Business
Ian,Roy,Nursing
Liz,Kit,Nursing
Mat,Tan,Medicine
Deb,Roy,Medicine
Ana,Ray,Medicine
Lyn,Poe,Computing
Amy,Raj,Nursing
Kim,Ash,Mining
Bec,Kid,Nursing
Eva,Fry,Computing
Eli,Lap,Business
Sam,Yim,Nursing
Joe,Hui,Mining
Liz,Jin,Nursing
Ric,Kuo,Medicine
Pam,Mak,Computing
Stu,Day,Business
Tom,Gad,Medicine
Bob,Bee,Computing
Jim,Ots,Computing
Tom,Mag,Medicine
Hal,Doe,Mining
Roy,Kim,Medicine
Vis,Cox,Nursing
Kay,Aga,Nursing
Reo,Hui,Nursing
Bob,Roe,Mining

 

Building a basic Node.js Express application

In the config folder, add a file named index.js with the following content:

module.exports = {  
    'database_name': 'school.db',
    // Setting port for server
    'port': process.env.PORT || 3000   
};

The above file serves as a configuration file that specifies the SQLite database name and the port number that will be used. As regards the port number, it takes the PORT environment variable, otherwise it defaults to 3000.

Add two files to the routes folder named index.js and students.js respectively. 

The content of routes/index.js is as follows:

const express = require("express");
const router = express.Router();

router.get("/", (req, res, next) => {
    res.json({"message":"Ok"})
});

module.exports = router;

The above will display a JSON object {"message":"Ok"} when the use points his browser to endpoint /.

The content of routes/student.js is as follows:

const express = require("express");
const router = express.Router();

router.get("/students", (req, res, next) => {
    res.json([
        {"id":1,"FirstName":"Ann","LastName":"Fay","School":"Mining"},
        {"id":2,"FirstName":"Ben","LastName":"Ray","School":"Nursing"}
    ])
});

module.exports = router;

The above code sets up an endpoint that displays a JSON array of some fixed sample data of students.

In the root folder of your application, create a file named server.js and add to it the following code:

const express = require("express")
const app = express()
const path = require('path');
const config = require('./config');

var bodyParser = require("body-parser");
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

var index = require("./routes/");
var students = require("./routes/students");

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

app.use("/", index);
app.use("/api", students);

// Start server
app.listen(config.port, () => {
    console.log(`Server running on port ${config.port}`);
});

 

What does the above code do?

  • The first four lines load express, path and config objects.
  • Next we load bodyParser so that we can extract json objects from the body of the response object
  • objects index & students that point to /routes/index.js and /routes/students.js respectively.
  • app.use(express.static(path.join(__dirname, 'public'))); sets a folder named public that will contain static files like *.css, *.js, *.html, images files, etc...
  • We then set / to go to object index and route /api to go to object students
  • Finally, we start our web server which listens on the port numbers specified in file config/index.js.

Start the node app by issuing the following command in a terminal window at the root of your application: 

nodemon

If you point your browser http://localhost:3000, you will see the following:


Pointing your browser to http://localhost:3000/api/students will reveal the following:


We now have a skeleton web app that we can build out towards our final product.

Importing CSV data into SQLite

We will use the csvtojson package to import CSV data that resides in the data/students.csv file into SQLite. 

Add a file named index.js to the database folder. Then, add the following code to database/index.js:

const sqlite3 = require('sqlite3').verbose()
const path = require('path');
const csv2json = require('csvtojson');
const config = require('../config');

const CSV_FILE = path.join(__dirname, "../data/students.csv")

let db = new sqlite3.Database(config.database_name, (err) => {
    if (err) {
      // Cannot open database
      console.error(err.message)
      throw err
    } else {
        console.log('Connected to the SQLite database.')
        db.run(`CREATE TABLE students (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            FirstName VARCHAR(80),
            LastName VARCHAR(80),
            School VARCHAR(50)
            )`,
        (err) => {
            if (err) {
                // Table already created
            } else {
                csv2json().fromFile(CSV_FILE)
                .then(data => {
                    var insert = 'INSERT INTO students (FirstName, LastName, School) VALUES (?,?,?)';
                    for(const item of data) {
                        db.run(insert, [item.FirstName,item.LastName,item.School]);
                    }
                }).catch(err => {
                    // log error if any
                    console.log(err);
                });
                // Table just created, creating some rows
            }
        });  
    }
});

module.exports = db

 

What does the above code do?

  • Packages for sqlite3 and csv2json are loaded to use SQLite and conversion from CSV to JSON libraries
  • We also load the path library so that we can use the path.join() function
  • The configuration file in config/index.js is referenced enabling us to access the database name
  • The location of our CSV file is defined in variable CSV_FILE
  • the command 'new sqlite3.Database()' creates a database file named school.db, if it does not already exist
  • Database table named students is created if it does not already exist
  • Function csv2json().fromFile() loads contents of the CSV file into a variable named data. We then iterate through the data collection to execute a SQL INSERT statement for each item in the collection

Reading data from students table in school.db SQLite database

In our routes/students.js API file, let's read data from our students database table.  Add the following to the top of the routes/students.js file: 

const db = require("../database");

The above allows us to access the db database object defined in database/index.js. 

Instead of hardcoding JSON data in the "/students" GET route, we can read data from the database, Therefore, replace the the router.get("/students .... code in router/students.js with the following:


router.get("/students", (req, res, next) => {
    var sql = "select * from students"
    var params = []
    db.all(sql, params, (err, rows) => {
        if (err) {
          res.status(400).json({"error":err.message});
          return;
        }
        res.json({
            "message":"success",
            "data":rows
        })
      });
}); 

We execute the SQL statement "SELECT * FROM students" to read contents of students table and return the data as an array of JSON objects.

Testing our API

Let's test our application. When you point your browser to http://localhost:3000/api/students, you will see a list of all the students in the database: 


Also note the creation of a SQLite database file named school.db in the root of your web app.


Creating a chart of students by school

We will use the freely available Google Charts library to render a pie-chart illustrating number of students in every school. The starting point is to create a aggregation of the data. This will be done by adding the following additional /chart route to routes/students.js just under the existing /students route:

router.get("/chart", (req, res, next) => {
    sql = "SELECT School as school, COUNT(*) as count";
    sql += " FROM students GROUP BY School";

    var params = []
    db.all(sql, params, (err, rows) => {
        if (err) {
          res.status(400).json({"error":err.message});
          return;
        }
        res.json(rows)
      });
});

Point your browser to http://localhost:3000/api/chart and you will see a list of aggregate data as follows: 


We can render this aggregate data into a pie chart. In the public folder, create a simple HTML file named chart.html with the following content:

<!DOCTYPE html>
<html lang="en">

<head>
    <title>Pie chart of students by school</title>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
</head>

<body>
    <div id="chart_div"></div>
    <script type="text/javascript">
        google.charts.load('current', {
            packages: ['corechart', 'bar']
        });
        google.charts.setOnLoadCallback(LoadData);
        function LoadData() {
            $.ajax({
                url: '/api/chart',
                dataType: "json",
                type: "GET",
                error: function (xhr, status, error) {
                    toastr.error(xhr.responseText);
                },
                success: function (data) {
                    PopulationChart(data);
                    return false;
                }
            });
            return false;
        }
        function PopulationChart(data) {
            var dataArray = [
                ['school', 'count']
            ];
            $.each(data, function (i, item) {
                dataArray.push([item.school, item.count]);
            });
            var data = google.visualization.arrayToDataTable(dataArray);
            var options = {
                title: 'Students by School',
                width: 700,
                height: 800
            };
            var divElement = document.getElementById('chart_div');
            var chart = new google.visualization.PieChart(divElement);
            chart.draw(data, options);
            return false;
        }  
    </script>
</body>

</html>

Note that we are making a ajax get call to endpoint /api/chart in the $.ajax() jQuery call. Also, note that new google.visualization.PieChart() generates a pie chart. With Google Charts, you can generate a multitude of chart types including bar, geo, column, area, etc...

When you point your browser to http://localhost:3000/chart.html you will see the following pie-chart.

No surprise, 27.9% of students are doing nursing during this age of covid.

CORS

In public/chart.html, change the URL in "url: '/api/chart', to "http://localhost:3000/api/chart" so that we can run the chart.html page outside of the current web server.

From within the file system of desktop computer, double click on file public/chart.html. The page loads in a browser but appears blank. Visit the browser developer tools console tab. You will see an error that resembles the following:


This suggests that we have a CORS issue. What is CORS?

Cross-origin resource sharing (CORS) is a browser mechanism which enables controlled access to resources located outside of a given domain. 

We need to enable CORS in our server app. This is done by adding the following code to server.js just before app.use("/", index); around line 15:

// enable CORS
app.use(function(req, res, next) {
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
    res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    next();
}); 

Now try double-clicking on public/chart.html (or refreshing the page) and you should see that it is working fine.


What have we learned in this tutorial?

  • initializing a node.js Express web app
  • building a basic Node.js Express web app
  • import a CSV file into SQLite database
  • work with SQLite in a Node.js application
  • creating a simple pie chart from aggregate data with Google Charts
  • enabling CORS in a Node.js Express application

Thanks for coming this far in this article. In my next article I will show you how you can take this app and turn it into a Docker image that can be pushed to Docker-Hub and, subsequently, hosted on Microsoft Azure.