├── .eslintrc ├── .github └── workflows │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── .npmignore ├── .releaserc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── index.ts ├── lib ├── axios-interceptor.spec.ts ├── axios-interceptor.ts ├── identity-functions.spec.ts ├── identity-functions.ts ├── index.ts └── interfaces │ ├── axios-error-custom-config.ts │ ├── axios-fulfilled-interceptor.ts │ ├── axios-rejected-interceptor.ts │ └── axios-response-custom-config.ts ├── nest-cli.json ├── package-lock.json ├── package.json ├── renovate.json ├── tsconfig.build.json └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint"], 4 | "extends": [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/eslint-recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "prettier", 9 | "plugin:prettier/recommended" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/no-explicit-any": 0 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: 12 | - 18.x # LTS 13 | - 20.x # Current 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | cache: "npm" 23 | 24 | - run: npm ci 25 | - run: npm run lint 26 | - run: npm run test:cov 27 | env: 28 | CI: true 29 | 30 | - name: Archive code coverage results 31 | uses: actions/upload-artifact@v3 32 | with: 33 | name: code-coverage-report 34 | path: coverage 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | version: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 20.x 19 | cache: "npm" 20 | 21 | - run: npm ci 22 | 23 | - run: npm install -g semantic-release @semantic-release/git @semantic-release/changelog @semantic-release/release-notes-generator @semantic-release/npm 24 | 25 | # needed for npm publishing 26 | - run: npm run build 27 | 28 | - run: semantic-release 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | NPM_TOKEN: ${{ secrets.npm_token }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # IDE 5 | /.idea 6 | /.awcache 7 | /.vscode 8 | 9 | # misc 10 | npm-debug.log 11 | .DS_Store 12 | 13 | # tests 14 | /test 15 | /coverage 16 | /.nyc_output 17 | 18 | # dist 19 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # source 2 | lib 3 | tests 4 | index.ts 5 | package-lock.json 6 | tslint.json 7 | tsconfig.json 8 | tsconfig.build.json 9 | nest-cli.json 10 | .eslintrc 11 | 12 | # build+coverage 13 | coverage 14 | dist/tsconfig.build.tsbuildinfo 15 | 16 | # github 17 | .github 18 | renovate.json 19 | .releaserc.json 20 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@semantic-release/release-notes-generator", 4 | "@semantic-release/changelog", 5 | "@semantic-release/npm", 6 | "@semantic-release/git" 7 | ], 8 | "repositoryUrl": "https://github.com/narando/nest-axios-interceptor", 9 | "branches": [ 10 | "main", 11 | {"name": "beta", "prerelease": true}, 12 | {"name": "alpha", "prerelease": true} 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [3.0.0](https://github.com/narando/nest-axios-interceptor/compare/v2.2.0...v3.0.0) (2023-09-16) 2 | 3 | 4 | ### Features 5 | 6 | * @nestjs/axios 2 & 3 compatibility ([#422](https://github.com/narando/nest-axios-interceptor/issues/422)) ([23f72fb](https://github.com/narando/nest-axios-interceptor/commit/23f72fbc9ad092109fb2bf5cae0dad499c97dbc6)) 7 | 8 | 9 | ### BREAKING CHANGES 10 | 11 | * Require @nestjs/axios ^2.0.0 || ^3.0.0 12 | 13 | # [2.2.0](https://github.com/narando/nest-axios-interceptor/compare/v2.1.0...v2.2.0) (2022-07-23) 14 | 15 | 16 | ### Features 17 | 18 | * **deps:** support NestJS 9 ([5a0e1ed](https://github.com/narando/nest-axios-interceptor/commit/5a0e1edaf6eec21372ca233559be5e5fb838f26f)) 19 | 20 | # [2.1.0](https://github.com/narando/nest-axios-interceptor/compare/v2.0.0...v2.1.0) (2022-06-27) 21 | 22 | 23 | ### Features 24 | 25 | * support @nestjs/axios up to 0.0.8 ([85cba93](https://github.com/narando/nest-axios-interceptor/commit/85cba93fffa7a9f66ebaa0c2626acdff9f7f4c1d)) 26 | 27 | # [2.0.0](https://github.com/narando/nest-axios-interceptor/compare/v1.2.2...v2.0.0) (2022-01-22) 28 | 29 | 30 | ### Features 31 | 32 | * **deps:** support NestJS 8 and @nestjs/axios ([24446b7](https://github.com/narando/nest-axios-interceptor/commit/24446b7d8e812815f8049af6293e270908fb1ea0)) 33 | 34 | 35 | ### BREAKING CHANGES 36 | 37 | * **deps:** Drop support for NestJS <8 and the HttpService from 38 | @nestjs/common. Instead add support for NestJS 8 and the HttpService from 39 | @nestjs/axios. 40 | 41 | ## [1.2.2](https://github.com/narando/nest-axios-interceptor/compare/v1.2.1...v1.2.2) (2022-01-13) 42 | 43 | 44 | ### Bug Fixes 45 | 46 | * **eslint:** update extended prettier rules ([d6d8882](https://github.com/narando/nest-axios-interceptor/commit/d6d88829ef51dc59ff1cedd3abc252a891a7a829)) 47 | 48 | ## [1.2.1](https://github.com/narando/nest-axios-interceptor/compare/v1.2.0...v1.2.1) (2022-01-13) 49 | 50 | 51 | ### Bug Fixes 52 | 53 | * **lib:** linting after prettier update ([65a5904](https://github.com/narando/nest-axios-interceptor/commit/65a59043979bd2a61c44d35653f8d22d0a74e2f3)) 54 | 55 | # [1.2.0](https://github.com/narando/nest-axios-interceptor/compare/v1.1.0...v1.2.0) (2022-01-12) 56 | 57 | 58 | ### Features 59 | 60 | * **deps:** bump to node 16 ([24a26ca](https://github.com/narando/nest-axios-interceptor/commit/24a26ca556bab847f502639c24998a80e84a9e98)) 61 | 62 | # [1.1.0](https://github.com/narando/nest-axios-interceptor/compare/v1.0.0...v1.1.0) (2020-05-12) 63 | 64 | 65 | ### Features 66 | 67 | * export AxiosResponseCustomConfig for accurate typings ([2f04bb3](https://github.com/narando/nest-axios-interceptor/commit/2f04bb3adf443b20c57e770038714a4a5a4e106b)) 68 | 69 | # 1.0.0 (2020-05-11) 70 | 71 | 72 | ### Features 73 | 74 | * add initial library implementation ([55fe54d](https://github.com/narando/nest-axios-interceptor/commit/55fe54dc8e88b446feed5518544a1d925e89ce77)) 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 narando GmbH 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @narando/nest-axios-interceptor 2 | 3 |

