typescript
37 lines · 8 steps
Streaming job progress with SSE in NestJS
A NestJS controller turns a job's progress stream into Server-Sent Events, with a heartbeat to keep the connection alive.
Explained by
highlit
1import { Controller, Param, Sse, MessageEvent } from '@nestjs/common';
2import { Observable, interval, merge } from 'rxjs';
3import { filter, map, takeWhile } from 'rxjs/operators';
4import { JobService } from './job.service';
5
6@Controller('jobs')
7export class JobProgressController {
8 constructor(private readonly jobs: JobService) {}
9
10 @Sse(':id/progress')
11 streamProgress(@Param('id') id: string): Observable<MessageEvent> {
12 const updates$ = this.jobs.progressStream(id).pipe(
13 filter((update) => update.jobId === id),
14 map((update) => ({
15 id: `${update.jobId}:${update.step}`,
16 type: update.status === 'failed' ? 'error' : 'progress',
17 data: {
18 step: update.step,
19 total: update.total,
20 percent: Math.round((update.step / update.total) * 100),
21 status: update.status,
22 message: update.message,
23 },
24 })),
25 takeWhile(
26 (event) => event.data.status !== 'completed' && event.type !== 'error',
27 true,
28 ),
29 );
30
31 const heartbeat$ = interval(15000).pipe(
32 map<number, MessageEvent>(() => ({ type: 'ping', data: { ts: Date.now() } })),
33 );
34
35 return merge(updates$, heartbeat$);
36 }
37}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1@Sse lets a controller return an Observable that NestJS pipes to the client as a live Server-Sent Event stream.
- 2takeWhile with its inclusive flag emits the terminal event before closing the stream, so clients see the final state.
- 3Merging a periodic heartbeat into a data stream keeps idle SSE connections from timing out.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect app = FastAPI()
Building a WebSocket chat with FastAPI
websockets
broadcast
connection-management
Intermediate
9 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
rust
use axum::{ extract::{Path, State}, response::sse::{Event, KeepAlive, Sse}, };
Streaming import progress with SSE in Axum
server-sent-events
streams
watch-channel
Advanced
7 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core'; interface Preferences { theme: 'light' | 'dark';
A signal-based preferences store in Angular
signals
state-management
persistence
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/streaming-job-progress-with-sse-in-nestjs-explained-typescript-de7e/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.