Chat-Room-API
Solo
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.

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.
Überblick
Mein erstes Messaging-System: ein Echtzeit-Chatraum, in dem ein React-Client und ein FastAPI-Backend über einen WebSocket sprechen, verpackt als zwei Docker-Images. Keine Datenbank, keine Accounts. Seite öffnen und du bist im Raum, mit einer ID, die aus deiner Verbindungszeit abgeleitet wird.

1788626897 und 1788626898 sprechen über denselben WebSocket.Wie es funktioniert
Der Server hält einen ConnectionManager, also eine schlichte Liste akzeptierter WebSockets. Jeder Client verbindet sich mit /ws/{client_id}. Jede Nachricht wird mit seiner ID und einem Zeitstempel umhüllt und dann an die gesamte Liste gesendet, den Absender eingeschlossen. Genau darin liegt das Design: ein Client rendert seine eigene Nachricht nie optimistisch, sondern das, was vom Server zurückkam. So bekommen alle Fenster dieselbe Reihenfolge.
@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)
Auf der React-Seite wird jede eingehende clientId mit der eigenen verglichen, um zu entscheiden, auf welcher Seite des Raums die Sprechblase landet. Deine Nachrichten rechts, alle anderen links.
Ein WebSocket ist kein roher TCP-Socket, aber die Form ist die, die ich immer wieder baue: ein Prozess besitzt die Wahrheit, andere die Darstellung, und eine dauerhafte Verbindung trägt die Vereinbarung. Dieselbe Naht habe ich in 2D-EMG-GAME und TRON von Hand geschrieben.
Starten
Zwei Container, getrennt gebaut und gemeinsam hochgefahren:
docker build -t server ./serverFastAPI && docker run -d -p 8000:8000 server
docker build -t client ./client && docker run -d -p 3000:3000 client
Rückblick
Es funktioniert, und es war das Erste, was ich gebaut habe und das zwei Rechner gleichzeitig nutzen konnten. Es hat auch einen echten Bug. Der Client hält seine Nachrichtenliste in einem useState, das der WebSocket-Handler beim Mounten einfängt, also hängt der angezeigte Verlauf davon ab, wer zuletzt getippt hat. Die Lehre ist geblieben: bei einem Abo, das den Render überlebt, liest man den State über den funktionalen Updater, setMessages(prev => [...prev, m]), niemals über die Closure.
Screenshots aus dem lokal wieder laufenden Projekt: FastAPI auf
:8000, React-Dev-Server auf:3000.
Vue d’ensemble
Mon premier système de messagerie : un salon de discussion temps réel où un client React et un back-end FastAPI dialoguent en WebSocket, empaquetés en deux images Docker. Pas de base de données, pas de comptes. Tu ouvres la page et tu es dans le salon, avec un identifiant dérivé de l’heure de ta connexion.

1788626897 et 1788626898 discutent à travers le même WebSocket.Comment ça marche
Le serveur tient un ConnectionManager, c’est-à-dire une simple liste de WebSockets acceptés. Chaque client se connecte à /ws/{client_id}. Chaque message qu’il envoie est enveloppé avec son identifiant et un horodatage, puis diffusé à toute la liste, expéditeur compris. Ce dernier point est le cœur du design : un client n’affiche jamais son propre message de façon optimiste, il affiche ce que le serveur lui a renvoyé, si bien que toutes les fenêtres obtiennent le même ordre.
@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)
Côté React, chaque clientId entrant est comparé au sien pour décider de quel côté du salon la bulle se place. Tes messages à droite, ceux des autres à gauche.
Une WebSocket n’est pas une socket TCP brute, mais la forme est celle que je reconstruis sans arrêt : un processus détient la vérité, les autres détiennent l’affichage, et une connexion persistante transporte le contrat. J’ai écrit la même jointure à la main dans 2D-EMG-GAME et TRON.
Lancer le projet
Deux conteneurs, construits séparément et démarrés ensemble :
docker build -t server ./serverFastAPI && docker run -d -p 8000:8000 server
docker build -t client ./client && docker run -d -p 3000:3000 client
Avec le recul
Ça marche, et c’était la première chose que j’aie construite que deux machines pouvaient utiliser en même temps. Il y a aussi un vrai bug. Le client garde sa liste de messages dans un useState capturé par le handler WebSocket au montage, donc l’historique affiché dépend de qui a tapé en dernier. La leçon est restée : avec un abonnement qui survit au rendu, on lit l’état via la forme fonctionnelle, setMessages(prev => [...prev, m]), jamais via la closure.
Captures faites depuis le projet relancé en local : FastAPI sur
:8000, serveur de dev React sur:3000.
概要
はじめてのメッセージングシステムです。React のクライアントと FastAPI のバックエンドが WebSocket で会話するリアルタイムのチャットルームで、2つの Docker イメージにまとめています。データベースもアカウントもありません。ページを開けばもう部屋の中で、接続時刻から作られた ID が割り当てられます。

