Create A Real Time Chat Application With The CEAN Stack and Socket.io

I’ve always wondered what it took to develop a real-time chat application. Being able to broadcast messages instantly to other people using the same application is a pretty cool concept. Since I’ve been playing around a lot with the Couchbase, Express, Angular, and Node.js (CEAN) stack, I figured it would be pretty awesome to try to make a chat application using those technologies. To make life easy, I found a pretty sweet library called Socket.io, which will do a lot of the tough broadcasting work.

We’re going to see how to create a very simple chat application, that with a little imagination, can become something incredible.

Before getting too deep into this, let’s break down our technologies and the part they’ll play within this application.

We’ll be using the CEAN stack, sometimes referred to as the CANE stack, for this particular application. Node.js will power our server and accept communications and broadcast communications to and from the client application. The Node.js server will make use of the Socket.io middleware and Express framework. Our client front end will be powered by AngularJS, HTML, CSS, and the client side Socket.io library. To spice things up, we’re going to be using Angular 2 which is currently cutting edge.

You might be wondering, well where does Couchbase fit into this? We are going to store our chat messages in Couchbase Server so people who join into the chatroom late, won’t miss out on previous conversations. You can use your imagination and do something with Big Data to analyze the chat data if you really wanted to.

To keep things organized, we’re going to tackle this project in the following parts:

  • The Node.js backend
  • The Angular 2 front-end
  • Couchbase integration

This should make things easier to follow.

The Node.js Backend

I’m going to assume you’ve already installed Node.js on your computer and the Node Package Manager (NPM) is fully functional. Our entire project will exist on our Desktop, so create a directory there if you haven’t already. From your Command Prompt (Windows) or Terminal (Mac and Linux), execute the following:

npm init -y

The above command will create a package.json file that looks similar to the following:

{
    "name": "cean-stack-real-time-chat",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [],
    "author": "",
    "license": "ISC"
}

Feel free to edit it as necessary.

Inside your project directory, create a models, public, and routes directory. The models directory will contain all the Couchbase database logic and the routes directory will contain all your RESTful API endpoints. The public directory will contain all your front-end application code, which in reality doesn’t have to exist within your project.

At the root of your project you should also create an app.js and config.json file. The app.js file will contain the core Node.js server logic and the config.json file will contain connection information for Couchbase Server.

Before we open any of these files and start coding we should probably download any project dependencies. From the Terminal or Command Prompt, execute the following to grab all the Node.js middleware:

npm install uuid express socket.io --save

It is safe to start designing our app.js file. Open it and add the following code:

var express = require("express");
var path = require("path");
var app = express();

var server = require("http").Server(app);
var io = require("socket.io").listen(server);

app.use(express.static(path.join(__dirname, "public")));
app.use("/scripts", express.static(__dirname + "/node_modules/"));

var routes = require("./routes/routes.js")(app);

io.on("connection", function(socket){
    socket.on("chat_message", function(msg){
        io.emit("chat_message", msg);
    });
});

server.listen(3000, function () {
    console.log("Listening on port %s...", server.address().port);
});

We need to understand what is happening here.

First we are including the middleware that we downloaded into our project. Then we are starting the Socket.io and telling it to listen for data.

app.use(express.static(path.join(__dirname, "public")));
app.use("/scripts", express.static(__dirname + "/node_modules/"));

The above two lines declare the public directory as part of our project as well as alias the node_modules directory to be accessed as scripts via the front-end. We need some of the Angular 2 libraries from node_modules which is why this is necessary. It may not be best to expose all your node_modules information here, but it works for the purpose of this example.

After we set up these public directories we need to define where our API routes will exist. To be honest we only have one route, but it needs a place to live. With that defined, we can move onto the heavy lifting of Socket.io:

io.on("connection", function(socket){
    socket.on("chat_message", function(msg){
        io.emit("chat_message", msg);
    });
});

When a connection is established to the server it will listen for broadcasts from that particular open socket. The broadcast in this scenario is a developer defined chat_message broadcast. When received, the server will send it to all other sockets that are connected, including the one who sent it.

