JoinDownload
This is drafted post. Please setdraft: falsein this .mdx file once ready to be published.

Create a chatroom using Websockets

7 Min Read

Vyom Srivastava

Want to create a chat application? We’re here to help you out. Today, there are a lot of messaging applications available on the internet like Facebook, Twitter, Slack, even companies have built their own chat application for internal as well as external communication apart from emails. Chat applications help you to communicate in real time unlike email which takes time to send the emails. But there’s a lot that goes behind the scenes, to chat in real time. One of the most important tech by which this real time is possible is WebSockets.

Now the main question is what are WebSockets?

WebSockets

WebSocket is an internet communication protocol which provides a two way channel to transfer the data. In a normal API, you have to make a request to get a response. On the other hand, in websockets you don’t have to make a request because the connection is always open and you just have to listen. When the data is transmitted then the client received the data with the help of the open connection opened by the sockets.

Now lets us start with the main focus that is Chatroom using NodeJS and WebSockets.

What we’ll cover here?

We’ll create a chatroom application backend using NodeJS, ExpressJS, MongoDB and WebSockets.

Prerequisites:

  • NodeJS
  • MongoDB
  • JWT
  • ExpressJS

Step -1: Environment Setup:

Now open your terminal and create a new folder, obviously you can use any name. I’ll use chatroom as the folder name here. So paste the below command on your terminal:

mkdir chatroom

Once the folder is created open the folder:

cd chatroom

Now initialize a new npm project by using command:

npm init Once everything is setup, it’s time to install the dependencies. So in your terminal paste the below command:

npm i cors @withvoid/make-validation express jsonwebtoken mongoose morgan socket.io uuid --save;

After installing the above packages, we can install nodemon also. Nodemon keeps watching all the files in a node project and restarts the server automatically when you make changes in any file. So it saved a lot of time here, to install it use the below command:

npm i nodemon --save-dev;

Now once the above is done we have to update our package.json file also. So open it in a code editor and paste the below JSON:

"scripts": {
"start": "nodemon server/index.js",
"start:server": "node server/index.js"
},

The above JSON defines the path of the server files which needs to be started when you run npm start

Now we don’t want to clutter the folder structure so we’ll create different folder like all the server related files will go in server folder so on your terminal and paste the below command:

mkdir server

Once the folder is created, open it by using the cd server command.

Step -2: Working on the Server files:

Once you’re in the server folder, create a file index.js and paste the below code in it:

import http from "http"
import express from "express"
import logger from "morgan"
import cors from "cors"
// routes
import indexRouter from "./routes/index.js"
import userRouter from "./routes/user.js"
import chatRoomRouter from "./routes/chatRoom.js"
import deleteRouter from "./routes/delete.js"
// middlewares
import { decode } from "./middlewares/jwt.js"
const app = express()
/** Get port from environment and store in Express. */
const port = process.env.PORT || "3000"
app.set("port", port)
app.use(logger("dev"))
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
app.use("/", indexRouter)
app.use("/users", userRouter)
app.use("/room", decode, chatRoomRouter)
app.use("/delete", deleteRouter)
/** catch 404 and forward to error handler */
app.use("*", (req, res) => {
return res.status(404).json({
success: false,
message: "API endpoint does not exist",
})
})
/** Create HTTP server. */
const server = http.createServer(app)
/** Listen on provided port, on all network interfaces. */
server.listen(port)
/** Event listener for HTTP server "listening" event. */
server.on("listening", () => {
console.log(`Listening on port:: http://localhost:${port}/`)
})

Code Explanation:

Here we’re importing all the packages and creating the server. This is also used to handle 404 errors. We’re telling the server to listen to http://localhost/ with the port number.

Step -3: Setting up Routes:

Now our server is set up, but we’ll use the APIs right? So we have to define an endpoint right? Hence, we’ll set up routes for different tasks like sending messages, chatrooms etc.

Create a folder routes by using command mkdir routes and created four files: index.js, user.js, index.js, chatRoom.js and delete.js.

Once done, paste the below code in routes/index.js:

import express from "express"
// controllers
import users from "../controllers/user.js"
// middlewares
import { encode } from "../middlewares/jwt.js"
const router = express.Router()
router.post("/login/:userId", encode, (req, res, next) => {})
export default router

Code Explanation:

Here we’re using express to create router, importing controller which is basically handles all the logic of the application, JWT which will act as a middleware to keep the connection secured and will be used for authentication purpose.

