Skip to content

Commit 86223f3

Browse files
Merge pull request #1 from keawade/up-front-interceptor-config
Feature: Enable defining Axios interceptors during module registration
2 parents 3912e43 + 28d3f7a commit 86223f3

File tree

4 files changed

+256
-27
lines changed

4 files changed

+256
-27
lines changed

README.md

Lines changed: 97 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,113 @@ export class MyCustomService {
3838
}
3939
```
4040

41-
### Custom Config
41+
### Custom Axios config
4242

4343
```typescript
4444
@Module({
4545
imports: [
4646
AxiosModule.register({
47-
baseURL: "https://example.com",
48-
headers: {
49-
"X-My-Header": "Is a value!",
47+
config: {
48+
baseURL: "https://example.com",
49+
headers: {
50+
"X-My-Header": "Is a value!",
51+
},
5052
},
5153
}),
5254
],
5355
})
5456
export class AppModule {}
5557
```
5658

59+
#### Register Axios interceptors
60+
61+
[Axios interceptors](https://axios-http.com/docs/interceptors) can be
62+
configured on Axios clients to intercept requests or responses before they are
63+
handled by `then` or `catch`.
64+
65+
```typescript
66+
import { create } from "axios";
67+
68+
const client = create();
69+
70+
client.interceptors.request.use(
71+
(request) => {
72+
return request;
73+
},
74+
(err) => {
75+
return err;
76+
},
77+
{},
78+
);
79+
80+
client.interceptors.response.use(
81+
(response) => {
82+
return response;
83+
},
84+
(err) => {
85+
return err;
86+
},
87+
{},
88+
);
89+
```
90+
91+
However, because NestJS abstracts away the construction of services, including
92+
this library's Axios wrapper which makes registering Axios interceptors
93+
difficult via the usual approach.
94+
95+
To resolve this, this module provides an additional way to configure
96+
interceptors via the module registration configuration:
97+
98+
```typescript
99+
@Module({
100+
imports: [
101+
AxiosModule.register({
102+
interceptors: {
103+
request: {
104+
onFulfilled: (requestConfig) => {
105+
return requestConfig;
106+
},
107+
onRejected: (err) => {
108+
return err;
109+
},
110+
options: {},
111+
},
112+
113+
response: {
114+
onFulfilled: (response) => {
115+
return response;
116+
},
117+
onRejected: (err) => {
118+
return err;
119+
},
120+
options: {},
121+
},
122+
},
123+
}),
124+
],
125+
})
126+
export class AppModule {}
127+
```
128+
129+
Interceptors can still be modified later via the Axios reference on the
130+
service:
131+
132+
```typescript
133+
import { HttpService } from "nestjs-axios-promise";
134+
135+
@Injectable()
136+
export class CatsService {
137+
public constructor(private readonly httpService: HttpService) {}
138+
139+
public doThing() {
140+
this.httpService.axios.interceptors.request.use((requestConfig) => {
141+
return requestConfig;
142+
});
143+
this.httpService.axios.interceptors.request.eject();
144+
}
145+
}
146+
```
147+
57148
### Why
58149

59150
Because I find our way using a promise
@@ -79,7 +170,8 @@ export class CatsService {
79170
}
80171
```
81172

82-
MUCH more preferrable to the nestjs way of using an observable. They're casting their observable to a promise anyways... :facepalm:
173+
MUCH more preferable to the NestJS way of using an observable. They're casting
174+
their observable to a promise anyways... :facepalm:
83175

84176
```ts
85177
import { catchError, firstValueFrom } from "rxjs";

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,6 @@
4646
"main": "dist/index.js",
4747
"private": false,
4848
"license": "MIT"
49-
}
49+
},
50+
"packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee"
5051
}

src/axios.module.ts

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,65 @@
1-
import { Module } from "@nestjs/common";
2-
import Axios, { AxiosRequestConfig } from "axios";
1+
import { ConfigurableModuleBuilder, Module } from "@nestjs/common";
2+
import {
3+
create,
4+
type AxiosRequestConfig,
5+
type AxiosResponse,
6+
type AxiosInterceptorOptions,
7+
type InternalAxiosRequestConfig,
8+
} from "axios";
39

410
import { AxiosService } from "./axios.service";
511
import { AXIOS_PROMISE_INSTANCE_TOKEN } from "./axios.constants";
612

13+
type AxiosModuleOptions = {
14+
config?: AxiosRequestConfig;
15+
interceptors?: {
16+
response?: {
17+
onFulfilled?: (value: AxiosResponse) => AxiosResponse | Promise<AxiosResponse>;
18+
onRejected?: (error: any) => any;
19+
options?: AxiosInterceptorOptions;
20+
};
21+
request?: {
22+
onFulfilled?: (
23+
value: InternalAxiosRequestConfig,
24+
) => InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig>;
25+
onRejected?: (error: any) => any;
26+
options?: AxiosInterceptorOptions;
27+
};
28+
};
29+
};
30+
31+
const { ConfigurableModuleClass, OPTIONS_TYPE, MODULE_OPTIONS_TOKEN } =
32+
new ConfigurableModuleBuilder<AxiosModuleOptions>().build();
33+
734
@Module({
8-
providers: [AxiosService],
35+
providers: [
36+
AxiosService,
37+
{
38+
provide: AXIOS_PROMISE_INSTANCE_TOKEN,
39+
inject: [MODULE_OPTIONS_TOKEN],
40+
useFactory: (options: typeof OPTIONS_TYPE = {}) => {
41+
const axiosInstance = create(options.config ?? {});
42+
43+
if (options.interceptors?.request) {
44+
axiosInstance.interceptors.request.use(
45+
options.interceptors.request.onFulfilled,
46+
options.interceptors.request.onRejected,
47+
options.interceptors.request.options,
48+
);
49+
}
50+
51+
if (options.interceptors?.response) {
52+
axiosInstance.interceptors.response.use(
53+
options.interceptors.response.onFulfilled,
54+
options.interceptors.response.onRejected,
55+
options.interceptors.response.options,
56+
);
57+
}
58+
59+
return axiosInstance;
60+
},
61+
},
62+
],
963
exports: [AxiosService],
1064
})
11-
export class AxiosModule {
12-
static register(config: AxiosRequestConfig = {}) {
13-
return {
14-
module: AxiosModule,
15-
providers: [
16-
{
17-
provide: AXIOS_PROMISE_INSTANCE_TOKEN,
18-
useValue: Axios.create(config),
19-
},
20-
],
21-
};
22-
}
23-
}
65+
export class AxiosModule extends ConfigurableModuleClass {}

test/axios.module.test.ts

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,111 @@ import { Test } from "@nestjs/testing";
33
import { AxiosModule, AxiosService } from "../src";
44

55
describe(AxiosModule, () => {
6-
let axiosService: AxiosService;
7-
8-
beforeEach(async () => {
6+
it("axios module export service", async () => {
97
const moduleRef = await Test.createTestingModule({
108
imports: [AxiosModule.register({})],
119
}).compile();
1210

13-
axiosService = moduleRef.get<AxiosService>(AxiosService);
11+
const axiosService = moduleRef.get<AxiosService>(AxiosService);
12+
13+
expect(axiosService).toBeDefined();
1414
});
1515

16-
it("axios module export service", () => {
16+
it("should allow registering asynchronously", async () => {
17+
const moduleRef = await Test.createTestingModule({
18+
imports: [
19+
AxiosModule.registerAsync({
20+
useFactory: () => {
21+
return {
22+
config: {},
23+
};
24+
},
25+
}),
26+
],
27+
}).compile();
28+
29+
const axiosService = moduleRef.get<AxiosService>(AxiosService);
30+
1731
expect(axiosService).toBeDefined();
1832
});
33+
34+
it("should register interceptors", async () => {
35+
const responseOnFulfilled = jest.fn();
36+
const requestOnFulfilled = jest.fn();
37+
38+
const moduleRef = await Test.createTestingModule({
39+
imports: [
40+
AxiosModule.register({
41+
interceptors: {
42+
response: {
43+
onFulfilled: (response) => {
44+
responseOnFulfilled("response");
45+
46+
return response;
47+
},
48+
},
49+
50+
request: {
51+
onFulfilled: (requestConfig) => {
52+
requestOnFulfilled("request");
53+
54+
return requestConfig;
55+
},
56+
},
57+
},
58+
}),
59+
],
60+
}).compile();
61+
62+
const axiosService = moduleRef.get<AxiosService>(AxiosService);
63+
64+
await axiosService.get("https://github.com");
65+
66+
expect(responseOnFulfilled).toHaveBeenCalledTimes(1);
67+
expect(responseOnFulfilled).toHaveBeenCalledWith("response");
68+
69+
expect(requestOnFulfilled).toHaveBeenCalledTimes(1);
70+
expect(requestOnFulfilled).toHaveBeenCalledWith("request");
71+
});
72+
73+
it("should async register interceptors", async () => {
74+
const responseOnFulfilled = jest.fn();
75+
const requestOnFulfilled = jest.fn();
76+
77+
const moduleRef = await Test.createTestingModule({
78+
imports: [
79+
AxiosModule.registerAsync({
80+
useFactory: () => ({
81+
interceptors: {
82+
response: {
83+
onFulfilled: (response) => {
84+
responseOnFulfilled("response");
85+
86+
return response;
87+
},
88+
},
89+
90+
request: {
91+
onFulfilled: (requestConfig) => {
92+
requestOnFulfilled("request");
93+
94+
return requestConfig;
95+
},
96+
},
97+
},
98+
}),
99+
}),
100+
],
101+
}).compile();
102+
103+
const axiosService = moduleRef.get<AxiosService>(AxiosService);
104+
105+
await axiosService.get("https://github.com");
106+
107+
expect(responseOnFulfilled).toHaveBeenCalledTimes(1);
108+
expect(responseOnFulfilled).toHaveBeenCalledWith("response");
109+
110+
expect(requestOnFulfilled).toHaveBeenCalledTimes(1);
111+
expect(requestOnFulfilled).toHaveBeenCalledWith("request");
112+
});
19113
});

0 commit comments

Comments
 (0)