4 | Easily build and configure axios interceptors for the NestJS HttpModule/HttpService. 5 |

6 | 7 |

8 | NPM Version 9 | Package License 10 | NPM Downloads 11 | CI Status 12 |

13 | 14 | ## Features 15 | 16 | - Define axios interceptors 17 | - Register interceptor on `HttpService.axiosRef` 18 | - Type-safe handling of custom options in request config 19 | 20 | ## Usage 21 | 22 | ### Installation 23 | 24 | Install this module: 25 | 26 | ```shell 27 | $ npm i @narando/nest-axios-interceptor 28 | ``` 29 | 30 | ### Creating an `AxiosInterceptor` 31 | 32 | Create a new module and import the `HttpModule`: 33 | 34 | ```typescript 35 | // cats.module.ts 36 | import { HttpModule, HttpService } from "@nestjs/axios"; 37 | 38 | @Module({ 39 | imports: [HttpModule], 40 | providers: [CatsService], 41 | }) 42 | export class CatsModule {} 43 | ``` 44 | 45 | Bootstrap your new interceptor with this boilerplate: 46 | 47 | ```typescript 48 | // logging.axios-interceptor.ts 49 | import { Injectable } from "@nestjs/common"; 50 | import { HttpService } from "@nestjs/axios"; 51 | import type { AxiosResponse, InternalAxiosRequestConfig } from "axios"; 52 | import { 53 | AxiosInterceptor, 54 | AxiosFulfilledInterceptor, 55 | AxiosRejectedInterceptor, 56 | } from "@narando/nest-axios-interceptor"; 57 | 58 | @Injectable() 59 | export class LoggingAxiosInterceptor extends AxiosInterceptor { 60 | constructor(httpService: HttpService) { 61 | super(httpService); 62 | } 63 | 64 | // requestFulfilled(): AxiosFulfilledInterceptor {} 65 | // requestRejected(): AxiosRejectedInterceptor {} 66 | // responseFulfilled(): AxiosFulfilledInterceptor {} 67 | // responseRejected(): AxiosRejectedInterceptor {} 68 | } 69 | ``` 70 | 71 | By default, the interceptor uses identity functions (no-op) for all 4 possible events. 72 | 73 | To add your behaviour, override the class methods for the events you want to handle and return a function that will be used in the interceptor. 74 | 75 | ```typescript 76 | // logging.axios-interceptor.ts 77 | @Injectable() 78 | export class LoggingAxiosInterceptor extends AxiosInterceptor { 79 | constructor(httpService: HttpService) { 80 | super(httpService); 81 | } 82 | 83 | requestFulfilled(): AxiosFulfilledInterceptor { 84 | return (config) => { 85 | // Log outgoing request 86 | console.log(`Request: ${config.method} ${config.path}`); 87 | 88 | return config; 89 | }; 90 | } 91 | 92 | // requestRejected(): AxiosRejectedInterceptor {} 93 | // responseFulfilled(): AxiosFulfilledInterceptor {} 94 | // responseRejected(): AxiosRejectedInterceptor {} 95 | } 96 | ``` 97 | 98 | ### Setting custom options to the request config 99 | 100 | If you want to pass-through data from on interceptor function to another, add it to the request config object. 101 | 102 | First, define your new request config type. To avoid conflicts with other interceptors, we will define a Symbol and use it as the object key: 103 | 104 | ```typescript 105 | // logging.axios-interceptor.ts 106 | const LOGGING_CONFIG_KEY = Symbol("kLoggingAxiosInterceptor"); 107 | 108 | // Merging our custom properties with the base config 109 | interface LoggingConfig extends InternalAxiosRequestConfig { 110 | [LOGGING_CONFIG_KEY]: { 111 | id: number; 112 | }; 113 | } 114 | ``` 115 | 116 | Now we have to update the interceptor to use this new config: 117 | 118 | ```diff 119 | // logging.axios-interceptor.ts 120 | @Injectable() 121 | - export class LoggingAxiosInterceptor extends AxiosInterceptor { 122 | + export class LoggingAxiosInterceptor extends AxiosInterceptor { 123 | constructor(httpService: HttpService) { 124 | super(httpService); 125 | } 126 | 127 | - requestFulfilled(): AxiosFulfilledInterceptor { 128 | + requestFulfilled(): AxiosFulfilledInterceptor { 129 | return (config) => { 130 | // Log outgoing request 131 | console.log(`Request: ${config.method} ${config.path}`); 132 | 133 | return config; 134 | }; 135 | } 136 | 137 | // requestRejected(): AxiosRejectedInterceptor {} 138 | - // responseFulfilled(): AxiosFulfilledInterceptor {} 139 | + // responseFulfilled(): AxiosFulfilledInterceptor> {} 140 | // responseRejected(): AxiosRejectedInterceptor {} 141 | } 142 | ``` 143 | 144 | With the updated typing, you can now use the extend configuration: 145 | 146 | ```typescript 147 | // logging.axios-interceptor.ts 148 | const LOGGING_CONFIG_KEY = Symbol("kLoggingAxiosInterceptor"); 149 | 150 | @Injectable() 151 | export class LoggingAxiosInterceptor extends AxiosInterceptor { 152 | constructor(httpService: HttpService) { 153 | super(httpService); 154 | } 155 | 156 | requestFulfilled(): AxiosFulfilledInterceptor { 157 | return (config) => { 158 | const requestId = 1234; 159 | 160 | config[LOGGING_CONFIG_KEY] = { 161 | id: requestId, 162 | }; 163 | // Log outgoing request 164 | console.log(`Request(ID=${requestId}): ${config.method} ${config.path}`); 165 | 166 | return config; 167 | }; 168 | } 169 | 170 | // requestRejected(): AxiosRejectedInterceptor {} 171 | 172 | responseFulfilled(): AxiosFulfilledInterceptor< 173 | AxiosResponseCustomConfig 174 | > { 175 | return (response) => { 176 | const requestId = response.config[LOGGING_CONFIG_KEY].id; 177 | // Log response 178 | console.log(`Response(ID=${requestId}): ${response.status}`); 179 | 180 | return response; 181 | }; 182 | } 183 | 184 | // responseRejected(): AxiosRejectedInterceptor {} 185 | } 186 | ``` 187 | 188 | ### Handling Errors 189 | 190 | By default, the axios error (rejected) interceptors pass the error with type `any`. This is not really helpful as we can't do anything with it. 191 | 192 | Internally, axios wraps all errors in a custom object `AxiosError`. We can use the class method `isAxiosError` to assert that the passed error is indeed of type `AxiosError`, and then process it how we want: 193 | 194 | ```typescript 195 | // logging.axios-interceptor.ts 196 | 197 | @Injectable() 198 | export class LoggingAxiosInterceptor extends AxiosInterceptor { 199 | constructor(httpService: HttpService) { 200 | super(httpService); 201 | } 202 | 203 | // requestFulfilled(): AxiosFulfilledInterceptor {} 204 | // requestRejected(): AxiosRejectedInterceptor {} 205 | // responseFulfilled(): AxiosFulfilledInterceptor {} 206 | 207 | responseRejected(): AxiosRejectedInterceptor { 208 | return (err) => { 209 | if (this.isAxiosError(err)) { 210 | const { config, response } = err; 211 | 212 | console.log( 213 | `Error ${response.status} in request "${config.method} ${config.path}` 214 | ); 215 | } else { 216 | console.error("Unexpected generic error", err); 217 | } 218 | 219 | throw err; 220 | }; 221 | } 222 | } 223 | ``` 224 | 225 | ## Upgrading 226 | 227 | ### Version Compatibility 228 | 229 | | nest-axios-interceptor | @nestjs/axios | @nestjs | 230 | | ---------------------- |---------------|------------| 231 | | 3.x | 2.x & 3.x | 9.x & 10.x | 232 | | 2.x | 1.x | 8.x | 233 | | 1.x | 0.x | 7.x | 234 | 235 | ### v2 to v3 236 | 237 | Version 3 requires: 238 | 239 | - @nestjs/axios > 2.0.0 240 | - @nestjs > 9.0.0 241 | 242 | The axios internal types for request configs changed (`AxiosRequestConfig` -> `InternalAxiosRequestConfig`), and you need to update your types to match. 243 | 244 | If you do not use custom configs, you can use this diff: 245 | 246 | ```diff 247 | // logging.axios-interceptor.ts 248 | import { Injectable } from "@nestjs/common"; 249 | import { HttpService } from "@nestjs/axios"; 250 | -import type { AxiosResponse, AxiosRequestConfig } from "axios"; 251 | +import type { AxiosResponse, InternalAxiosRequestConfig } from "axios"; 252 | import { 253 | AxiosInterceptor, 254 | AxiosFulfilledInterceptor, 255 | AxiosRejectedInterceptor, 256 | } from "@narando/nest-axios-interceptor"; 257 | 258 | @Injectable() 259 | export class LoggingAxiosInterceptor extends AxiosInterceptor { 260 | constructor(httpService: HttpService) { 261 | super(httpService); 262 | } 263 | 264 | - // requestFulfilled(): AxiosFulfilledInterceptor {} 265 | + // requestFulfilled(): AxiosFulfilledInterceptor {} 266 | // requestRejected(): AxiosRejectedInterceptor {} 267 | // responseFulfilled(): AxiosFulfilledInterceptor {} 268 | // responseRejected(): AxiosRejectedInterceptor {} 269 | } 270 | ``` 271 | 272 | If you use custom configs, you also need to change the custom config: 273 | 274 | ```diff 275 | // logging.axios-interceptor.ts 276 | const LOGGING_CONFIG_KEY = Symbol("kLoggingAxiosInterceptor"); 277 | 278 | // Merging our custom properties with the base config 279 | -interface LoggingConfig extends AxiosRequestConfig { 280 | +interface LoggingConfig extends InternalAxiosRequestConfig { 281 | [LOGGING_CONFIG_KEY]: { 282 | id: number; 283 | }; 284 | } 285 | ``` 286 | 287 | ## License 288 | 289 | This repository is published under the [MIT License](./LICENSE). 290 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./dist"; 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function __export(m) { 3 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; 4 | } 5 | exports.__esModule = true; 6 | __export(require("./dist")); 7 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from "./dist"; 2 | -------------------------------------------------------------------------------- /lib/axios-interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | 3 | import { HttpService } from "@nestjs/axios"; 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 5 | import { Injectable } from "@nestjs/common"; 6 | import { Test } from "@nestjs/testing"; 7 | import { AxiosError } from "axios"; 8 | import { AxiosInterceptor } from "./axios-interceptor"; 9 | 10 | // AxiosInterceptor is abstract and can not be instantiated 11 | @Injectable() 12 | class TestAxiosInterceptor extends AxiosInterceptor { 13 | constructor(httpService: HttpService) { 14 | super(httpService); 15 | } 16 | } 17 | 18 | describe("AxiosInterceptor", () => { 19 | let axiosInterceptor: AxiosInterceptor; 20 | let httpService: HttpService; 21 | 22 | beforeEach(async () => { 23 | const moduleRef = await Test.createTestingModule({ 24 | providers: [ 25 | TestAxiosInterceptor, 26 | { 27 | provide: HttpService, 28 | useFactory: (): HttpService => 29 | ({ 30 | axiosRef: {}, 31 | }) as any as HttpService, 32 | }, 33 | ], 34 | }).compile(); 35 | 36 | axiosInterceptor = 37 | moduleRef.get(TestAxiosInterceptor); 38 | httpService = moduleRef.get(HttpService); 39 | }); 40 | 41 | it("should be defined", () => { 42 | expect(axiosInterceptor).toBeDefined(); 43 | expect(httpService).toBeDefined(); 44 | }); 45 | 46 | describe("onModuleInit", () => { 47 | it("should call registerInterceptors", () => { 48 | const registerInterceptors = jest 49 | .spyOn( 50 | axiosInterceptor, 51 | // @ts-ignore 52 | "registerInterceptors", 53 | ) 54 | // @ts-ignore 55 | .mockReturnValue(); // Typing require 1 argument, but function has return type `void`/never 56 | 57 | axiosInterceptor.onModuleInit(); 58 | 59 | expect(registerInterceptors).toHaveBeenCalledTimes(1); 60 | }); 61 | }); 62 | 63 | describe("registerInterceptors", () => { 64 | let requestUse: jest.Mock; 65 | let responseUse: jest.Mock; 66 | 67 | beforeEach(() => { 68 | requestUse = jest.fn(); 69 | responseUse = jest.fn(); 70 | 71 | httpService.axiosRef.interceptors = { 72 | request: { use: requestUse } as any, 73 | response: { use: responseUse } as any, 74 | }; 75 | }); 76 | 77 | it("should register interceptors on the axios instance", () => { 78 | // @ts-ignore 79 | axiosInterceptor.registerInterceptors(); 80 | 81 | expect(requestUse).toHaveBeenCalledTimes(1); 82 | expect(requestUse).toHaveBeenCalledWith( 83 | expect.any(Function), 84 | expect.any(Function), 85 | ); 86 | 87 | expect(responseUse).toHaveBeenCalledTimes(1); 88 | expect(responseUse).toHaveBeenCalledWith( 89 | expect.any(Function), 90 | expect.any(Function), 91 | ); 92 | }); 93 | 94 | it("should get the interceptors from the class methods", () => { 95 | const requestFulfilledReturnFunction = jest.fn(); 96 | const requestRejectedReturnFunction = jest.fn(); 97 | const responseFulfilledReturnFunction = jest.fn(); 98 | const responseRejectedReturnFunction = jest.fn(); 99 | 100 | const requestFulfilled = jest 101 | .spyOn(axiosInterceptor as any, "requestFulfilled") 102 | .mockReturnValue(requestFulfilledReturnFunction); 103 | const requestRejected = jest 104 | .spyOn(axiosInterceptor as any, "requestRejected") 105 | .mockReturnValue(requestRejectedReturnFunction); 106 | const responseFulfilled = jest 107 | .spyOn(axiosInterceptor as any, "responseFulfilled") 108 | .mockReturnValue(responseFulfilledReturnFunction); 109 | const responseRejected = jest 110 | .spyOn(axiosInterceptor as any, "responseRejected") 111 | .mockReturnValue(responseRejectedReturnFunction); 112 | 113 | // @ts-ignore 114 | axiosInterceptor.registerInterceptors(); 115 | 116 | expect(requestFulfilled).toHaveBeenCalledTimes(1); 117 | expect(requestRejected).toHaveBeenCalledTimes(1); 118 | expect(responseFulfilled).toHaveBeenCalledTimes(1); 119 | expect(responseRejected).toHaveBeenCalledTimes(1); 120 | 121 | expect(requestUse).toHaveBeenCalledTimes(1); 122 | expect(requestUse).toHaveBeenCalledWith( 123 | requestFulfilledReturnFunction, 124 | requestRejectedReturnFunction, 125 | ); 126 | 127 | expect(responseUse).toHaveBeenCalledTimes(1); 128 | expect(responseUse).toHaveBeenCalledWith( 129 | responseFulfilledReturnFunction, 130 | responseRejectedReturnFunction, 131 | ); 132 | }); 133 | }); 134 | 135 | describe("isAxiosError", () => { 136 | it("should return true for AxiosError", () => { 137 | const axiosError: AxiosError = new Error() as AxiosError; 138 | axiosError.toJSON = jest.fn(); 139 | axiosError.isAxiosError = true; 140 | axiosError.config = {} as any; 141 | 142 | // @ts-ignore 143 | expect(axiosInterceptor.isAxiosError(axiosError)).toBe(true); 144 | }); 145 | 146 | it("should return false for normal Error", () => { 147 | const normalError: Error = new Error(); 148 | 149 | // @ts-ignore 150 | expect(axiosInterceptor.isAxiosError(normalError)).toBe(false); 151 | }); 152 | }); 153 | }); 154 | -------------------------------------------------------------------------------- /lib/axios-interceptor.ts: -------------------------------------------------------------------------------- 1 | import type { HttpService } from "@nestjs/axios"; 2 | import type { OnModuleInit } from "@nestjs/common"; 3 | import type { 4 | AxiosError, 5 | AxiosInterceptorManager, 6 | AxiosResponse, 7 | InternalAxiosRequestConfig, 8 | } from "axios"; 9 | import { identityFulfilled, identityRejected } from "./identity-functions"; 10 | import { AxiosErrorCustomConfig } from "./interfaces/axios-error-custom-config"; 11 | import { AxiosFulfilledInterceptor } from "./interfaces/axios-fulfilled-interceptor"; 12 | import { AxiosRejectedInterceptor } from "./interfaces/axios-rejected-interceptor"; 13 | import { AxiosResponseCustomConfig } from "./interfaces/axios-response-custom-config"; 14 | 15 | export abstract class AxiosInterceptor< 16 | TRequestConfig extends 17 | InternalAxiosRequestConfig = InternalAxiosRequestConfig, 18 | TResponse extends AxiosResponse = AxiosResponseCustomConfig, 19 | TAxiosError extends AxiosError = AxiosErrorCustomConfig, 20 | > implements OnModuleInit 21 | { 22 | protected readonly httpService: HttpService; 23 | 24 | constructor(httpService: HttpService) { 25 | this.httpService = httpService; 26 | } 27 | 28 | public onModuleInit(): void { 29 | this.registerInterceptors(); 30 | } 31 | 32 | private registerInterceptors(): void { 33 | const { axiosRef: axios } = this.httpService; 34 | 35 | type RequestManager = AxiosInterceptorManager; 36 | type ResponseManager = AxiosInterceptorManager; 37 | 38 | (axios.interceptors.request as RequestManager).use( 39 | this.requestFulfilled(), 40 | this.requestRejected(), 41 | ); 42 | 43 | (axios.interceptors.response as ResponseManager).use( 44 | this.responseFulfilled(), 45 | this.responseRejected(), 46 | ); 47 | } 48 | 49 | /** 50 | * Implement this function to do something before request is sent. 51 | */ 52 | protected requestFulfilled(): AxiosFulfilledInterceptor { 53 | // Noop by default 54 | return identityFulfilled; 55 | } 56 | 57 | /** 58 | * Implement this function to do something with request error. 59 | */ 60 | protected requestRejected(): AxiosRejectedInterceptor { 61 | // Noop by default 62 | return identityRejected; 63 | } 64 | 65 | /** 66 | * Implement this function to do something with response data. 67 | */ 68 | protected responseFulfilled(): AxiosFulfilledInterceptor { 69 | // Noop by default 70 | return identityFulfilled; 71 | } 72 | 73 | /** 74 | * Implement this function to do something with response error. 75 | */ 76 | protected responseRejected(): AxiosRejectedInterceptor { 77 | // Noop by default 78 | return identityRejected; 79 | } 80 | 81 | protected isAxiosError(err: any): err is TAxiosError { 82 | return !!(err.isAxiosError && err.isAxiosError === true); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/identity-functions.spec.ts: -------------------------------------------------------------------------------- 1 | import { identityFulfilled, identityRejected } from "./identity-functions"; 2 | 3 | describe("Identity Functions", () => { 4 | describe("identityFulfilled", () => { 5 | it("should return the value", () => { 6 | const complicatedValue = { 7 | key: "foo", 8 | bar: 2, 9 | baz: { 10 | hello: "world", 11 | }, 12 | }; 13 | 14 | expect(identityFulfilled(complicatedValue)).toBe(complicatedValue); 15 | expect(identityFulfilled(complicatedValue)).toEqual({ 16 | key: "foo", 17 | bar: 2, 18 | baz: { 19 | hello: "world", 20 | }, 21 | }); 22 | }); 23 | }); 24 | 25 | describe("identityRejected", () => { 26 | it("should throw the value", () => { 27 | const err = new Error(); 28 | expect(identityRejected(err)).rejects.toThrow(err); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /lib/identity-functions.ts: -------------------------------------------------------------------------------- 1 | export const identityFulfilled = (value: T): T => value; 2 | 3 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 4 | export const identityRejected = (err: any): Promise => 5 | Promise.reject(err); 6 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export { AxiosInterceptor } from "./axios-interceptor"; 2 | 3 | export type { AxiosFulfilledInterceptor } from "./interfaces/axios-fulfilled-interceptor"; 4 | export type { AxiosRejectedInterceptor } from "./interfaces/axios-rejected-interceptor"; 5 | export type { AxiosResponseCustomConfig } from "./interfaces/axios-response-custom-config"; 6 | -------------------------------------------------------------------------------- /lib/interfaces/axios-error-custom-config.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosError, InternalAxiosRequestConfig } from "axios"; 2 | 3 | export interface AxiosErrorCustomConfig< 4 | TConfig extends InternalAxiosRequestConfig, 5 | > extends AxiosError { 6 | config: TConfig; 7 | } 8 | -------------------------------------------------------------------------------- /lib/interfaces/axios-fulfilled-interceptor.ts: -------------------------------------------------------------------------------- 1 | export type AxiosFulfilledInterceptor = (value: T) => T | Promise; 2 | -------------------------------------------------------------------------------- /lib/interfaces/axios-rejected-interceptor.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | export type AxiosRejectedInterceptor = (error: any) => any; 3 | -------------------------------------------------------------------------------- /lib/interfaces/axios-response-custom-config.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosResponse, InternalAxiosRequestConfig } from "axios"; 2 | 3 | export interface AxiosResponseCustomConfig< 4 | TConfig extends InternalAxiosRequestConfig, 5 | > extends AxiosResponse { 6 | config: TConfig; 7 | } 8 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "lib" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@narando/nest-axios-interceptor", 3 | "version": "3.0.0", 4 | "description": "", 5 | "author": "Julian Tölle ", 6 | "license": "MIT", 7 | "scripts": { 8 | "prebuild": "rimraf dist", 9 | "build": "nest build", 10 | "format": "prettier --write \"lib/**/*.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 'lib/**/*.{js,ts}'", 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 | "peerDependencies": { 23 | "@nestjs/axios": "^2.0.0 || ^3.0.0", 24 | "@nestjs/common": "^9.0.0 || ^10.0.0", 25 | "@nestjs/core": "^9.0.0 || ^10.0.0", 26 | "reflect-metadata": "^0.1.13", 27 | "rxjs": "^7.0.0" 28 | }, 29 | "devDependencies": { 30 | "@nestjs/axios": "3.1.3", 31 | "@nestjs/cli": "10.4.9", 32 | "@nestjs/common": "10.4.15", 33 | "@nestjs/core": "10.4.15", 34 | "@nestjs/schematics": "10.2.3", 35 | "@nestjs/testing": "10.4.15", 36 | "@types/express": "5.0.0", 37 | "@types/jest": "29.5.14", 38 | "@types/node": "22.10.10", 39 | "@types/supertest": "6.0.2", 40 | "@typescript-eslint/eslint-plugin": "8.21.0", 41 | "@typescript-eslint/parser": "8.21.0", 42 | "eslint": "8.57.1", 43 | "eslint-config-prettier": "9.1.0", 44 | "eslint-plugin-prettier": "5.2.3", 45 | "jest": "29.7.0", 46 | "prettier": "3.4.2", 47 | "reflect-metadata": "0.1.13", 48 | "rimraf": "6.0.1", 49 | "rxjs": "7.8.1", 50 | "supertest": "7.0.0", 51 | "ts-jest": "29.2.5", 52 | "ts-loader": "9.5.2", 53 | "ts-node": "10.9.2", 54 | "tsconfig-paths": "4.2.0", 55 | "typescript": "5.7.3" 56 | }, 57 | "jest": { 58 | "moduleFileExtensions": [ 59 | "js", 60 | "json", 61 | "ts" 62 | ], 63 | "rootDir": "lib", 64 | "testRegex": ".spec.ts$", 65 | "transform": { 66 | "^.+\\.(t|j)s$": "ts-jest" 67 | }, 68 | "coverageDirectory": "../coverage", 69 | "collectCoverageFrom": [ 70 | "**/*.ts" 71 | ], 72 | "testEnvironment": "node" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "semanticCommits": true, 3 | "packageRules": [ 4 | { 5 | "depTypeList": ["devDependencies"], 6 | "automerge": true 7 | } 8 | ], 9 | "extends": ["config:base", "schedule:weekly"], 10 | "reviewers": ["MarcMogdanz"] 11 | } 12 | -------------------------------------------------------------------------------- /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 | "target": "es2021", 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "baseUrl": "./", 12 | "incremental": true 13 | }, 14 | "include": ["lib/**/*"], 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | --------------------------------------------------------------------------------