Once the above is done, open routes.user.js and paste the below code:

import express from "express"
// controllers
import user from "../controllers/user.js"
const router = express.Router()
router
.get("/", user.onGetAllUsers)
.post("/", user.onCreateUser)
.get("/:id", user.onGetUserById)
.delete("/:id", user.onDeleteUserById)
export default router

Code Explanation:

This router is used to handle all the requests which are related to user management. Here you can see we’re using a user controller and we have also defined 4 API endpoints for creating, deleting, updating and fetching the users.

As you can see above we’re not creating a mess by just putting all the routes in a single file, this will help to manage all the code very easily and complies with DRY standards.

Now we’ll work on the chatroom routes so open the chatRoom.js and paste the below code:

import express from "express"
// controllers
import chatRoom from "../controllers/chatRoom.js"
const router = express.Router()
router
.get("/", chatRoom.getRecentConversation)
.get("/:roomId", chatRoom.getConversationByRoomId)
.post("/initiate", chatRoom.initiate)
.post("/:roomId/message", chatRoom.postMessage)
.put("/:roomId/mark-read", chatRoom.markConversationReadByRoomId)
export default router

Code Explanation:

Similar to user routes, we’re defining the API endpoints which will help a user to initiate a chat, fetch the recent conversation , send a message and mark as read.

Now open routes/delete.js and paste the below code:

import express from "express"
// controllers
import deleteController from "../controllers/delete.js"
const router = express.Router()
router
.delete("/room/:roomId", deleteController.deleteRoomById)
.delete("/message/:messageId", deleteController.deleteMessageById)
export default router

Code Explanation:

This is a very simple router, which will delete a chat room and message respectively. You can see that we’re passing it in the delete function, which means that it is DELETE method.

Step -4: Setting up Controllers:

Since we have completed the routes creation part, now we have to work on the controller. What are controllers? They’re like the brain of the whole architecture, this is where all the logic is stored. When an API is called, the router invokes the controller and gets all the responses. In other words you can say that all the logics are stored in the Controller.

So create another folder controllers using command mkdir controller and create three files inside it: user.js, chatroom.js, and delete.js. We won’t be creating any index.js here because we don’t want to call any controller by default.

Let’s start with controlers/delete.js first, so open the file and paste the below code:

import ChatRoomModel from "../models/ChatRoom.js"
import ChatMessageModel from "../models/ChatMessage.js"
export default {
deleteRoomById: async (req, res) => {
try {
const { roomId } = req.params
const room = await ChatRoomModel.remove({ _id: roomId })
const messages = await ChatMessageModel.remove({
chatRoomId: roomId,
})
return res.status(200).json({
success: true,
message: "Operation performed successfully",
deletedRoomsCount: room.deletedCount,
deletedMessagesCount: messages.deletedCount,
})
} catch (error) {
return res.status(500).json({ success: false, error: error })
}
},
deleteMessageById: async (req, res) => {
try {
const { messageId } = req.params
const message = await ChatMessageModel.remove({
_id: messageId,
})
return res.status(200).json({
success: true,
deletedMessagesCount: message.deletedCount,
})
} catch (error) {
return res.status(500).json({ success: false, error: error })
}
},
}

Code Explanation:

Here we’re importing chatroom and chatmessage models. I’ll explain about the models later in the article. Let's focus on the controller first. We have an async function deleteRoomById, this will delete the chatroom by taking the roomId as the parameter. There’s another async function deleteMessageById that is similar to deleteRoomById but deletes the message instead of a complete chatroom. You might have noticed that our code is wrapped under try-catch. This is to detect the error. We’re passing response 200 in case of success and 500 in case of any failure.

Once the delete.js controller is done, let’s move ahead with the other ones. So open controllers/user.js in the code editor and paste the below code:

