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:
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
- Integrate React in your Astro project
- Enabling server-side rendering in Astro
- Create a server-sent events API in Astro
- Configure the Astro frontend to listen to server-sent events
- Deploy the Astro application to Koyeb
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 dependenciesYes
to make changes to Astro configuration fileYes
to make changes totsconfig.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 componentsreact-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 forreact
package@types/react-dom
: Type definitions forreact-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 dependenciesYes
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:
- Select the GitHub deployment method.
- 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. - Enter a name for the application or use the provided one.
- 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.