Finally, we start listening on port 3000 for activity.

Now we can jump into defining our route. Inside your project’s routes directory, create a file called routes.js and add the following code:

var appRouter = function(app) {

    app.get("/fetch", function(req, res) {
        res.send({message: "woot"});
    });

};

module.exports = appRouter;

There isn’t anything exiting in the above. Not until we hook up Couchbase. However, this is just a route we’ll use for fetching all NoSQL documents that exist in the database. This will catch up other chatroom users with the conversation.

Congratulations, the Node.js setup is done! We just need to get Couchbase integrated and our front-end completed.

The Angular 2 Front-End

This might be a little exotic to you if you’ve never seen Angular 2 before. In particular, we’ll be using TypeScript with Angular 2, which means we’ll need to define a tsconfig.json file. Before we do that though, we need to include the rest of our dependencies. At the root of your project using the Command Prompt or Terminal, execute the following:

npm install [email protected] systemjs typescript --save

Feel free to use a different version of Angular 2 if the 2.0.0-beta.0 has become out of date.

Create public/tsconfig.json and add the following to it:

{
    "compilerOptions": {
        "target": "ES5",
        "module": "commonjs",
        "sourceMap": true,
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "removeComments": false,
        "noImplicitAny": false
    }
}

Now our IDE should recognize and compile TypeScript for us.

Next we need to create an index.html file that will contain all our script and style includes. This file will sit next to the tsconfig.json file. In your project’s public directory, create index.html and add the following code:

<!doctype html>
<html>
    <head>
        <title>Couchbase Chat</title>
        <style>
            * { margin: 0; padding: 0; box-sizing: border-box; }
            body { font: 13px Helvetica, Arial; }
            form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
            form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
            form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
            #messages { list-style-type: none; margin: 0; padding: 0; }
            #messages li { padding: 5px 10px; }
            #messages li:nth-child(odd) { background: #eee; }
        </style>
        <script src="scripts/angular2/bundles/angular2-polyfills.js"></script>
        <script src="scripts/systemjs/dist/system.src.js"></script>
        <script src="scripts/rxjs/bundles/Rx.js"></script>
        <script src="scripts/angular2/bundles/angular2.dev.js"></script>
        <script src="scripts/angular2/bundles/http.dev.js"></script>
        <script src="scripts/angular2/bundles/router.dev.js"></script>
        <script>
            System.config({
                packages: {'app': {defaultExtension: 'js'}}
            });
            System.import('app/app');
        </script>
    </head>
    <body>
        <my-app>Loading...</my-app>
        <script src="js/socket.io-1.2.0.js"></script>
    </body>
</html>

A few things to note here. I did not create the styles, but instead took them from the Socket.io documentation. That is pretty much where my documentation looting ends.

Notice all the script includes referencing the scripts directory? That is the alias we created in the Node.js section. The System commands are pointing to the app directory that we haven’t created yet. This is where the meat of our application will reside.

Don’t forget to download the Socket.io client JavaScript library. It should be placed in your project’s public/js directory.

Time to go one level deeper. Create public/app/app.ts and include the following:

import {
    Component,
    View,
    provide
} from "angular2/core";

import {bootstrap} from "angular2/platform/browser";

import {
    RouteConfig,
    RouterLink,
    RouterOutlet,
    Route,
    ROUTER_DIRECTIVES,
    ROUTER_PROVIDERS,
    Location,
    LocationStrategy,
    HashLocationStrategy,
    Router
} from 'angular2/router';

import { DefaultPage } from './default/default';

@Component({
    selector: "my-app",
    templateUrl: "./app/app.html",
    directives: [ROUTER_DIRECTIVES]
})

@RouteConfig([
    { path: "/", as: "Default", component: DefaultPage },
])

class App {

    router: Router;
    location: Location;

    constructor(router: Router, location: Location) {
        this.router = router;
        this.location = location;
    }

}

bootstrap(App, [ROUTER_PROVIDERS, provide(LocationStrategy, {useClass: HashLocationStrategy})]);

