Mar 12, 2024
12 min read

Using Astro and Server-Sent Events (SSE) to Build Realtime In-App Notifications

Introduction

Server-sent events (SSE) are a simple way to create real-time updates in web applications over HTTP connections. They are particularly useful for scenarios where you want to deliver real-time updates or notifications to users without refreshing the page.

In this tutorial, you'll create and deploy an in-app notifications application written with Astro using server-sent events (SSE) on Koyeb. You'll learn how to implement in-app notifications using SSE, allowing your application to display real-time updates to users as they interact with it.

By the end of this tutorial, you'll have a functioning Astro application that displays three in-app notification toasts with the message "Deploy To Koyeb" as users load the application in their browsers.

You can deploy the in-app notifications Astro application as configured in this guide using the Deploy to Koyeb button below:

Deploy to Koyeb

Note: You can take a look at the application we will be building in this tutorial in the project GitHub repository.

Requirements

To successfully follow this tutorial, you will need the following:

  • Node.js and npm installed. The demo app in this tutorial uses version 18 of Node.js.
  • Git installed on your local computer.
  • A Koyeb account to deploy the application.

Steps

To complete this guide and deploy the in-app notifications Astro application, you'll need to follow these steps:

Create a new Astro application

Let's get started by creating a new Astro project. Open your terminal and run the following command:

npm create astro@latest in-app-notifications

npm create astro is the recommended way to scaffold an Astro project quickly.

When prompted, choose: Make the following selections when prompted:

  • How would you like to start your new project? Empty
  • Do you plan to write TypeScript? Yes
  • How strict should TypeScript be? Strict
  • Install dependencies? Yes
  • Initialize a new git repository? Yes

Once the initialization completes, you can move into the project directory and start the app:

cd in-app-notifications
npm run dev

The app should be running on http://localhost:4321. Press CTRL-C to stop the development server so that we can integrate React into the application.

Integrate React in your Astro project

We will use React to create the user interface for in-app notifications via toasts.

To add React, open your terminal and run the following command:

npx astro add react

npx allows us to execute npm package binaries (astro in our case) without having to install the binaries globally.

When prompted, choose:

  • Yes to install the React dependencies
  • Yes to make changes to Astro configuration file
  • Yes to make changes to tsconfig.json file

React should now be integrated with your Astro project. The process installed the following dependencies:

  • react: The package containing the functionality necessary to define React components
  • react-dom: The package that serves as an entry point to the DOM and server renderers for React
  • @astrojs/react: The integration that enables hydration of React components in Astro
  • @types/react: Type definitions for react package
  • @types/react-dom: Type definitions for react-dom package

Enabling server-side rendering in Astro

To use server-sent events to send notifications in real-time, you need server-side code. Enable server-side rendering in your Astro project by executing the following command in your terminal:

npx astro add node

When prompted, choose:

  • Yes to install the Node dependencies
  • Yes to make changes to Astro configuration file

Your Astro project should now be integrated with a Node.js server. The integration process installed the following dependency:

  • @astrojs/node: The adapter that allows you to server-side render your Astro application

The port by default, to which Koyeb will forward incoming requests is 8000. To respond to incoming requests to 8000 port from your Astro application, make the following additions in astro.config.mjs file:

import node from '@astrojs/node'
import react from '@astrojs/react'
import { defineConfig } from 'astro/config'

const port = parseInt(process.env.PORT) || 8000 // [!code ++]

// https://astro.build/config
export default defineConfig({
  server: { // [!code ++]
    port: port, // [!code ++]
    host: '0.0.0.0', // [!code ++]
  }, // [!code ++]
  integrations: [react()],
  output: 'server',
  adapter: node({
    mode: 'standalone',
  }),
})

To run the build of your application in production mode, make the following changes in package.json file:

{
  "name": "in-app-notifications",
  "type": "module",
  "version": "0.0.1",
  "scripts": {
    "dev": "astro dev",
    "start": "astro dev", // [!code --]
    "start": "node dist/server/entry.mjs", // [!code ++]
    "build": "astro check && astro build",
    "preview": "astro preview",
    "astro": "astro"
  },
  "dependencies": {
    "@astrojs/check": "^0.5.6",
    "@astrojs/node": "^8.2.3",
    "@astrojs/react": "^3.0.10",
    "@types/react": "^18.2.64",
    "@types/react-dom": "^18.2.21",
    "astro": "^4.4.15",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "typescript": "^5.4.2"
  }
}

