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 { Controller, All, Req,
Catch-all routes and error shaping in NestJS
exception-handling
routing
middleware
Intermediate
6 steps
typescript
import { ArgumentsHost, Catch, ConflictException,
Turning TypeORM lock errors into 409s in NestJS
exception-handling
optimistic-locking
http-status
Intermediate
6 steps
typescript
import { Component } from '@angular/core'; import { RouterLink, RouterLinkActive } from '@angular/router'; import { NgFor } from '@angular/common';
Building an active-route navbar in Angular
routing
standalone-components
accessibility
Intermediate
6 steps
typescript
type Masker = (value: string) => string; const maskEmail: Masker = (value) => { const [local, domain] = value.split("@");
Recursively masking sensitive data for logs
recursion
regex
data-masking
Intermediate
9 steps
typescript
interface ParsedName { first: string; middle: string; last: string;
Parsing human names into structured parts
parsing
string-manipulation
normalization
Intermediate
9 steps
typescript
import { useCallback, useEffect, useRef, useState } from "react"; interface UseResendCooldownOptions { cooldownSeconds?: number;
A resend cooldown hook in React
custom-hooks
timers
state-management
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.