// utils
import makeValidation from "@withvoid/make-validation"
// models
import UserModel, { USER_TYPES } from "../models/User.js"
export default {
onGetAllUsers: async (req, res) => {
try {
const users = await UserModel.getUsers()
return res.status(200).json({ success: true, users })
} catch (error) {
return res.status(500).json({ success: false, error: error })
}
},
onGetUserById: async (req, res) => {
try {
const user = await UserModel.getUserById(req.params.id)
return res.status(200).json({ success: true, user })
} catch (error) {
return res.status(500).json({ success: false, error: error })
}
},
onCreateUser: async (req, res) => {
try {
const validation = makeValidation(types => ({
payload: req.body,
checks: {
firstName: { type: types.string },
lastName: { type: types.string },
type: { type: types.enum, options: { enum: USER_TYPES } },
},
}))
if (!validation.success)
return res.status(400).json({ ...validation })
const { firstName, lastName, type } = req.body
const user = await UserModel.createUser(
firstName,
lastName,
type
)
return res.status(200).json({ success: true, user })
} catch (error) {
return res.status(500).json({ success: false, error: error })
}
},
onDeleteUserById: async (req, res) => {
try {
const user = await UserModel.deleteByUserById(req.params.id)
return res.status(200).json({
success: true,
message: `Deleted a count of ${user.deletedCount} user.`,
})
} catch (error) {
return res.status(500).json({ success: false, error: error })
}
},
}

Code Explanation:

Here we have defined four async functions: onGetAllUsers, onGetUserById, onCreateUser, onDeleteUserById. These function call their respective model functions and will perform the CRUD operation in MongoDB.

Now let's move to our last controller that is chatRoom.js. This is going to be an interesting part, so open controllers/chatRoom.js and paste the below code:

// utils
import makeValidation from "@withvoid/make-validation"
// models
import ChatRoomModel, { CHAT_ROOM_TYPES } from "../models/ChatRoom.js"
import ChatMessageModel from "../models/ChatMessage.js"
import UserModel from "../models/User.js"
export default {
initiate: async (req, res) => {
try {
const validation = makeValidation(types => ({
payload: req.body,
checks: {
userIds: {
type: types.array,
options: { unique: true, empty: false, stringOnly: true },
},
type: {
type: types.enum,
options: { enum: CHAT_ROOM_TYPES },
},
},
}))
if (!validation.success)
return res.status(400).json({ ...validation })
const { userIds, type } = req.body
const { userId: chatInitiator } = req
const allUserIds = [...userIds, chatInitiator]
const chatRoom = await ChatRoomModel.initiateChat(
allUserIds,
type,
chatInitiator
)
return res.status(200).json({ success: true, chatRoom })
} catch (error) {
return res.status(500).json({ success: false, error: error })
}
},
postMessage: async (req, res) => {
try {
const { roomId } = req.params
const validation = makeValidation(types => ({
payload: req.body,
checks: {
messageText: { type: types.string },
},
}))
if (!validation.success)
return res.status(400).json({ ...validation })
const messagePayload = {
messageText: req.body.messageText,
}
const currentLoggedUser = req.userId
const post = await ChatMessageModel.createPostInChatRoom(
roomId,
messagePayload,
currentLoggedUser
)
global.io.sockets
.in(roomId)
.emit("new message", { message: post })
return res.status(200).json({ success: true, post })
} catch (error) {
return res.status(500).json({ success: false, error: error })
}
},
getRecentConversation: async (req, res) => {
try {
const currentLoggedUser = req.userId
const options = {
page: parseInt(req.query.page) || 0,
limit: parseInt(req.query.limit) || 10,
}
const rooms = await ChatRoomModel.getChatRoomsByUserId(
currentLoggedUser
)
const roomIds = rooms.map(room => room._id)
const recentConversation = await ChatMessageModel.getRecentConversation(
roomIds,
options,
currentLoggedUser
)
return res
.status(200)
.json({ success: true, conversation: recentConversation })
} catch (error) {
return res.status(500).json({ success: false, error: error })
}
},
getConversationByRoomId: async (req, res) => {
try {
const { roomId } = req.params
const room = await ChatRoomModel.getChatRoomByRoomId(roomId)
if (!room) {
return res.status(400).json({
success: false,
message: "No room exists for this id",
})
}
const users = await UserModel.getUserByIds(room.userIds)
const options = {
page: parseInt(req.query.page) || 0,
limit: parseInt(req.query.limit) || 10,
}
const conversation = await ChatMessageModel.getConversationByRoomId(
roomId,
options
)
return res.status(200).json({
success: true,
conversation,
users,
})
} catch (error) {
return res.status(500).json({ success: false, error })
}
},
markConversationReadByRoomId: async (req, res) => {
try {
const { roomId } = req.params
const room = await ChatRoomModel.getChatRoomByRoomId(roomId)
if (!room) {
return res.status(400).json({
success: false,
message: "No room exists for this id",
})
}
const currentLoggedUser = req.userId
const result = await ChatMessageModel.markMessageRead(
roomId,
currentLoggedUser
)
return res.status(200).json({ success: true, data: result })
} catch (error) {
console.log(error)
return res.status(500).json({ success: false, error })
}
},
}

