Real-time NodeJs chat application using AWS WebSocket and Lambda
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:
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:
- An AWS account.
- You have installed NodeJs on your local machine.
- 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.
Implementation Steps
Let’s break down the implementation into steps:
Setting Up AWS WebSocket API
- Navigate to the AWS Management Console and open the API Gateway service.
- Create a new WebSocket API.
- Define
$connect
,$disconnect
, and$message
routes and integrate them with lambda functions.
Creating Lambda Functions
- You need to make a lambda function to perform tasks like sending messages, connecting, and disconnecting clients.
- For every web-socket API event lambda, get trigger with
routeKey
as its action. - Write a switch statement in order to handle
routeKey
action for$connect
,$disconnect
, and$message
routes - On the
$connect
action, call the functionon_connect
. It will maintain the connection ID in the database. - On the
$disconnect
action, call the functionon_disconnect
. This function will remove the connection ID from the database. - On the
$message
action, call the functionon_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.
- 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!