- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
Practice: Adding Real-time Updates to a Bun Application
WebSockets power the real-time web, enabling instant updates across browsers, apps, and devices. In this lab, you'll build a chat application where users can join from either a web browser or the terminal. You'll see messages appear instantly across both interfaces, giving you hands-on experience with real-time communication and the flexibility of Bun.
Lab Info
Table of Contents
-
Challenge
Intro to Real-Time Updates
Intro to Real-Time Updates
Real-time applications allow users to see updates instantly, without needing to refresh or reload. This is essential for chat apps, collaborative tools, live dashboards, and more.
WebSockets are a protocol that enables two-way, persistent communication between a client (like a browser or terminal) and a server.
Unlike HTTP, which is request/response, WebSockets allow the server to push updates to clients as soon as something happens.
What to Expect in This Lab
In this lab, you'll use BUN and WebSockets to build a basic chat application. You'll set up a chat server and connect both terminal and web clients, enabling real-time communication between them.
As you work through the lab, try to complete each task on your own first. After each task, you'll find a Solution block with detailed steps, so you can review and compare your implementation. Completed solution files are also available in the
solutionsdirectory for reference.
To begin, click the right arrow.
-
Challenge
Bun WebSocket Server
Bun WebSocket Server
Begin by opening the starter file, server.ts. Right now, it only serves the
index.htmlstatic file and does not support WebSocket connections.Your goal is to update the server so it can upgrade HTTP requests to a WebSocket connection. This is an essential step for enabling real-time communication in your chat application.
Try implementing this functionality on your own before checking the solution below.
Solution
----- ```typescript const success = server.upgrade(request); // return new Response(Bun.file("index.html")); ``` Instead of serving the static file, this change attempts to upgrade all incoming requests to WebSocket connections. With this in place, your server can start handling real-time communication for your chat app.In a subsequent step, you'll improve this logic so the server can handle both serving the static file and upgrading requests to WebSocket connections.
Starting the WebSocket Server
On the right, you'll see two terminal windows: the Server terminal on the left and the Client terminal on the right.
In the Server terminal, the following command has been executed to automatically restart the Bun server whenever you make changes to the
server.tsfile:bun run server.ts --watchIf you run into any problems, restart the server by pressing
ctrl+c, then run the command again. You can press the up arrow key to quickly access previous commands.Testing the WebSocket Server
To test your WebSocket connection, run the following command in the Client terminal:
wscat -c ws://localhost:3000You will notice the following error message in the left Server terminal. You may need to scroll up to see it:
TypeError:
To enable websocket support,
set the "websocket" object in Bun.serve({})This error indicates that your server is not yet configured to handle WebSocket connections yet.
Configuring WebSocket Handlers
To fix this error, you need to add a
websocketproperty to yourBun.serveconfiguration. Inside this property, define amessagehandler to process messages sent from connected clients. For now, simply log each message to the console.After adding the message handler, your changes will be saved automatically, and the server in the left **Server** terminal will restart on its own.Solution
----- ```typescript fetch: (request, server) => { ... }, websocket: { message: (ws, msg) => { console.log("Received message:", msg); } } ``` Notice that the `message` handler configuration is the minimum requirement for a WebSocket server to function. It allows the server to receive messages from clients.To verify that everything is working, use
wscatagain to connect to your server:wscat -c ws://localhost:3000Once connected, you'll be prompted to enter a message in the Client terminal. Each time you send a message, you should see a corresponding log appear in the Server terminal, confirming that your WebSocket server is receiving and handling messages correctly.
Great job! You've set up a basic WebSocket server with Bun. Next, you'll handle additional WebSocket connection events.
-
Challenge
Connection Events
Connection Events
In addition to the required
messagehandler, WebSocket servers can also handle connection events such as when a client connects or disconnects. This allows you to manage resources.Following the
messagehandler, implement handlers for theopenandcloseevents. For now just log a message to the console when these events occur.Solution
----- ```typescript websocket: { message: {. . .}, open: (ws) => { console.log("WebSocket connection opened"); }, close: (ws) => { console.log("WebSocket connection closed:"); } } ``` With these handlers, your WebSocket server can now react to client connections, disconnections, and messages, enabling real-time interactive communication and setting the stage for advanced features like broadcasting. < /details>Testing Connection Events
Now that you have implemented the connection event handlers, you can test your server again by using the same
wscatcommand in the Client terminal.wscat -c ws://localhost:3000After connecting, you should see the message from the
openhandler appear in the Server terminal.To disconnect from the WebSocket server, press
ctrl+cin the Client terminal. This will trigger thecloseevent handler, and you should see a message in the Server terminal indicating that the WebSocket connection has closed.Client Messaging
so far, you have only logged the messages received from the client. You can also send messages back to the client using the
sendmethod of the WebSocket object.In the
messagehandler, modify the code to echo the received message back to the client.Solution
----- ```typescript message(ws, msg) { console.log("WebSocket message received:", msg); ws.send(`Echo: ${msg}`); }This simple implementation will "Echo" the message back to the client, confirming receipt and demonstrating two-way communication. < /details> You could also send a message back to the client when the connection is opened. <details><summary>Solution</summary> ----- ```typescript open: (ws) => { console.log("WebSocket connection opened"); ws.send("Welcome to the WebSocket server!"); }Now when the client connects, they will receive a welcome message. < /details>
After reconnecting
wscatin the Client terminal, you should receive the welcome message from the server. Also, any text you type and send at the prompt will be echoed back to you.
With connection event handlers set up, you’re ready to add more advanced features to your WebSocket server, such as broadcasting messages or processing client commands. However, to serve both the static
index.htmlclient and handle WebSocket connections, you’ll need to implement routing logic that can differentiate between standard HTTP requests and WebSocket upgrade requests. This will be covered in the next step. -
Challenge
Conditional Routing
Conditional Routing
Earlier, you treated every incoming request as a potential WebSocket upgrade. While this approach worked initially, it doesn't allow you to properly serve both WebSocket connections and standard HTTP requests.
To address this, implement routing logic that can differentiate between regular HTTP requests (which should serve the HTML page) and WebSocket upgrade requests.
Routing Options
There are several ways to implement conditional routing, each with varying levels of robustness. For this exercise, you'll use a straightforward method: try to upgrade the request to a WebSocket. If the upgrade is unsuccessful, handle it as a standard HTTP request and serve the HTML page.
Solution
----- ```typescript fetch(request, server) { const success = server.upgrade(request); if (!success) { return new Response(Bun.file("index.html")); } } ``` This method attempts to upgrade each incoming request to a WebSocket connection. If the upgrade fails—which typically means the request is a standard HTTP request—it serves the `index.html` file instead.While there are other possible reasons for a WebSocket upgrade to fail, for this exercise, you can assume that a failed upgrade indicates a regular HTTP request.
< /details>
Verify Routing Implementation
You can use
wscatto reconnect and confirm that the WebSocket server is still functioning as expected.wscat -c ws://localhost:3000Next, open the Simple Browser tab and refresh the page. You should see that the
index.htmlfile is served correctly.However, the page is not yet connected to the WebSocket server. This is because the client-side code to establish a WebSocket connection from the HTML page has not been implemented yet.
Continue to the next step to implement the client-side WebSocket connection in your HTML page.
-
Challenge
Web Client
Web Client
Now that your server is hosting both the WebSocket server and serving the HTML page, it's time to make your web page interactive by connecting it to the WebSocket server.
Open the index.html file. You'll notice it contains a simple HTML structure, but currently, the page is static and does not communicate with the WebSocket server.
In the
<script>section, thewsvariable is not yet initialized. To connect to the WebSocket server, setwsto a new instance of the built-inWebSocket()client. Since the HTML and WebSocket server are hosted on the same domain, you can usewss://${location.host}to dynamically determine the address.Note: that due to how this lab environment hosts web pages you will need to use
wssinstead ofwsto establish a secure WebSocket connection.Solution
----- ```typescript const ws = new WebSocket(`wss://${location.host}`); ```Once the connection is established, you can handle messages from the server by setting the
ws.onmessageevent handler. Whenever the server sends a message, this event is triggered, and you can use the event'sdataproperty to display the message in thediv.logusing thelog()function.Solution
----- ```typescript ws.onmessage = (e) => log('Server: ' + e.data); ```When you refresh the Simple Browser page, the welcome message from the server’s
open()handler should appear.Sending Messages
Back on the
index.htmlfile, notice that the Send button is configured to call thesendMessage()function when clicked.Now that the
wsconnection is set up, modify thesendMessage()function so it sends the message to the WebSocket server using thews.send()method.Solution
----- ```typescript function sendMessage() { const input = document.getElementById('messageInput') as HTMLInputElement; const message = input.value; ws.send(message); input.value = ''; } ```After refreshing the Simple Browser page, you should see a Welcome message from the server. To send a message, type it into the input field and click the Send button.
You'll also see that each message sent from the client is logged in the Server terminal, while the Simple Browser displays the server's Echo response.
Great job connecting your web client to the WebSocket server!
Next, you'll improve the chat experience by updating your code to handle messages sent from the server and broadcast them to all connected clients. This will allow every user to see messages from anyone in real time.
-
Challenge
Implement Message Broadcasting
Implement Message Broadcasting
To enable real-time chat, every message needs to be delivered instantly to all connected clients. This way, when one client sends a message, everyone else receives it right away.
A typical approach for this is the publish-subscribe (pub-sub) pattern: the server publishes messages, and all connected clients subscribe to receive them.
Implementing Broadcast Messaging
1. Subscribe New Connections
To broadcast messages, the server must keep track of each WebSocket connection. Using the pub-sub pattern, each WebSocket can subscribe to one or more channels. This enables the server to send messages to all clients subscribed to a specific channel.
In the
server.tsfile, update theopenhandler so that each new WebSocket connection subscribes to themessagechannel. This channel will serve as the chat room, allowing all connected clients to exchange messages in real time.{
Solution
open: (ws) => { ws.send("Welcome to the WebSocket server test!"); ws.subscribe("message"); },When a WebSocket subscribes to the
messagechannel, the server automatically tracks which channels each client is interested in. This allows the server to efficiently broadcast messages to all connected clients who are subscribed.Alternatively, you could manually keep a list of connected clients and send messages to each one individually. However, the pub-sub approach is generally more efficient and scalable, especially as your user base grows.
2. Publish Messages to All Subscribers
Now that clients are subscribed to the
messagechannel, you need to handle incoming messages and broadcast them to all subscribers.Solution
----- ```typescript message: (ws, msg) => { console.log("Received message:", msg); ws.publish("message", msg); } ``` When a client sends a message, the server publishes it to the `message` channel. All other connected clients subscribed to this channel will receive the broadcasted message.Alternatively, you can use
server.publish("message", msg)to broadcast the message to every client subscribed to themessagechannel, including the sender.3. Unsubscribe on Disconnect
To maintain server performance and prevent memory leaks, always unsubscribe clients from their channels when they disconnect. This ensures that messages are not sent to clients that are no longer connected and keeps resource usage efficient.
When a client disconnects, remove their subscription from the channel to clean up resources.
Solution
----- ```typescript close: (ws) => { console.log("WebSocket connection closed."); ws.unsubscribe("message"); } ```Since this implementation only uses the
messagechannel, cleaning up subscriptions is straightforward. As your application evolves to support multiple channels, you'll need to add logic to manage and clean up subscriptions for each active channel.Testing Your Implementation
To test your message broadcasting implementation, try connecting with multiple clients using both the Simple Browser and
wscat.You can refresh the Simple Browser to create a new WebSocket connection. At the same time, run the following
wscatcommand in your terminal to connect an additional client:wscat -c ws://localhost:8080Once you have two clients connected, try sending a message from one client. You should see the message appear instantly in the other client, confirming that message broadcasting is working correctly.
Congratulations!
You’ve successfully added message broadcasting to your chat app. Now, every user receives updates in real time. This sets the stage for building more advanced features, such as user presence, private messaging, and chat rooms. Well done!
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.