Real-time NodeJs chat application using AWS WebSocket and Lambda

Pravin Mahale
Globant
Published in
8 min readJan 3, 2024
Real Time — Chat Application

A chat app lets people talk to each other online in real-time. You can use it on the web, on your phone, or on your computer.

Real-time communication is more important than ever in today’s fast-paced world. Many people use messaging apps to stay connected with coworkers, friends, and family. Chat apps can be intimidating, but AWS makes them easy to handle. In this blog post, you will learn how to make a live chat app with AWS WebSockets and Lambda. The final app will look like this:

Real-time chat application

Basic concepts and requirements

This is what you need.

  • AWS Lambda: AWS Lambda is a service that allows you to run code without dealing with servers.
  • AWS WebSockets: API Gateway WebSocket APIs are bidirectional. Clients can send messages to a service. Services can also send messages to clients.

Why AWS WebSockets and Lambda? AWS offers various services to create scalable, dependable, and affordable applications. For a real-time chat application, AWS WebSockets and Lambda are a powerful combination. Web sockets allow fast, two-way communication. Lambda functions handle messages and events.

You will need:

  1. An AWS account.
  2. You have installed NodeJs on your local machine.
  3. Installed and configured the AWS Command Line Interface (CLI).

The architectural flow diagram

Servers and clients connect through WebSockets in a bidirectional manner. Servers can now actively push messages to clients. This can have a big impact on some applications. In the architectural flow diagram below, the client communicates with the WebSocket API. The Web-socket API disseminates client action through different routes. These include $connect, $message, and $disconnect. Based on the route, different lambdas get triggered and perform specific actions.

Architectural flow for chat application with AWS web-socket and Lambda

Implementation Steps

Let’s break down the implementation into steps:

Setting Up AWS WebSocket API

  1. Navigate to the AWS Management Console and open the API Gateway service.
  2. Create a new WebSocket API.
  3. Define $connect, $disconnect, and $message routes and integrate them with lambda functions.
Fill all required information and routes step by step

Creating Lambda Functions

  1. You need to make a lambda function to perform tasks like sending messages, connecting, and disconnecting clients.
  2. For every web-socket API event lambda, get trigger with routeKey as its action.
  3. Write a switch statement in order to handle routeKey action for $connect, $disconnect, and $message routes
  4. On the $connect action, call the function on_connect. It will maintain the connection ID in the database.
  5. On the $disconnect action, call the function on_disconnect. This function will remove the connection ID from the database.
  6. On the $message action, call the function on_message. This function manages sending messages against connection IDs.
export const handler = async (event) => {
const {
body,
requestContext: { routeKey, connectionId, domainName, stage },
queryStringParameters = {},
} = event;

switch (routeKey) {
case "$connect":
const { user_name, user_id } = queryStringParameters;
await on_connect(connectionId, user_name, user_id);
break;
case "$disconnect":
await on_disconnect(connectionId);
break;
case "message":
const callbackUrl = `https://${domainName}/${stage}`;
await on_message(connectionId, body, callbackUrl);
break;
default:
break;
}

const response = {
statusCode: 200,
body: JSON.stringify("success"),
};
return response;
};

4. Create a local function on_connect to store connectionIds into DynamoDB against user IDs. You need to store those connectionIds to send messages on client devices. You use the PutItemCommand method to create a new record of the connectionId against the userID.

const on_connect = async (connectionId, user_name, user_id) => {
try {
if (!connectionId) return;

const command = new PutItemCommand({
TableName: "Customers",
Item: {
Id: { S: connectionId },
Uid: { S: user_id },
Name: { S: user_name },
},
});
const res = await client.send(command);
} catch (e) {
console.log("Error in on_connect", e.message);
}
};

5. Create a local function on_disconnect to remove connectionIds from DynamoDB against user IDs. You need to remove those connectionIds to make sure we do not send messages on client devices that are disconnected. You use the DeleteItemCommand method to remove the record of the connectionId .