Code Explanation:

Here we have got some async functions and they’ll perform different operations:

initiate: This function is used to initiate the chat and check whether the user is validated or not then only it proceeds.

postMessage: As the name suggests, this is used to post a message and takes two parameters messageText and roomId.

getRecentConversation: It fetches the last 10 conversations in the chatroom by default and takes two parameters userId and roomIds.

markConversationReadByRoomId: This is same as the blue tick in whatsapp or “Seen” in Facebook. It takes only one parameter that is roomId.

Step -5: Setting up Middleware for authentication

Here we’ll be using JWT or JSON Web Tokens for authentication purposes. So it’s time to create one more folder, middlewares and create a file jwt.js and open middlewares/jwt.js in the code editor to paste the below code:

import jwt from "jsonwebtoken"
// models
import UserModel from "../models/User.js"
const SECRET_KEY = "some-secret-key"
export const encode = async (req, res, next) => {
try {
const { userId } = req.params
const user = await UserModel.getUserById(userId)
const payload = {
userId: user._id,
userType: user.type,
}
const authToken = jwt.sign(payload, SECRET_KEY)
req.authToken = authToken
next()
} catch (error) {
return res
.status(400)
.json({ success: false, message: error.error })
}
}
export const decode = (req, res, next) => {
if (!req.headers["authorization"]) {
return res
.status(400)
.json({ success: false, message: "No access token provided" })
}
const accessToken = req.headers.authorization.split(" ")[1]
try {
const decoded = jwt.verify(accessToken, SECRET_KEY)
req.userId = decoded.userId
req.userType = decoded.type
return next()
} catch (error) {
return res
.status(401)
.json({ success: false, message: error.message })
}
}

Code Explanation:

Here we’re importing User.js and the JWT package. You have to define a SECRET_KEY which will be used for hashing purposes and we also have to make sure it is not accessible by anyone. For simplicity I have kept it here but you can keep it in an environment variable also to make it more secured.

Step -6: Setting up Database:

Here we’re using MongoDB so now we have to make a connection to the database. Create another folder config and inside it create two files index.js and mongo.js.

Now open index.js and paste the below code:

const config = {
db: {
url: "localhost:27017",
name: "chatdb",
},
}
export default config

Code Explanation:

Here we’re just mentioning the host url and name of the database.

Once the above is done, open mongo.js and paste the below code:

