In this tutorial, we will look at Server Sent Events: a built-in EventSource class that allows you to maintain a connection to the server and receive events from it.
You can read about what SSE is and what it is used for here .
What exactly are we going to do?
We will write a simple server that, upon the client's request, will send him data of 10 random users, and the client will use this data to generate user cards.
The server will be implemented in Node.js , the client in JavaScript. Bootstrap will be used for styling , and Random User Generator will be used as an API .
The project code is here...
If you are interested, please follow me.
Training
Create a directory
sse-tut
:
mkdir sse-tut
We go into it and initialize the project:
cd sse-tut
yarn init -y
//
npm init -y
Install
axios
:
yarn add axios
//
npm i axios
axios will be used to get user data.
Editing
package.json
:
"main": "server.js",
"scripts": {
"start": "node server.js"
},
Project structure:
sse-tut
--node_modules
--client.js
--index.html
--package.json
--server.js
--yarn.lock
Contents
index.html
:
<head>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<style>
.card {
margin: 0 auto;
max-width: 512px;
}
img {
margin: 1rem;
max-width: 320px;
}
p {
margin: 1rem;
}
</style>
</head>
<body>
<main class="container text-center">
<h1>Server-Sent Events Tutorial</h1>
<button class="btn btn-primary" data-type="start-btn">Start</button>
<button class="btn btn-danger" data-type="stop-btn" disabled>Stop</button>
<p data-type="event-log"></p>
</main>
<script src="client.js"></script>
</body>
Server
Let's start implementing the server.
We open
server.js
.
We connect http and axios, define the port:
const http = require('http')
const axios = require('axios')
const PORT = process.env.PORT || 3000
We create a function for receiving user data:
const getUserData = async () => {
const response = await axios.get('https://randomuser.me/api')
//
console.log(response)
return response.data.results[0]
}
Create a counter for the number of sent users:
let i = 1
We write the function of sending data to the client:
const sendUserData = (req, res) => {
// - 200
//
// -
//
res.writeHead(200, {
Connection: 'keep-alive',
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache'
})
// 2
const timer = setInterval(async () => {
// 10
if (i > 10) {
//
clearInterval(timer)
// , 10
console.log('10 users has been sent.')
// -1
// ,
res.write('id: -1\ndata:\n\n')
//
res.end()
return
}
//
const data = await getUserData()
//
// event -
// id - ;
// retry -
// data -
res.write(`event: randomUser\nid: ${i}\nretry: 5000\ndata: ${JSON.stringify(data)}\n\n`)
// ,
console.log('User data has been sent.')
//
i++
}, 2000)
//
req.on('close', () => {
clearInterval(timer)
res.end()
console.log('Client closed the connection.')
})
}
We create and start the server:
http.createServer((req, res) => {
// CORS
res.setHeader('Access-Control-Allow-Origin', '*')
// - getUser
if (req.url === '/getUsers') {
//
sendUserData(req, res)
} else {
// , , ,
//
res.writeHead(404)
res.end()
}
}).listen(PORT, () => console.log('Server ready.'))
Full server code:
const http = require('http')
const axios = require('axios')
const PORT = process.env.PORT || 3000
const getUserData = async () => {
const response = await axios.get('https://randomuser.me/api')
return response.data.results[0]
}
let i = 1
const sendUserData = (req, res) => {
res.writeHead(200, {
Connection: 'keep-alive',
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache'
})
const timer = setInterval(async () => {
if (i > 10) {
clearInterval(timer)
console.log('10 users has been sent.')
res.write('id: -1\ndata:\n\n')
res.end()
return
}
const data = await getUserData()
res.write(`event: randomUser\nid: ${i}\nretry: 5000\ndata: ${JSON.stringify(data)}\n\n`)
console.log('User data has been sent.')
i++
}, 2000)
req.on('close', () => {
clearInterval(timer)
res.end()
console.log('Client closed the connection.')
})
}
http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', '*')
if (req.url === '/getUsers') {
sendUserData(req, res)
} else {
res.writeHead(404)
res.end()
}
}).listen(PORT, () => console.log('Server ready.'))
We execute the command
yarn start
or npm start
. The terminal displays the message "Server ready." Opening http://localhost:3000
: We are
finished with the server, go to the client side of the application.
Client
Open the file
client.js
.
Create a function for generating a custom card template:
const getTemplate = user => `
<div class="card">
<div class="row">
<div class="col-md-4">
<img src="${user.img}" class="card-img" alt="user-photo">
</div>
<div class="col-md-8">
<div class="card-body">
<h5 class="card-title">${user.id !== null ? `Id: ${user.id}` : `User hasn't id`}</h5>
<p class="card-text">Name: ${user.name}</p>
<p class="card-text">Username: ${user.username}</p>
<p class="card-text">Email: ${user.email}</p>
<p class="card-text">Age: ${user.age}</p>
</div>
</div>
</div>
</div>
`
The template is generated using the following data: user ID (if any), name, login, email address and age of the user.
We are starting to implement the main functionality:
class App {
constructor(selector) {
// -
this.$ = document.querySelector(selector)
//
this.#init()
}
#init() {
this.startBtn = this.$.querySelector('[data-type="start-btn"]')
this.stopBtn = this.$.querySelector('[data-type="stop-btn"]')
//
this.eventLog = this.$.querySelector('[data-type="event-log"]')
//
this.clickHandler = this.clickHandler.bind(this)
//
this.$.addEventListener('click', this.clickHandler)
}
clickHandler(e) {
//
if (e.target.tagName === 'BUTTON') {
//
// ,
//
const {
type
} = e.target.dataset
if (type === 'start-btn') {
this.startEvents()
} else if (type === 'stop-btn') {
this.stopEvents()
}
//
this.changeDisabled()
}
}
changeDisabled() {
if (this.stopBtn.disabled) {
this.stopBtn.disabled = false
this.startBtn.disabled = true
} else {
this.stopBtn.disabled = true
this.startBtn.disabled = false
}
}
//...
First, we implement closing the connection:
stopEvents() {
this.eventSource.close()
// ,
this.eventLog.textContent = 'Event stream closed by client.'
}
Let's move on to opening the stream of events:
startEvents() {
//
this.eventSource = new EventSource('http://localhost:3000/getUsers')
// ,
this.eventLog.textContent = 'Accepting data from the server.'
// -1
this.eventSource.addEventListener('message', e => {
if (e.lastEventId === '-1') {
//
this.eventSource.close()
//
this.eventLog.textContent = 'End of stream from the server.'
this.changeDisabled()
}
//
}, {once: true})
}
We handle the "randomUser" custom event:
this.eventSource.addEventListener('randomUser', e => {
//
const userData = JSON.parse(e.data)
//
console.log(userData)
//
const {
id,
name,
login,
email,
dob,
picture
} = userData
// ,
const i = id.value
const fullName = `${name.first} ${name.last}`
const username = login.username
const age = dob.age
const img = picture.large
const user = {
id: i,
name: fullName,
username,
email,
age,
img
}
//
const template = getTemplate(user)
//
this.$.insertAdjacentHTML('beforeend', template)
})
Don't forget to implement error handling:
this.eventSource.addEventListener('error', e => {
this.eventSource.close()
this.eventLog.textContent = `Got an error: ${e}`
this.changeDisabled()
}, {once: true})
Finally, we initialize the application:
const app = new App('main')
Full client code:
const getTemplate = user => `
<div class="card">
<div class="row">
<div class="col-md-4">
<img src="${user.img}" class="card-img" alt="user-photo">
</div>
<div class="col-md-8">
<div class="card-body">
<h5 class="card-title">${user.id !== null ? `Id: ${user.id}` : `User hasn't id`}</h5>
<p class="card-text">Name: ${user.name}</p>
<p class="card-text">Username: ${user.username}</p>
<p class="card-text">Email: ${user.email}</p>
<p class="card-text">Age: ${user.age}</p>
</div>
</div>
</div>
</div>
`
class App {
constructor(selector) {
this.$ = document.querySelector(selector)
this.#init()
}
#init() {
this.startBtn = this.$.querySelector('[data-type="start-btn"]')
this.stopBtn = this.$.querySelector('[data-type="stop-btn"]')
this.eventLog = this.$.querySelector('[data-type="event-log"]')
this.clickHandler = this.clickHandler.bind(this)
this.$.addEventListener('click', this.clickHandler)
}
clickHandler(e) {
if (e.target.tagName === 'BUTTON') {
const {
type
} = e.target.dataset
if (type === 'start-btn') {
this.startEvents()
} else if (type === 'stop-btn') {
this.stopEvents()
}
this.changeDisabled()
}
}
changeDisabled() {
if (this.stopBtn.disabled) {
this.stopBtn.disabled = false
this.startBtn.disabled = true
} else {
this.stopBtn.disabled = true
this.startBtn.disabled = false
}
}
startEvents() {
this.eventSource = new EventSource('http://localhost:3000/getUsers')
this.eventLog.textContent = 'Accepting data from the server.'
this.eventSource.addEventListener('message', e => {
if (e.lastEventId === '-1') {
this.eventSource.close()
this.eventLog.textContent = 'End of stream from the server.'
this.changeDisabled()
}
}, {once: true})
this.eventSource.addEventListener('randomUser', e => {
const userData = JSON.parse(e.data)
console.log(userData)
const {
id,
name,
login,
email,
dob,
picture
} = userData
const i = id.value
const fullName = `${name.first} ${name.last}`
const username = login.username
const age = dob.age
const img = picture.large
const user = {
id: i,
name: fullName,
username,
email,
age,
img
}
const template = getTemplate(user)
this.$.insertAdjacentHTML('beforeend', template)
})
this.eventSource.addEventListener('error', e => {
this.eventSource.close()
this.eventLog.textContent = `Got an error: ${e}`
this.changeDisabled()
}, {once: true})
}
stopEvents() {
this.eventSource.close()
this.eventLog.textContent = 'Event stream closed by client.'
}
}
const app = new App('main')
Restart the server just in case. We open
http://localhost:3000
. Click on the "Start" button:
We start receiving data from the server and rendering user cards.
If you click on the "Stop" button, sending data will be suspended:
Press "Start" again, sending data continues.
When the limit (10 users) is reached, the server sends an identifier with a value of -1 and closes the connection. The client, in turn, also closes the event stream:
As you can see, SSE is very similar to websockets. The disadvantage is that messages are unidirectional: messages can only be sent by the server. The advantage is automatic reconnection and ease of implementation.
Support for this technology today is 95%:
I hope you enjoyed the article. Thank you for attention.