Server-Sent Events with Node.js

Server-sent events (SSE) allows clients to receive updates from a server over a single HTTP connection without having to poll the server for updates. This makes it a great fit for applications that need to display real-time data such as stock tickers, news feeds, or social media updates.

SSE vs WebSockets

You might be wondering how SSE differs from WebSockets. The primary difference is that WebSockets are a two-way communication protocol that allows clients to both send and receive data to and from the server. SSE, on the other hand, is a one-way communication protocol that only allows the server to send messages to the client.

For example, you might use WebSockets to build a chat application where clients can send messages to each other. Whereas you might use SSE to build an activity feed for your application where the server sends updates to the client.

Creating an SSE endpoint

First things first, we'll need to create an endpoint that streams events to the client. For this, we'll be using endpts, a platform that makes it easy to build and deploy serverless APIs with TypeScript.

Let's define a route that will allow a client to subscribe to the SSE endpoint by sending a GET request to /events:

// routes/events.ts

import type { Route } from '@endpts/types'

export default {
  method: 'GET',
  path: '/events',
  async handler() {
    let eventCount = 0

    // create a stream that emits events every 500ms to
    // simulate some real-time data source
    const stream = new ReadableStream({
      start(controller) {
        controller.enqueue('data: stream started\n\n')
      },
      async pull(controller) {
        controller.enqueue(`id: ${eventCount}\n`)
        controller.enqueue(`data: event #${eventCount}\n\n`)

        eventCount++
        return new Promise((r) => setTimeout(r, 500))
      },
    })

    return new Response(stream, {
      headers: {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        Connection: 'keep-alive',
      },
    })
  },
} satisfies Route

In the code above, we create a ReadableStream that emits events every 500ms. We then return a Response object with the stream as the body and the appropriate headers for SSE.

Since endpts Serverless Functions support streaming out-of-the box, the data will sent to the client as soon as it's available instead of buffering it in memory.

Subscribing to the SSE endpoint

Next, let's create the client-side code that subscribes to the SSE endpoint we just created.

// routes/index.ts

import type { Route } from '@endpts/types'

// HTML page to render the server-sent events
const pageHtml = `
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>server-sent events - endpts</title>
  </head>
  <body>
    <h1>Server-sent events</h1>
    <button onclick="source.close()">Stop event stream</button>

    <ul id="events"></ul>

    <script>
      const events = document.getElementById("events");
      const source = new EventSource("/events");
      source.onmessage = (event) => {
        events.innerHTML += "<li>" + event.data + "</li>";
      };
    </script>
  </body>
</html>
`

export default {
  method: 'GET',
  path: '/',
  async handler() {
    return new Response(pageHtml, {
      headers: {
        'Content-Type': 'text/html',
      },
    })
  },
} satisfies Route

In the route above, we return an HTML page that subscribes to the SSE endpoint we created earlier.

The JavaScript snippet creates a new EventSource object that connects to the /events endpoint and listens for events, which we then append to the events list element to display on the page:

const events = document.getElementById('events')
const source = new EventSource('/events')

source.onmessage = (event) => {
  events.innerHTML += '<li>' + event.data + '</li>'
}

The great thing about SSE is that the browser does most of the heavy lifting for us. Once the browser executes the snippet above, it will automatically open a connection to the /events endpoint and start receiving events from the server. Additionally, if the connection is lost, the browser will automatically try to reconnect to the endpoint, passing the last event ID received in the Last-Event-ID header.

Testing our SSE endpoint

If you have the endpts-samples/server-sent-events repository cloned locally, you can run the following command to install the dependencies and start the development server (make sure you're using Node.js v18 or higher):

npm install && npm run dev
Starting the endpts development server

Then, we can open up our browser to http://localhost:3000 to see the events being streamed to the client:

Subscribing to the SSE endpoint in the browser

Deploying our application

Now that we have our application working locally, let's deploy it to endpts. Head over to the endpts dashboard and create a new project. For the Source URL, you can use the sample repository: https://github.com/endpts-samples/server-sent-events:

Deploying the project via the endpts dashboard

Hit the Deploy button. Once the project has been deployed successfully, you can grab the deployment URL and open it in your browser to see the events being streamed to the client:

Deployment details