You are relaxing and listening to music in your office, then all of a sudden your desk phone rings, your boss hurriedly says “We need a chat room built and ready in less than 30 minutes to discuss a new project we are going to work on soon”. As the only developer around, you said “Ready in a bit!” with no idea about how to build one.
Don’t worry, get ready to build a simple chat room using Manifold, Express, Socket.io and Zeit. This tutorial will show you how easy it is to use one of Redis’ awesome feature called pub/Sub with Socket.io, to send and receive messages. Time to get to the action!
Redis, which means REmote DIctionary Server, is an open source, in-memory data structure store. It can be used as a key-value database, cache and message broker. It supports a wide range of data structures such as strings, hashes, sets, lists etc. It also has on-disk persistence, which can be achieved by regularly dumping data to the disk or by appending commands to a log.
However, we are interested in one of Redis feature called Pub/Sub. Redis Pub/Sub allows a publisher (sender) send a message to a channel without knowing if there is any interested subscriber (receiver). Also, a subscriber expresses interest in a channel to receive messages without any knowledge of a publisher. Basically, a publisher is like a satellite antenna that sends out messages into deep space without knowing if aliens exist to receive the messages, while a subscriber is like a base station on Earth listening on a radio channel hoping to receive a message from deep space, without knowing if aliens are broadcasting.
Publishers and Subscribers are decoupled to make the process very fast and improve scalability since both subscribers and publishers are not aware of each other.
Pros of using Redis PubSub
-
It is very fast, since it makes use of in-memory data stores.
-
Slow subscriber can not delay publishers from broadcasting messages, since it is not queue based.
-
The simplicity allows users to be flexible and easily scale applications
Cons of using Redis PubSub
- It is not capable of persistence, which means messages are not saved or cached. Once a subscriber misses a message, there is no way it can get the message again. However, there are measures you can put in place to compensate, as we will see later in this tutorial.
You can read up on Redis here.
First, we need to set up Redis. Instead of spending time installing and configuring Redis on your server, you can head over to Manifold and create an instance. If you don’t have a Manifold account, you can quickly create one.
insert image
Once you are logged in, create a new project and provision a RedisGreen resource, this shouldn’t take very much time.
insert image
Click on Download .env
button once the resource has been created, to download the .env
file containing the credentials.
insert image
Copy the downloaded .env
file and paste it in your project root directory.
To get started, we are going to install some node modules to get the chat room ready quickly. Ensure you have Node and NPM installed, then open your command line or terminal and run this:
npm install bluebird body-parser express node-env-file path pug redis socket.io --save
npm install babel-cli babel-preset-env nodemon --save-dev
The command above will install ExpressJS framework, Redis client, Bluebird to promisify the Redis client, Socket.io, Pug as view template engine, node-env-file to configure environment file (.env) and body-parser to parse body requests, especially for POST methods.
Also, we are going to use Babel to transpile our JavaScript from ES6.
Quick Note: Socket.io allows real-time communication among clients. We will use it to send event-based messages between the web clients and the server.
Create a folder lib
in your project root folder and create a file named redis.js, i.e lib/redis.js
and copy the code below:
"use strict";
import redis from "redis";
import promise from "bluebird";
import env from "node-env-file";
env("./.env");
const REDIS_URL = process.env.REDIS_URL;
promise.promisifyAll(redis.RedisClient.prototype);
promise.promisifyAll(redis.Multi.prototype);
export let client = () => {
return new Promise((resolve, reject) => {
let connector = redis.createClient(REDIS_URL);
connector.on("error", () => {
reject("Redis Connection failed");
});
connector.on("connect", () => {
resolve(connector);
});
});
};
In the code snippet above, we did the following:
- Imported the Redis, Bluebird and node-env-file modules
- Set the environment file
- Promisify the Redis module
- Created a function that returns the Redis client or an error
- Exported the client
Since the connection to the Redis server is asynchronous, there is a high chance that our Redis client might not have been created and returned before we need it, this can be disastrous! Therefore to prevent a problem, the client()
function returns a promise that either resolves with the Redis client or rejects with an error.
The client will be used to publish messages, subscribe to channels and listen for messages and to store published messages so that they are available after the broadcast.
Awesome right?!
Now, we are going to declare and export some functions that we will use later during this tutorial. The reason why we are creating this helper functions, is to make our code base more readable and well organised. Our helper functions are going to help communicate with Redis to perform various actions which include
- Fetching all chat messages
- Storing messages using list data type
- Fetching all users
- Adding users using set data type
- Deleting a user
Create a file functions.js
in lib
folder i.e lib/functions.js
and add the code snippet below
"use strict";
import { client } from "../lib/redis";
export let fetchMessages = () => {
return new Promise((resolve, reject) => {
client().then(
res => {
res.lrangeAsync("messages", 0, -1).then(
messages => {
resolve(messages);
},
err => {
reject(err);
}
);
},
err => {
reject("Redis connection failed: " + err);
}
);
});
};
export let addMessage = message => {
return new Promise((resolve, reject) => {
client().then(
res => {
res
.multi()
.rpush("messages", message)
.execAsync()
.then(
res => {
resolve(res);
},
err => {
reject(err);
}
);
},
err => {
reject("Redis connection failed: " + err);
}
);
});
};
export let fetchActiveUsers = () => {
return new Promise((resolve, reject) => {
client().then(
res => {
res.smembersAsync("users").then(
users => {
resolve(users);
},
err => {
reject(err);
}
);
},
err => {
reject("Redis connection failed: " + err);
}
);
});
};
export let addActiveUser = user => {
return new Promise((resolve, reject) => {
client().then(
res => {
res
.multi()
.sadd("users", user)
.execAsync()
.then(
res => {
if (res[0] === 1) {
resolve("User added");
}
reject("User already in list");
},
err => {
reject(err);
}
);
},
err => {
reject("Redis connection failed: " + err);
}
);
});
};
export let removeActiveUser = user => {
return new Promise((resolve, reject) => {
client().then(
res => {
res
.multi()
.srem("users", user)
.execAsync()
.then(
res => {
if (res === 1) {
resolve("User removed");
}
reject("User is not in list");
},
err => {
reject(err);
}
);
},
err => {
reject("Redis connection failed: " + err);
}
);
});
};
in the code snippet above, we imported the Redis client function and declared five functions: fetchMessages()
, addMessage()
, fetchActiveUsers()
, addActiveUser()
and removeActiveUser()
.
Before we continue, you might want to catch up on Redis data types here. Let's quickly take a look at what is happening in each function
-
fetchMessages()
: This function returns a promise that resolves with an array of messages or rejects with an error. Onceclient()
promise has been resolved, it fetches all the messages in the listmessages
using the promisified LRANGE commandlrangeAsync()
-
addMessage()
: This function returns a promise that resolves with the length of the list added or rejects with an error message. Onceclient()
promise has been resolved, it makes use of a promisified transaction, which queues up the RPUSH command that inserts message into the listmessages
and execute. -
fetchActiveUsers()
: This function returns a promise that resolves with an array of users or rejects with an error. Onceclient()
promise has been resolved, it uses the promisified SMEMBER commandsmemberAsync()
to fetch all users in the setusers
-
addActiveUser()
: Onceclient()
promise has been resolved, this function uses a promisified transaction which queues up the SADD command that inserts a user into the setusers
and execute. It returns a promise that resolves with the number of element added to the set or rejects with an error message. A Redis set doesn't allow repetition of elements, therefore it makes it more convenient to store unique elements like usernames. -
removeActiveUser()
: Onceclient()
promise has been resolved, this function uses a promisified transaction that queues up the SREM command that removes a user from the setusers
and then execute. This function returns a promise that resolves with the number of elements removed or rejects with an error message
Routes basically handle how our web app send HTTP responses to various endpoints . A route usually has a path (e.g '/messages'), a verb (e.g GET) and a handler function. For this tutorial, we are going to need routes for the following
- The homepage of the app
- The chat room
- Fetching all the messages
- Sending a message
- Adding a user to the chat room
- Removing a user from the chat room
Create a file routes.js
in the server
folder created earlier , i.e server/routes.js
and add the code below into it
"use strict";
import express from "express";
import { client } from "../lib/redis";
import * as helper from "../lib/functions";
const router = express.Router();
let fetchMessages = () => {
return helper.fetchMessages().then(
res => {
return res;
},
err => {
console.log(err);
}
);
};
let fetchUsers = () => {
return helper.fetchActiveUsers().then(
res => {
return res;
},
err => {
console.log(err);
}
);
};
In the code snippet above, we did the following
- Imported Express, the Redis client and our helper functions
- Created an instance of Express Router
- Created two functions that return messages and active users stored on Redis, using the
fetchMessages()
andfetchActiveUsers()
helper functions
Next, let's create some of our app routes, paste the code snippet below into routes.js
export let home = router.get("/", (req, res) => {
res.render("index", { title: "Chat Room" });
});
export let chatRoom = router.get("/chat/:username", (req, res) => {
res.render("room", { user: req.params.username });
});
export let messages = router.get("/messages", (req, res) => {
fetchMessages().then(messages => {
res.send(messages);
});
});
export let users = router.get("/users", (req, res) => {
fetchUsers().then(u => {
res.send(u);
});
});
In the code snippet above, we created and exported the routes for the following
- Displaying the homepage
- Displaying the chat room
- Returning all messages to the client
- Returning all active users to the client
Let's create more routes that will be needed in the app, paste the code snippet below to routes.js
export let createUser = router.post("/user", (req, res) => {
let users;
let user = req.body.user;
fetchUsers().then(u => {
users = u;
if (users.indexOf(user) === -1) {
helper.addActiveUser(user).then(
() => {
client().then(
client => {
let msg = {
message:
req.body.user +
" just joined the chat room",
user: "system"
};
client.publish("chatMessages", JSON.stringify(msg));
client.publish(
"activeUsers",
JSON.stringify(fetchUsers())
);
helper.addMessage(JSON.stringify(msg)).then(
() => {
res.send({
status: 200,
message: "User joined"
});
},
err => {
console.log(err);
}
);
},
err => {
console.log(err);
}
);
},
err => {
console.log(err);
}
);
} else {
res.send({ status: 403, message: "User already exist" });
}
});
});
export let deleteUser = router.delete("/user", (req, res) => {
let users;
let user = req.body.user;
fetchUsers().then(u => {
users = u;
if (users.indexOf(user) !== -1) {
helper.removeActiveUser(user).then(
() => {
client().then(
client => {
let msg = {
message: req.body.user + " just left the chat room",
user: "system"
};
client.publish("chatMessages", JSON.stringify(msg));
client.publish(
"activeUsers",
JSON.stringify(fetchUsers())
);
helper.addMessage(JSON.stringify(msg)).then(
() => {
res.send({
status: 200,
message: "User removed"
});
},
err => {
console.log(err);
}
);
},
err => {
console.log(err);
}
);
},
err => {
console.log(err);
}
);
} else {
res.send({ status: 403, message: "User does not exist" });
}
});
});
export let createMessage = router.post("/message", (req, res) => {
let msg = {
message: req.body.msg,
user: req.body.user
};
client().then(
client => {
client.publish("chatMessages", JSON.stringify(msg));
helper.addMessage(JSON.stringify(msg)).then(
() => {
res.send({
status: 200,
message: "Message sent"
});
},
err => {
console.log(err);
}
);
},
err => {
console.log(err);
}
);
});
In the code snippet above, we created and exported the routes for the following
- Creating a user
- Deleting a user
- Creating a message
Let's take a look at each of the routes above.
createUser
: This route handles adding a user to active users. Once a user has been added, a message announcing the new user to chat room and the updated active users are published using the Redis client. After that, the new message is then stored in Redis and a HTTP response is sent.
-deleteUser
: This route handles removing a user from active users. After the user is removed from the set users
on Redis, a message announcing that the user has left the chat room and the the updated active users are published using the Redis client. Finally, the message is then stored on Redis and a HTTP response is sent.
createMessage
: This route deals with messages sent by users in the chat room. Once a message is sent, it is published using the Redis client and then stored on Redis.
For this tutorial, we will use Express as our Node framework. Express is a minimal and flexible framework that provides a robust set of features for web and mobile applications. Create a folder server
and create a file server.js
in it, i.e server/server.js
and copy the code snippet below into it:
"use strict";
import express from "express";
import bodyParser from "body-parser";
import socket from "socket.io";
import path from "path";
import env from "node-env-file";
import { client } from "../lib/redis";
import * as routes from "./routes";
env(".env");
const PORT = process.env.APP_PORT;
const app = express();
// view engine setup
app.set("views", path.join(__dirname, "../public/views"));
app.set("view engine", "pug");
// static files folder setup
app.use(express.static(path.join(__dirname, "../public/assets")));
// Middleware to parse request body
app.use(
bodyParser.urlencoded({
extended: true
})
);
In the code snippet above, we did the following
- Imported Express, bodyParser, socket.io, path, node-env-file modules
- Imported the Redis client we created in
lib/redis.js
- Imported the routes in
server/routes.js
- Set the environment file
- Created the instance of Express
- Set Pug as our view template engine and configured the path to our templates
- Configured the path of our static files (stylesheets, scripts, etc)
- Configured a middleware to use body-parser to parse the request body
Next, we are going to subscribe to channels using the Redis client and listen for messages published on those channels. Once someone sends a message or joins the chat room, we will send the message or updated users list to all users using Socket.io. Copy the code snippet below to server.js:
client().then(
res => {
// App routes
app.get("/", routes.home);
app.get("/chat/:username", routes.chatRoom);
app.get("/messages", routes.messages);
app.get("/users", routes.users);
app.post("/user", routes.createUser);
app.delete("/user", routes.deleteUser);
app.post("/message", routes.createMessage);
//Start the server
const server = app.listen(PORT, () => {
console.log("Server Started");
});
const io = socket.listen(server);
// //listen and emit messages and user events (leave or join) using socket.io
io.on("connection", socket => {
// subscribe to Pub/Sub channels
res.subscribe("chatMessages");
res.subscribe("activeUsers");
res.on("message", (channel, message) => {
if (channel === "chatMessages") {
socket.emit("message", JSON.parse(message));
} else {
socket.emit("users", JSON.parse(message));
}
});
});
},
err => {
console.log("Redis connection failed: ", err);
}
);
We subscribed to two channels: chatMessages
and activeUsers
. Whenever a message is published, an event message
is triggered. In the code snippet above, we did the following
- We declared the application endpoints
- We started the server
- We created an instance of Socket.io using the server we started
- Once Socket.io establishes connection, we used the Redis client to subscribe to
chatMessages
andactiveUsers
channels - We checked for any
message
event triggered by a published message. - We then broadcasted the message with Socket.io, using
message
event for messages fromchatMessage
channel andusers
event for messages fromactiveUsers
channel
The /
endpoint displays the homepage of the app, by rendering the index view template, while the /chat/:username
renders the chat room using the room view template. We will cover the view templates later in this tutorial.
The GET /messages
endpoint returns the history of messages shared in the chat room while GET /users
returns the list of active users stored.
The POST /user
endpoint handles adding users to active users. The POST /message
handles publishing and storing of messages sent by users. The DELETE /user
endpoint removes users from active users.
Remember we said something about Pug (formerly known as Jade) earlier on? Yes! We are using it as our view template engine, alongside CSS & JQuery to build the front end client for the chat room.
Create a folder views and a file in it master.pug so that the path is views/master.pug. This file will be the parent template, which other child templates will inherit. Copy the code snippet below:
doctype html
html
head
title= title
link(rel='stylesheet', href='https://fonts.googleapis.com/css?family=Raleway:300,400,700')
meta(name='viewport', content='width=device-width, initial-scale=1, maximum-scale=1')
link(type='text/css', rel='stylesheet', href='/css/style.css')
body
block content
script(src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js')
script(src='/js/script.js')
block script
When we extend this file, we can override specific areas labeled blocks. In the code snippet above, we have two blocks: contentand script. We will be extending the file in our child templates using those blocks. Also, Pug relies on indentation to create parent and child elements; therefore, we have to pay attention to that. You can check out pug's documentation.
Next, we create index.pug, the template file we called in our GET /endpoint. It will hold the structure of the homepage. Copy the code snippet below:
extends master
block content
div.container
h1.title=title
div.joinbox
form.join
input.username(type="text", placeholder="Enter your username", required="required")
input.submit(type="submit", value="Join Room")
In the code snippet above, we are extending master.pug and overriding the block content to hold the structure we want. For the homepage, we are going to display a form to collect the username and submit it.
Next, let’s create the template file for the chat room, create file room.pug and paste the code snippet below:
extends master
block content
div.room
div.chat
div.users
h2.title Active Users
div.clearfix
div.sendbox
form.send
textarea.message(placeholder='Type your message here', required='required')
input.name(type='hidden', value=user)
input.submit(type='submit', value='Send')
block script
script(src='/socket.io/socket.io.js')
script(src='../js/chat.js')
Here, we added the socket.io script and the script handling the room, by overriding the block script.
Next, we need to style the structure we have defined above. Create a stylesheet style.css in public/css, so that the path is public/css/style.css and paste below into it:
*{
padding: 0;
margin: 0;
box-sizing: border-box;
}
html, p, textarea, h1, h2{
font-family: Raleway, "Lucida Grande", Helvetica, Arial, sans-serif;
font-size: 10px;
}
body{
background: #23074d; /* fallback for old browsers */
background: -webkit-linear-gradient(to right, #cc5333, #23074d); /* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(to right, #cc5333, #23074d); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
height: 100vh;
position: relative;
}
h1{
font-size: 6rem;
color: #fff;
text-align: center;
}
.container{
position: absolute;
top: 50%;
left: 0;
right: 0;
transform: translateY(-50%);
}
.joinbox{
max-width: 500px;
border:2px solid #ffffff;
padding: 4rem;
width:70%;
margin: 5rem auto 0;
border-radius: 5px;
}
.joinbox .username{
display: block;
width: 100%;
margin:auto;
padding: 1.5rem;
background-color: transparent;
border: none;
border-bottom: 2px solid #ffffff;
color: #ffffff;
font-size: 2rem;
}
.joinbox .submit{
background-color: #ffffff;
padding: 1.5rem;
border: none;
/*border: 2px solid #ffffff;*/
display: block;
width: 100%;
margin: 3rem auto 0;
color: #772D40;
font-size: 2rem;
transition: all ease 500ms;
}
.joinbox .submit:hover{
box-shadow: 0 0 5px rgba(0,0,0,0.7);
}
.joinbox .submit[disabled="disabled"]{
opacity: 0.7;
cursor: not-allowed;
}
.alert{
background-color: #cab0a9;
color: #772D40;
display: block;
padding: 0.5rem 1rem;
font-size: 1.4rem;
}
.clearfix{
content: ' ';
height: 0;
clear: both;
position: relative;
}
.room{
width: 100%;
position: fixed;
bottom: 0;
}
.chat{
width:68%;
margin: auto 1%;
background-color: #ffffff;
padding: 2rem;
color: #772D40;
display: inline-block;
overflow-y: auto;
max-height:500px;
vertical-align: bottom;
}
.chat .item{
padding: 0.75rem 0;
display: block;
border-bottom: thin solid #f0f0f0;
font-size: 1.4rem;
}
.chat .item:last-child{
border-bottom: none;
}
.chat .user{
color: #23074d;
display: block;
font-weight: 700;
}
.chat .system{
color: #cc5333;
display: block;
font-weight: 700;
}
.users{
background-color: #ffffff;
margin: auto 1%;
width:28%;
color: #772D40;
display: inline-block;
font-size: 1.6rem;
max-height:500px;
overflow-y: auto;
vertical-align: bottom;
}
.users .title{
background-color: #772D40;
color: #ffffff;
padding: 1rem 2rem;
margin-bottom: 1rem;
text-align: center;
display: block;
font-size: 1.8rem;
}
.users .item{
padding: 1.2rem 2rem;
display: block;
border-bottom: thin solid #f0f0f0;
font-size: 1.5rem;
}
.users .item:last-child{
border-bottom: none;
}
.send{
margin-top: 2rem;
display: block;
background-color: #ffffff;
padding: 2rem;
}
.send .message{
display: inline-block;
width:72%;
margin-right: 3%;
background-color: #f0f0f0;
border: none;
font-size: 1.6rem;
padding: 1.5rem;
box-shadow: 0 0 2px rgba(0,0,0,0.2);
}
.send .submit{
display: inline-block;
width:25%;
background-color: #772D40;
padding: 1.5rem;
border: none;
color: #ffffff;
font-size: 2rem;
vertical-align: bottom;
transition: all ease 500ms;
}
Finally, we need make our username registration form and chat room work. We are going to make this possible by using jQuery; a javascript library, to make requests to the endpoints.
First, let’s create the script to handle username registration. Create file script.js and paste the code snippet below:
$(document).ready(function(){
//Registration form submission
$('.join').on("submit", function(e) {
e.preventDefault();
$('.submit').prop('disabled', true);
//Register user
$.post('/user', {user: $('.username').val()})
.done(function(res){
if(res.status === 200){
window.location = 'chat/'+$('.username').val();
}else{
$('.submit').prop('disabled', false);
$('.join').prepend('<p class="alert">The username is already taken!</p>');
setTimeout(function(){
$('.alert').fadeOut(500, function () {
$(this).remove();
})
}, 2000)
}
});
});
});
Once the user submits the form, a POST request is sent to /userwith the username. If the response status is 200, we redirect to the chat room, else, we display a message that disappears after 2 seconds.
Next, let’s power our chat room. We need to display the chat history, active users, update the message wall when another user sends a message and also update the users list when a user joins or leave. Create a file chat.js in public/js folder and copy the code snippet below:
$(document).ready(function () {
const socket = io();
//Get the chat history
$.get('/messages')
.done(function (res) {
$.each(res, function (index, value) {
value = JSON.parse(value);
console.log(value);
if (value.user === 'system') {
$('.chat').append('<p class="item"><span class="system">' + value.user + ': </span><span class="msg">' + value.message + '</span></p>');
} else {
$('.chat').append('<p class="item"><span class="user">' + value.user + ': </span><span class="msg">' + value.message + '</span></p>');
}
});
$('.chat').animate({'scrollTop': 999999}, 200);
});
//Get the list of all active users
$.get('/users')
.done(function (res) {
$.each(res, function (index, value) {
$('.users').append('<p class="item">' + value + '</span>');
});
});
//Message box submission using the 'Enter' key
$('.room .message').on("keydown", function (e) {
if (e.keyCode === 13) {
e.preventDefault();
let user = $('.name').val();
let msg = $('.message').val();
$.post('/message', {user: user, msg: msg})
.done(function () {
$('.message').val('');
$('.submit').prop('disabled', false);
});
}
});
//Message box submission
$('.room').on("submit", function (e) {
e.preventDefault();
let user = $('.name').val();
let msg = $('.message').val();
$.post('/message', {user: user, msg: msg})
.done(function () {
$('.message').val('');
$('.submit').prop('disabled', false);
});
});
//Remove user from active user list just before closing the window
window.onbeforeunload = function () {
$.ajax({
method: 'DELETE',
url: '/user',
data: {user: $('.name').val()}
})
.done(function (msg) {
alert(msg.message);
});
return null;
};
//Listens to when a chat message is broadcasted and displays it
socket.on('message', function (data) {
console.log(data);
let username = data.user;
let message = data.message;
if (username === 'system') {
$('.chat').append('<p class="item"><span class="system">' + username + ': </span><span class="msg">' + message + '</span></p>');
} else {
$('.chat').append('<p class="item"><span class="user">' + username + ': </span><span class="msg">' + message + '</span></p>');
}
$('.chat').animate({'scrollTop': 999999}, 200);
});
//Listens to when the active user list is updated and broadcasted
socket.on('users', function (data) {
$('.users .item').remove();
$.each(data, function (index, value) {
$('.users').append('<p class="item">' + value + '</span>');
});
});
});
In the code snippet above, we did the following
- Created an instance of socket.io named
socket
- We fetched all the messages from the server and displayed it
- We fetched the list of all active users from the server and displayed it
- Send messages submitted by users to the server, either by clicking the send button or hitting enter.
- Remove a user from the active user list on the server, just before the browser tab or window is closed
- listen for messages emitted by the server and add them to the message wall using socket.io
- Listen for updated active users list and display them using socket.io
Our chat room is now ready for deployment!
Before we deploy the application for usage, we need to quickly edit package.json in the root folder, so that we can start the app automatically, when we deploy. Add this to the file:
"scripts": {
"babel-node": "babel-node --presets=env",
"start": "nodemon --exec npm run babel-node -- ./server/server.js"
}
Finally, time to deploy! We will use a simple tool called now by Zeit. Let’s quickly install this, run this in your terminal:
npm install -g now
Once the installation is done, navigate to the project root folder in your terminal and run the now command. If it is your first time, you will be prompted to create an account. Once you are done, run the now command again, a URL will be generated and your project files uploaded.
insert image
You can now access the simple application via the URL generated. Pretty straightforward!
insert image
We have been able to focus more on building our chat room, without having to worry about infrastructure. The ability to provision resources very fast, all from a single dashboard means we get to focus more on writing code. Don’t stop here, get creative and improve on the code in this tutorial, you can find the source code for this tutorial on Github.