Server-Sent Events
Nest’s @Sse() decorator works out of the box. The adapter bridges Nest’s Node-stream SSE machinery onto a Bun-native ReadableStream, so events are streamed over Web Streams with no Node http server underneath.
Basic stream
Section titled “Basic stream”A handler decorated with @Sse() returns an Observable<MessageEvent>. Each emitted value is serialized into the W3C text/event-stream format.
import { Controller, Sse } from '@nestjs/common';import { interval, map, type Observable } from 'rxjs';
@Controller('events')export class EventsController { @Sse('clock') clock(): Observable<{ data: { now: string } }> { return interval(1000).pipe( map(() => ({ data: { now: new Date().toISOString() } })), ); }}Consume it from the browser:
const source = new EventSource('/events/clock');source.onmessage = (e) => console.log(JSON.parse(e.data));Message shape
Section titled “Message shape”Each value follows Nest’s MessageEvent shape:
| Field | Effect on the wire |
|---|---|
data | The payload. Objects are JSON-stringified; strings are sent verbatim. |
type | Emitted as event: <type>, so addEventListener('<type>', …) fires. |
id | Emitted as id: <id> (auto-incremented when omitted). |
retry | Reconnection hint in milliseconds. |
@Sse('notifications')notifications(): Observable<{ type: string; data: unknown }> { return this.notifications$.pipe( map((n) => ({ type: 'notification', data: n })), );}Backpressure & disconnects
Section titled “Backpressure & disconnects”The adapter applies real backpressure: when the client reads slower than the Observable emits, writes pause until the socket drains, so a fast producer can’t buffer without bound.
When the client disconnects, the request’s AbortSignal fires, Nest unsubscribes from your Observable, and any teardown logic runs:
@Sse('feed')feed(): Observable<{ data: unknown }> { return new Observable((subscriber) => { const handle = subscribe(subscriber); return () => handle.unsubscribe(); // runs on client disconnect });}