const on_disconnect = async (connectionId) => {
try {
if (!connectionId) return;

const command = new DeleteItemCommand({
TableName: "Customers",
Key: {
Uid: { S: connectionId },
},
});
const res = await client.send(command);
} catch (e) {
console.log("Error in on_disconnect", e.message);
}
};

6. Create a local function on_message to send messages from the sender to the receiver. In this function, you need to fetch a connectionId from the database by using ScanCommand method and send a message to the respective connectionIds using PostToConnectionCommand method of web-socket API

const on_message = async (connectionId, body, callbackUrl) => {
try {
if (!connectionId) return;

if (typeof body != Object) {
body = JSON.parse(body);
}
const { sender_id, sender_name, msg } = body;

const command = new ScanCommand({
TableName: "Customers",
});
const res = await client.send(command);
if (res && res.Items && res.Items.length) {
await Promise.all(
res.Items.map(async (obj) => {
try {
const clientApi = new ApiGatewayManagementApiClient({
endpoint: callbackUrl,
});
const requestParams = {
ConnectionId: obj.Id.S,
Data: `{"sender_name":"${sender_name}","sender_id":"${sender_id}","msg":"${msg}"}`,
};
const command = new PostToConnectionCommand(requestParams);
const resApi = await clientApi.send(command);
} catch (e) {
console.log(e);
}
})
);
}
} catch (e) {
console.log("Error in on_message", e.message);
}
};

Your final lambda function code will look like below:

import {
PutItemCommand,
DeleteItemCommand,
ScanCommand,
DynamoDBClient,
} from "@aws-sdk/client-dynamodb";
import {
ApiGatewayManagementApiClient,
PostToConnectionCommand,
} from "@aws-sdk/client-apigatewaymanagementapi";

const client = new DynamoDBClient({});

const on_connect = async (connectionId, user_name, user_id) => {
try {
if (!connectionId) return;

const command = new PutItemCommand({
TableName: "Customers",
Item: {
Id: { S: connectionId },
Uid: { S: user_id },
Name: { S: user_name },
},
});
const res = await client.send(command);
} catch (e) {
console.log("Error in on_connect", e.message);
}
};

const on_disconnect = async (connectionId) => {
try {
if (!connectionId) return;

const command = new DeleteItemCommand({
TableName: "Customers",
Key: {
Uid: { S: connectionId },
},
});
const res = await client.send(command);
} catch (e) {
console.log("Error in on_disconnect", e.message);
}
};

const on_message = async (connectionId, body, callbackUrl) => {
try {
if (!connectionId) return;

if (typeof body != Object) {
body = JSON.parse(body);
}
const { sender_id, sender_name, msg } = body;

const command = new ScanCommand({
TableName: "Customers",
});
const res = await client.send(command);
if (res && res.Items && res.Items.length) {
await Promise.all(
res.Items.map(async (obj) => {
try {
const clientApi = new ApiGatewayManagementApiClient({
endpoint: callbackUrl,
});
const requestParams = {
ConnectionId: obj.Id.S,
Data: `{"sender_name":"${sender_name}","sender_id":"${sender_id}","msg":"${msg}"}`,
};
const command = new PostToConnectionCommand(requestParams);
const resApi = await clientApi.send(command);
} catch (e) {
console.log(e);
}
})
);
}
} catch (e) {
console.log("Error in on_message", e.message);
}
};

export const handler = async (event) => {
const {
body,
requestContext: { routeKey, connectionId, domainName, stage },
queryStringParameters = {},
} = event;

switch (routeKey) {
case "$connect":
const { user_name, user_id } = queryStringParameters;
await on_connect(connectionId, user_name, user_id);
break;
case "$disconnect":
await on_disconnect(connectionId);
break;
case "message":
const callbackUrl = `https://${domainName}/${stage}`;
await on_message(connectionId, body, callbackUrl);
break;
default:
break;
}

const response = {
statusCode: 200,
body: JSON.stringify("success"),
};
return response;
};

Building the Frontend

Create a user interface with HTML, CSS, JavaScript, and jQuery. You can use any alternate tech stack for building client interfaces like vanilla JS+HTML, Simple Javascript + HTML, or any related client interface.

  1. Establish WebSocket connections to your AWS API Gateway.
