← projects

Chat-Room-API

Solo

ReactFastAPIWebSocketPythonDockerDocker Compose

GitHub

Overview

My first messaging system: a real-time chat room where a React client and a FastAPI back-end talk over a WebSocket, packaged as two Docker images. No database, no accounts. Open the page and you are in the room, with an id derived from your connection time.

Two browser windows of the chat, each with its own client id, showing the same conversation
Two clients, two ids, one server. 1788626897 and 1788626898 talking through the same WebSocket.

How it works

The server keeps a ConnectionManager, which is a plain list of accepted WebSockets. Each client connects to /ws/{client_id}. Every message it sends gets wrapped with its id and a timestamp, then broadcast to the whole list, sender included. That last detail is the design: a client never renders its own message optimistically, it renders what came back from the server, so every window ends up with the same ordering.

@app.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: int):
    await manager.connect(websocket)
    try:
        while True:
            data = await websocket.receive_text()
            await manager.broadcast(json.dumps(
                {"time": current_time, "clientId": client_id, "message": data}))
    except WebSocketDisconnect:
        manager.disconnect(websocket)

The React side compares each incoming clientId to its own to decide which side of the room the bubble goes on. Your messages right, everyone else’s left.

A WebSocket is not a raw TCP socket, but the shape is the one I keep rebuilding: one process owns the truth, others own the display, and a persistent connection carries the agreement. I wrote the same seam by hand in 2D-EMG-GAME and TRON.

Running it

Two containers, built independently and brought up together:

docker build -t server ./serverFastAPI && docker run -d -p 8000:8000 server
docker build -t client ./client       && docker run -d -p 3000:3000 client

Looking back

It works, and it was the first thing I built that two machines could use at once. It also has a real bug. The client keeps its message list in a useState captured by the WebSocket handler at mount, so the history it shows depends on who typed last. The lesson stuck: with a subscription that outlives the render, read state through the functional updater, setMessages(prev => [...prev, m]), never through the closure.

Screenshots taken from the project running locally: FastAPI on :8000, React dev server on :3000.