Build an SSE stream
Frame an event stream
Return the correct content type and encode events as lines separated by a blank line.
An SSE response is plain HTTP that never ends. The server sets Content-Type: text/event-stream and writes events. Each event is line based text, and a blank line tells the browser “dispatch this one now.”
Writing raw JSON without framing looks live in one tab and breaks everywhere else.
Headers and one event
const http = require('http')
const server = http.createServer((req, res) => {
if (req.url !== '/events') {
res.writeHead(404)
res.end()
return
}
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
})
res.write('event: incident.created\n')
res.write('id: evt_85\n')
res.write('data: {"id":42,"title":"Database failover"}\n\n')
req.on('close', () => res.end())
})
server.listen(3000)
Start the server, then inspect the raw bytes:
curl -N http://localhost:3000/events
You should see the three field lines, then a blank line, then the connection stays open. Two events means two blank-line-terminated blocks, not one concatenated JSON blob.
Client side
The browser API is EventSource:
const source = new EventSource('/events')
source.addEventListener('incident.created', (event) => {
console.log(event.lastEventId, JSON.parse(event.data))
})
When the second event arrives, the console prints evt_85 and the parsed object. That is the proof framing worked.
Common mistake
Dumping {"id":42}{"id":43} into the body without data: lines and \n\n separators will not produce two browser events. The parser needs the field lines.
Keep the first version tiny: one route, two hard-coded events, curl plus one browser tab. Add auth and replay after the framing is boring.
Wrong framing looks like this
If you res.write(JSON.stringify({ id: 42 })) without data: lines, curl shows one blob and the browser never fires incident.created. Fixing headers alone will not help until the body uses field lines plus \n\n.
Multi-line JSON belongs in one data: line or several data: lines the spec joins together. Do not pretty-print raw JSON without the prefix.
Try this on your own project: emit two named events from one endpoint, curl the stream, and confirm the browser fires two separate listener calls.
Lesson completed