On this page
article
Project: WebSocket Chat Room
Build a real-time chat application with FastAPI WebSockets — connection manager, room broadcast, message history, and a simple HTML client.
Build a multi-room chat server where clients connect over WebSockets and receive messages instantly — no polling required.
What You’ll Build
WebSocket /ws/{room} Join a room, send/receive messages
GET / Simple HTML chat client
GET /rooms List active rooms and user counts
Features:
- Multiple chat rooms
- Broadcast messages to all clients in a room
- Connection/disconnection notifications
- In-memory message history (last 50 messages per room)
Prerequisites
- Async Programming
- FastAPI Getting Started
- Basic HTML/JavaScript (client is provided)
Setup
mkdir chat-app && cd chat-app
python -m venv .venv && source .venv/bin/activate
pip install "fastapi[standard]" uvicorn
Project Structure
chat-app/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── manager.py
│ └── static/
│ └── index.html
└── tests/
└── test_chat.py
Connection Manager
Central class to track active WebSocket connections per room:
# app/manager.py
from fastapi import WebSocket
from collections import defaultdict
import json
class ConnectionManager:
def __init__(self):
self.active: dict[str, list[WebSocket]] = defaultdict(list)
self.history: dict[str, list[dict]] = defaultdict(list)
self.MAX_HISTORY = 50
async def connect(self, room: str, websocket: WebSocket, username: str):
await websocket.accept()
self.active[room].append(websocket)
await self.broadcast(room, {
"type": "system",
"message": f"{username} joined the room",
}, exclude=websocket)
# Send recent history to the new client
for msg in self.history[room][-self.MAX_HISTORY:]:
await websocket.send_json(msg)
def disconnect(self, room: str, websocket: WebSocket):
if websocket in self.active[room]:
self.active[room].remove(websocket)
async def broadcast(self, room: str, data: dict, exclude: WebSocket | None = None):
self.history[room].append(data)
if len(self.history[room]) > self.MAX_HISTORY:
self.history[room] = self.history[room][-self.MAX_HISTORY:]
dead = []
for connection in self.active[room]:
if connection is exclude:
continue
try:
await connection.send_json(data)
except Exception:
dead.append(connection)
for conn in dead:
self.disconnect(room, conn)
def room_stats(self) -> dict[str, int]:
return {room: len(conns) for room, conns in self.active.items() if conns}
manager = ConnectionManager()
WebSocket Endpoint
# app/main.py
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Query
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from pathlib import Path
from app.manager import manager
app = FastAPI(title="Chat App")
app.mount("/static", StaticFiles(directory=Path(__file__).parent / "static"), name="static")
@app.get("/")
async def home():
html = (Path(__file__).parent / "static" / "index.html").read_text()
return HTMLResponse(html)
@app.get("/rooms")
async def list_rooms():
return manager.room_stats()
@app.websocket("/ws/{room}")
async def websocket_endpoint(
websocket: WebSocket,
room: str,
username: str = Query(default="Anonymous"),
):
await manager.connect(room, websocket, username)
try:
while True:
text = await websocket.receive_text()
await manager.broadcast(room, {
"type": "message",
"username": username,
"message": text,
})
except WebSocketDisconnect:
manager.disconnect(room, websocket)
await manager.broadcast(room, {
"type": "system",
"message": f"{username} left the room",
})
HTML Client
<!-- app/static/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Chat Room</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 2rem auto; }
#messages { border: 1px solid #ccc; height: 300px; overflow-y: auto; padding: 1rem; }
.system { color: #888; font-style: italic; }
input, button { margin-top: 0.5rem; padding: 0.5rem; }
</style>
</head>
<body>
<h1>Python Chat</h1>
<label>Username: <input id="username" value="Alice"></label>
<label>Room: <input id="room" value="general"></label>
<button onclick="connect()">Connect</button>
<div id="messages"></div>
<input id="input" placeholder="Type a message..." disabled>
<button id="send" onclick="sendMessage()" disabled>Send</button>
<script>
let ws;
const messages = document.getElementById("messages");
const input = document.getElementById("input");
const sendBtn = document.getElementById("send");
function connect() {
const username = document.getElementById("username").value;
const room = document.getElementById("room").value;
ws = new WebSocket(`ws://${location.host}/ws/${room}?username=${encodeURIComponent(username)}`);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
const div = document.createElement("div");
if (data.type === "system") {
div.className = "system";
div.textContent = data.message;
} else {
div.textContent = `${data.username}: ${data.message}`;
}
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
};
ws.onopen = () => {
input.disabled = false;
sendBtn.disabled = false;
};
}
function sendMessage() {
if (ws && input.value.trim()) {
ws.send(input.value);
input.value = "";
}
}
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") sendMessage();
});
</script>
</body>
</html>
Run the Server
uvicorn app.main:app --reload
# Open http://127.0.0.1:8000 in two browser tabs
Join the same room with different usernames and chat in real time.
Testing
# tests/test_chat.py
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_list_rooms_empty():
response = client.get("/rooms")
assert response.status_code == 200
assert isinstance(response.json(), dict)
def test_websocket_chat():
with client.websocket_connect("/ws/general?username=Alice") as ws1:
with client.websocket_connect("/ws/general?username=Bob") as ws2:
ws1.send_text("Hello Bob!")
data = ws2.receive_json()
assert data["type"] == "message"
assert data["username"] == "Alice"
assert data["message"] == "Hello Bob!"
Run: pytest tests/ -v
How It Works
Client A ──WebSocket──┐
├── ConnectionManager ── broadcast ──► all clients in room
Client B ──WebSocket──┘
- Client opens WebSocket to
/ws/{room}?username=... - Server accepts connection and adds to room’s active list
- Incoming text is broadcast as JSON to every client in the room
- On disconnect, server removes client and notifies others
Concepts Applied
- Async Programming — async WebSocket handlers
- FastAPI — routing and static files
- FastAPI Testing — WebSocket test client
- Network Programming — real-time protocols
Bonus Challenges
- Persist messages — store history in Redis or PostgreSQL instead of memory
- Authentication — require JWT before WebSocket upgrade
- Private messages — direct messaging between two users
- Rate limiting — throttle messages per user (prevent spam)
- Typing indicators — broadcast
{type: "typing"}events - Deploy with Docker — see DevOps
Real-time features like chat, live dashboards, and notifications all build on this WebSocket pattern.