Cómo crear tableros en tiempo real con Angular y NestJS usando Server-Sent Events

Cuando necesito crear aplicaciones en tiempo real con Angular, la mayor parte del tiempo uso SignalR. Sin embargo, hace unos días un amigo me mencionó SSE. Nunca antes había escuchado sobre esta tecnología ni tenido la oportunidad de verla en acción, así que me puse a investigar y me sorprendió lo fácil y potente que resulta en ciertos escenarios.
Server-Sent Events (SSE) nos permite enviar actualizaciones o un flujo de datos al navegador mediante HTTP. Funciona a través de la API EventSource en JavaScript y nos permite transmitir datos del servidor al cliente sobre una conexión HTTP estándar.
¿Cómo funciona?
Primero, el cliente inicia la conexión enviando una petición HTTP. El servidor responde con los encabezados text/event-stream y transfer-encoding: chunked para indicar que la conexión permanecerá abierta. Esta configuración le permite al servidor enviar eventos como un flujo de texto al cliente, usando la propiedad data y un id opcional.
Cuando el cliente cierra la pestaña o sale de la página, el servidor cierra la conexión automáticamente. Esto funciona muy bien, pero también podemos indicarle al servidor cuándo detener la emisión de forma manual.
Por ejemplo, podemos usar SSE para enviar notificaciones del servidor al cliente de manera muy sencilla. Ya sabemos qué es SSE y cuándo conviene usarlo, así que pasemos a la práctica con un caso real y sencillo.
El escenario
Queremos crear un marcador deportivo para mostrar el resultado de un partido transmitido en vivo por el servidor, con la opción de pausar o detener las actualizaciones. Algo como esto:

La forma más práctica de lograrlo es combinar Angular y NestJS usando Nx.
-
Angular: la aplicación cliente para mostrar el tablero y conectarse a la API en tiempo real.
-
NestJS: nos permite crear un endpoint SSE fácilmente gracias al decorador
@sse. -
Nx: nos ayuda a configurar Angular y NestJS juntos en un único monorepo.
¡Todo listo, empecemos!
Configuración del proyecto
Primero, usamos Nx para crear un workspace vacío llamado thescore ejecutando el comando npx create-nx-workspace@latest thescore. Al iniciar, el asistente nos preguntará qué stack queremos usar: elegimos none (ninguno) y seleccionamos el tipo de monorepo integrado (integrated monorepo).
¿Por qué elegir none? Como estamos trabajando con Nx, prefiero instalar los schematics y generadores de forma manual tanto para NestJS como para Angular.

Con el workspace listo, pasemos a crear la API.
La API con NestJS
Primero agregamos el generador de Nx para NestJS con nx add @nx/nest y generamos la API score-api ejecutando nx g @nx/nest:app score-api.
nx add @nx/nest
nx g @nx/nest:app score-apiA continuación, generamos el controlador games ejecutando el comando nx g @nx/nest:controller src/app/controllers/games.
nx g @nx/nest:controller src/app/controllers/games
√ Where should the controller be generated? · score-api/src/app/controllers/games.ts
CREATE score-api/src/app/controllers/games.controller.ts
CREATE score-api/src/app/controllers/games.controller.spec.ts
UPDATE score-api/src/app/app.module.tsAbrimos el archivo games.controller.ts. Antes de la clase del controlador, definimos el tipo para el puntaje del juego:
type GameScore = { data: { game: { lakers: number; denver: number } } };Dentro de la clase GamesController, declaramos un Subject actionSubject y un Observable action$ para detener la emisión. Luego definimos el método scores con el decorador @Sse retornando un Observable de GameScore.
@Controller('games')
export class GamesController {
private actionSubject = new Subject<boolean>();
private action$ = this.actionSubject.asObservable();
@Sse('scores')
scores(): Observable<GameScore> {
}
}Vamos a declarar un valor inicial para nuestro juego. Usando el observable interval, emitiremos el estado del partido cada 2 segundos. En el flujo usaremos el operador tap para actualizar el juego con valores aleatorios que incrementen el marcador. Encadenamos esto con takeUntil para detener la emisión en cuanto se emita una acción, y usamos el operador map para devolver el objeto con el formato esperado.
El código final queda así:
@Sse('scores')
scores(): Observable<GameScore> {
const game = {
lakers: 0,
denver: 0,
};
return interval(2000).pipe(
tap(() => {
game.lakers += Math.floor(Math.random() * 4) + 1;
game.denver += Math.floor(Math.random() * 4) + 1;
}),
takeUntil(this.action$),
map(() => ({ data: { game } }))
);
}Para detener la emisión, creamos el método stopCounter con el decorador @Post y usamos actionSubject para emitir el nuevo valor.
@Post('stop')
@HttpCode(HttpStatus.OK)
stopCounter() {
this.actionSubject.next(false);
}Antes de iniciar la API, abre
main.tsy habilita CORS en la aplicación llamando al métodoapp.enableCors()dentro de la función bootstrap.
Desde la terminal, ejecutamos la API de NestJS con el comando nx run score-api:serve.

Abrimos el navegador e ingresamos a http://localhost:3000/api.