Once that's done, execute the following command in your terminal to see the current Astro application in action on localhost:8000:

npm run build && npm run start

Once the application is built, you can also set the PORT environment variable to run the application on a different port:

PORT=5555 npm run start

Press CTRL-C to stop the server. Next we will move on to create a server-sent events API in your Astro application.

Create a server-sent events API in Astro

Server-sent events provide a method for delivering real-time updates without the overhead of repeated client requests. It's different from the conventional request-response process as SSE facilitates a streamlined flow of data from server to client through a persistent connection.

Creating API routes in Astro is as simple as creating a Typescript file and exporting an HTTP method function (such as GET). To start creating your own server-sent events API endpoint to send notifications, create an sse.ts file in the src/pages directory, and add the snippet below in it:

// File: src/pages/sse.ts

export async function GET() {
  const encoder = new TextEncoder()
  const message = 'Deploy To Koyeb'
  // Create a streaming response
  const customReadable = new ReadableStream({
    async start(controller) {
      // Emit three notifications after 1 second each
      controller.enqueue(encoder.encode(`data: ${message}\n\n`))
      await new Promise((r) => setTimeout(r, 1000))
      controller.enqueue(encoder.encode(`data: ${message}\n\n`))
      await new Promise((r) => setTimeout(r, 1000))
      controller.enqueue(encoder.encode(`data: ${message}\n\n`))
      // Close the stream after sending the three notifications
      controller.close()
    },
  })
  // Return the stream response and keep the connection alive
  return new Response(customReadable, {
    // Set the headers for Server-Sent Events (SSE)
    headers: {
      Connection: 'keep-alive',
      'Content-Encoding': 'none',
      'Cache-Control': 'no-cache, no-transform',
      'Content-Type': 'text/event-stream; charset=utf-8',
    },
  })
}

The code begins with exporting a GET function handler. Inside the function, we instantiate a TextEncoder object that will create UTF-8 bytes from a given text-based notification.

Afterwards, it creates a ReadableStream instance that will be responsible for queueing notifications for the endpoint in the form of byte data. Three notifications events are sent through the stream via the asynchronous start callback function, with a delay of 1 second between each event. Finally, the function returns a standard web Response made up of the ReadableStream instance created above, along with the following headers:

  • Connection: keep-alive: To persistent the connection between client and server.
  • Content-Encoding: none: To indicate that the content is not encoded or transformed.
  • Cache-Control: no-cache, no-transform: To prevent caching and content transformation.
  • Content-type: text/event-stream; charset=utf-8: To set the MIME type for server-sent events with UTF-8 encoding.

Configure the Astro frontend to listen to Server-Sent Events

To get started creating a React component that listens to server-sent events, create a components directory in the src directory by typing:

mkdir src/components

Inside this components directory, create a Notifications.jsx file with the following code:

// File: src/components/Notifications.jsx
import { useEffect } from 'react'

export default function () {
  useEffect(() => {
    // Initiate the first call to connect to SSE API
    const eventSource = new EventSource('/sse')

    const messageListener = (event) => {
      const tmp = event.data
      // Do something with the obtained message
      console.log(tmp)
    }

    const closeListener = () => eventSource.close()

    // Close the connection to SSE API if any error
    eventSource.addEventListener('error', closeListener)

    // Listen to events received via the SSE API
    eventSource.addEventListener('message', messageListener)

    // As the component unmounts, close listeners to SSE API
    return () => {
      eventSource.removeEventListener('error', closeListener)
      eventSource.removeEventListener('message', messageListener)
      eventSource.close()
    }
  }, [])

  return <></>
}

The code above begins with exporting a React component. Inside this component, no user interface is returned as it's meant only to listen to server-sent events and create toast notifications. The useEffect hook is used with no reactive dependencies to only trigger this once as the component gets mounted on the DOM. An EventSource instance is created to establish a connection to the Server-Sent Events Endpoint. Then, event listeners are created for the following events:

  • message: fired when data is received through an event source.
  • error: fired when a connection with an event source is closed or fails to open.

Finally, the code includes a cleanup function to remove the event listeners and close the connection to the EventSource instance when the component is unmounted, preventing potential memory leaks.

To use this React component on the home page of your application, make the following changes in src/pages/index.astro file:

---
import Notifications from '../components/Notifications' <!-- [!code ++] -->
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width" />
    <meta name="generator" content="{Astro.generator}" />
    <title>Astro</title>
  </head>
  <body>
    <h1>Astro</h1>
    <Notifications client:load /> <!-- [!code ++] -->
  </body>
