Say you have an application that needs to display real-time data to the user. For example, a dashboard that displays the number of users currently online, or a chat application that displays new messages as they arrive, or a stock market application that displays the latest stock prices.
This is a common problem that many applications face, and usually, the first solution that comes to mind is to use WebSockets. However, WebSockets are not the only solution to this problem. In fact, there is a much simpler solution that is often overlooked or unknown to many developers: Server-Sent Events (SSE).
In this post, I'll walk you through setting up Server-Sent Events (SSE) in Astro to build a lightweight real-time chat server.
What are Server-Sent Events (SSE)?
Server-Sent Events (SSE) is a web standard enabling servers to push data to clients over a single, long-lived HTTP connection. It's a perfect fit for unidirectional data flows where the server needs to update the client, such as sending JSON payloads, text updates, or HTML fragments to dynamically refresh the UI.
One of the key advantages of SSE is its native support in modern browsers, eliminating the need for external libraries or polyfills. It's also generally easier on server resources compared to WebSockets, especially when bidirectional communication isn't required.
Setting Up Astro
Before diving into the chat functionality, you'll need an Astro project. If you're starting from scratch, use the following command to create a fresh Astro project and add the Node.js adapter.
Creating the Chat Logic
First, we need to create a basic chat controller that will handle the chat logic on the server. This is the heart of our application. It will store messages in memory and will provide a way for us to subscribe to new messages as they arrive.
Let's create a file called chat.ts
in the src/controllers
directory and let's get to work.
Next, I'm going to create a messages
array to store the messages in memory with a getMessages()
method to retrieve the messages and an addMessage()
method to add new messages to the array.
Finally, I'm going to create a subscribe()
and unsubscribe()
method to allow us to subscribe to new messages as they arrive. For this, I'm going to use the EventEmitter
class from Node.js.
As you can see, I'm using the on()
method to subscribe to the message
event and the off()
method to unsubscribe from the message
event. I'm also using the emit()
method to emit the message
event when a new message is added to the messages
array.
Creating the Chat Page
Now that we have the chat logic in place, let's create a page to display the chat messages.
Here, we are server-side rendering the messages using the getMessages()
method from the ChatController
class. We are also adding a form to allow us to send new messages to the server.
Handle Form Submission
In the same file, let's add some JavaScript to handle the form submission.
This code here is pretty straightforward. We are adding an event listener to the submit
event of the form and sending a POST
request to the /pages/api/chat
endpoint with the message as the body of the request. We are also clearing the input field after the request is sent to make it a bit more user-friendly.
Note: I am using the is:inline
attribute to inline the JavaScript code in the HTML file.
Creating the Chat API
However, this code here doesn't do anything yet. We need to create an endpoint to handle the POST
request and add the message to the messages
array.
Here we are creating a REST API endpoint that will handle the POST
request and add the message to the messages
array. We are also returning a 204
status code to indicate that the request was successful.
With this in place, we can now send messages to the server. However, if you try this out, you'll notice that new messages are not being reflected in the UI. This is because we are not updating the UI when new messages are added to the messages
array.
This is where SSE comes in. Let's see how we can use SSE to stream the messages to the browser in real-time.
Setting Up SSE Endpoint
To set up SSE, we need to create a new endpoint that will handle the GET
request and send the messages to the client as they arrive.
Let's break this down. First, we are creating a new ReadableStream
and passing it to the Response
constructor. This will allow us to send data to the client as it arrives.
ReadableStream
takes a start()
function with a controller
argument. This controller
argument allows us to enqueue data to the stream using the enqueue()
method. This is how we will send data to the client.
Next, we are creating a sendEvent()
function that will take the data as an argument and send it to the client.
SSE requires us to send the data in a specific format. Each message must be prefixed with data:
and suffixed with \n\n
. This is why we are using the JSON.stringify()
method to convert the data to a string and then prefixing it with data:
and suffixed with \n\n
. We are then using the TextEncoder
class to convert the string to a Uint8Array
before sending it to the client.
There are other optional fields that we can send to the client, such as id
, event
, and retry
. However, we are not going to use them in this example.
Finally, we are adding an event listener to the request.signal
object to handle the connection closing. This will allow us to close the stream when the connection is closed.
Subscribing to New Messages
This code here, however, does not do anything yet. We need to bring in the ChatController
class and subscribe to new messages as they arrive.
Here, every time a new connection is made, we are subscribing to new messages using the subscribe()
method from the ChatController
class. We are also unsubscribing from new messages when the connection is closed using the unsubscribe()
method from the ChatController
class. This will ensure that we don't send messages to the client when the connection is closed.
Subscribing to SSE
Now that we have the SSE endpoint in place, let's go back to the chat page and subscribe to the SSE endpoint.
Browsers that support SSE (which is most modern browsers) provide a built-in EventSource
class that allows us to subscribe to SSE endpoints. We can then use the onmessage
event handler to listen for new messages and consume them as they arrive.
In this case, I am using the onmessage
event handler to listen for new messages, parse the data, and append it to the list of messages.
Bringing It All Together
With all the components in place, it's time to test our real-time chat application.
Run the development server:
Open the application in your browser, send messages, and watch as they appear in real-time across multiple tabs or windows.
That is cool, isn't it? 😎
Conclusion
That's it! We have successfully set up Server-Sent Events (SSE) in Astro and created a simple chat server that streams messages to the browser in real-time.
The nice thing about SSEs is that they are simple to use and they work over a single, long-lived HTTP connection. This makes them ideal for sending JSON, text data, or even HTML snippets to update the client's UI in real-time.
Additionally, a nice feature of the EventSource
class is that it will automatically reconnect if the connection is lost. This means that if the connection is lost for any reason, the client will automatically reconnect and continue receiving messages.
I hope this post has encouraged you to give it a try and see how it can help you build better applications.
Happy coding, and until next time, keep streaming those events! 🚀
Interested in LogSnag?