import mongoose from "mongoose"
import config from "./index.js"
const CONNECTION_URL = `mongodb://${config.db.url}/${config.db.name}`
mongoose.connect(CONNECTION_URL, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
mongoose.connection.on("connected", () => {
console.log("Mongo has connected succesfully")
})
mongoose.connection.on("reconnected", () => {
console.log("Mongo has reconnected")
})
mongoose.connection.on("error", error => {
console.log("Mongo connection has an error", error)
mongoose.disconnect()
})
mongoose.connection.on("disconnected", () => {
console.log("Mongo connection is disconnected")
})

Code Explanation:

Here we’re importing mongoose and index.js and connecting to the database. If you don’t know how to set the MongoDB, you can check out our article on MongoDB.

Step -7: Setting up Models:

Don’t know about Models? Don’t worry, I’ll explain. In an MVC architecture, ideally Model comes between Controller and Database. It is used to simplify the complexity of the project by keeping the business logic outside of the Controller logic.

Now create a folder models and create three files inside it: User.js. ChatRoom.js and ChatMessage.js. Once done open User.js and paste the below code:

import mongoose from "mongoose"
import { v4 as uuidv4 } from "uuid"
export const USER_TYPES = {
CONSUMER: "consumer",
SUPPORT: "support",
}
const userSchema = new mongoose.Schema(
{
_id: {
type: String,
default: () => uuidv4().replace(/\-/g, ""),
},
firstName: String,
lastName: String,
type: String,
},
{
timestamps: true,
collection: "users",
}
)
/**
* @param {String} firstName
* @param {String} lastName
* @returns {Object} new user object created
*/
userSchema.statics.createUser = async function (
firstName,
lastName,
type
) {
try {
const user = await this.create({ firstName, lastName, type })
return user
} catch (error) {
throw error
}
}
/**
* @param {String} id, user id
* @return {Object} User profile object
*/
userSchema.statics.getUserById = async function (id) {
try {
const user = await this.findOne({ _id: id })
if (!user) throw { error: "No user with this id found" }
return user
} catch (error) {
throw error
}
}
/**
* @return {Array} List of all users
*/
userSchema.statics.getUsers = async function () {
try {
const users = await this.find()
return users
} catch (error) {
throw error
}
}
/**
* @param {Array} ids, string of user ids
* @return {Array of Objects} users list
*/
userSchema.statics.getUserByIds = async function (ids) {
try {
const users = await this.find({ _id: { $in: ids } })
return users
} catch (error) {
throw error
}
}
/**
* @param {String} id - id of user
* @return {Object} - details of action performed
*/
userSchema.statics.deleteByUserById = async function (id) {
try {
const result = await this.remove({ _id: id })
return result
} catch (error) {
throw error
}
}
export default mongoose.model("User", userSchema)

Code Explanation:

I won’t be explaining about the schemas and all, if you want to know about the basic terminologies in MongoDB then you can check out our article on MongoDB.

As you can see we have created different functions deleteByUserById, getUserByIds, getUsers, createUser. Remember we were calling these functions in the user controller. Here we have defined what these model functions will do. They run their respective queries in the database and perform the CRUD operations. We also have defined the userSchema schema which has first name, last name, type and creation time.

Once the above is done now open models/ChatRoom.js and paste the below code:

import mongoose from "mongoose"
import { v4 as uuidv4 } from "uuid"
export const CHAT_ROOM_TYPES = {
CONSUMER_TO_CONSUMER: "consumer-to-consumer",
CONSUMER_TO_SUPPORT: "consumer-to-support",
}
const chatRoomSchema = new mongoose.Schema(
{
_id: {
type: String,
default: () => uuidv4().replace(/\-/g, ""),
},
userIds: Array,
type: String,
chatInitiator: String,
},
{
timestamps: true,
collection: "chatrooms",
}
)
/**
* @param {String} userId - id of user
* @return {Array} array of all chatroom that the user belongs to
*/
chatRoomSchema.statics.getChatRoomsByUserId = async function (
userId
) {
try {
const rooms = await this.find({ userIds: { $all: [userId] } })
return rooms
} catch (error) {
throw error
}
}
/**
* @param {String} roomId - id of chatroom
* @return {Object} chatroom
*/
chatRoomSchema.statics.getChatRoomByRoomId = async function (roomId) {
try {
const room = await this.findOne({ _id: roomId })
return room
} catch (error) {
throw error
}
}
/**
* @param {Array} userIds - array of strings of userIds
* @param {String} chatInitiator - user who initiated the chat
* @param {CHAT_ROOM_TYPES} type
*/
chatRoomSchema.statics.initiateChat = async function (
userIds,
type,
chatInitiator
) {
try {
const availableRoom = await this.findOne({
userIds: {
$size: userIds.length,
$all: [...userIds],
},
type,
})
if (availableRoom) {
return {
isNew: false,
message: "retrieving an old chat room",
chatRoomId: availableRoom._doc._id,
type: availableRoom._doc.type,
}
}
const newRoom = await this.create({
userIds,
type,
chatInitiator,
})
return {
isNew: true,
message: "creating a new chatroom",
chatRoomId: newRoom._doc._id,
type: newRoom._doc.type,
}
} catch (error) {
console.log("error on start chat method", error)
throw error
}
}
export default mongoose.model("ChatRoom", chatRoomSchema)

And also open controllers/ChatMessage.js to paste the below code:

import mongoose from "mongoose"
import { v4 as uuidv4 } from "uuid"
const MESSAGE_TYPES = {
TYPE_TEXT: "text",
}
const readByRecipientSchema = new mongoose.Schema(
{
_id: false,
readByUserId: String,
readAt: {
type: Date,
default: Date.now(),
},
},
{
timestamps: false,
}
)
const chatMessageSchema = new mongoose.Schema(
{
_id: {
type: String,
default: () => uuidv4().replace(/\-/g, ""),
},
chatRoomId: String,
message: mongoose.Schema.Types.Mixed,
type: {
type: String,
default: () => MESSAGE_TYPES.TYPE_TEXT,
},
postedByUser: String,
readByRecipients: [readByRecipientSchema],
},
{
timestamps: true,
collection: "chatmessages",
}
)
/**
* This method will create a post in chat
*
* @param {String} roomId - id of chat room
* @param {Object} message - message you want to post in the chat room
* @param {String} postedByUser - user who is posting the message
*/
chatMessageSchema.statics.createPostInChatRoom = async function (
chatRoomId,
message,
postedByUser
) {
try {
const post = await this.create({
chatRoomId,
message,
postedByUser,
readByRecipients: { readByUserId: postedByUser },
})
const aggregate = await this.aggregate([
// get post where _id = post._id
{ $match: { _id: post._id } },
// do a join on another table called users, and
// get me a user whose _id = postedByUser
{
$lookup: {
from: "users",
localField: "postedByUser",
foreignField: "_id",
as: "postedByUser",
},
},
{ $unwind: "$postedByUser" },
// do a join on another table called chatrooms, and
// get me a chatroom whose _id = chatRoomId
{
$lookup: {
from: "chatrooms",
localField: "chatRoomId",
foreignField: "_id",
as: "chatRoomInfo",
},
},
{ $unwind: "$chatRoomInfo" },
{ $unwind: "$chatRoomInfo.userIds" },
// do a join on another table called users, and
// get me a user whose _id = userIds
{
$lookup: {
from: "users",
localField: "chatRoomInfo.userIds",
foreignField: "_id",
as: "chatRoomInfo.userProfile",
},
},
{ $unwind: "$chatRoomInfo.userProfile" },
// group data
{
$group: {
_id: "$chatRoomInfo._id",
postId: { $last: "$_id" },
chatRoomId: { $last: "$chatRoomInfo._id" },
message: { $last: "$message" },
type: { $last: "$type" },
postedByUser: { $last: "$postedByUser" },
readByRecipients: { $last: "$readByRecipients" },
chatRoomInfo: { $addToSet: "$chatRoomInfo.userProfile" },
createdAt: { $last: "$createdAt" },
updatedAt: { $last: "$updatedAt" },
},
},
])
return aggregate[0]
} catch (error) {
throw error
}
}
/**
* @param {String} chatRoomId - chat room id
*/
chatMessageSchema.statics.getConversationByRoomId = async function (
chatRoomId,
options = {}
) {
try {
return this.aggregate([
{ $match: { chatRoomId } },
{ $sort: { createdAt: -1 } },
// do a join on another table called users, and
// get me a user whose _id = postedByUser
{
$lookup: {
from: "users",
localField: "postedByUser",
foreignField: "_id",
as: "postedByUser",
},
},
{ $unwind: "$postedByUser" },
// apply pagination
{ $skip: options.page * options.limit },
{ $limit: options.limit },
{ $sort: { createdAt: 1 } },
])
} catch (error) {
throw error
}
}
/**
* @param {String} chatRoomId - chat room id
* @param {String} currentUserOnlineId - user id
*/
chatMessageSchema.statics.markMessageRead = async function (
chatRoomId,
currentUserOnlineId
) {
try {
return this.updateMany(
{
chatRoomId,
"readByRecipients.readByUserId": { $ne: currentUserOnlineId },
},
{
$addToSet: {
readByRecipients: { readByUserId: currentUserOnlineId },
},
},
{
multi: true,
}
)
} catch (error) {
throw error
}
}
/**
* @param {Array} chatRoomIds - chat room ids
* @param {{ page, limit }} options - pagination options
* @param {String} currentUserOnlineId - user id
*/
chatMessageSchema.statics.getRecentConversation = async function (
chatRoomIds,
options,
currentUserOnlineId
) {
try {
return this.aggregate([
{ $match: { chatRoomId: { $in: chatRoomIds } } },
{
$group: {
_id: "$chatRoomId",
messageId: { $last: "$_id" },
chatRoomId: { $last: "$chatRoomId" },
message: { $last: "$message" },
type: { $last: "$type" },
postedByUser: { $last: "$postedByUser" },
createdAt: { $last: "$createdAt" },
readByRecipients: { $last: "$readByRecipients" },
},
},
{ $sort: { createdAt: -1 } },
// do a join on another table called users, and
// get me a user whose _id = postedByUser
{
$lookup: {
from: "users",
localField: "postedByUser",
foreignField: "_id",
as: "postedByUser",
},
},
{ $unwind: "$postedByUser" },
// do a join on another table called chatrooms, and
// get me room details
{
$lookup: {
from: "chatrooms",
localField: "_id",
foreignField: "_id",
as: "roomInfo",
},
},
{ $unwind: "$roomInfo" },
{ $unwind: "$roomInfo.userIds" },
// do a join on another table called users
{
$lookup: {
from: "users",
localField: "roomInfo.userIds",
foreignField: "_id",
as: "roomInfo.userProfile",
},
},
{ $unwind: "$readByRecipients" },
// do a join on another table called users
{
$lookup: {
from: "users",
localField: "readByRecipients.readByUserId",
foreignField: "_id",
as: "readByRecipients.readByUser",
},
},
{
$group: {
_id: "$roomInfo._id",
messageId: { $last: "$messageId" },
chatRoomId: { $last: "$chatRoomId" },
message: { $last: "$message" },
type: { $last: "$type" },
postedByUser: { $last: "$postedByUser" },
readByRecipients: { $addToSet: "$readByRecipients" },
roomInfo: { $addToSet: "$roomInfo.userProfile" },
createdAt: { $last: "$createdAt" },
},
},
// apply pagination
{ $skip: options.page * options.limit },
{ $limit: options.limit },
])
} catch (error) {
throw error
}
}
export default mongoose.model("ChatMessage", chatMessageSchema)

Code Explanation:

In the above two files we have defined the schema like readByRecipientSchema, chatMessageSchema, chatRoomSchema and performing the CRUD operations on our MongoDB.

Step -8: Setting up WebSockets:

This is a chat application right? So it has to be in real-time, that’s why we’re implementing WebSockets here.

So create a folder utils and create a file WebSockets.js to paste the below code:

class WebSockets {
users = []
connection(client) {
// event fired when the chat room is disconnected
client.on("disconnect", () => {
this.users = this.users.filter(
user => user.socketId !== client.id
)
})
// add identity of user mapped to the socket id
client.on("identity", userId => {
this.users.push({
socketId: client.id,
userId: userId,
})
})
// subscribe person to chat & other user as well
client.on("subscribe", (room, otherUserId = "") => {
this.subscribeOtherUser(room, otherUserId)
client.join(room)
})
// mute a chat room
client.on("unsubscribe", room => {
client.leave(room)
})
}
subscribeOtherUser(room, otherUserId) {
const userSockets = this.users.filter(
user => user.userId === otherUserId
)
userSockets.map(userInfo => {
const socketConn = global.io.sockets.connected(
userInfo.socketId
)
if (socketConn) {
socketConn.join(room)
}
})
}
}
export default new WebSockets()

Code Explanation:

We have created a class WebSockets where we are handling socket connections in the application. A notification is sent on each event like subscribe or you can say when a user joins the chat, when a user disconnects, unsubscribe - when a user wants to mute a chat room. Connection is the main heart of the WebSocket class which takes clients as input, in our case it’s our own server.

Step -9: Testing The API and Websocket

We have covered all the parts here now it’s time to test our APIs and Websocket. Let’s open our favourite client that is Firecamp. Our first step is to create some users. Let’s start with user named “Kira” and “Shinigami”

So hit endpoint http://localhost:3000/users/ and pass the below payload:

{
"firstName": "Kira",
"lastName": "N",
"type": "consumer"
}

This will return something like this:

ws1

In a similar manner, create another user with the name “Shinigami”. Once you’re done you can note down both of the user ids, this will help us to login.

To get the access token we have to use this endpoint http://localhost:3000/login/YOUR_USER_ID/ with POST method. This will return the access token.

Now it’s time to initiate the chat. We have defined the chat initiate point at http://localhost:3000/room/initiate and just pass the below payload and bearer token:

{
"userIds": ["USER ID 1", "USER ID 2"],
"type": "consumer-to-support"
}

This will return something like this:

ws1

Lets send a message in the chatroom. This we can do with the websocket. So open another tab in Firecamp and select websocket. You should see the below screen:

ws1

Now use ws://localhost:3000/socket.io/?EIO=1&transport=websocket and under header pass the access token and click on connect. If everything is fine you’ll see that the connection has been made:

ws1

Once it’s connected, in the message section you can pass your message and send. Your message will be delivered in real time.

Final Words

We have created a mini chat application using NodeJS, Websockets and ExpressJS. We also have used JWT as an API authentication method here which helped us to make a secure connection between client and server. You can do a lot more with websockets, this is just a small piece of cake.

CONTENT

Links

DownloadDocChange LogsCookiesTerms & ConditionsPrivacy PolicyContact Us

Apps & Integrations

HTTPGraphQLWebsocketSocketIO

Firecamp Newsletter