Muy bien, ya tenemos lista nuestra API con Server-Sent Events. Ahora es momento de consumirla desde nuestra aplicación en Angular. ¡Vamos a ello!
Consumir Server-Sent Events en Angular
Llegó el momento de consumir los eventos en la aplicación de Angular. Hacemos algo similar al paso anterior, agregando soporte para Angular con el comando nx add @nx/angular, y luego generamos la aplicación score-live ejecutando nx g @nx/angular:app score-live.
$ nx add @nx/angular
$ nx g @nx/angular:app score-liveAhora creamos el servicio para interactuar con la API ejecutando el comando nx g @nx/angular:service src/app/services/score.
$ nx g @nx/angular:service src/app/services/score
NX Generating @nx/angular:service
CREATE score-live/src/app/services/score.service.spec.ts
CREATE score-live/src/app/services/score.service.tsAntes de continuar, para poder realizar peticiones HTTP a la API debemos registrar provideHttpClient en app.config.
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { appRoutes } from './app.routes';
import {provideHttpClient} from "@angular/common/http";
export const appConfig: ApplicationConfig = {
providers: [provideRouter(appRoutes), provideHttpClient()],
};Abrimos el archivo score.service.ts y definimos el tipo GameScore con las siguientes propiedades:
export type GameScore = {
lakers: number;
denver: number;
};Luego agregamos las siguientes propiedades a la clase ScoreService:
-
API: la URL de nuestra API.
-
sseSource: instancia de
EventSourcepara recibir las actualizaciones del servidor. -
http: inyección de
HttpClientpara realizar peticiones a la API. -
scoreSubject:
Subjectpara emitir los nuevos valores. -
scores$: Observable con el puntaje
GameScore, usando el operadorstartWithpara definir el valor inicial.
El código se ve de la siguiente manera:
export class ScoreService {
private API = 'http://localhost:3000/api/games/scores';
private sseSource = new EventSource(`${this.API}/scores`);
private http = inject(HttpClient);
private scoreSubject$ = new Subject<GameScore>();
public scores$ = this.scoreSubject$.asObservable().pipe(
startWith({
lakers: 0,
denver: 0,
})
);
}A continuación, definimos el método getFeed. Nos suscribimos al evento message mediante addEventListener en sseSource para recibir la información cada vez que el servidor emita datos, y usamos scoreSubject$ para emitir el puntaje del juego.
También vinculamos el método onerror para capturar cualquier error y mostrarlo en la consola.
private getFeed(): void {
this.sseSource.addEventListener('message', (e: MessageEvent) => {
const { game } = JSON.parse(e.data);
this.scoreSubject$.next(game);
});
this.sseSource.onerror = () => {
console.error('😭 sse error');
};
}El siguiente paso es permitir que el componente interactúe con el servicio mediante dos métodos: start, que invoca getFeed(), y stop, que hace una petición POST al endpoint de detención de la API.
El código final del servicio queda así:
public start(): void {
this.getFeed();
}
public stop(): void {
this.http.post(`${this.API}/stop`, {});
}Es momento de usar estos datos en app.component.ts. Inyectamos ScoreService y declaramos el observable gameScore$. Además, agregamos los métodos start y stop para iniciar y detener el flujo desde el servicio.
export class AppComponent {
private scoreService = inject(ScoreService);
public gameScore$ = this.scoreService.scores$;
public stop(): void {
this.scoreService.stop();
}
public start(): void {
this.scoreService.start();
}
}En la plantilla HTML, nos suscribimos a gameScore$ usando el pipe async y renderizamos los datos en pantalla:
<div class="flex justify-center items-center h-screen">
<div class="bg-gray-800 text-white p-4 rounded-lg text-center">
<div class="flex justify-between mb-2">
<span class="flex-grow font-bold text-yellow-500 p-4">Lakers</span>
<span class="flex-grow font-bold text-blue-500 p-4">Nuggets</span>
</div>
<div class="flex justify-between">
@if (gameScore$ | async; as score) {
<span class="text-4xl font-bold w-1/2">{{score.lakers}}</span>
<span class="text-4xl font-bold w-1/2">{{score.denver}}</span>
}
</div>
<div class="mt-4">
<button class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-full mr-2"
(click)="start()">Start</button>
<button class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-full"
(click)="stop()">Stop</button>
</div>
</div>
</div>En este proyecto estoy usando Tailwind CSS. Si quieres aprender a configurarlo en tu monorepo, puedes revisar las instrucciones oficiales aquí: 👉🏽 https://nx.dev/recipes/angular/using-tailwind-css-with-angular-projects
Guardamos los cambios, recargamos la página y listo: ¡ya tenemos nuestro marcador funcionando con datos en tiempo real! 🎉

Si abres la pestaña Network de las herramientas de desarrollo, puedes ver la conexión enviando datos continuos al cliente:

Conclusión
Hemos repasado los conceptos básicos de SSE y su sencilla implementación con NestJS, además de cómo consumir los eventos desde Angular. A diferencia de soluciones como SignalR o WebSockets, SSE no cuenta con comunicación bidireccional de forma nativa, pero podemos combinarlo con endpoints HTTP estándar para indicarle al servidor cuándo iniciar o detener la transmisión.
-
Código fuente: https://github.com/danywalls/sse-with-angular-nestjs
-
Server-Sent Events (MDN): https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
-
NestJS SSE: https://docs.nestjs.com/techniques/server-sent-events
Modern frontend development — Angular, React, TypeScript, accessibility, and performance.
Artículos Relacionados
Adiós Hashnode, hola Vercel: Cómo migré mi blog a Next.js y Vercel
Migré mi blog de Hashnode a un sitio personalizado con Next.js en Vercel. Aquí te cuento por qué lo hice, cómo moví todo mi contenido y por qué Vercel es el mejor lugar para mis artículos.
Cómo gestionar errores HTTP en NestJS sin complicaciones
Cuando desarrollamos aplicaciones, pueden ocurrir errores en la API como pedir datos que no existen o perder la conexión a la base de datos. Aprende a capturar y manejar errores en NestJS con respuestas claras.
Compartir este artículo
Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.
Software real. Lecciones reales.
Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.