Skip to content

Commit e7c1979

Browse files
authored
refactor(web): replace axios with fetch (#1653)
* refactor(web): replace axios with fetch * refactor(web): replace CompassApi with BaseApi and remove unused compass.api files * refactor: update API calls in various modules to use BaseApi instead of CompassApi * chore: remove compass.api and related utility files * feat: introduce base API structure for improved request handling and error management * test: add unit tests for new base API functionality * refactor(email): enhance error handling with KitApiError class * feat: introduce KitApiError for improved error management in email service * refactor: replace EmailServiceError with KitApiError for consistency * chore: update error handling logic to utilize BaseError and genericError * test: ensure robust error logging and handling in email service methods
1 parent d31ccb2 commit e7c1979

File tree

21 files changed

+444
-252
lines changed

21 files changed

+444
-252
lines changed

bun.lock

Lines changed: 4 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend/src/email/email.service.ts

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import axios from "axios";
1+
import { BaseError } from "@core/errors/errors.base";
22
import { Logger } from "@core/logger/winston.logger";
33
import { mapCompassUserToEmailSubscriber } from "@core/mappers/subscriber/map.subscriber";
44
import {
@@ -9,13 +9,50 @@ import { type Schema_User } from "@core/types/user.types";
99
import { ENV } from "@backend/common/constants/env.constants";
1010
import { isMissingUserTagId } from "@backend/common/constants/env.util";
1111
import { EmailerError } from "@backend/common/errors/emailer/emailer.errors";
12-
import { error } from "@backend/common/errors/handlers/error.handler";
12+
import {
13+
error,
14+
genericError,
15+
} from "@backend/common/errors/handlers/error.handler";
1316
import {
1417
type Response_TagSubscriber,
1518
type Response_UpsertSubscriber,
1619
} from "./email.types";
1720

1821
const logger = Logger("app:email.service");
22+
23+
/**
24+
* Internal error class for Kit API failures.
25+
* Used to pass response details from post() to callers, who then
26+
* transform it into appropriate domain errors using error().
27+
*/
28+
class KitApiError extends Error {
29+
constructor(
30+
message: string,
31+
public readonly method: string,
32+
public readonly url: string,
33+
public readonly status?: number,
34+
public readonly data?: unknown,
35+
) {
36+
super(message);
37+
this.name = "KitApiError";
38+
Object.setPrototypeOf(this, new.target.prototype);
39+
Error.captureStackTrace(this);
40+
}
41+
}
42+
43+
const getResponseData = async (response: Response): Promise<unknown> => {
44+
const text = await response.text();
45+
if (!text) {
46+
return undefined;
47+
}
48+
49+
try {
50+
return JSON.parse(text);
51+
} catch {
52+
return text;
53+
}
54+
};
55+
1956
class EmailService {
2057
private static headers: { headers: Record<string, string> };
2158
private static readonly baseUrl = "https://api.kit.com/v4";
@@ -64,18 +101,22 @@ class EmailService {
64101
// add tag to subscriber
65102
logger.info(`Tagging subscriber: ${subscriber.email_address}`);
66103
const url = `${this.baseUrl}/tags/${tagId}/subscribers/${subId}`;
67-
const result = await axios.post(url, {}, this.headers);
68-
return result.data;
104+
return await this.post<Response_TagSubscriber>(url, {});
69105
} catch (err) {
70-
if (axios.isAxiosError(err)) {
106+
if (err instanceof BaseError) {
107+
throw err;
108+
}
109+
110+
if (err instanceof KitApiError) {
71111
logger.error({
72112
message: err.message,
73-
status: err.response?.status,
74-
data: err.response?.data,
75-
url: err.config?.url,
76-
method: err.config?.method,
113+
status: err.status,
114+
data: err.data,
115+
method: err.method,
116+
url: err.url,
77117
});
78-
switch (err.response?.status) {
118+
119+
switch (err.status) {
79120
case 401:
80121
throw error(
81122
EmailerError.InvalidSecret,
@@ -84,11 +125,11 @@ class EmailService {
84125
case 404:
85126
throw error(EmailerError.InvalidTagId, "Subscriber was not tagged");
86127
default:
87-
throw err;
128+
throw genericError(err, "Failed to tag subscriber");
88129
}
89-
} else {
90-
throw err;
91130
}
131+
132+
throw genericError(err, "Failed to tag subscriber");
92133
}
93134
}
94135

@@ -104,8 +145,28 @@ class EmailService {
104145
}
105146

106147
const url = `${this.baseUrl}/subscribers`;
107-
const result = await axios.post(url, data, this.headers);
108-
return result.data;
148+
return await this.post<Response_UpsertSubscriber>(url, data);
149+
}
150+
151+
private static async post<T>(url: string, body: object): Promise<T> {
152+
const response = await fetch(url, {
153+
body: JSON.stringify(body),
154+
headers: this.headers.headers,
155+
method: "POST",
156+
});
157+
const data = await getResponseData(response);
158+
159+
if (!response.ok) {
160+
throw new KitApiError(
161+
`Kit request failed with status ${response.status}`,
162+
"POST",
163+
url,
164+
response.status,
165+
data,
166+
);
167+
}
168+
169+
return data as T;
109170
}
110171
}
111172

packages/scripts/src/commands/seed.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import axios from "axios";
21
import pkg from "inquirer";
32
import { ObjectId } from "mongodb";
43
import { getApiBaseUrl, log } from "@scripts/common/cli.utils";
@@ -16,9 +15,18 @@ async function createEvent(
1615
baseUrl: string,
1716
accessToken: string,
1817
) {
19-
await axios.post(`${baseUrl}/event`, events, {
20-
headers: { Cookie: `sAccessToken=${accessToken}` },
18+
const response = await fetch(`${baseUrl}/event`, {
19+
body: JSON.stringify(events),
20+
headers: {
21+
"Content-Type": "application/json",
22+
Cookie: `sAccessToken=${accessToken}`,
23+
},
24+
method: "POST",
2125
});
26+
27+
if (!response.ok) {
28+
throw new Error(`Failed to create events: ${response.status}`);
29+
}
2230
}
2331

2432
async function seedEvents(userInput: string) {

packages/web/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
"@react-oauth/google": "^0.7.0",
2020
"@reduxjs/toolkit": "^1.6.1",
2121
"@tanstack/react-hotkeys": "^0.3.1",
22-
"axios": "^1.2.2",
2322
"classnames": "^2.3.1",
2423
"dayjs": "^1.10.7",
2524
"dexie": "^4.2.1",

packages/web/src/auth/google/hooks/useConnectGoogle/useConnectGoogle.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ describe("useConnectGoogle", () => {
292292
userMetadataStatus: "loaded",
293293
});
294294
mockAuthApi.connectGoogle.mockRejectedValueOnce({
295-
isAxiosError: true,
295+
config: { url: "/auth/google/connect" },
296296
response: {
297297
data: {
298298
code: "GOOGLE_ACCOUNT_ALREADY_CONNECTED",

packages/web/src/auth/google/hooks/useConnectGoogle/useConnectGoogle.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { type AxiosError, isAxiosError } from "axios";
21
import { useCallback, useSyncExternalStore } from "react";
32
import { GOOGLE_REVOKED } from "@core/constants/sse.constants";
43
import { type GoogleConnectionState } from "@core/types/user.types";
@@ -14,11 +13,12 @@ import {
1413
} from "@web/auth/google/state/google.sync.state";
1514
import { syncPendingLocalEvents } from "@web/auth/google/util/google.auth.util";
1615
import { AuthApi } from "@web/common/apis/auth.api";
16+
import { SyncApi } from "@web/common/apis/sync.api";
1717
import {
1818
getApiErrorCode,
19+
isApiError,
1920
parseGoogleConnectError,
20-
} from "@web/common/apis/compass.api.util";
21-
import { SyncApi } from "@web/common/apis/sync.api";
21+
} from "@web/common/apis/util/api.util";
2222
import { GOOGLE_REPAIR_FAILED_TOAST_ID } from "@web/common/constants/toast.constants";
2323
import { showErrorToast } from "@web/common/utils/toast/error-toast.util";
2424
import {
@@ -69,7 +69,7 @@ export const useConnectGoogle = (): UseConnectGoogleResult => {
6969
try {
7070
await AuthApi.connectGoogle(googleConnectRequest);
7171
} catch (error) {
72-
if (isAxiosError(error)) {
72+
if (isApiError(error)) {
7373
const message = parseGoogleConnectError(error)?.message;
7474

7575
if (message) {
@@ -103,7 +103,7 @@ export const useConnectGoogle = (): UseConnectGoogleResult => {
103103
} catch (error) {
104104
clearGoogleSyncIndicatorOverride();
105105
const isGoogleRevoked =
106-
getApiErrorCode(error as AxiosError) === GOOGLE_REVOKED;
106+
isApiError(error) && getApiErrorCode(error) === GOOGLE_REVOKED;
107107

108108
if (isGoogleRevoked) {
109109
return;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { type Status } from "@core/errors/status.codes";
2+
3+
export type ApiAdapter = <T>(
4+
config: ApiRequestConfig & { body?: unknown },
5+
) => Promise<ApiResponse<T>>;
6+
7+
export interface ApiError extends Error {
8+
config?: ApiRequestConfig;
9+
response?: ApiResponse<unknown>;
10+
}
11+
12+
export interface ApiRequestConfig {
13+
headers?: HeadersInit;
14+
method?: string;
15+
url?: string;
16+
}
17+
18+
export interface ApiResponse<T> {
19+
config: ApiRequestConfig;
20+
data: T;
21+
headers: Headers;
22+
status: number;
23+
statusText: string;
24+
}
25+
26+
export type ApiMethodConfig = Pick<ApiRequestConfig, "headers">;
27+
28+
export type SignoutStatus =
29+
| Status.UNAUTHORIZED
30+
| Status.NOT_FOUND
31+
| Status.GONE;

packages/web/src/common/apis/auth.api.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import {
44
type Result_Auth_Compass,
55
} from "@core/types/auth.types";
66
import { type GoogleAuthConfig } from "@web/auth/google/hooks/googe.auth.types";
7-
import { CompassApi } from "@web/common/apis/compass.api";
7+
import { BaseApi } from "@web/common/apis/base/base.api";
88

99
const AuthApi = {
1010
async loginOrSignup(data: GoogleAuthConfig): Promise<Result_Auth_Compass> {
11-
const response = await CompassApi.post<Result_Auth_Compass>(
11+
const response = await BaseApi.post<Result_Auth_Compass>(
1212
`/signinup`,
1313
data,
1414
{ headers: { rid: "thirdparty" } },
@@ -20,7 +20,7 @@ const AuthApi = {
2020
async connectGoogle(
2121
data: GoogleAuthCodeRequest,
2222
): Promise<GoogleConnectResponse> {
23-
const response = await CompassApi.post<GoogleConnectResponse>(
23+
const response = await BaseApi.post<GoogleConnectResponse>(
2424
`/auth/google/connect`,
2525
data,
2626
);

0 commit comments

Comments
 (0)