├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── validate-pr.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .release-it.js ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── jest.config.js ├── nest-cli.json ├── package.json ├── sonar-project.properties ├── src ├── constants.ts ├── correlation-id.middleware.spec.ts ├── correlation-id.middleware.ts ├── correlation.module.ts ├── correlation.service.spec.ts ├── correlation.service.ts ├── index.ts ├── interfaces │ └── correlation-config.interface.ts └── withCorrelation.function.ts ├── test ├── correlation.e2e-spec.ts ├── jest-e2e.json └── test-module │ ├── test.controller.ts │ ├── test.module.ts │ └── test.service.ts ├── tsconfig.build.json ├── tsconfig.e2e.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: [ 19 | '.eslintrc.js', 20 | 'node_modules/', 21 | 'coverage/', 22 | 'dist/', 23 | 'jest.config.js', 24 | ], 25 | rules: { 26 | '@typescript-eslint/interface-name-prefix': 'off', 27 | '@typescript-eslint/explicit-function-return-type': 'off', 28 | '@typescript-eslint/explicit-module-boundary-types': 'off', 29 | '@typescript-eslint/no-explicit-any': 'off', 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | cache: 'yarn' 19 | cache-dependency-path: yarn.lock 20 | - run: yarn install --frozen-lockfile --silent 21 | - uses: JS-DevTools/npm-publish@v1 22 | with: 23 | token: ${{ secrets.NPM_TOKEN }} 24 | -------------------------------------------------------------------------------- /.github/workflows/validate-pr.yml: -------------------------------------------------------------------------------- 1 | name: Validate PR 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | types: [opened, synchronize, reopened] 11 | 12 | permissions: 13 | pull-requests: read 14 | 15 | jobs: 16 | Lint: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: 16 23 | cache: 'yarn' 24 | cache-dependency-path: yarn.lock 25 | - run: yarn install --frozen-lockfile --silent 26 | - run: yarn lint 27 | Build: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v3 31 | - uses: actions/setup-node@v3 32 | with: 33 | node-version: 16 34 | cache: 'yarn' 35 | cache-dependency-path: yarn.lock 36 | - run: yarn install --frozen-lockfile --silent 37 | - run: yarn build 38 | Test: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v3 42 | - uses: actions/setup-node@v3 43 | with: 44 | node-version: 16 45 | cache: 'yarn' 46 | cache-dependency-path: yarn.lock 47 | - run: yarn install --frozen-lockfile --silent 48 | - run: yarn test:cov --ci 49 | - run: yarn test:e2e --ci 50 | - name: SonarCloud Scan 51 | uses: SonarSource/sonarcloud-github-action@master 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | SONAR_TOKEN: ${{ secrets. SONAR_TOKEN }} 55 | with: 56 | args: -Dsonar.projectKey=Evanion_nestjs-correlation-id 57 | -Dsonar.organization=evanion 58 | -Dsonar.projectVersion=${{github.run_number}} 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .vscode 3 | src 4 | test 5 | .eslintrc.js 6 | .gitignore 7 | .npmignore 8 | .prettierrc 9 | .release-it.json 10 | jest.config.js 11 | nest-cli.json 12 | sonar-project.properties 13 | tsconfig.json 14 | tsconfig.build.json 15 | tsconfig.e2e.json 16 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /.release-it.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | git: { 3 | commitMessage: 'v${version}', 4 | tagName: 'v${version}', 5 | }, 6 | github: { 7 | release: true, 8 | releaseName: 'v${version}', 9 | tokenRef: 'GITHUB_AUTH', 10 | }, 11 | npm: { 12 | publish: false, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true, 8 | "**/Thumbs.db": true, 9 | "**/node_modules": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | RELEASE 1.0.0 2 | 3 | - Initial release 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. [Fork it](https://help.github.com/articles/fork-a-repo/) 4 | 2. Install dependencies (`npm install`) 5 | 3. Create your feature branch (`git checkout -b my-new-feature`) 6 | 4. Commit your changes (`git commit -am 'Added some feature'`) 7 | 5. Test your changes (`npm test`) 8 | 6. Push to the branch (`git push origin my-new-feature`) 9 | 7. [Create new Pull Request](https://help.github.com/articles/creating-a-pull-request/) 10 | 11 | ## Testing 12 | 13 | We use [Jest](https://github.com/facebook/jest) to write tests. Run our test suite with this command: 14 | 15 | ``` 16 | npm test 17 | ``` 18 | 19 | ## Code Style 20 | 21 | We use [Prettier](https://prettier.io/) and tslint to maintain code style and best practices. 22 | Please make sure your PR adheres to the guides by running: 23 | 24 | ``` 25 | npm run format 26 | ``` 27 | 28 | and 29 | ``` 30 | npm run lint 31 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mikael Pettersson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Nest.js Correlation ID middleware

2 | 3 |

Transparently include correlation IDs in all requests

4 | 5 |
6 | 7 | Built with NestJS 8 | 9 |
10 | 11 | ### Why? 12 | 13 | When debugging an issue in your applications logs, it helps to be able to follow a specific request up and down your whole stack. This is usually done by including a `correlation-id` (aka `Request-id`) header in all your requests, and forwarding the same id across all your microservices. 14 | 15 | ### Installation 16 | 17 | ```bash 18 | yarn add @evanion/nestjs-correlation-id 19 | ``` 20 | 21 | ```bash 22 | npm install @evanion/nestjs-correlation-id 23 | ``` 24 | 25 | ### How to use 26 | 27 | Add the middleware to your `AppModule` 28 | 29 | ```ts 30 | import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; 31 | import { 32 | CorrelationIdMiddleware, 33 | CorrelationModule, 34 | } from '@evanion/nestjs-correlation-id'; 35 | 36 | @Module({ 37 | imports: [CorrelationModule.forRoot()], 38 | }) 39 | export class AppModule implements NestModule { 40 | configure(consumer: MiddlewareConsumer) { 41 | consumer.apply(CorrelationIdMiddleware).forRoutes('*'); 42 | } 43 | } 44 | ``` 45 | 46 | And then just inject the correlation middleware in your HttpService by calling the `registerAsync` method with the `withCorrelation` function. 47 | 48 | ```ts 49 | import { HttpModule } from '@nestjs/axios'; 50 | import { withCorrelation } from '@evanion/nestjs-correlation-id'; 51 | 52 | @Module({ 53 | imports: [HttpModule.registerAsync(withCorrelation())], 54 | controllers: [UsersController], 55 | providers: [UsersService], 56 | }) 57 | export class UsersModule {} 58 | ``` 59 | 60 | You can now use the `HttpService` as usual in your `UsersService` and `UsersController` 61 | 62 | ### Customize 63 | 64 | You can easily customize the header and ID by including a config when you register the module 65 | 66 | ```ts 67 | @Module({ 68 | imports: [CorrelationModule.forRoot({ 69 | header: string 70 | generator: () => string 71 | })] 72 | }) 73 | export class AppModule implements NestModule { 74 | configure(consumer: MiddlewareConsumer) { 75 | consumer.apply(CorrelationIdMiddleware).forRoutes('*'); 76 | } 77 | } 78 | ``` 79 | 80 | #### Add `correlationId` to logs 81 | 82 | In order to add the correlation ID to your logs, you can use the `CorrelationService` service to get the current correlationId. 83 | 84 | In the following example, we are using the [@ntegral/nestjs-sentry](https://github.com/ntegral/nestjs-sentry) package, but you can use any package or provider you like. 85 | 86 | ```ts 87 | import { CorrelationService } from '@evanion/nestjs-correlation-id'; 88 | import { Injectable, NestMiddleware } from '@nestjs/common'; 89 | import { InjectSentry, SentryService } from '@ntegral/nestjs-sentry'; 90 | import { NextFunction, Request, Response } from 'express'; 91 | 92 | @Injectable() 93 | export class SentryMiddleware implements NestMiddleware { 94 | constructor( 95 | private readonly correlationService: CorrelationService, 96 | @InjectSentry() private readonly sentryService: SentryService, 97 | ) {} 98 | 99 | async use(_req: Request, _res: Response, next: NextFunction) { 100 | const correlationId = await this.correlationService.getCorrelationId(); 101 | this.sentryService.instance().configureScope((scope) => { 102 | scope.setTag('correlationId', correlationId); 103 | }); 104 | next(); 105 | } 106 | } 107 | ``` 108 | 109 | Then add it to your `AppModule` 110 | 111 | ```ts 112 | import { Module } from '@nestjs-common'; 113 | import { SentryModule } from '@ntegral/nestjs-sentry'; 114 | import { CorrelationModule } from '@evanion/nestjs-correlation-id'; 115 | import { SentryMiddleware } from './middleware/sentry.middleware'; 116 | 117 | @Module({ 118 | imports: [ 119 | CorrelationModule.forRoot(), 120 | SentryModule.forRoot({ 121 | // ... your config 122 | }), 123 | ], 124 | }) 125 | export class AppModule implements NestModule { 126 | configure(consumer: MiddlewareConsumer) { 127 | consumer.apply(CorrelationIdMiddleware).forRoutes('*'); 128 | consumer.apply(SentryMiddleware).forRoutes('*'); 129 | } 130 | } 131 | ``` 132 | 133 | If you need to manually set the correlationId anywhere in your application. You can use the `CorrelationService` service to set the correlationId. 134 | 135 | ```ts 136 | this.correlationService.setCorrelationId('some_correlation_id'); 137 | ``` 138 | 139 | see [e2e tests](/test) for a fully working example 140 | 141 | ## Change Log 142 | 143 | See [Changelog](CHANGELOG.md) for more information. 144 | 145 | ## Contributing 146 | 147 | Contributions welcome! See [Contributing](CONTRIBUTING.md). 148 | 149 | ## Author 150 | 151 | **Mikael Pettersson (Evanion on [Discord](https://discord.gg/G7Qnnhy))** 152 | 153 | ## License 154 | 155 | Licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 156 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'json', 'ts'], 3 | rootDir: 'src', 4 | testRegex: '.spec.ts$', 5 | transform: { 6 | '^.+\\.(t|j)s$': 'ts-jest', 7 | }, 8 | coverageDirectory: '../coverage', 9 | coveragePathIgnorePatterns: [ 10 | '.eslintrc.js', 11 | 'jest.config', 12 | 'node_modules', 13 | '/coverage', 14 | '/dist', 15 | 'test', 16 | '.interface.ts', 17 | '.module.ts', 18 | '.spec.ts', 19 | 'index.ts', 20 | ], 21 | testEnvironment: 'node', 22 | }; 23 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "ts", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@evanion/nestjs-correlation-id", 3 | "version": "1.0.4", 4 | "description": "Transparently forward or add correlation id to all requests", 5 | "author": "Mikael Pettersson ", 6 | "license": "MIT", 7 | "readmeFilename": "README.md", 8 | "main": "dist/index.js", 9 | "files": [ 10 | "dist/**/*", 11 | "*.md" 12 | ], 13 | "scripts": { 14 | "start:dev": "tsc -w", 15 | "build": "tsc", 16 | "prepare": "npm run build", 17 | "format": "prettier --write \"src/**/*.ts\"", 18 | "lint": "eslint \"src/**/*.ts\"", 19 | "lint:fix": "eslint --fix \"src/**/*.ts\"", 20 | "test": "jest", 21 | "test:watch": "jest --watch", 22 | "test:cov": "jest --coverage", 23 | "test:e2e": "jest --config ./test/jest-e2e.json", 24 | "release": "release-it" 25 | }, 26 | "keywords": [ 27 | "nestjs", 28 | "nestjs-middleware", 29 | "middleware", 30 | "correlation", 31 | "correlation-id", 32 | "request", 33 | "request-id" 34 | ], 35 | "publishConfig": { 36 | "access": "public" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "https://github.com/evanion/nestjs-correlation-id" 41 | }, 42 | "bugs": "https://github.com/evanion/nestjs-correlation-id/issues", 43 | "peerDependencies": { 44 | "@nestjs/axios": "^0.1.0 || ^1.0.0 || ^2.0.0 || ^3.00", 45 | "@nestjs/common": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" 46 | }, 47 | "dependencies": { 48 | "uuid": "^9.0.0" 49 | }, 50 | "devDependencies": { 51 | "@nestjs/axios": "^3.0.0", 52 | "@nestjs/common": "^10.0.5", 53 | "@nestjs/core": "^10.0.5", 54 | "@nestjs/platform-express": "^10.0.5", 55 | "@nestjs/testing": "10.0.5", 56 | "@types/express": "4.17.17", 57 | "@types/jest": "29.5.3", 58 | "@types/supertest": "2.0.12", 59 | "@typescript-eslint/eslint-plugin": "^6.0.0", 60 | "@typescript-eslint/parser": "^6.0.0", 61 | "axios": "^1.4.0", 62 | "eslint": "^8.44.0", 63 | "eslint-config-prettier": "^8.8.0", 64 | "eslint-plugin-prettier": "^5.0.0", 65 | "jest": "29.6.1", 66 | "prettier": "^3.0.0", 67 | "reflect-metadata": "^0.1.13", 68 | "release-it": "^16.1.0", 69 | "rxjs": "^7.8.1", 70 | "supertest": "6.3.3", 71 | "ts-jest": "29.1.1", 72 | "ts-node": "10.9.1", 73 | "tsc-watch": "6.0.4", 74 | "tsconfig-paths": "4.2.0", 75 | "typescript": "5.1.6", 76 | "uuid": "^9.0.0" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=Evanion_nestjs-correlation-id 2 | sonar.javascript.lcov.reportPaths=./coverage/lcov.info -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const CORRELATION_ID_HEADER = 'X-Correlation-Id'; 2 | export const CORRELATION_CONFIG_TOKEN = 'CORRELATION_CONFIG'; 3 | -------------------------------------------------------------------------------- /src/correlation-id.middleware.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | /* eslint-disable @typescript-eslint/no-empty-function */ 3 | import { CorrelationIdMiddleware } from './correlation-id.middleware'; 4 | import { CorrelationService } from './correlation.service'; 5 | import { CorrelationConfig } from './interfaces/correlation-config.interface'; 6 | 7 | const mockCorrelationConfig: CorrelationConfig = { 8 | header: 'x-correlation-id', 9 | generator: () => '12345', 10 | }; 11 | 12 | // @ts-expect-error: Test mock 13 | const mockCorrelationService: CorrelationService = { 14 | getCorrelationId: jest.fn().mockImplementation(() => 'test123'), 15 | setCorrelationId: jest.fn(), 16 | }; 17 | 18 | describe('CorrelationIdMiddleware', () => { 19 | let middleware: CorrelationIdMiddleware; 20 | beforeEach(() => { 21 | middleware = new CorrelationIdMiddleware( 22 | mockCorrelationService, 23 | mockCorrelationConfig, 24 | ); 25 | }); 26 | 27 | it('should be defined', () => { 28 | expect(middleware).toBeDefined(); 29 | }); 30 | 31 | it('should set the correlation id in request object', () => { 32 | const req = { 33 | get: jest.fn(), 34 | headers: {}, 35 | }; 36 | const res = { 37 | get: jest.fn(), 38 | set: jest.fn(), 39 | headers: {}, 40 | }; 41 | jest.spyOn(res, 'set'); 42 | middleware.use(req as any, res as any, jest.fn()); 43 | 44 | expect(req.headers['x-correlation-id']).toBe('test123'); 45 | }); 46 | 47 | it('should set the correlation id in response object', () => { 48 | const req = { 49 | get: jest.fn().mockImplementation(() => 'test123'), 50 | headers: {}, 51 | }; 52 | const res = { 53 | get: jest.fn(), 54 | set: jest.fn(), 55 | headers: {}, 56 | }; 57 | jest.spyOn(res, 'set'); 58 | middleware.use(req as any, res as any, () => {}); 59 | 60 | expect(res.set).toHaveBeenCalledWith('x-correlation-id', 'test123'); 61 | }); 62 | 63 | it('should set the correlation id in correlationService', () => { 64 | const req = { 65 | get: jest.fn().mockImplementation(() => 'test123'), 66 | headers: {}, 67 | }; 68 | const res = { 69 | get: jest.fn(), 70 | set: jest.fn(), 71 | headers: {}, 72 | }; 73 | jest.spyOn(res, 'set'); 74 | middleware.use(req as any, res as any, jest.fn()); 75 | 76 | expect(mockCorrelationService.setCorrelationId).toHaveBeenCalledWith( 77 | 'test123', 78 | ); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /src/correlation-id.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, NestMiddleware } from '@nestjs/common'; 2 | import { Request, Response } from 'express'; 3 | import { CORRELATION_CONFIG_TOKEN } from './constants'; 4 | import { CorrelationService } from './correlation.service'; 5 | import { CorrelationConfig } from './interfaces/correlation-config.interface'; 6 | 7 | @Injectable() 8 | export class CorrelationIdMiddleware implements NestMiddleware { 9 | constructor( 10 | private correlationService: CorrelationService, 11 | @Inject(CORRELATION_CONFIG_TOKEN) 12 | private correlationConfig: CorrelationConfig, 13 | ) {} 14 | use(req: Request, res: Response, next: () => void) { 15 | const { header } = this.correlationConfig; 16 | const correlationId = 17 | req.get(header) || this.correlationService.getCorrelationId(); 18 | 19 | if (!req.headers[header]) req.headers[header] = correlationId; 20 | if (!res.get(header)) res.set(header, correlationId); 21 | 22 | this.correlationService.setCorrelationId(correlationId); 23 | next(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/correlation.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module, Provider } from '@nestjs/common'; 2 | import { v4 as UUIDv4 } from 'uuid'; 3 | import { CORRELATION_CONFIG_TOKEN, CORRELATION_ID_HEADER } from './constants'; 4 | import { CorrelationService } from './correlation.service'; 5 | import { CorrelationConfig } from './interfaces/correlation-config.interface'; 6 | 7 | @Module({}) 8 | export class CorrelationModule { 9 | static forRoot(config?: Partial): DynamicModule { 10 | const correlationConfigProvider: Provider = { 11 | provide: CORRELATION_CONFIG_TOKEN, 12 | useValue: { 13 | ...config, 14 | header: config?.header || CORRELATION_ID_HEADER, 15 | generator: config?.generator || UUIDv4, 16 | }, 17 | }; 18 | return { 19 | global: true, 20 | module: CorrelationModule, 21 | providers: [correlationConfigProvider, CorrelationService], 22 | exports: [correlationConfigProvider, CorrelationService], 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/correlation.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { CORRELATION_CONFIG_TOKEN, CORRELATION_ID_HEADER } from './constants'; 3 | import { CorrelationService } from './correlation.service'; 4 | 5 | describe('CorrelationService', () => { 6 | let service: CorrelationService; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | providers: [ 11 | CorrelationService, 12 | { 13 | provide: CORRELATION_CONFIG_TOKEN, 14 | useValue: { 15 | header: CORRELATION_ID_HEADER, 16 | generator: () => 'test-id', 17 | }, 18 | }, 19 | ], 20 | }).compile(); 21 | 22 | service = await module.resolve(CorrelationService); 23 | }); 24 | 25 | it('should be defined', () => { 26 | expect(service).toBeDefined(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/correlation.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable, Scope } from '@nestjs/common'; 2 | import { v4 as UUIDv4 } from 'uuid'; 3 | import { CORRELATION_CONFIG_TOKEN } from './constants'; 4 | import { CorrelationConfig } from './interfaces/correlation-config.interface'; 5 | 6 | @Injectable({ scope: Scope.REQUEST }) 7 | export class CorrelationService { 8 | private correlationId: string; 9 | 10 | constructor( 11 | @Inject(CORRELATION_CONFIG_TOKEN) correlationConfig: CorrelationConfig, 12 | ) { 13 | this.correlationId = correlationConfig.generator 14 | ? correlationConfig.generator() 15 | : UUIDv4(); 16 | } 17 | 18 | getCorrelationId(): string { 19 | return this.correlationId; 20 | } 21 | setCorrelationId(correlationId: string): void { 22 | this.correlationId = correlationId; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './correlation-id.middleware'; 2 | export * from './correlation.service'; 3 | export * from './correlation.module'; 4 | export * from './withCorrelation.function'; 5 | export * from './constants'; 6 | -------------------------------------------------------------------------------- /src/interfaces/correlation-config.interface.ts: -------------------------------------------------------------------------------- 1 | export interface CorrelationConfig { 2 | header: string; 3 | generator: () => string; 4 | } 5 | -------------------------------------------------------------------------------- /src/withCorrelation.function.ts: -------------------------------------------------------------------------------- 1 | import { HttpModuleOptions } from '@nestjs/axios'; 2 | import { CORRELATION_ID_HEADER } from './constants'; 3 | import { CorrelationModule } from './correlation.module'; 4 | import { CorrelationService } from './correlation.service'; 5 | 6 | export const withCorrelation = (config?: HttpModuleOptions) => ({ 7 | imports: [CorrelationModule], 8 | useFactory: async (correlationService: CorrelationService) => ({ 9 | ...config, 10 | headers: { 11 | ...(config?.headers && config.headers), 12 | [CORRELATION_ID_HEADER]: correlationService.getCorrelationId(), 13 | }, 14 | }), 15 | inject: [CorrelationService], 16 | }); 17 | -------------------------------------------------------------------------------- /test/correlation.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { 3 | HttpStatus, 4 | INestApplication, 5 | MiddlewareConsumer, 6 | Module, 7 | NestModule, 8 | } from '@nestjs/common'; 9 | import * as request from 'supertest'; 10 | import { CorrelationIdMiddleware, CorrelationModule } from '../src'; 11 | import { TestModule } from './test-module/test.module'; 12 | import { HttpService } from '@nestjs/axios'; 13 | import { of } from 'rxjs'; 14 | import { AxiosRequestHeaders } from 'axios'; 15 | 16 | @Module({ 17 | imports: [CorrelationModule.forRoot(), TestModule], 18 | }) 19 | class AppModule implements NestModule { 20 | configure(consumer: MiddlewareConsumer) { 21 | consumer.apply(CorrelationIdMiddleware).forRoutes('*'); 22 | } 23 | } 24 | 25 | describe('CorrelationMiddleware (e2e)', () => { 26 | let app: INestApplication; 27 | let httpService: HttpService; 28 | 29 | beforeEach(async () => { 30 | const moduleFixture: TestingModule = await Test.createTestingModule({ 31 | imports: [AppModule], 32 | }).compile(); 33 | 34 | httpService = moduleFixture.get(HttpService); 35 | 36 | app = moduleFixture.createNestApplication(); 37 | 38 | await app.init(); 39 | 40 | jest.spyOn(httpService, 'get').mockImplementation(() => 41 | of({ 42 | config: { url: 'http://example.com/test', method: 'GET', headers: {} as AxiosRequestHeaders }, 43 | headers: { 44 | connection: 'keep-alive', 45 | 'content-type': 'application/json', 46 | }, 47 | status: HttpStatus.OK, 48 | statusText: 'OK', 49 | data: { foo: 'bar' }, 50 | }), 51 | ); 52 | }); 53 | 54 | it('/ (GET)', () => { 55 | return request(app.getHttpServer()).get('/').expect(200).expect('pong'); 56 | }); 57 | 58 | it('/ (GET) with correlation id', async () => { 59 | const result = await request(app.getHttpServer()) 60 | .get('/') 61 | .set('X-Correlation-Id', 'test-id-1'); 62 | 63 | expect(result.headers['x-correlation-id']).toBe('test-id-1'); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": "../", 4 | "modulePaths": [""], 5 | "testEnvironment": "node", 6 | "testRegex": ".e2e-spec.ts$", 7 | "transform": { 8 | "^.+\\.(t|j)s$": "ts-jest" 9 | }, 10 | "globals": { 11 | "ts-jest": { 12 | "tsConfig": "tsconfig.e2e.json" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/test-module/test.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { TestService } from './test.service'; 3 | 4 | @Controller() 5 | export class TestController { 6 | constructor(private readonly testService: TestService) {} 7 | 8 | @Get() 9 | ping() { 10 | return 'pong'; 11 | } 12 | 13 | @Get('/axios') 14 | test() { 15 | return this.testService.getTest(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/test-module/test.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule } from '@nestjs/axios'; 2 | import { Module } from '@nestjs/common'; 3 | import { withCorrelation } from 'src'; 4 | import { TestController } from './test.controller'; 5 | import { TestService } from './test.service'; 6 | 7 | @Module({ 8 | controllers: [TestController], 9 | providers: [TestService], 10 | imports: [HttpModule.registerAsync(withCorrelation())], 11 | }) 12 | export class TestModule {} 13 | -------------------------------------------------------------------------------- /test/test-module/test.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpService } from '@nestjs/axios'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { lastValueFrom, map } from 'rxjs'; 4 | 5 | @Injectable() 6 | export class TestService { 7 | constructor(private httpService: HttpService) {} 8 | 9 | getTest() { 10 | return lastValueFrom( 11 | this.httpService 12 | .get('http://example.com/test') 13 | .pipe(map((res) => res.data)), 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { "rootDir": "./src" }, 3 | "extends": "./tsconfig.json", 4 | "exclude": ["node_modules", "test/**/*.ts", "dist", "**/*spec.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { "rootDir": "./" }, 3 | "extends": "./tsconfig.json", 4 | "include": ["src/**/*.ts", "test/**/*.ts"], 5 | "exclude": ["node_modules", "dist", "**/*spec.ts"] 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es6", 9 | "sourceMap": false, 10 | "outDir": "./dist", 11 | "rootDir": "./src", 12 | "baseUrl": "./", 13 | "noLib": false 14 | }, 15 | "include": ["src/**/*.ts"], 16 | "exclude": ["node_modules"] 17 | } 18 | --------------------------------------------------------------------------------