In the near future, I plan to publish a large article on Habr, where I will analyze in detail the development of an anonymous chat in the Telegram MiniApp format. We will create a service for face-to-face communication, which will work inside Telegram and provide instant messaging.
Project technology stack:
FastAPI + WebSocket – for real time and messaging
Redis – for quick search and connection of interlocutors
PostgreSQL – for storing messages and user information
Vue 3 + Pinia – for a convenient and responsive interface
Telegram Mini Apps API – to embed the chat directly into Telegram
In addition to the technical analysis, in the article I will also touch on the issue of monetization. One of the most effective ways to monetize Telegram MiniApp is advertising. This allows the service to remain free for users, and the developer to earn money on the popularity of the project.
As an advertising platform, I will consider RichAds - they offer excellent tools for integrating advertising into Telegram MiniApps, which makes them a logical choice for such a project.
But before we dive into MiniApp development, let's get the basics down. In this article, we will get acquainted with web sockets, learn how they work, and learn how to implement them on FastAPI.
This is only the first part of a large project, which I will refer to in future material to make it more structured and easier to read.
So, what is WebSocket and how can it help us in developing a chat? Let's figure it out.
WebSocket: principles of operation
Imagine a phone call: you dial a number (establish a connection) once, and after that you can freely talk without having to dial the number again for each phrase. WebSocket works in a similar way - it is a technology that creates a permanent "communication channel" between the user's browser and the server, allowing them to exchange messages in real time.
Let's get some terminology straight:
The client is what the person uses: a web page in a browser, an application on a phone or computer
The server is a remote computer that stores and processes our application data
Key advantages of WebSocket:
Persistent connection: like a phone call – establish a connection once and communicate
Fast operation: no time is wasted on reconnection
Two-way communication: both the client and the server can start communicating first
Resource saving: service information is transmitted only at the beginning of the connection
Comparison of approaches to implementing a chat
Classic method (HTTP):
Imagine that you send letters:
Write a message and send it to the server
The server saves the letter
Other chat participants must constantly check the "mailbox" (make requests to the server)
Only after checking will they see the new message
Even if the check is done automatically (for example, via AJAX), you still have to constantly "look into the mailbox", which creates an extra load.
Disadvantages:
The server gets tired of constant checks
Messages arrive with a delay
A lot of Internet traffic is spent
WebSocket Method:
Now imagine a group call:
All participants are connected to the general conversation
When someone speaks, everyone hears it at once
No need to constantly check for new messages
Communication happens instantly
Advantages:
Messages arrive instantly
The server is less loaded
Internet traffic is saved
True real-time communication
This approach works great not only in chats, but also in online games, collaborative document editors and anywhere where an instant response to user actions is needed.
Next, we will create a simple chat on FastAPI to see how it works in practice. For simplicity of the example, we will make both the server and client parts in one application, although in a real "Tet-a-Tet" chat the interface will be a separate application on VueJS3.
What project are we going to develop?
Today we will create a full-fledged FullStack application – a group chat, in which users will be able to:
Create rooms for communication
Join existing rooms
Exchange messages in real time
All participants in the same room will instantly receive new messages without having to refresh the page.
Tech stack
To implement the project, we will use modern and convenient tools:
Python + FastAPI + WebSockets – for the server part and organizing real time
JavaScript – for establishing connections via web sockets and page dynamics
TailwindCSS – for fast and convenient interface styling
HTML + Jinja2 – for rendering pages with data
Amverum Cloud – for fast and easy deployment
Development stages
1. Server part (Backend)
Developing a class for managing web sockets
Creating an endpoint for handling connections via WebSocket
Implementing routes (endpoints) for rendering HTML pages
2. Client part (Frontend)
Developing two HTML pages
Adding JavaScript logic for connecting to WebSocket
Implementing dynamic interface updates
3. Project deployment
A group chat is pointless without external access, so in conclusion we will deploy the project on the Internet.
For deployment, we will use Amverum Cloud – a service that allows you to deploy a FastAPI application in just a couple of minutes. The advantages of this solution:
Automatic HTTPS certificates
Free domain name
Webhooks support (for example, for integration with Telegram bots)
Thus, as a result of the work, we will have a ready, expanded and working group chat that can be used, tested and expanded.
Project preparation
Let's start with project preparation. Let me remind you that we will be writing in the Python FastAPI framework, which is great for implementing WebSocket due to its asynchronous nature and simple API.
Open IDE and create a new project
Create a requirements.txt file and fill it in as follows:
fastapi==0.115.8 # The web framework itself
websockets==15.0 # Library for working with WebSocket
uvicorn==0.34.0. # ASGI server for running the application
jinja2==3.1.5 # Template engine for rendering HTML
- Install libraries:
pip install -r requirements.txt
- Prepare the project structure:
my_chat_project/
├── requirements.txt # Project dependencies file
├── app/ # Main application directory
│ ├── templates/ # Directory with HTML templates
│ │ ├── home.html # Home page template
│ │ └── index.html # Main application template
│ ├── static/ # Directory with static files (JS, CSS)
│ │ └── index.js # JavaScript for index.html
│ ├── api/ # Directory with API routes
│ │ ├── router_page.py # Routes for pages (HTML)
│ │ └── router_socket.py # Routes for WebSocket connections
│ └── main.py # Main application file (launch)
Writing the server part
We will describe the main logic of the server part of our application in the router_socket.py file. This file will be responsible for managing WebSocket connections and sending messages between users.
Importing the necessary modules
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from typing import Dict
FastAPI — used to create a web application.
WebSocket, WebSocketDisconnect — classes for working with WebSocket connections. The first allows you to establish connections, the second — to handle their termination.
Dict — a data type for convenient annotation of the structure for storing user connections.
Initializing the router
FastAPI uses routers (APIRouter) to organize endpoints into logical blocks. In this case, let's create a router with the prefix /ws/chat:
router = APIRouter(prefix="/ws/chat")
This route will handle all WebSocket requests related to the chat.
Creating a connection manager
For convenient connection management, let's create a ConnectionManager class. It will be responsible for connecting and disconnecting users, as well as for sending messages.
Full class code:
class ConnectionManager:
def __init__(self):
# Store active connections as {room_id: {user_id: WebSocket}}
self.active_connections: Dict[int, Dict[int, WebSocket]] = {}
async def connect(self, websocket: WebSocket, room_id: int, user_id: int):
"""
Establishes a connection with the user.
websocket.accept() — confirms the connection.
"""
await websocket.accept()
if room_id not in self.active_connections:
self.active_connections[room_id] = {}
self.active_connections[room_id][user_id] = websocket
def disconnect(self, room_id: int, user_id: int):
"""
Closes the connection and removes it from the list of active ones connections.
If there are no more users in the room, deletes the room.
"""
if room_id in self.active_connections and user_id in self.active_connections[room_id]:
del self.active_connections[room_id][user_id]
if not self.active_connections[room_id]:
del self.active_connections[room_id]
async def broadcast(self, message: str, room_id: int, sender_id: int):
"""
Broadcasts a message to all users in the room.
"""
if room_id in self.active_connections:
for user_id, connection in self.active_connections[room_id].items():
message_with_class = {
"text": message,
"is_self": user_id == sender_id
}
await connection.send_json(message_with_class)
Code breakdown
- Class constructor
self.active_connections is a dictionary that stores active connections grouped by rooms (room_id).
Each room (room_id) stores connected users as {user_id: WebSocket}.
- connect
Accepts a WebSocket connection, a room id (room_id), and a user (user_id).
Acknowledges the connection (websocket.accept()).
Adds the WebSocket to self.active_connections.
- disconnect
Removes the user's WebSocket from self.active_connections.
If there are no users left in the room, deletes the room.
- broadcast
Sends a message to all users in the room.
Additionally adds the is_self flag so that the client can visually highlight its messages.
Initializing the connection manager
Let's create an instance of the ConnectionManager class, which we will use later:
manager = ConnectionManager()
Creating a WebSocket endpoint
Now let's create a WebSocket endpoint that will manage user connections and message transmission in the chat.
@router.websocket("/{room_id}/{user_id}")
async def websocket_endpoint(websocket: WebSocket, room_id: int, user_id: int, username: str):
await manager.connect(websocket, room_id, user_id)
await manager.broadcast(f"{username} (ID: {user_id}) has joined the chat.", room_id, user_id)
try:
while True:
data = await websocket.receive_text()
await manager.broadcast(f"{username} (ID: {user_id}): {data}", room_id, user_id)
except WebSocketDisconnect:
manager.disconnect(room_id, user_id)
await manager.broadcast(f"{username} (ID: {user_id}) has left the chat.", room_id, user_id)
Code breakdown
- Route
Endpoint accepts three parameters from URL: room_id, user_id, username.
Each user connects via URL /ws/chat/{room_id}/{user_id}.
- Connection
await manager.connect(...) — adds user to active connections list.
await manager.broadcast(...) — notifies all users of the room about new member.
- Receiving and sending messages
Infinite loop (while True) listens for incoming messages via websocket.receive_text().
After receiving message, it is broadcast to all users of the room via manager.broadcast(...).
- Disconnection
If connection is interrupted (WebSocketDisconnect), manager.disconnect(...) is called.
Sends a message to the chat about the user leaving.
In this section, we created the server part of the chat on WebSocket using FastAPI. We:
Figured out imports and routers.
Created ConnectionManager to manage connections.
Wrote a WebSocket endpoint to receive and send messages.
In the next section, we will look at how to connect the client part and test WebSocket connections.
Writing the client part
The client part will conditionally consist of two main stages:
Description of endpoints for rendering HTML pages
Writing the frontend part itself: HTML + CSS + JS
In this section, we will describe the endpoints that will serve HTML pages. We will implement them in the app/router_page.py file.
Imports
First, import the necessary modules:
from fastapi import APIRouter, Request, Form
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
import random
Let's figure out why we need each import:
APIRouter – allows you to organize routing in the application.
Request – a request object that is passed to the template during rendering.
Form – used to process data transmitted via an HTML form.
Jinja2Templates – needed to work with HTML templates.
HTMLResponse – specifies that the endpoint returns an HTML page.
Random - to generate a random user ID
Initializing the route and renderer
Before describing the endpoints, let's create a templates object that will indicate where the HTML templates are stored, and initialize the router:
templates = Jinja2Templates(directory='app/templates')
router = APIRouter()
Endpoints description
Now let's create two endpoints. The first one will be responsible for rendering the main page, and the second one - for the room page (our group chat).
Endpoint for the main page
@router.get("/", response_class=HTMLResponse)
async def home_page(request: Request):
return templates.TemplateResponse("home.html", {"request": request})
This endpoint returns the main page home.html. It will contain a form for entering the chat, which we will analyze later.
Endpoint for joining chat
@router.post("/join_chat", response_class=HTMLResponse)
async def join_chat(request: Request, username: str = Form(...), room_id: int = Form(...)):
# Simple user_id generation
user_id = random.randint(100, 100000)
return templates.TemplateResponse("index.html",
{"request": request,
"room_id": room_id,
"username": username,
"user_id": user_id}
)
This endpoint does several things:
Gets username and room_id from the form (using Form(...)).
Generates a random user ID in the range from 100 to 100000
Returns the HTML page index.html with the parameters passed to the template:
room_id – ID of the room the user enters.
username – username.
user_id – generated user ID.
Why is Form(...) used?
FastAPI has several ways to pass data to an endpoint. In this case, Form(...) specifies that the username and room_id parameters are passed through an HTML form using the POST method. This is convenient for processing data entered by the user on a web page.
If we were passing data in the URL (as query parameters), we would have to use Query(...), and if we were passing JSON – Body(...).
At this point, we have endpoints ready for working with HTML pages. In the next section, we will implement the frontend: create HTML templates, styles, and connect WebSocket.
Frontend implementation: HTML, CSS, and JavaScript
Now that we have the server part and endpoints for rendering pages, let's move on to creating the user interface. Our client part will include:
HTML - for the page structure,
CSS (TailwindCSS) - for styling,
JavaScript - for event handling and working with WebSocket.
1. HTML structure
We will create two main HTML files:
home.html – the main page with the chat login form.
index.html – the chat page where real-time communication will take place.
Home page (home.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login to chat</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 flex flex-col items-center justify-center min-h-screen p-4">
<h1 class="text-3xl font-bold mb-6">Welcome to the chat</h1>
<form action="/join_chat" method="post" class="bg-white p-6 rounded-lg shadow-md w-full max-w-md">
<label class="block" text-gray-700">Enter your name:</label>
<input type="text" name="username" required
class="w-full p-2 border border-gray-300 rounded-lg mt-2 focus:ring-2 focus:ring-blue-500">
<label class="block text-gray-700 mt-4">Enter room ID:</label>
<input type="number" name="room_id" required min="1"
class="w-full p-2 border border-gray-300 rounded-lg mt-2 focus:ring-2 focus:ring-blue-500">
<button type="submit" class="w-full bg-blue-500 text-white px-4 ru-2 rounded-lg mt-4 hover:bg-blue-600">Log in
chat
</button>
</form>
</body>
</html>
Code breakdown
This template does the following:
Displays a greeting title.
Shows a login form consisting of two fields:
Username (username) – a regular text field.
Room ID (room_id) – a field for entering a numeric identifier.
- After clicking the "Enter chat" button, the data is sent to the server via POST (/join_chat).
Why just enter the room ID?
In production projects, you could select a room from a list, but in our case it is easier to let the user enter the ID themselves. If the room exists, they will connect, if not, they will create a new one.
Chat page (index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket Chat - Room {{ room_id }}</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 flex flex-col items-center p-4">
<h1 class="text-2xl font-bold mb-4">WebSocket Chat - Room {{ room_id }}</h1>
<!-- Hidden element for storing room data -->
<div id="room-data"
data-room-id="{{ room_id }}"
data-username="{{ username }}"
data-user-id="{{ user_id }}"
class="hidden">
</div>
<!-- Message area -->
<div id="messages"
class="w-full max-w-lg h-96 overflow-y-auto border border-gray-300 bg-white p-4 rounded-lg shadow-md">
</div>
<!-- Input field and button -->
<div class="flex mt-4 w-full max-w-lg">
<input id="messageInput"
type="text"
placeholder="Enter a message"
class="flex-1 p-2 border border-gray-300 rounded-l-lg focus:outline-none focus:ring-2 focus:ring-blue-500"/>
<button onclick="sendMessage()"
class="bg-blue-500 text-white px-4 py-2 rounded-r-lg hover:bg-blue-600">Submit
</button>
</div>
<script src="/static/index.js"></script>
</body>
</html>
Code breakdown
This template creates a chat interface:
Displays a title with the room number ({{ room_id }}).
Hidden block #room-data
Stores data sent by the server (room_id, username, user_id).
Made hidden with class="hidden".
In the future, JavaScript will extract this data.
Message area (#messages) - user messages will appear here.
Input field and "Send" button
Field (#messageInput) for entering text.
The onclick="sendMessage()" button sends a message.
- Connecting index.js
- The /static/index.js file will describe the logic of interaction with WebSocket.
Why the hidden #room-data block?
Passing server variables directly to JS code is not always convenient. With Jinja2, you can output them to HTML and then read them in JavaScript.
2. Including styles
We use TailwindCSS, which is included via CDN:
<script src="https://cdn.tailwindcss.com"></script>
This allows us to write compact and flexible CSS code directly in HTML, for example:
<body class="bg-gray-100 flex flex-col items-center p-4">
Why TailwindCSS?
Eliminates the need to write custom CSS files.
Allows you to quickly style elements.
Good for prototyping.
3. How pages interact with the server
The user goes to the main page of the site (/).
Enters the name and room ID.
Clicks "Join the chat" - the request is sent to /join_chat.
The server returns index.html with the parameters passed to it.
The page loads, and the JS code starts working with WebSocket.
What's next?
We have prepared the interface, but the chat cannot send messages yet. In the next section, we will implement JavaScript logic that will allow us to connect to WebSocket, send and receive messages.
Implementing WebSocket in JavaScript
Now that we have an interface, it's time to add logic for exchanging messages in real time. We will use WebSocket for communication between the client and the server.
Implementing WebSocket in JavaScript
Now that we have the interface, it's time to add the logic for exchanging real-time messages. We will use WebSocket for communication between the client and the server.
A small but important digression
I want you to have a correct understanding: implementing WebSocket, and especially chats, on the backend side is not such a difficult task.
For us, backend developers, it is enough to write a few dozen lines of code to create a WebSocket connection, handle client connections and forward messages to all participants of a chat, room or, for example, an online game.
But if anyone has to really sweat, it is the frontend developers.
Why?
In addition to the visual component of the interface, they need to establish a connection to the server correctly.
Handle WebSocket events: connection, disconnection, receiving and sending messages.
Organize convenient and dynamic display of messages in real time.
Of course, this work is not super complicated, but the main difficulties when working with WebSocket often arise on the frontend.
Next, I will show a simple example of how to implement a WebSocket connection in JavaScript and how to work with it on the client side.
We describe the further code in the file app/static/index.js.
1. WebSocket initialization
First, we need to get the user and room data. Since we passed it via Jinja2 in a hidden HTML block (#room-data), we will extract it using JavaScript:
// Get data from a hidden element
const roomData = document.getElementById("room-data");
const roomId = roomData.getAttribute("data-room-id");
const username = roomData.getAttribute("data-username");
const userId = roomData.getAttribute("data-user-id");
We will need this data to establish a WebSocket connection.
2. Setting up a WebSocket connection
Let's create a connection to the server:
const ws = new WebSocket(`ws://localhost:8000/ws/chat/${roomId}/${userId}?username=${username}`);
At the deployment stage to the Amverum Cloud service, we will need to replace this link with a live https domain.
What's going on here?
We create a WebSocket connection at ws://localhost:8000/ws/chat/.
We pass the room ID (roomId), user ID (userId), and username (username) to the URL.
The server uses this data to identify the connection.
Additionally, we add event handlers to track the connection state:
ws.onopen = () => {
console.log("Connection established");
};
ws.onclose = () => {
console.log("Connection closed");
};
3. Receiving messages
When the server sends a message, the onmessage handler is triggered. We parse the JSON data and add it to the chat area:
ws.onmessage = (event) => {
const messages = document.getElementById("messages");
const messageData = JSON.parse(event.data);
const message = document.createElement("div");
// Define styles depending on the sender
if (messageData.is_self) {
message.className = "p-2 my-1 bg-blue-500 text-white rounded-md self-end max-w-xs ml-auto";
} else {
message.className = "p-2 my-1 bg-gray-200 text-black rounded-md self-start max-w-xs";
}
message.textContent = messageData.text;
messages.appendChild(message);
messages.scrollTop = messages.scrollHeight; // Auto scroll down
};
Code parsing:
JSON.parse(event.data) – converts a JSON string to an object.
If is_self == true, then the message was sent by the current user, and it is displayed on the right (in blue).
Otherwise, the message is from another user, and it is displayed on the left (in gray).
Autoscroll (messages.scrollTop = messages.scrollHeight) – so that new messages are always visible.
4. Sending messages
Let's create a function for sending messages:
function sendMessage() {
const input = document.getElementById("messageInput");
if (input.value.trim()) {
ws.send(input.value);
input.value = '';
}
}
How does it work?
Take the text from the input field (#messageInput).
If the message is not empty, send it via ws.send().
Clear the input field.
Additionally, to send messages by Enter, add an event handler:
document.getElementById("messageInput").addEventListener("keypress", (e) => {
if (e.key === "Enter") {
sendMessage();
}
});
Now the user can press Enter and the message will be sent automatically.
5. Full chat cycle
Connect to the WebSocket server when the page loads.
Wait for incoming messages and display them in the chat window.
When sending a message, the user enters the text and presses the button or Enter.
The server transmits the message to all participants in the room.
The chat is updated in real time without reloading the page.
Thanks to WebSocket and a small amount of JavaScript code, we have a full-fledged chat that works in real time.
Now we just need to configure the main file (the launch file) and we can start testing our web application.
Final settings and launching the application
At this stage, we only need to make the final settings and launch our application.
We will work with the file app/main.py, where we will implement three key tasks:
Initialize the FastAPI application
Add processing of static files (for example, JS, CSS)
Register all routes
Setting up main.py
Add the necessary imports and describe the basic configuration:
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from app.api.router_page import router as router_page
from app.api.router_socket import router as router_socket
app = FastAPI()
# Connect the folder with static files
app.mount('/static', StaticFiles(directory='app/static'), 'static')
# Register routes
app.include_router(router_socket)
app.include_router(router_page)
Since our web application is responsible for rendering HTML pages itself, we do not need CORS settings.
Launching the application
To start, we use uvicorn, a lightweight ASGI server that we installed earlier.
Basic command:
uvicorn main:app
By default, the server will go up to http://127.0.0.1:8000.
If you need to configure the launch parameters, add flags:
--host 0.0.0.0 – makes the server accessible from the external network
--port 8005 – specifies a specific port
--reload – enables auto-restart when changing the code (useful in development)
Example: Launch on port 8005 with auto-update of the code
uvicorn main:app --port 8005 --reload
After a successful launch, the server is ready to work, and we can connect to the chat! 🎉
Below is a short screencast demonstrating the application in action (3 users in one room):
This is all great, but chatting with yourself is not that exciting. It would be great to make this chat available to everyone. And the Amverum Cloud service will help us with this.
Why Amverum?
I chose this service because it is easy to deploy applications. You can see for yourself. To deploy an application similar to ours, you only need to follow a few simple steps:
Register on the service.
Click on the "Create project" button.
Upload the project files to the service. This can be done either through the interface on the site or using Git commands.
Fill in the necessary configurations on the site (for example, select a programming language for the project or specify a command to launch it).
Attach a free https domain name to the project or register your own.
The deployment process takes only a couple of minutes. In addition, each new user receives a bonus of 1$ to the main balance. The choice of the service is obvious.
Let's perform the deployment.
Register on the Amverum Cloud website, if you haven't registered yet
Click on "Applications"
Then select "Create an application" and follow the step-by-step instructions.
Name the project and choose a tariff plan (a Tester plan is suitable for educational purposes)
Upload files in a convenient way method
Fill in the configurations. The command to run will be: uvicorn app.main:app --host 0.0.0.0 --port 8000
Next, to activate the free https domain, we need to go to the created project, then go to the "Domains" tab and activate the free domain, as shown below:
Now we need to make some changes to the static/index.js file. In particular, we must replace the web socket connection string.
Before:
const ws = new WebSocket(`ws://localhost:8000/ws/chat/${roomId}/${userId}?username=${username}`);
After:
const ws = new WebSocket(`wss://easysocketfastap-yakvenalex.amverum.com/ws/chat/${roomId}/${userId}username=${username}`);
Note that we not only replaced the link to the domain name, but also corrected the format from ws to wss. Be careful!
Now all that remains is to rewrite the index.js file in Amverum and rebuild the project.
You can see the working code here: https://easysocketfastap-yakvenalex.amverum.com. The full source code of the project, as well as other exclusive content that I do not publish on Habr, can be found in my free telegram channel "Easy Path to Python".
By the way, I analyzed the project we talked about today in my half-hour video, available on YouTube and RuTube.
Conclusion
Today's material is a methodological basis that will prepare you for analyzing a more complex and large-scale project: a Telegram bot with MiniApp for anonymous chat "Tete-a-Tet". In the next article, we will analyze this project in detail, and the knowledge gained here will help you master it easier.
I tried to make the material as accessible and understandable as possible, so that even complex topics became simpler. If you found this article useful, don't forget to support it with a like or a comment - it motivates me to prepare even more interesting content.
And if you want even more practice and useful analysis, subscribe to my Telegram channel "Easy Path to Python" - there are already almost 3000 participants, and we regularly discuss interesting topics on development.
That's all for now. See you in the next materials!
Top comments (0)