const WEBSOCKET_API =
"wss://xxxxxxxxx.execute-api.ap-south-1.amazonaws.com/production";

2. Check if WebSocket is supported by your browser or not

if ("WebSocket" in window) {
console.log("WebSocket is supported by your Browser!");
}

3. Initialize WebSocket connection with the connection string

let ws = new WebSocket(
`${WEBSOCKET_API}?query_param1=${query_param1}&query_param_n=${query_param_n}`
);

4. Create one function, e.g. webSocketTest, to receive events from the web-socket API and initialize that function from the document-ready function

function webSocketTest(user_name, user_id) {
if ("WebSocket" in window) {
console.log("WebSocket is supported by your Browser!");

let ws = new WebSocket(
`${WEBSOCKET_API}?user_name=${user_name}&user_id=${user_id}`
);

ws.onopen = function () {
web_socket = ws;
// when connection opens
};

ws.onmessage = function (evt) {
// trigger when websocket received message
};

ws.onclose = function () {
// trigger when connection get closed
};
} else {
console.log("WebSocket NOT supported by your Browser!");
}
}

5. Send a message to the web socket

web_socket.send(
`{"sender_id":"${user_id}","sender_name":"${user_name}", "msg":"${msg}"}`
);

Your final code for receiving and sending messages from the client side will look like this:

const WEBSOCKET_API =
"wss://xxxxxxxxx.execute-api.ap-south-1.amazonaws.com/production";
let web_socket;

const buildMessage = (data) => {
if (data && typeof data != Object) {
data = JSON.parse(data);
}
const { sender_name, sender_id, msg } = data;
if (sender_name && sender_id && msg) {
let sender_class = "";
if (sender_id == localStorage.user_id) {
sender_class = "darker";
}
$(".mainbody").append(`
<div class="container ${sender_class}">
<div class="profile_letter">${sender_name.substr(0, 1)}</div>
<p class="profile_name">${sender_name}</p>
<p>${msg}</p>
<span class="time-right">${moment().fromNow()}</span>
</div>
`);

const objDiv = document.getElementById("mainbody");
objDiv.scrollTop = objDiv.scrollHeight;
}
};

function webSocketTest(user_name, user_id) {
if ("WebSocket" in window) {
console.log("WebSocket is supported by your Browser!");

let ws = new WebSocket(
`${WEBSOCKET_API}?user_name=${user_name}&user_id=${user_id}`
);

ws.onopen = function () {
web_socket = ws;
};

ws.onmessage = function (evt) {
buildMessage(evt.data);
};

ws.onclose = function () {
console.log("Connection is closed...");
};
} else {
console.log("WebSocket NOT supported by your Browser!");
}
}

$(document).ready(function () {
const { user_name, user_id } = localStorage;
$("#user_name").html(user_name);
webSocketTest(user_name, user_id);

$("#message").keypress(function (e) {
var key = e.which;
if (key == 13) {
const msg = $("#message").val();
if (!msg.trim()) {
return false;
}

if (web_socket) {
web_socket.send(
`{"sender_id":"${user_id}","sender_name":"${user_name}", "msg":"${msg}"}`
);
$("#message").val("");
}
}
});
});

Authentication and Authorization

You can use AWS Cognito or a custom JWT authentication mechanism.

  • Scaling and Load Balancing: Consider using auto-scaling and load balancing to handle more users. Use AWS Elastic Beanstalk or ECS for containerized applications.
  • Testing and Deployment: Testing is an important phase in which you need to check web-socket events, client-side handling of events, and authorization things like JWT tokens or any other query parameters.
  • Monitoring and Logging: Use AWS CloudWatch to watch and log application performance for troubleshooting purposes.

Conclusion

Creating real-time chat apps with AWS WebSockets, Lambda, and Node.js is rewarding. With AWS, you can build a robust chat platform that’s scalable and reliable. To create a chat app that meets user needs and grows with your company, use the right tools and plan. Happy coding!

--

--