How To Build A Group Chat App With Vanilla JS, Twilio And Node.js

Chat is becoming an increasingly popular communication medium in both business and social contexts. Businesses use chat for customer and employee intra-company communication like with Slack, Microsoft Teams, Chanty, HubSpot Live Chat, Help Scout, etc. Most social networks and communication apps also offer chat as an option by default, like on Instagram, Facebook, Reddit, and Twitter. Other apps like Discord, Whatsapp, and Telegram are mostly chat-based, with group chats being one of their main functionalities.

While there exist numerous products to facilitate chat, you may need a custom-tailored solution for your site that fits your particular communication needs. For example, many of these products are stand-alone apps and may not be able to integrate within your own site. Having your users leave your website to chat may not be the greatest option as it can affect user experience and conversion. On the flip side, building a chat app from scratch can be a daunting and sometimes overwhelming task. However, by using APIs like Twilio Conversations you can simplify the process of creating them. These communication APIs handle group creation, adding participants, sending messages, notifications, among other important chat functions. Backend apps that use these APIs only have to handle authentication and make calls to these APIs. Front-end apps then display conversations, groups, and messages from the backend.

In this tutorial, you will learn how to create a group chat app using the Twilio Conversations API. The front end for this app will be built using HTML, CSS, and Vanilla JavaScript. It will allow users to create group chats, send invites, login, as well as send and receive messages. The backend will be a Node.js app. It will provide authentication tokens for chat invitees and manage chat creation.


Before you can start this tutorial, you need to have the following:

The Backend App

To send chat messages using Twilio API, you need a conversation. Chat messages are sent and received within a conversation. The people sending the messages are called participants. A participant can only send a message within a conversation if they are added to it. Both conversations and participants are created using the Twilio API. The backend app will perform this function.

A participant needs an access token to send a message and get their subscribed conversations. The front-end portion of this project will use this access token. The backend app creates the token and sends it to the frontend. There it will be used to load conversations and messages.

Project Starter

You’ll call the backend app twilio-chat-server. A scaffolded project starter for it is available on Github. To clone the project and get the starter, run:

git clone
cd twilio-chat-server
git checkout starter

The backend app takes this structure:

├── app.js
├── config/
├── controllers/
├── package.json
├── routes/
└── utils/

To run the app, you’ll use the node index.js command.


The backend app needs 8 dependencies. You can install them by running:

npm i 

Here’s a list of each of the dependencies:

  • connect-mongo connects to MongoDB, which you’ll use as a session store;
  • cors handles ;
  • dotenv loads environment variables from the .env file that you will create in a later step;
  • express is the web framework you’ll use for the backend;
  • express-session provides middleware to handle session data;
  • http-errors helps create server errors;
  • morgan handles logging;
  • twilio creates the Twilio client, generates tokens, creates conversations, and adds participants.


The config folder is responsible for loading configuration from environment variables. The configuration is grouped into three categories: configuration for CORS, Twilio, and the MongoDB session DB. When the environment is development, you will load config from the .env file using dotenv.

Start by creating the .env file on the terminal. This file is already added to the .gitignore file to prevent the sensitive values it contains from being checked into the repository.

touch .env

Here’s what your .env should look like:

# Session DB Config

# Twilio Config

# CORS Client Config

You can learn how to create a user for your session DB from this MongoDB manual entry. Once you create a session database and a user who can write to it, you can fill the SESSION_DB_USER, SESSION_DB_PASS, and SESSION_DB_NAME values. If you’re running a local instance of MongoDB, the SESSION_DB_HOST would be localhost, and the SESSION_DB_PORT usually is 27017. The SESSION_DB_SECRET is used by express-session to sign the session ID cookie, and it can be any secret string you set.

In the next step, you will get credentials from the Twilio Console. The credentials should be assigned to the variables with the TWILIO_ prefix. During local development, the front-end client will run on http://localhost:3000. So, you can use this value for the CORS_CLIENT_DOMAIN environment variable.

Add the following code to to load environment variables.

import dotenv from 'dotenv';