</html>

The changes above import the Notifications React component and use Astro's client:load directive to make sure that this React component is loaded and hydrated immediately on the page.

To display toast notifications whenever data is received through an event source, we need to install sonner, an opinionated toast component for React, by executing the following command:

npm install sonner

To render toast notifications in a React component using sonner, make the following changes in the Notifications component:

// File: src/components/Notifications.jsx
import { useEffect } from 'react'
import { Toaster, toast } from 'sonner' // [!code ++]

export default function () {
  useEffect(() => {
    // Initiate the first call to connect to SSE API
    const eventSource = new EventSource('/sse')

    const messageListener = (event) => {
      const tmp = event.data
      console.log(tmp) // [!code --]
      // Display toast notifications
      toast(tmp) // [!code ++]
    }

    const closeListener = () => eventSource.close()

    // Close the connection to SSE API if any error
    eventSource.addEventListener('error', closeListener)

    // Listen to events received via the SSE API
    eventSource.addEventListener('message', messageListener)

    // As the component unmounts, close listeners to SSE API
    return () => {
      eventSource.removeEventListener('error', closeListener)
      eventSource.removeEventListener('message', messageListener)
      eventSource.close()
    }
  }, [])

  return <></> // [!code --]
  return ( // [!code ++]
    <> // [!code ++]
      <Toaster /> // [!code ++]
    </> // [!code ++]
  ) // [!code ++]
}

The changes above import the toast utility function which renders a toast in the user interface, and the <Toaster /> component that contains relevant styles and props for the toast's React component. Afterwards, it invokes the toast function to render a toast whenever data is received through an event source. The changes end with returning the Toaster component as the only element to be rendered in the Notifications component.

You've successfully created in-app notifications in an Astro application that uses server-sent events to trigger toast notifications for users. In the upcoming section, you will proceed to deploy the application online on the Koyeb platform.

Deploy the Astro application to Koyeb

Koyeb is a developer-friendly serverless platform to deploy apps globally. No-ops, servers, or infrastructure management are required. It supports different tech stacks such as Rust, Golang, Python, PHP, Node.js, Ruby, and Docker.

With the app now complete, the final step is to deploy it online on Koyeb.

We will use git-driven deployment to deploy on Koyeb. To do this, we need to create a new GitHub repository from the GitHub web interface or by using the GitHub CLI with the following command:

gh repo create <YOUR_GITHUB_REPOSITORY> --private

Astro initialized a git repository as well as a compatible .gitignore file when we created our application. Add the new remote on GitHub to the repository and ensure that we're committing to the expected branch by typing:

git remote add origin git@github.com:<YOUR_GITHUB_USERNAME>/<YOUR_GITHUB_REPOSITORY>.git
git branch -M main

Add all the files in your project directory to the git repository and push them to GitHub:

git add :/
git commit -m "Initial commit"
git push -u origin main

To deploy the code on the GitHub repository, visit the Koyeb control panel and while on the Overview tab, click Create Web Service to start the deployment process. On the App deployment page:

  1. Select the GitHub deployment method.
  2. Choose the repository for your code from the repository drop-down menu. Alternatively, deploy from the example repository associated with this tutorial by entering https://github.com/koyeb/example-in-app-notifications-sse in the public repository field.
  3. Enter a name for the application or use the provided one.
  4. Finally, initiate the deployment process by clicking Deploy.

During the deployment on Koyeb, the process identifies the build and start scripts outlined in the package.json file, using them to build and launch the application. The deployment progress can be tracked through the logs presented. Upon the completion of deployment and the successful execution of vital health checks, your application will be operational.

If you would like to look at the code for the demo application, you can find it here.

Conclusion

In this tutorial, you created an in-app notifications application with Astro using server-sent events. You've also gained some experience with real-time communication between servers and clients, and understanding how it can help you build dynamic web applications.

Given that the application was deployed using the Git deployment option, subsequent code push to the deployed branch will automatically initiate a new build for your application. Changes to your application will become live once the deployment is successful. In the event of a failed deployment, Koyeb retains the last operational production deployment, ensuring the uninterrupted operation of your application.


Deploy AI apps to production in minutes

Koyeb is a developer-friendly serverless platform to deploy apps globally. No-ops, servers, or infrastructure management.
All systems operational
© Koyeb