Simple chat application using Websockets with FastAPI

Hey Guys, here I’m again tinkering with FastAPI. This time I’ll be trying to build an extremely simple chat web application. I’ll be using Websockets with FastAPI.

A full explanation of Websockets will need a whole article, so I’m assuming that the reader have some familiarity with this protocol. That is more like an upgrade of HTTP instead of a protocol itself.

Another valid point to be made, is that this was a result of me hacking around with the FastAPI documentation. So as a result, I haven’t taught very deeply on a better approach. On another post I will present you a more mature approach to this problem. For now let’s just have fun learning stuffs.

What we want to build? Partial description of requirements.

Before we start coding let’s think first what we want to make. Well the idea is to build a simple web page, that simulate the behavior of a chat room.

Desired behavior: - Open the page on a browser. - All the clients connected to the same URL, but through different browsers, can chat between them.

Basic design of the behavior

Client1 and Client2 connect to the URL, let’s say is http://localhost:8000. After this connection, every message from Client1 will be broadcasted to the others clients connected on the room, in this case would be Client2. So a broad view of the pattern would be like in the ASCII picture:


   Client 1

     ___T_                              SERVER (FastAPI)
    | O O |                             +---------+
    |__n__|           Message 1         |         |
 >===]__o[===<    --------------->      |         |
     [o__]            Message 2         |         |
     ]| |[        <--------------       |         |
    [_| |_]                             |         |
                                        |         |
                                        |         | 
   Client 2                             |         |
                                        |         |
     ___T_                              |         |
    | O O |                             |         |
    |__n__|          Message 2          |         |
 >===]__o[===<   --------------->       |         |
     [o__]           Message 1          |         |
     ]| |[       <--------------        |         |
    [_| |_]                             +---------+

Robot ascii generated with go-asciibot

Client 1 send message to the server, server broadcast the message to the other clients on the room.

Requirements

$ pip install fastapi
  • Uvicorn
$ pip install uvicorn

Server code

Let’s jump into the code. I will divide it on two sections, one for the server code and the other for the client code.

Well as I said previously I’ll need an endpoint to serve the HTML page, that in this case will contain the client code. So we’ll need to learn how to serve an HTML with FastAPI. Our HTML code will be on the file index.html, on the same folder as the main.py.


from fastapi import [FastAPI](https://fastapi.tiangolo.com/)

app = [FastAPI](https://fastapi.tiangolo.com/)()

html = ""
with open('index.html', 'r') as f:
    html = f.read()

@app.get("/")
async def get():
    return HTMLResponse(html)

So what’s happening here? First we make a simple import of the FastAPI class, to be able to create our application. Next to that, we read the content of the index.html file and store its content on the variable called html. There’s must be a better way to do this, but for our case it solves our problems.

You may be wondering what is the content of the HTML file, we’ll see don’t worry.

And for last we define our endpoint, where we can access the content of the index.html file. In this way if you visit http://localhost:8000 the browser will render the HTML.

So what’s next? Until now, we’re only able to render the content of the HTML, but we haven’t handled yet other situations, right? How do we differentiate a client from another one? Let’s solve this.

from typing import List
from fastapi import [FastAPI](https://fastapi.tiangolo.com/), WebSocket
from fastapi.responses import HTMLResponse

class ConnectionManager:
    def __init__(self):
        self.connections: List[WebSocket] = []

    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        self.connections.append(websocket)

    async def broadcast(self, data: str):
        for connection in self.connections:
            await connection.send_text(data)


manager = ConnectionManager()


@app.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: int):
    await manager.connect(websocket)
    while True:
        data = await websocket.receive_text()
        await manager.broadcast(f"Client {client_id}: {data}")

The class called ConnectionManager, as the name suggests, is the class we’re going to use to handle the connection of different clients.

  • The connect method on this class, take as argument a Websocket client. This client start to accept message from the browser and is added to a list with all the clients.
  • The broadcast method, take as argument a message and broadcast its content to any other client on the room.

Every client will connect to http://localhost:8000/ws/{client_id}, where {client_id} will be a integer that identify this client. After it is connected it will start accepting messages from the browser. Any of this message will be broadcasted later to any client on the room.

Client code

Now comes to see the client code, that in our case will be the HTML code.


<!DOCTYPE html>
<html>
    <head>
        <title>Chat</title>
    </head>
    <body>
        <h1>WebSocket Chat</h1>
        <button onClick="showForm(event)" id="connect">Connect</button>
        <form action="" onsubmit="sendMessage(event)" id="form" style="display: none">
            <input type="text" id="messageText" autocomplete="off"/>
            <button>Send</button>
        </form>
        <ul id='messages'>
        </ul>
        <script>
            var clientID = Date.now();
            var ws = new WebSocket(`ws://localhost:8000/ws/${clientID}`);

            function processMessage(event) {
                var messages = document.getElementById('messages')
                var message = document.createElement('li')
                var content = document.createTextNode(event.data)
                message.appendChild(content);
                messages.appendChild(message);
            }

            ws.onmessage = processMessage;

            function sendMessage(event) {
                var input = document.getElementById("messageText")
                var message = document.createElement('li')
                var content = document.createTextNode(input.value)
                message.appendChild(content);
                messages.appendChild(message);
                ws.send(input.value);

                input.value = ''
                event.preventDefault()
            }
            
            function showForm(event) {
                var button = document.getElementById("connect");
                var form = document.getElementById("form");
                button.style.display = "none";
                form.style.display = "block";
            }

        </script>
    </body>
</html>

Ups!! That’s a lot of information, so let’s chunk it and try to understand at least the most important parts. The pure HTML, has two principal components, the form and the button.

  • The button. After it’s clicked the button will be hidden from the client, and the form will be visible. Only that no more tricks. This is done through javascript, on the showForm method.

  • The form, contains an input field where the messages are written, and a send button that will trigger the method sendMessage on the JavaScript side.

After we press the send button, we will send the text to the server using the Websocket connection. In this way when the server gets the message it will broadcast it to any other client on the room.

How to run it?

uvicorn main:app --reload

The --reload flag is to be able to recover on failure, very useful.

That’s all folks, and definitely need to improve my writing skills in the meanwhile I’ll keep practicing.

Testing the application

Open at least two web browsers and connect to localhost:8000