if (process.env.NODE_ENV == 'development') {

const corsClient = {
    domain: process.env.CORS_CLIENT_DOMAIN

const sessionDB = {
    host: process.env.SESSION_DB_HOST,
    user: process.env.SESSION_DB_USER,
    pass: process.env.SESSION_DB_PASS,
    port: process.env.SESSION_DB_PORT,
    name: process.env.SESSION_DB_NAME,
    secret: process.env.SESSION_DB_SECRET

const twilioConfig = {
    accountSid: process.env.TWILIO_ACCOUNT_SID,
    authToken: process.env.TWILIO_AUTH_TOKEN,
    apiKey: process.env.TWILIO_API_KEY,
    apiSecret: process.env.TWILIO_API_SECRET

const port = process.env.PORT || '8000';

export { corsClient, port, sessionDB, twilioConfig };

The environment variables are grouped into categories based on what they do. Each of the configuration categories has its own object variable, and they are all exported for use in other parts of the app.

Getting Twilio Credentials From the Console

To build this project, you’ll need four different Twilio credentials: an Account SID, an Auth Token, an API key, and an API secret. In the console, on the General Settings page, scroll down to the API Credentials section. This is where you will find your Account SID and Auth Token.

To get an API Key and Secret, go to the . You can see it in the screenshot below. Click the + button to go to the New API Key page.

On this page, add a key name and leave the KEY TYPE as Standard, then click Create API Key. Copy the API key and secret. You will add all these credentials in a .env file as you shall see in subsequent steps.


The backend app needs two utility functions. One will create a token, and the other will wrap async controllers and handle errors for them.

In , add the following code to create a function called createToken that will generate Twilio access tokens:

import { twilioConfig } from '../config/index.js';
import twilio from 'twilio';

function createToken(username, serviceSid) {
    const AccessToken = twilio.jwt.AccessToken;
    const ChatGrant = AccessToken.ChatGrant;

    const token = new AccessToken(
        { identity: username }

    const chatGrant = new ChatGrant({
        serviceSid: serviceSid,


    return token.toJwt();

In this function, you generate access tokens using your Account SID, API key, and API secret. You can optionally supply a unique identity which could be a username, email, etc. After creating a token, you have to add a chat grant to it. The chat grant can take a conversation service ID among other optional values. Lastly, you’ll convert the token to a JWT and return it.

The file contains an asyncWrapper function that wraps async controller functions and catches any errors they throw. Paste the following code into this file:

function asyncWrapper(controller) {
    return (req, res, next) => Promise.resolve(controller(req, res, next)).catch(next);

export { asyncWrapper, createToken };


The backend app has four controllers: two for authentication and two for handling conversations. The first auth controller creates a token, and the second deletes it. One of the conversations controllers creates new conversations, while the other adds participants to existing conversations.

Conversation Controllers

In the file, add these imports and code for the StartConversation controller:

import { twilioConfig } from '../config/index.js';
import { createToken } from '../utils/token.js';
import twilio from 'twilio';

async function StartConversation(req, res, next) {
    const client = twilio(twilioConfig.accountSid, twilioConfig.authToken);

    const { conversationTitle, username } = req.body;

    try {
        if (conversationTitle && username) {
            const conversation = await client.conversations.conversations
                .create({ friendlyName: conversationTitle });

            req.session.token = createToken(username, conversation.chatServiceSid);
            req.session.username = username;

            const participant = await client.conversations.conversations(conversation.sid)
                .participants.create({ identity: username })

            res.send({ conversation, participant });
        } else {
            next({ message: 'Missing conversation title or username' });
    catch (error) {
        next({ error, message: 'There was a problem creating your conversation' });

The StartConversation controller first creates a Twilio client using your twilioConfig.accountSid and twilioConfig.authToken which you get from config/index.js.

Next, it creates a conversation. It needs a conversation title for this, which it gets from the request body. A user has to be added to a conversation before they can participate in it. A participant cannot send a message without an access token. So, it generates an access token using the username provided in the request body and the conversation.chatServiceSid. Then the user identified by the username is added to the conversation. The controller completes by responding with the newly created conversation and participant.

Next, you need to create the AddParticipant controller. To do this, add the following code below what you just added in the controllers/conversations.js file above:

async function AddParticipant(req, res, next) {
    const client = twilio(twilioConfig.accountSid, twilioConfig.authToken);

    const { username } = req.body;
    const conversationSid =;

    try {
        const conversation = await client.conversations.conversations

        if (username && conversationSid) {
            req.session.token = createToken(username, conversation.chatServiceSid);
            req.session.username = username;

            const participant = await client.conversations.conversations(conversationSid)
                .participants.create({ identity: username })

            res.send({ conversation, participant });
        } else {
            next({ message: 'Missing username or conversation Sid' });
    } catch (error) {
        next({ error, message: 'There was a problem adding a participant' });

export { AddParticipant, StartConversation };

The AddParticipant controller adds new participants to already existing conversations. Using the conversationSid provided as a route parameter, it fetches the conversation. It then creates a token for the user and adds them to the conversation using their username from the request body. Lastly, it sends the conversation and participant as a response.

Auth Controllers

The two controllers in are called GetToken and DeleteToken. Add them to the file by copying and pasting this code:

function GetToken(req, res, next) {
    if (req.session.token) {
        res.send({ token: req.session.token, username: req.session.username });
    } else {
        next({ status: 404, message: 'Token not set' });

function DeleteToken(req, res, _next) {
    delete req.session.token;
    delete req.session.username;

    res.send({ message: 'Session destroyed' });

export { DeleteToken, GetToken };

The GetToken controller retrieves the token and username from the session if they exist and returns them as a response. DeleteToken deletes the session.


The routes folder has three files: index.js, conversations.js, and auth.js.

Add these auth routes to the file by adding this code:

import { Router } from 'express';

import { DeleteToken, GetToken } from '../controllers/auth.js';

var router = Router();

router.get('/', GetToken);
router.delete('/', DeleteToken);

export default router;

The GET route at the / path returns a token while the DELETE route deletes a token.

Next, copy and paste the following code to the file:

import { Router } from 'express';
import { AddParticipant, StartConversation } from '../controllers/conversations.js';
import { asyncWrapper } from '../utils/controller.js';

var router = Router();'/', asyncWrapper(StartConversation));'/:id/participants', asyncWrapper(AddParticipant));

export default router;

In this file, the conversations router is created. A POST route for creating conversations with the path / and another POST route for adding participants with the path /:id/participants are added to the router.

Lastly, add the following code to your new file.

import { Router } from 'express';

import authRouter from './auth.js';
import conversationRouter from './conversations.js';

var router = Router();

router.use('/auth/token', authRouter);
router.use('/api/conversations', conversationRouter);

export default router;

By adding the conversation and auth routers here, you are making them available at /api/conversations and /auth/token to the main router respectively. The router is then exported.

The Backend App

Now it’s time to put the backend pieces together. Open the file in your text editor and paste in the following code:

import cors from 'cors';
import createError from 'http-errors';
import express, { json, urlencoded } from 'express';
import logger from 'morgan';
import session from 'express-session';
import store from 'connect-mongo';

import { corsClient, port, sessionDB } from './config/index.js';

import router from './routes/index.js';

var app = express();

app.use(urlencoded({ extended: false }));

    origin: corsClient.domain,
    credentials: true,
    methods: ['GET', 'POST', 'DELETE'],
    maxAge: 3600 * 1000,
    allowedHeaders: ['Content-Type', 'Range'],
    exposedHeaders: ['Accept-Ranges', 'Content-Encoding', 'Content-Length', 'Content-Range']
app.options('*', cors());

    store: store.create({
        mongoUrl: mongodb://${sessionDB.user}:${sessionDB.pass}@${}:${sessionDB.port}/${},
        mongoOptions: { useUnifiedTopology: true },
        collectionName: 'sessions'
    secret: sessionDB.secret,
    cookie: {
        maxAge: 3600 * 1000,
        sameSite: 'strict'
    name: 'twilio.sid',
    resave: false,
    saveUninitialized: true

app.use('/', router);

app.use(function (_req, _res, next) {
    next(createError(404, 'Route does not exist.'));

app.use(function (err, _req, res, _next) {
    res.status(err.status || 500).send(err);


This file starts off by creating the express app. It then sets up JSON and URL-encoded payload parsing and adds the logging middleware. Next, it sets up CORS and the session handling. As mentioned earlier, MongoDB is used as the session store.

After all that is set up, it then adds the router created in the earlier step before configuring error handling. Lastly, it makes the app listen to and accept connections at the port specified in the .env file. If you haven’t set the port, the app will listen on port 8000.

Once you’re finished creating the backend app, make sure MongoDB is running and start it by running this command on the terminal:

NODE_ENV=development npm start

You pass the NODE_ENV=development variable, so that configuration is loaded from the local .env file.

The Front-end

The front-end portion of this project serves a couple of functions. It allows users to create conversations, see the list of conversations they are a part of, invite others to conversations they created, and send messages within conversations. These roles are achieved by four pages:

  • a conversations page,
  • a chat page,
  • an error page,
  • a login page.

You’ll call the front-end app twilio-chat-app. A scaffolded starter exists for it on Github. To clone the project and get the starter, run:

git clone
cd twilio-vanilla-js-chat-app
git checkout starter

The app takes this structure:

├── index.html
├── pages
│   ├── chat.html
│   ├── conversation.html
│   ├── error.html
│   └── login.html
├── scripts
│   ├── chat.js
│   ├── conversation.js
│   └── login.js
└── styles
    ├── chat.css
    ├── main.css
    └── simple-page.css

The styling and HTML markup have already been added for each of the pages in the starter. This section will only cover the scripts you have to add.


The app has two dependencies: and . You’ll use axios to make requests to the backend app and @twilio/conversations to send and fetch messages and conversations in scripts. You can install them on the terminal by running:

npm i

The Index Page

This page serves as a landing page for the app. You can ) here. It uses two CSS stylesheets: which all pages use and which smaller, less complicated pages use.

You can find the contents of these stylesheets linked in the earlier paragraph. Here is a screenshot of what this page will look like:

The Error Page

This page is shown when an error occurs. can be found here. If an error occurs, a user can click the button to go to the home page. There, they can try what they were attempting again.

The Conversations Page

On this page, a user provides the title of a conversation to be created and their username to a form.

can be found here. Add the following code to the scripts/conversation.js file:

window.twilioChat = window.twilioChat || {};

function createConversation() {
    let convoForm = document.getElementById('convoForm');
    let formData = new FormData(convoForm);

    let body = Object.fromEntries(formData.entries()) || {};

    let submitBtn = document.getElementById('submitConvo');
    submitBtn.innerText = "Creating..."
    submitBtn.disabled = true; = 'wait';

        url: '/api/conversations',
        baseURL: 'http://localhost:8000',
        method: 'post',
        withCredentials: true,
        data: body
        .then(() => {
            window.twilioChat.username = body.username;
            location.href = '/pages/chat.html';
        .catch(() => {
            location.href = '/pages/error.html';

When a user clicks the Submit button, the createConversation function is called. In it, the contents of the form are collected and used in the body of a POST request made to http://localhost:8000/api/conversations/ in the backend.

You will use axios to make the request. If the request is successful, a conversation is created and the user is added to it. The user will then be redirected to the chat page where they can send messages in the conversation.

Below is a screenshot of the conversations page:

The Chat Page

On this page, a user will view a list of conversations they are part of and send messages to them. You can find here and here.

The scripts/chat.js file starts out by defining a namespace twilioDemo.

window.twilioChat = window.twilioChat || {};

Add the initClient function below. It is responsible for initializing the Twilio client and loading conversations.

async function initClient() {
    try {
        const response = await axios.request({
            url: '/auth/token',
            baseURL: 'http://localhost:8000',
            method: 'GETget',
            withCredentials: true

        window.twilioChat.username =;
        window.twilioChat.client = await Twilio.Conversations.Client.create(;

        let conversations = await window.twilioChat.client.getSubscribedConversations();

        let conversationCont, conversationName;

        const sideNav = document.getElementById('side-nav');

        for (let conv of conversations.items) {
            conversationCont = document.createElement('button');
   = conv.sid;
            conversationCont.value = conv.sid;
            conversationCont.onclick = async () => {
                await setConversation(conv.sid, conv.channelState.friendlyName);

            conversationName = document.createElement('h3');
            conversationName.innerText = ? ${conv.channelState.friendlyName};

    catch {
        location.href = '/pages/error.html';

When the page loads, initClient fetches the user’s access token from the backend, then uses it to initialise the client. Once the client is initialised, it’s used to fetch all the conversations the user is subscribed to. After that, the conversations are loaded onto the side-nav. In case any error occurs, the user is sent to the error page.

The setConversion function loads a single conversation. Copy and paste the code below in the file to add it:

async function setConversation(sid, name) {
    try {
        window.twilioChat.selectedConvSid = sid;

        document.getElementById('chat-title').innerText = '+ ' + name;

        document.getElementById('loading-chat').style.display = 'flex';
        document.getElementById('messages').style.display = 'none';

        let submitButton = document.getElementById('submitMessage')
        submitButton.disabled = true;

        let inviteButton = document.getElementById('invite-button')
        inviteButton.disabled = true;

        window.twilioChat.selectedConversation = await window.twilioChat.client.getConversationBySid(window.twilioChat.selectedConvSid);

        const messages = await window.twilioChat.selectedConversation.getMessages();

        addMessagesToChatArea(messages.items, true);

        window.twilioChat.selectedConversation.on('messageAdded', msg => addMessagesToChatArea([msg], false));

        submitButton.disabled = false;
        inviteButton.disabled = false;
    } catch {
        showError('loading the conversation you selected');

When a user clicks on a particular conversation, setConversation is called. This function receives the conversation SID and name and uses the SID to fetch the conversation and its messages. The messages are then added to the chat area. Lastly, a listener is added to watch for new messages added to the conversation. These new messages are appended to the chat area when they are received. In case any errors occur, an error message is displayed.

This is a screenshot of the chat page:

Next, you’ll add the addMessagedToChatArea function which loads conversation messages.

function addMessagesToChatArea(messages, clearMessages) {
    let cont, msgCont, msgAuthor, timestamp;

    const chatArea = document.getElementById('messages');

    if (clearMessages) {
        document.getElementById('loading-chat').style.display = 'none'; = 'flex';

    for (const msg of messages) {
        cont = document.createElement('div');
        if ( == window.twilioChat.username) {
        } else {

        msgCont = document.createElement('div');

        msgAuthor = document.createElement('p');
        msgAuthor.innerText =;

        timestamp = document.createElement('p');
        timestamp.innerText = msg.state.timestamp;

        msgCont.innerText += msg.state.body;



    chatArea.scrollTop = chatArea.scrollHeight;

The function addMessagesToChatArea adds messages of the current conversation to the chat area when it is selected from the side nav. It is also called when new messages are added to the current conversation. A loading message is usually displayed as the messages are being fetched. Before the conversation messages are added, this loading message is removed. Messages from the current user are aligned to the right, while all other messages from group participants are aligned to the left.

This is what the loading message looks like:

Add the sendMessage function to send messages:

function sendMessage() {
    let submitBtn = document.getElementById('submitMessage');
    submitBtn.disabled = true;

    let messageForm = document.getElementById('message-input');
    let messageData = new FormData(messageForm);

    const msg = messageData.get('chat-message');

        .then(() => {
            document.getElementById('chat-message').value = '';
            submitBtn.disabled = false;
        .catch(() => {
            showError('sending your message');
            submitBtn.disabled = false;

When the user sends a message, the sendMessage function is called. It gets the message text from the text area and disables the submit button. Then using the currently selected conversation, the message is sent using its sendMessage method. If successful, the text area is cleared and the submit button is re-enabled. If unsuccessful, an error message is displayed instead.

The showError method displays an error message when it is called; hideError hides it.

function showError(msg) {
    document.getElementById('error-message').style.display = 'flex';
    document.getElementById('error-text').innerText = There was a problem ${msg ? msg : 'fulfilling your request'}.;

function hideError() {
    document.getElementById('error-message').style.display = 'none';

This is what this error message will look like:

The logout function logouts out the current user. It does this by making a request to the backend which clears their session. The user is then redirected to the conversation page, so they can create a new conversation if they’d like.

function logout(logoutButton) {
    logoutButton.disabled = true; = 'wait';

        url: '/auth/token',
        baseURL: 'http://localhost:8000',
        method: 'DELETEdelete',
        withCredentials: true
        .then(() => {
            location.href = '/pages/conversation.html';
        .catch(() => {
            location.href = '/pages/error.html';

Add the inviteFriend function to send conversation invites:

async function inviteFriend() {
    try {
        const link = http://localhost:3000/pages/login.html?sid=${window.twilioChat.selectedConvSid};

        await navigator.clipboard.writeText(link);

        alert(The link below has been copied to your clipboard.\n\n${link}\n\nYou can invite a friend to chat by sending it to them.);
    } catch {
        showError('preparing your chat invite');

To invite other people to participate in the conversation, the current user can send another person a link. This link is to the login page and contains the current conversation SID as a query parameter. When they click the invite button, the link is added to their clipboard. An alert is then displayed giving invite instructions.

Here is a screenshot of the invite alert:

The Login Page

On this page, a user logs in when they are invited to a conversation. You can at this link.

In scripts/login.js, the login function is responsible for logging in conversation invitees. Copy its code below and add it to the aforementioned file:

function login() {
    const convParams = new URLSearchParams(;
    const conv = Object.fromEntries(convParams.entries());

    if (conv.sid) {
        let submitBtn = document.getElementById('login-button');
        submitBtn.innerText = 'Logging in...';
        submitBtn.disabled = true; = 'wait';

        let loginForm = document.getElementById('loginForm');
        let formData = new FormData(loginForm);
        let body = Object.fromEntries(formData.entries());

            url: /api/conversations/${conv.sid}/participants,
            baseURL: 'http://localhost:8000',
            method: 'POSTpost',
            withCredentials: true,
            data: body
            .then(() => {
                location.href = '/pages/chat.html';
            .catch(() => {
                location.href = '/pages/error.html';
    } else {
        location.href = '/pages/conversation.html';

The login function takes the conversation sid query parameter from the URL and the username from the form. It then makes a POST request to api/conversations/{sid}/participants/ on the backend app. The backend app adds the user to the conversation and generates an access token for messaging. If successful, a session is started in the backend for the user.

The user is then redirected to the chat page, but if the request returns an error, they are redirected to the error page. If there is no conversation sid query parameter in the URL, the user is redirected to the conversation page.

Below is a screenshot of the login page:

Running the App

Before you can start the front-end app, make sure that the backend app is running. As mentioned earlier, you can start the backend app using this command on the terminal:

NODE_ENV=development npm start

To serve the front-end app, run this command in a different terminal window:

http-server -p 3000

This serves the app at http://localhost:3000. Once it’s running, head on over to http://localhost:3000/pages/conversation.html; set a name for your conversation and add your username, then create it. When you get to the chat page, click on the conversation, then click the Invite button.

In a separate incognito window, paste the invite link and put a different username. Once you’re on the chat page in the incognito window, you can begin chatting with yourself. You can send messages back and forth between the user in the first window and the second user in the incognito window in the same conversation.


In this tutorial, you learned how to create a chat app using Twilio Conversations and Vanilla JS. You created a Node.js app that generates user access tokens, maintains a session for them, creates conversations, and adds users to them as participants. You also created a front-end app using HTML, CSS, and Vanilla JS. This app should allow users to create conversations, send messages, and invite other people to chat. It should get access tokens from the backend app and use them to perform these functions. I hope this tutorial gave you a better understanding of how Twilio Conversations works and how to use it for chat messaging.

To find out more about Twilio Conversations and what else you could do with it, check out its documentation linked here. You can also find the source code for the backend app on Github here, and the code for the front-end app here.

This content was originally published here.