This is the base of our Angular 2 logic. We’re essentially just defining a page router so that we can isolate our logic in specific pages. Our project will only have a DefaultPage, but you can easily expand upon it to add more pages. More information on Angular 2 page navigation can be found in a previous post that I wrote.

The public/app/app.ts file pairs with public/app/app.html which should contain the following code:

<router-outlet></router-outlet>

More theming can be done here if desired, but for this project, our core theming is in the page itself, not the parent pages.

Our single page will be found in the public/app/default directory. The file pair that exists in that directory will be default.ts and default.html.

Let’s start by cracking open the public/app/default/default.ts file and adding the following code:

import {Component, View} from "angular2/core";
import {Http, HTTP_PROVIDERS} from "angular2/http";

declare var io: any;

@Component({
    selector: 'default',
    viewProviders: [HTTP_PROVIDERS]
})

@View({
    templateUrl: 'app/default/default.html'
})

export class DefaultPage {

    messages: Array<String>;
    chatBox: String;
    socket: any;

    constructor(http: Http) {
        this.messages = [];
        http.get("/fetch").subscribe((success) => {
            var data = success.json();
            for(var i = 0; i < data.length; i++) {
                this.messages.push(data[i].message);
            }
        }, (error) => {
            console.log(JSON.stringify(error));
        });
        this.chatBox = "";
        this.socket = io();
        this.socket.on("chat_message", (msg) => {
            this.messages.push(msg);
        });
    }

    send(message) {
        this.socket.emit("chat_message", message);
        this.chatBox = "";
    }
}

To break this file down, we’re including the Angular 2 Component and View dependencies as well as the dependency required for making HTTP requests. Remember, our application will at some point in time, make HTTP requests to get data from our database.

Notice the declare var io: any; line. TypeScript won’t naturally understand the functions and classes in the Socket.io JavaScript library so we must first declare it.

Inside our DefaultPage class we define our variables.

The constructor will be executed as soon as the page launches. We are initializing our messages array and making an HTTP request against Node.js for any data that might exist in the database. Up until now we are just returning a constant {message: "woot"}, but that will change real soon. We are also firing up the Socket.io client library and pushing any received messages to the messages array.

The send(message) function will send messages to the Node.js server and clear the input box on the front end.

Now let’s jump into the public/app/default/default.html file and add the following few lines of code:

<ul id="messages">
    <li *ngFor="#message of messages">{{message}}</li>
</ul>
<form action="">
    <input [(ngModel)]="chatBox" autocomplete="off" /><button (click)="send(chatBox)">Send</button>
</form>

In this UI file we’re looping through the messages array and displaying each message in a list. The input box is mapped to our chatBox string and it is passed in when we perform a button press.

To use the front-end you’ll need to compile the TypeScript files. Some editors like Atom do this for you, but if you don’t have JavaScript files automatically being generated, run tsc from the Terminal with the directory that contains tsconfig.json as the current working directory.

Congratulations, your front-end with Angular 2 and TypeScript is now complete! Now we just need to get our database layer included.

Couchbase Integration

Let’s get Couchbase into our project for storing chat messages. If you’re unfamiliar, Couchbase is a NoSQL document database that is open source. It is incredibly easy to install and configure on your machine. Head over to the official Couchbase downloads page and get the latest version.

Once installed, create a new Couchbase bucket called web-chat or whatever you prefer. We’ll be using N1QL, which is SQL-like queries, against our data. To use N1QL we need to have at least one index created on our database bucket. This can be done through the Couchbase Query (CBQ) client installed with Couchbase.

To run CBQ on a Mac, execute the following from the Terminal:

./Applications/Couchbase Server.app/Contents/Resources/couchbase-core/bin/cbq

If you’re on Windows, run CBQ in the Command Prompt by executing the following:

C:/Program Files/Couchbase/Server/bin/cbq.exe

With CBQ up and running we need to create our index. Execute the following:

CREATE PRIMARY INDEX ON `web-chat` USING GSI;

Wew, Couchbase is now ready to go.