1788626897 と 1788626898 が同じ WebSocket 越しに会話しています。仕組み
サーバーは ConnectionManager、つまり受け入れた WebSocket の単純なリストを持ちます。各クライアントは /ws/{client_id} に接続します。送られたメッセージは ID とタイムスタンプで包まれ、送信者自身も含めてリスト全体にブロードキャストされます。この最後の点が設計の要です。クライアントは自分のメッセージを先読みで描画せず、サーバーから返ってきたものを描画するので、どのウィンドウでも並び順が一致します。
@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)
React 側では、届いた clientId を自分のものと比べて、吹き出しを部屋のどちら側に出すかを決めます。自分のメッセージは右、他の人のは左です。
WebSocket は生の TCP ソケットとは別物ですが、形は私が何度も作り直しているものと同じです。あるプロセスが真実を持ち、他のプロセスが表示を持ち、持続的な接続がその取り決めを運ぶ。同じ継ぎ目を 2D-EMG-GAME と TRON では手書きで実装しました。
動かし方
2つのコンテナを個別にビルドし、まとめて起動します。
docker build -t server ./serverFastAPI && docker run -d -p 8000:8000 server
docker build -t client ./client && docker run -d -p 3000:3000 client
いま振り返ると
動きますし、2台のマシンが同時に使えるものを作ったのはこれが最初でした。同時に、はっきりしたバグもあります。クライアントはメッセージ一覧を useState に持っていて、それをマウント時の WebSocket ハンドラが取り込んでしまうため、表示される履歴が「最後に打った人」に左右されます。この教訓は残りました。レンダーより長生きする購読では、状態は関数形式の更新 setMessages(prev => [...prev, m]) で読むこと。クロージャ越しに読んではいけません。
スクリーンショットはローカルで動かし直したプロジェクトから。FastAPI は
:8000、React の開発サーバーは:3000です。
概览
我的第一个消息系统:一个实时聊天室,React 客户端与 FastAPI 后端通过 WebSocket 通信,打包成两个 Docker 镜像。没有数据库,也没有账号。打开页面你就在房间里了,ID 由你的连接时间生成。

1788626897 与 1788626898 通过同一个 WebSocket 对话。工作方式
服务器维护一个 ConnectionManager,也就是一份已接受的 WebSocket 列表。每个客户端连接到 /ws/{client_id}。它发出的每条消息都会被套上自己的 ID 和时间戳,然后广播给整份列表,包括发送者本人。最后这一点就是设计所在:客户端从不乐观地渲染自己的消息,而是渲染服务器回传的内容,于是所有窗口得到的顺序完全一致。
@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)
React 这边会把收到的 clientId 和自己的比对,决定气泡出现在房间的哪一侧。你的消息在右,别人的在左。
WebSocket 并不等于原始的 TCP 套接字,但形态正是我反复搭建的那一种:一个进程掌握真相,另一些进程掌握显示,一条持久连接承载它们之间的约定。同样的接缝,我在 2D-EMG-GAME 和 TRON 里是手写实现的。
如何运行
两个容器,分别构建,一起启动:
docker build -t server ./serverFastAPI && docker run -d -p 8000:8000 server
docker build -t client ./client && docker run -d -p 3000:3000 client
现在回看
它能用,而且是我做出的第一个能让两台机器同时使用的东西。它也确实有个 bug。客户端把消息列表放在 useState 里,而 WebSocket 处理函数在挂载时就把它捕获了,于是显示出来的历史取决于谁最后打字。这条教训留了下来:当订阅的生命周期长于渲染时,要用函数式更新读状态,setMessages(prev => [...prev, m]),绝不要走闭包。
截图取自本地重新跑起来的项目:FastAPI 在
:8000,React 开发服务器在:3000。