├── .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 | 23 | 25 | 26 | 27 | 28 | 30 |
31 |
32 |
33 | 34 | 35 |
36 |
37 |
38 | Hi User ID, welcome to your dashboard! 39 |
40 |
41 |
42 |
43 | 44 |
45 | 46 | 54 |
55 | 56 | 57 |
58 | 59 | 62 |
63 | 64 | 65 |
66 | 70 |
71 |
72 |
73 | 74 | 75 |
76 |

Switch User

77 |
78 | User 1 79 | User 2 80 | User 3 81 | User 4 82 | User 5 83 |
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 | --------------------------------------------------------------------------------