Skip to content

Commit 57a80aa

Browse files
committed
feat!: replace axios with native fetch
Drop axios dependency in favor of Node.js native fetch (available since Node 20). Production dependencies reduced from 24 packages to 1 (tslib). BREAKING CHANGE: AxiosAdapter removed, replaced by FetchAdapter. AxiosRequestConfig type replaced by RequestConfig. New HttpError export replaces axios error format.
1 parent 76544b0 commit 57a80aa

File tree

11 files changed

+117
-212
lines changed

11 files changed

+117
-212
lines changed

AGENTS.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@
5353
```
5454
src/
5555
├── index.ts → EQP class (main entry point, re-exports all types)
56-
├── Adapters.ts → AxiosAdapter (default HTTP client)
56+
├── FetchAdapter.ts → FetchAdapter (default HTTP client using native fetch)
57+
├── HttpError.ts → HttpError class for non-2xx responses
5758
├── AuthenticatedAdapter.ts → OAuth2 session + Bearer token injection
5859
├── services/
5960
│ ├── CallbackService.ts → Webhook registration + event parsing
@@ -73,7 +74,7 @@ src/
7374

7475
**Key patterns:**
7576

76-
- Adapter pattern for HTTP abstraction (`Adapter` interface → `AxiosAdapter`)
77+
- Adapter pattern for HTTP abstraction (`Adapter` interface → `FetchAdapter`)
7778
- `AuthenticatedAdapter` wraps any `Adapter` with automatic OAuth2 token management
7879
- `|MAGE_ID|` placeholder in URLs gets replaced with authenticated user's `mage_id`
7980
- One service class per API domain, each receives the authenticated adapter

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ const { EQP } = require('@netresearch/node-magento-eqp');
7474
| `environment` | `'production' \| 'sandbox'` | `'production'` | API environment |
7575
| `autoRefresh` | `boolean` | `false` | _Reserved for future use_ |
7676
| `expiresIn` | `number` | `360` | _Reserved for future use_ |
77-
| `adapter` | `Adapter` | `AxiosAdapter` | Custom HTTP adapter |
77+
| `adapter` | `Adapter` | `FetchAdapter` | Custom HTTP adapter |
7878

7979
## API documentation
8080

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
"typescript-eslint": "^8.0.0"
3333
},
3434
"dependencies": {
35-
"axios": "1.13.5",
3635
"tslib": "^2.6.0"
3736
},
3837
"lint-staged": {

src/AGENTS.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ TypeScript API wrapper for Adobe Commerce (Magento) Marketplace EQP API. Adapter
1111
| File | Purpose |
1212
| ----------------------------- | ---------------------------------------------------------------------------- |
1313
| `index.ts` | `EQP` class — main entry point, instantiates services, re-exports all types |
14-
| `Adapters.ts` | `AxiosAdapter` — default HTTP client implementing `Adapter` interface |
14+
| `FetchAdapter.ts` | `FetchAdapter` — default HTTP client using native `fetch` |
15+
| `HttpError.ts` | `HttpError` — error class for non-2xx HTTP responses |
1516
| `AuthenticatedAdapter.ts` | OAuth2 session management, Bearer token injection, `\|MAGE_ID\|` replacement |
1617
| `services/CallbackService.ts` | Webhook registration (`registerCallback`) + event parsing (`parseCallback`) |
1718
| `services/FileService.ts` | File upload metadata retrieval |
@@ -59,6 +60,6 @@ TypeScript API wrapper for Adobe Commerce (Magento) Marketplace EQP API. Adapter
5960
## When stuck
6061

6162
- TypeScript handbook: https://www.typescriptlang.org/docs
62-
- Axios docs: https://axios-http.com/docs/intro
63+
- Fetch API: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
6364
- Magento EQP API: https://developer.adobe.com/commerce/marketplace/guides/eqp/v1/
6465
- Check root AGENTS.md for project-wide conventions

src/Adapters.ts

Lines changed: 0 additions & 30 deletions
This file was deleted.

src/AuthenticatedAdapter.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { AxiosRequestConfig } from 'axios';
1+
import { RequestConfig } from './types/adapters';
22
import { Adapter } from './types/adapters';
33

44
const replaceMageIdInURL = (url: string, mageId: string) => url.replace(/\|MAGE_ID\|/, mageId);
5-
const addHeaders = (config: AxiosRequestConfig | undefined, headers: Record<string, string>) => ({ ...config, headers: { ...config?.headers, ...headers } });
5+
const addHeaders = (config: RequestConfig | undefined, headers: Record<string, string>) => ({ ...config, headers: { ...config?.headers, ...headers } });
66

77
export class AuthenticatedAdapter {
88
constructor(
@@ -11,27 +11,27 @@ export class AuthenticatedAdapter {
1111
appSecret: string;
1212
appId: string;
1313
}
14-
) { }
14+
) {}
1515

16-
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
16+
async get<T>(url: string, config?: RequestConfig): Promise<T> {
1717
const [mageId, headers] = await this.authenticate();
1818

1919
return this.baseAdapter.get(replaceMageIdInURL(url, mageId), addHeaders(config, headers));
2020
}
2121

22-
async post<T>(url: string, body: unknown, config?: AxiosRequestConfig): Promise<T> {
22+
async post<T>(url: string, body: unknown, config?: RequestConfig): Promise<T> {
2323
const [mageId, headers] = await this.authenticate();
2424

2525
return this.baseAdapter.post(replaceMageIdInURL(url, mageId), body, addHeaders(config, headers));
2626
}
2727

28-
async put<T>(url: string, body: unknown, config?: AxiosRequestConfig): Promise<T> {
28+
async put<T>(url: string, body: unknown, config?: RequestConfig): Promise<T> {
2929
const [mageId, headers] = await this.authenticate();
3030

3131
return this.baseAdapter.put(replaceMageIdInURL(url, mageId), body, addHeaders(config, headers));
3232
}
3333

34-
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
34+
async delete<T>(url: string, config?: RequestConfig): Promise<T> {
3535
const [mageId, headers] = await this.authenticate();
3636

3737
return this.baseAdapter.delete(replaceMageIdInURL(url, mageId), addHeaders(config, headers));

src/FetchAdapter.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Adapter, RequestConfig } from './types/adapters';
2+
import { HttpError } from './HttpError';
3+
4+
export class FetchAdapter implements Adapter {
5+
protected readonly baseURL: string;
6+
protected readonly defaultHeaders: Record<string, string> = {};
7+
8+
constructor(baseURL: string) {
9+
this.baseURL = baseURL;
10+
}
11+
12+
setHeader(header: string, value: string): void {
13+
this.defaultHeaders[header] = value;
14+
}
15+
16+
async get<T>(url: string, config?: RequestConfig): Promise<T> {
17+
return this.request<T>('GET', url, undefined, config);
18+
}
19+
20+
async post<T>(url: string, body: unknown, config?: RequestConfig): Promise<T> {
21+
return this.request<T>('POST', url, body, config);
22+
}
23+
24+
async put<T>(url: string, body: unknown, config?: RequestConfig): Promise<T> {
25+
return this.request<T>('PUT', url, body, config);
26+
}
27+
28+
async delete<T>(url: string, config?: RequestConfig): Promise<T> {
29+
return this.request<T>('DELETE', url, undefined, config);
30+
}
31+
32+
protected async request<T>(method: string, path: string, body: unknown, config?: RequestConfig): Promise<T> {
33+
let fullURL = this.baseURL + path;
34+
35+
if (config?.params) {
36+
const params = new URLSearchParams();
37+
for (const [key, value] of Object.entries(config.params)) {
38+
if (value !== undefined) {
39+
params.set(key, String(value));
40+
}
41+
}
42+
const qs = params.toString();
43+
if (qs) {
44+
fullURL += '?' + qs;
45+
}
46+
}
47+
48+
const headers: Record<string, string> = {
49+
'content-type': 'application/json',
50+
...this.defaultHeaders,
51+
...config?.headers
52+
};
53+
54+
if (config?.auth) {
55+
const encoded = Buffer.from(`${config.auth.username}:${config.auth.password}`).toString('base64');
56+
headers['authorization'] = `Basic ${encoded}`;
57+
}
58+
59+
const response = await fetch(fullURL, {
60+
method,
61+
headers,
62+
body: body !== undefined ? JSON.stringify(body) : undefined
63+
});
64+
65+
if (!response.ok) {
66+
let data: unknown;
67+
try {
68+
data = await response.json();
69+
} catch {
70+
data = await response.text();
71+
}
72+
throw new HttpError(response.status, response.statusText, data);
73+
}
74+
75+
return (await response.json()) as T;
76+
}
77+
}

src/HttpError.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export class HttpError extends Error {
2+
readonly status: number;
3+
readonly statusText: string;
4+
readonly data: unknown;
5+
6+
constructor(status: number, statusText: string, data: unknown) {
7+
super(`HTTP ${status}: ${statusText}`);
8+
this.name = 'HttpError';
9+
this.status = status;
10+
this.statusText = statusText;
11+
this.data = data;
12+
}
13+
}

src/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AxiosAdapter } from './Adapters';
1+
import { FetchAdapter } from './FetchAdapter';
22
import { AuthenticatedAdapter } from './AuthenticatedAdapter';
33
import { CallbackService } from './services/CallbackService';
44
import { FileService } from './services/FileService';
@@ -8,7 +8,8 @@ import { ReportService } from './services/ReportService';
88
import { UserService } from './services/UserService';
99
import { EQPOptions } from './types/options';
1010

11-
export { AxiosAdapter } from './Adapters';
11+
export { FetchAdapter } from './FetchAdapter';
12+
export { HttpError } from './HttpError';
1213
export { AuthenticatedAdapter } from './AuthenticatedAdapter';
1314
export * from './services';
1415
export * from './types';
@@ -35,7 +36,7 @@ export class EQP {
3536
options.environment ??= 'production';
3637

3738
this.adapter = new AuthenticatedAdapter(
38-
options.adapter ?? new AxiosAdapter(`https://commercedeveloper${options.environment === 'sandbox' ? '-sandbox' : ''}-api.adobe.com/rest/v1`),
39+
options.adapter ?? new FetchAdapter(`https://commercedeveloper${options.environment === 'sandbox' ? '-sandbox' : ''}-api.adobe.com/rest/v1`),
3940
{
4041
appId: options.appId,
4142
appSecret: options.appSecret

src/types/adapters.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
import { AxiosRequestConfig } from 'axios';
1+
export interface RequestConfig {
2+
auth?: { username: string; password: string };
3+
headers?: Record<string, string>;
4+
params?: Record<string, string | number | boolean | undefined>;
5+
}
26

37
export interface Adapter {
48
setHeader(header: string, value: string): void;
59

6-
get<T>(url: string, config?: AxiosRequestConfig): Promise<T>;
7-
post<T>(url: string, body: unknown, config?: AxiosRequestConfig): Promise<T>;
8-
put<T>(url: string, body: unknown, config?: AxiosRequestConfig): Promise<T>;
9-
delete<T>(url: string, config?: AxiosRequestConfig): Promise<T>;
10+
get<T>(url: string, config?: RequestConfig): Promise<T>;
11+
post<T>(url: string, body: unknown, config?: RequestConfig): Promise<T>;
12+
put<T>(url: string, body: unknown, config?: RequestConfig): Promise<T>;
13+
delete<T>(url: string, config?: RequestConfig): Promise<T>;
1014
}

0 commit comments

Comments
 (0)