For the following tutorial, we will be using Express & Node.js, as long as cURL for simulating POST requests. This can also be done using any programming language that has an implementation of the HTTP protocol.
Create the Node configuration
touch package.json
{
"type": "module",
"scripts": {
"start": "node sources/server/index.js
}
}
Install Express
npm install --save --save-exact express
Create the server file
mkdir -p sources/server
touch sources/server/index.js
import express from "express"
// Express server initialization
const server = express()
// Port exposing the HTTP server
const port = 8000
// Host to listen for HTTP requests
const host = "0.0.0.0"
// Used for hosting our client files (HTML, CSS, JavaScript, ...)
server.use(express.static("sources/client"))
// Server-Sent Event HTTP route
server.get("/api/users/event", (request, response) => {
// Used to prevent the browser from closing the connection after receiving the first body
response.setHeader("Connection", "keep-alive")
// Used to tell the browser that we are sending Server-Sent Events
response.setHeader("Content-Type", "text/event-stream")
// Used as a security measure to prevent caching the data we send
response.setHeader("Cache-Control", "no-cache")
// Loop to simulate realtime data updates at regular intervals
setInterval(() => {
// Expected format that the browser will decode for use in the client
// write is used to prevent closing the connection from the server with a send, json, end, ...
response.write("event: update\ndata: Hello, world!\n\n")
}, 1000)
})
// Start listening for HTTP requests
server.listen(port, host, () => {
// Displaying the host & port listened in the console for debugging purposes
console.log(`Listening on http://${host}:${port}`)
})
Create the HTML file
mkdir -p sources/client
touch sources/client/index.html
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Test for demonstrating how to use the Server-Sent Events">
<title>Server-Sent Events</title>
</head>
<body>
<script src="./index.js" type="module"></script>
</body>
</html>
Create the JavaScript file
touch sources/client/index.js
const usersEventSource = new EventSource("/api/users/event")
usersEventSource.addEventListener("open", () => {
console.log("Connected to the server")
})
usersEventSource.addEventListener("error", error => {
console.error("Oops! Something went wrong, here is why")
console.error(error)
})
usersEventSource.addEventListener("update", event => {
console.log("Successfully received a notification from the server")
console.log(event.data)
})
Start the server
npm start
Open the browser
Open the browser at http://localhost:8000, or using the port and host of your choice if you have decided to change those.
You should see in the console that the message gets updated every second.
If you open the network tab of your browser, you should also see that it do not sends periodic HTTP requests, this is because we established an connection that remains alive even after the body is received from the server which is the standard behavior when received data from a server to a client.
Let's improve this example by creating a more real-world example.
Install BodyParser
npm install --save --save-exact body-parser
Add a middleware for using BodyParser
import express from "express"
// Let us import the BodyParser library
import bodyParser from "body-parser"
const server = express()
const port = 8000
const host = "0.0.0.0"
server.use(express.static("sources/client"))
// Let us parse the request body and access the
// JSON content easily
server.use(bodyParser.json())
server.get("/api/users/event", (request, response) => {
response.setHeader("Connection", "keep-alive")
response.setHeader("Content-Type", "text/event-stream")
response.setHeader("Cache-Control", "no-cache")
setInterval(() => {
response.write("event: update\ndata: Hello, world!\n\n")
}, 1000)
})
server.listen(port, host, () => {
console.log(`Listening on http://${host}:${port}`)
})
Add a route for creating a user
import express from "express"
import bodyParser from "body-parser"
// Import the EventEmitter class
import { EventEmitter } from "events"
const server = express()
const port = 8000
const host = "0.0.0.0"
// Create the event emitter for sending and receiving updates
const usersEventEmitter = new EventEmitter()
server.use(express.static("sources/client"))
server.use(bodyParser.json())
server.get("/api/users/event", (request, response) => {
response.setHeader("Connection", "keep-alive")
response.setHeader("Content-Type", "text/event-stream")
response.setHeader("Cache-Control", "no-cache")
// Wait and listen for user updates
usersEventEmitter.on("user", user => {
// Sends back the user that has been created with the /api/users route
response.write(`event: update\ndata: ${JSON.stringify(user)}\n\n`)
})
})
// Adds a new route handler for user creation requests
server.post("/api/users", (request, response) => {
// Get the firstname from the parsed JSON body
const firstname = request.body.firstname
// Get the lastname from the parsed JSON body
const lastname = request.body.lastname
// Create a user object
const user = {firstname, lastname}
// Sends the event to the /api/users/event route
usersEventEmitter.emit("user", user)
// Tells the client that the user has been created
response.status(201)
// Closes the HTTP connection without any body
response.end()
})
server.listen(port, host, () => {
console.log(`Listening on http://${host}:${port}`)
})
Restart the server
npm start
Reload the browser
You should have nothing in the console, that is normal because we didn't create a user yet. Let's do that now.
Create a new user using cURL
curl -X POST -H 'Content-Type: application/json' -d '{"firstname":"John","lastname":"DOE"}' localhost:8000/api/users
Go back to your browser. You should now see a new notification.
Notice that you only get the notification once you created a new user.
Purpose
Server-Sent Event are really great if you need to get updated data in realtime from your server, such as the state of an order, or a notification system.
They are also really easy to setup and does not require implementing a whole new protocol with the added complexity of following an RFC. They are implemented as a Web API in all modern browsers and the server implementation is very trivial.
For most case scenario where you need to fetch data from the server using polling or long polling, Server-Sent Events can be a more efficient alternative since it only creates one HTTP connection and do not fetch data when it is not needed.
Limitations
You won't be able to send back realtime data from the client to the server, and in this case using an implementation of the Websocket protocol will be mandatory.
What's next?
- Choose a database
- Store the user created in the POST /api/users route in the database
- Before connecting to the EventSource, fetch the list of users that are already stored in the database
- Find a way to reconnect to the server in case the connection is closed (error, network issue, etc...)
- Ditch fetching users in realtime and start from scratch by creating an instant chat messaging system by using both HTTP & SSE requests
Bonus
Here is what it would look like from the server side if we were to create a SSE server in PHP even though it sounds less interesting than in Node.js.
<?php
header("Content-Type: text/event-stream");
header("Connection: keep-alive");
header("Cache-control: no-cache");
while (true) {
try {
$connection = new PDO("driver:dbname=name;host=localhost", "user", "pass");
$query = $connection->query("SELECT * FROM users");
$users = $query->fetchAll(PDO::FETCH_ASSOC);
$data = json_encode($users);
$event = "message";
echo "event: $event\ndata: $data\n\n";
sleep(5);
} catch (Exception $exception) {
// TODO: register the error somewhere
}
}
Top comments (1)
Amazing, appreciate you did the example showing PHP code as well!