Breaking

How to create FastAPI WebSocket Chat Application with Vanilla JavaScript

This is a tutorial to build a chat application using WebSocket, not to be vague you are not going to use the socket.io library in this case, instead, you will use web socket API. There are two parts to this application just like most of the applications in the modern day. In the backend, you will use FastAPI which is incredibly fast and gaining popularity, and in the frontend, you will use Vanilla JavaScript. In another tutorial, we will use a front-end framework such as React.js or Vue.js.

Create a server folder where all of our backend code will be allocated. First of all, create a virtual environment for the backend Python project

# On Linux or Mac
python3 -m venv virtual-env
# On Windows, invoke the venv command as follows:
c:\>c:\Python35\python -m venv virtual-env

# Activate virtual environment On Linux or Mac
source virtual-env/bin/activate
# Activate virtual environment On Windows
virtual-env\Scripts\activate.bat

Create a requirements.txt file and add some packages there to track those packages with an appropriate version

fastapi==0.95.0
uvicorn[standard]
websockets==10.4

Install all those packages with the pip package manager by running a single command.

pip install -r requirements.txt

Now create a main.py file where all of our logic is for fast API and WebSocket server. The entire code is been explained line by line in a video tutorial on youtube.

from copy import deepcopy, copy
from fastapi import FastAPI, WebSocket, WebSocketException, WebSocketDisconnect
import json

app = FastAPI()

@app.get('/api')
def test_index():
    return {"detail": "Server is Working"}

room_list = []

# Send message to all users
async def broadcast_to_room(message: str, except_user):
    res = list(filter(lambda i: i['socket'] == except_user, room_list))
    for room in room_list:
        if except_user != room['socket']:
            await room['socket'].send_text(json.dumps(
            	{'msg': message, 'userId': res[0]['client_id']}))

def remove_room(except_room):
    new_room_list = copy(room_list)
    room_list.clear()
    for room in new_room_list:
        if except_room != room['socket']:
            room_list.append(room)
    print("room_list append - ", room_list)

@app.websocket('/ws/{client_id}')
async def websocket_endpoint(websocket: WebSocket, client_id: str):
    try:
        await websocket.accept()
        client = {
            "client_id": client_id,
            "socket": websocket
        }
        room_list.append(client)
        print("Connection established")
        while True:
            data = await websocket.receive_text()
            # await websocket.send_text(f"Message text was: {data}")
            await broadcast_to_room(data, websocket)
    except WebSocketDisconnect as e:
        remove_room(websocket)

The backend code is been done by now and needs to be run the code in order to find any kind of error.

uvicorn main:app --reload

Once everything is running successfully on the backend it is time for creating the frontend for this application. Create a client folder where all of the front-end code will be allocated. Moreover, create an index.html file to display chat app content on the web. I have used tailwind CSS for styling this web app.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket - client</title>
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-slate-300 p-0 m-0">
    <header class="bg-slate-900 text-slate-100 mb-12">
        <div class="container mx-auto">
            <ul class="flex py-4">
                <li class="list-none"><a 
                class="no-underline text-slate-100 mr-4 text-xl" 
                href="#">Home</a></li>
                <li class="list-none"><a 
                class="no-underline text-slate-100 mr-4 text-xl" 
                href="#">About</a></li>
                <li class="list-none"><a 
                class="no-underline text-slate-100 mr-4 text-xl" 
                href="#">Contact</a></li>
            </ul>
        </div>
    </header>
    <main>
        <div class="container mx-auto">
            <h1 class="text-xl text-slate-800 font-bold">Chat App</h1>
            <div 
class="message-box w-full flex-col h-96 overflow-y-scroll bg-slate-500" id="message-box">
                <!-- <div 
class="w-full flex justify-start">
                    <div 
class="box-bordered p-1 bg-slate-500 w-8/12 text-slate-100 rounded mb-1">
                        <p>His message</p>
                        <p>Email</p>
                    </div>
                </div> -->
            </div>
            <form id="message-form">
                <div class="input-group mb-2 flex flex-row">
                    <input type="text" id="message"
class="border-0 bg-slate-800 text-slate-300 py-2 px-1 w-11/12 outline-0"
                        placeholder="Enter your message">
<button type="submit" 
class="bg-slate-900 text-slate-300 border-0 w-1/12 outline-0">Send</button>
                </div>
            </form>
        </div>
    </main>
    <footer class="bg-slate-600 text-slate-400 w-full py-2 mt-80">
        <div class="container mx-auto">This is footer</div>
    </footer>
    <script src="main.js"></script>
</body>
</html>


There is one more file we need in order to create all of our frontend logic which is main.js.

document.addEventListener('DOMContentLoaded', (DOMEvent) => {
    DOMEvent.preventDefault();

    const messageFormEl = document.getElementById('message-form');
    const messageEl = document.getElementById('message');
    const messageBoxEl = document.getElementById('message-box');

    // Unique ID for all user
    const userId = window.crypto.randomUUID();
    function messageAppend(myMessage, msgContent) {
        let sideOff = 'justify-start',
            bgColor = 'bg-slate-700', 
            specificUser = userId;
        if (myMessage) {
            sideOff = 'justify-end';
            bgColor = 'bg-indigo-500';
        }else{
            specificUser = msgContent.userId;
        }
        const msgString = `
            <div class="w-full flex ${sideOff}">
                <div class="box-bordered p-1 ${bgColor} w-8/12 text-slate-100 rounded mb-1">
                    <p>${msgContent.msg}</p>
                    <p>${specificUser}</p>
                </div>
            </div>
            `;
        

        const domParser = new DOMParser();
        const msgEl = domParser.parseFromString(msgString, 'text/html').body.firstElementChild;
        messageBoxEl.append(msgEl);
    }

    // Create WebSocket connection.
    const socket = new WebSocket(`ws://localhost:8000/ws/${userId}`);

    // // Connection opened
    socket.addEventListener('open', (socketEvent) => {
        console.log("Connection is open");
    });

    // Close connection
    socket.addEventListener('close', (socketEvent) => {
        console.log("Connection is closed");
    });

    // // Listen for messages
    socket.addEventListener('message', (socketEvent) => {
        console.log("Message from server ", socketEvent.data);
        // console.log(JSON.parse(socketEvent.data));
        messageAppend(false, JSON.parse(socketEvent.data));
    });

    const errorMap = new Map();
    messageFormEl.addEventListener('submit', (event) => {
        event.preventDefault();
        if (messageEl.value === '') {
            console.log("Enter a message");
            errorMap.set('invalid_message', 'Please enter a message');
        } else {
            socket.send(messageEl.value);
            messageAppend(true, {msg: messageEl.value, userId: null});
            errorMap.clear();
            event.target.reset();
        }
    });
});

// docs 
// https://websockets.readthedocs.io/en/stable/index.html
// https://fastapi.tiangolo.com/advanced/websockets/#websockets-client
// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
// https://tailwindcss.com/docs/installation

Open index.html in any browser and this will work. If anything went wrong check your console of the browser and try to debug by yourself.

No comments:

Powered by Blogger.