We need to define our server and bucket information in the config.json file we created at the beginning of the tutorial. Open your project’s config.json file and add the following:

{
    "couchbase": {
        "server": "127.0.0.1:8091",
        "bucket": "web-chat"
    }
}

Remember to change things where appropriate.

We never included the Couchbase Node.js SDK, so do that by executing the following at the root of your project:

npm install couchbase --save

Head back into your project’s app.js file as we need to make some edits:

var express = require("express");
var couchbase = require("couchbase");
var path = require("path");
var config = require("./config");
var app = express();

var server = require("http").Server(app);
var io = require("socket.io").listen(server);

module.exports.bucket = (new couchbase.Cluster(config.couchbase.server)).openBucket(config.couchbase.bucket);

app.use(express.static(path.join(__dirname, "public")));
app.use("/scripts", express.static(__dirname + "/node_modules/"));

var routes = require("./routes/routes.js")(app);
var ChatModel = require("./models/chatmodel.js");

io.on("connection", function(socket){
    socket.on("chat_message", function(msg){
        ChatModel.create({message: msg}, function(error, result) {
            if(error) {
                console.log(JSON.stringify(error));
            }
            io.emit("chat_message", msg);
        });
    });
});

server.listen(3000, function () {
    console.log("Listening on port %s...", server.address().port);
});

We’re including the Couchbase middleware and the configuration file that we have at the root of our project. We are opening a connection to the server as well as the particular bucket. We included a ChatModel that is foreign to us at the moment, but won’t be soon. The ChatModel is where all our database logic will exist.

Finally, inside the Socket.io listener block, we are calling the ChatModel.create method every time a new message is received. This method will save the messages to the database and then broadcast them to the clients.

Let’s take a look at that ChatModel class. Open your project’s models/chatmodel.js file and include the following code:

var uuid = require("uuid");
var db = require("../app").bucket;
var config = require("../config");
var N1qlQuery = require('couchbase').N1qlQuery;

function ChatModel() { };

ChatModel.create = function(data, callback) {
    var chatMessage = {
        id: uuid.v4(),
        message: data.message
    };
    db.insert("chat::" + chatMessage.id, chatMessage, function(error, result) {
        if(error) {
            return callback(error, null);
        }
        return callback(null, result);
    });
}

ChatModel.getAll = function(callback) {
    var statement = "SELECT id, message " +
                    "FROM `" + config.couchbase.bucket + "`";
    var query = N1qlQuery.fromString(statement).consistency(N1qlQuery.Consistency.REQUEST_PLUS);
    db.query(query, function(error, result) {
        if(error) {
            return callback(error, null);
        }
        callback(null, result);
    });
};

module.exports = ChatModel;

We have two functions in this class. The create function will take the chat message, create a unique id, and then store it into Couchbase. The getAll function will run a N1QL query to get all documents from the bucket. Both functions are very basic and can be expanded upon with a little imagination.

What actually calls the getAll function though? This is where our /fetch route comes into play. Head back into your project’s routes/routes.js file and change it to look like the following:

var ChatModel = require("../models/chatmodel");

var appRouter = function(app) {

    app.get("/fetch", function(req, res) {
        ChatModel.getAll(function(error, result) {
            if(error) {
                return res.status(400).send(error);
            }
            return res.send(result);
        });
    });

};

module.exports = appRouter;

We are done! Our chat application now works with a database.

To run this project, type node app.js from your Terminal or Command Prompt at the root of the project.

Conclusion

We just made a pretty massive full stack application. We made use of the now bleeding edge Angular 2 framework in addition to Couchbase, Node.js, Express Framework, and Socket.io. With all of these technologies combined we were able to make a real time chat application that stores messages in a NoSQL database to be accessed at a later time.

The full source code to this tutorial can be found on GitHub.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in Java, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Apache Cordova. Nic writes about his development experiences related to making web and mobile development easier to understand.

Search

Follow Us

Subscribe

Subscribe to my newsletter for monthly tips and tricks on subjects such as mobile, web, and game development.

Subscribe on YouTube

Support This Site