├── .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 |
9 |
10 |
11 |
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 |
--------------------------------------------------------------------------------