├── .eslintrc.js
├── .gitignore
├── .prettierrc
├── Dockerfile
├── README.md
├── client
└── index.html
├── nest-cli.json
├── package.json
├── src
├── app.controller.ts
├── app.module.ts
├── constants.ts
├── main.ts
└── notifications.service.ts
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | tsconfigRootDir: __dirname,
6 | sourceType: 'module',
7 | },
8 | plugins: ['@typescript-eslint/eslint-plugin'],
9 | extends: [
10 | 'plugin:@typescript-eslint/recommended',
11 | 'plugin:prettier/recommended',
12 | ],
13 | root: true,
14 | env: {
15 | node: true,
16 | jest: true,
17 | },
18 | ignorePatterns: ['.eslintrc.js'],
19 | rules: {
20 | '@typescript-eslint/interface-name-prefix': 'off',
21 | '@typescript-eslint/explicit-function-return-type': 'off',
22 | '@typescript-eslint/explicit-module-boundary-types': 'off',
23 | '@typescript-eslint/no-explicit-any': 'off',
24 | },
25 | };
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist
3 | /node_modules
4 |
5 | # Logs
6 | logs
7 | *.log
8 | npm-debug.log*
9 | pnpm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 | lerna-debug.log*
13 |
14 | # OS
15 | .DS_Store
16 |
17 | # Tests
18 | /coverage
19 | /.nyc_output
20 |
21 | # IDEs and editors
22 | /.idea
23 | .project
24 | .classpath
25 | .c9/
26 | *.launch
27 | .settings/
28 | *.sublime-workspace
29 |
30 | # IDE - VSCode
31 | .vscode/*
32 | !.vscode/settings.json
33 | !.vscode/tasks.json
34 | !.vscode/launch.json
35 | !.vscode/extensions.json
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Use the official Node.js image as the base image
2 | FROM node:20
3 |
4 | # Set the working directory inside the container
5 | WORKDIR /usr/src/app
6 |
7 | # Copy package.json and package-lock.json to the working directory
8 | COPY package*.json ./
9 |
10 | # Install the application dependencies
11 | RUN npm install
12 |
13 | # Copy the rest of the application files
14 | COPY . .
15 |
16 | # Build the NestJS application
17 | RUN npm run build
18 |
19 | # Expose the application port
20 | EXPOSE 3000
21 |
22 | # Command to run the application
23 | CMD ["node", "dist/main"]
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Nest.js Server-Sent Events (SSE) Example
2 |
3 | This is a simple Nest.js application demonstrating the use of Server-Sent Events (SSE) to send real-time notifications to clients. The project includes a basic backend service to handle notifications and a frontend example to display them.
4 |
5 | ## Features
6 |
7 | - **Backend**: A Nest.js server that provides endpoints to post notifications and stream notifications to clients in real-time using SSE.
8 | - **Frontend**: A simple HTML page to demonstrate receiving notifications in real-time and displaying them with a notification badge.
9 |
10 | ## Installation
11 |
12 | 1. Clone the repository:
13 |
14 | ```bash
15 | git clone git@github.com:peterkracik/nestjs-server-sent-events.git
16 | cd nestjs-server-sent-events
17 | ```
18 |
19 | 2. Install the dependencies:
20 |
21 | ```bash
22 | yarn install
23 | ```
24 |
25 | ## Running the Application
26 |
27 | 1. Start the Nest.js server:
28 |
29 | ```bash
30 | yarn start:dev
31 | ```
32 |
33 | The server will run on `http://localhost:3000`.
34 | Provide a query parameter `user` to the frontend to specify the user ID. For example, `http://localhost:3000/?user=1`.
35 |
36 | ## Endpoints
37 |
38 | ### POST /api/notifications
39 |
40 | - **Description**: Receives a new notification and adds it to the store.
41 | - **Request Body**:
42 | ```json
43 | {
44 | "message": "string",
45 | "userId": "string"
46 | }
47 | ```
48 |
49 | ### GET /api/notifications/:id
50 |
51 | - **Description**: Returns all notifications for a given user.
52 | - **Parameters**:
53 | - `id`: User ID
54 |
55 | ### SSE /api/notifications/:id/stream
56 |
57 | - **Description**: Returns a stream of notifications for a given user.
58 | - **Parameters**:
59 | - `id`: User ID
60 |
61 | ## Frontend Example
62 |
63 | The frontend is a simple HTML page using Tailwind CSS for styling. It demonstrates how to:
64 |
65 | - Send notifications via a form.
66 | - Display notifications count using a badge.
67 | - Receive real-time notifications using SSE.
68 |
69 | ### How to Use
70 |
71 | 1. Open the `http://localhost:3000/?user=1` file in a browser.
72 | 2. Use the form to send notifications to a user.
73 | 3. The notification count badge will update in real-time as new notifications are received.
74 |
75 | ## Dependencies
76 |
77 | - `@nestjs/common`: Core Nest.js components.
78 | - `@nestjs/event-emitter`: Used to emit and listen to events within the application.
79 | - `rxjs`: Library for reactive programming using Observables.
80 |
81 | ## License
82 |
83 | This project is licensed under the MIT License.
84 |
85 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Dashboard
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Dashboard
18 |
19 |
20 |
21 |
27 |
28 |
0
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | Hi User ID, welcome to your dashboard!
39 |
40 |
41 |
73 |
74 |
75 |
76 |
Switch User
77 |
84 |
85 |
86 |
87 |
166 |
167 |
168 |
169 |
170 |
--------------------------------------------------------------------------------
/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/nest-cli",
3 | "collection": "@nestjs/schematics",
4 | "sourceRoot": "src",
5 | "compilerOptions": {
6 | "deleteOutDir": true
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nest-sse",
3 | "version": "0.0.1",
4 | "description": "",
5 | "author": "",
6 | "private": true,
7 | "license": "UNLICENSED",
8 | "scripts": {
9 | "build": "nest build",
10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
11 | "start": "nest start",
12 | "start:dev": "nest start --watch",
13 | "start:debug": "nest start --debug --watch",
14 | "start:prod": "node dist/main",
15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
16 | "test": "jest",
17 | "test:watch": "jest --watch",
18 | "test:cov": "jest --coverage",
19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
20 | "test:e2e": "jest --config ./test/jest-e2e.json"
21 | },
22 | "dependencies": {
23 | "@nestjs/common": "^10.0.0",
24 | "@nestjs/core": "^10.0.0",
25 | "@nestjs/event-emitter": "^2.1.1",
26 | "@nestjs/platform-express": "^10.0.0",
27 | "@nestjs/serve-static": "^4.0.2",
28 | "reflect-metadata": "^0.1.13",
29 | "rxjs": "^7.8.1"
30 | },
31 | "devDependencies": {
32 | "@nestjs/cli": "^10.0.0",
33 | "@nestjs/schematics": "^10.0.0",
34 | "@nestjs/testing": "^10.0.0",
35 | "@types/express": "^4.17.17",
36 | "@types/jest": "^29.5.2",
37 | "@types/node": "^20.3.1",
38 | "@types/supertest": "^2.0.12",
39 | "@typescript-eslint/eslint-plugin": "^6.0.0",
40 | "@typescript-eslint/parser": "^6.0.0",
41 | "eslint": "^8.42.0",
42 | "eslint-config-prettier": "^9.0.0",
43 | "eslint-plugin-prettier": "^5.0.0",
44 | "jest": "^29.5.0",
45 | "prettier": "^3.0.0",
46 | "source-map-support": "^0.5.21",
47 | "supertest": "^6.3.3",
48 | "ts-jest": "^29.1.0",
49 | "ts-loader": "^9.4.3",
50 | "ts-node": "^10.9.1",
51 | "tsconfig-paths": "^4.2.0",
52 | "typescript": "^5.1.3"
53 | },
54 | "jest": {
55 | "moduleFileExtensions": [
56 | "js",
57 | "json",
58 | "ts"
59 | ],
60 | "rootDir": "src",
61 | "testRegex": ".*\\.spec\\.ts$",
62 | "transform": {
63 | "^.+\\.(t|j)s$": "ts-jest"
64 | },
65 | "collectCoverageFrom": [
66 | "**/*.(t|j)s"
67 | ],
68 | "coverageDirectory": "../coverage",
69 | "testEnvironment": "node"
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Post, Body, Controller, Param, Sse, Get } from '@nestjs/common';
2 | import { map, fromEvent, Observable, filter } from 'rxjs';
3 | import { EventEmitter2 } from '@nestjs/event-emitter';
4 | import { NotificationsService, Notification } from './notifications.service';
5 | import { EVENT_USER_NOTIFICATION } from './constants';
6 |
7 | @Controller()
8 | export class AppController {
9 | constructor(
10 | private readonly notificationService: NotificationsService,
11 | private readonly eventEmitter: EventEmitter2,
12 | ) {}
13 |
14 | // This method receives a new notification and adds it to the store
15 | @Post('notifications')
16 | createNotification(@Body() notification: Notification) {
17 | console.log('Notification received:', notification);
18 | this.notificationService.add(notification);
19 | }
20 |
21 | // This method returns all notifications for a given user
22 | @Get('notifications/:id')
23 | getNotificationsForUser(@Param('id') id: string): Notification[] {
24 | return this.notificationService.getNotificationsForUser(id);
25 | }
26 |
27 | // This method returns a stream of notifications for a given user
28 | @Sse('notifications/:id/stream')
29 | notifications(@Param('id') id: string): Observable {
30 | // Return an observable that emits notifications for the given user
31 | return fromEvent(this.eventEmitter, EVENT_USER_NOTIFICATION).pipe(
32 | // Filter notifications by user ID
33 | filter((payload: Notification) => payload.userId === id),
34 | // Map the payload to a MessageEvent
35 | map(
36 | (payload) =>
37 | // Create a new MessageEvent with the notification payload
38 | new MessageEvent('message', {
39 | data: JSON.stringify(payload),
40 | } as MessageEventInit),
41 | ),
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { NotificationsService } from './notifications.service';
3 | import { EventEmitterModule } from '@nestjs/event-emitter';
4 | import { ServeStaticModule } from '@nestjs/serve-static';
5 | import { join } from 'path';
6 | import { AppController } from './app.controller';
7 |
8 | @Module({
9 | imports: [
10 | EventEmitterModule.forRoot(),
11 | ServeStaticModule.forRoot({
12 | rootPath: join(__dirname, '..', 'client'),
13 | exclude: ['/api/(.*)'],
14 | }),
15 | ],
16 | controllers: [AppController],
17 | providers: [NotificationsService],
18 | })
19 | export class AppModule {}
20 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const EVENT_USER_NOTIFICATION = 'user.notification';
2 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 |
4 | async function bootstrap() {
5 | const app = await NestFactory.create(AppModule, { cors: true });
6 | app.setGlobalPrefix('api');
7 | await app.listen(3000);
8 | }
9 | bootstrap();
10 |
--------------------------------------------------------------------------------
/src/notifications.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { EVENT_USER_NOTIFICATION } from './constants';
3 | import { EventEmitter2 } from '@nestjs/event-emitter';
4 |
5 | export interface Notification {
6 | id: string;
7 | message: string;
8 | userId: string;
9 | }
10 |
11 | @Injectable()
12 | export class NotificationsService {
13 |
14 | // This is a simple in-memory store for notifications
15 | private readonly notifications: Notification[] = [];
16 |
17 | constructor(private eventEmitter: EventEmitter2) { }
18 |
19 | // This method adds a new notification to the store and emits an event
20 | add(notification: Notification) {
21 | // Add the notification to the in-memory store
22 | this.notifications.push(notification);
23 |
24 | // Emit an event to notify the client
25 | this.eventEmitter.emit(EVENT_USER_NOTIFICATION, notification);
26 | }
27 |
28 | // This method returns all notifications for a given user
29 | getNotificationsForUser(id: string): Notification[] {
30 | // Filter the notifications by the user ID
31 | return this.notifications.filter(
32 | (notification) => notification.userId === id,
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "ES2021",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "incremental": true,
14 | "skipLibCheck": true,
15 | "strictNullChecks": false,
16 | "noImplicitAny": false,
17 | "strictBindCallApply": false,
18 | "forceConsistentCasingInFileNames": false,
19 | "noFallthroughCasesInSwitch": false
20 | }
21 | }
22 |
--------------------------------------------------------------------------------