axiomate 1.0.2 → 26.22.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of axiomate might be problematic. Click here for more details.
- package/README.md +409 -34
- package/package.json +21 -8
- package/src/_store.js +13 -9
- package/src/cache.js +96 -0
- package/src/createApi.js +234 -93
- package/src/index.d.ts +235 -0
- package/src/index.js +10 -10
- package/src/interceptors.js +21 -36
- package/src/mock.js +49 -0
- package/src/pagination.js +70 -0
- package/src/useApi.js +52 -35
package/README.md
CHANGED
|
@@ -1,8 +1,31 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Axiomate
|
|
2
2
|
|
|
3
|
-
> Zero-boilerplate API client — define once, use everywhere.
|
|
3
|
+
> Zero-boilerplate API client for JavaScript & React — define once, use everywhere.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Axiomate solves a problem every frontend developer faces: writing the same `axios.get(...)` with headers, tokens, and error handling in every single component. With Axiomate, you define your entire backend API in one file and call it anywhere with a single clean line.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What's Inside
|
|
10
|
+
|
|
11
|
+
Axiomate ships with everything you need out of the box:
|
|
12
|
+
|
|
13
|
+
- **`createApi`** — define all your endpoints in one place, get clean callable functions back
|
|
14
|
+
- **`setTokenGetter`** — tell Axiomate where your auth token lives, it attaches it automatically
|
|
15
|
+
- **`addRequestInterceptor`** — run logic before every outgoing request
|
|
16
|
+
- **`addResponseInterceptor`** — run logic after every successful response
|
|
17
|
+
- **`addErrorInterceptor`** — handle errors globally (401 logout, 500 alerts, etc.)
|
|
18
|
+
- **`useApi`** — React hook with loading, error, data, and optimistic updates built in
|
|
19
|
+
- **`createPaginator`** — paginate any list endpoint with next, goTo, reset, and hasMore
|
|
20
|
+
- **`mock`** — develop and test without a real backend
|
|
21
|
+
- **`cache`** — response caching with TTL, Stale While Revalidate, and auto-eviction
|
|
22
|
+
- **Request deduplication** — same request fired twice? Only one network call goes out
|
|
23
|
+
- **Retry on failure** — auto retry with configurable count and delay
|
|
24
|
+
- **Dynamic URL params** — `/user/:id` resolves to `/user/5` automatically
|
|
25
|
+
- **File upload** — `upload: true` handles FormData and headers for you
|
|
26
|
+
- **Per-endpoint timeout** — heavy endpoints get more time, light ones stay fast
|
|
27
|
+
- **Full TypeScript support** — typed responses, typed hooks, typed paginators
|
|
28
|
+
- **Multiple isolated instances** — two backends? No conflict, no overwrite
|
|
6
29
|
|
|
7
30
|
---
|
|
8
31
|
|
|
@@ -14,131 +37,483 @@ npm install axiomate axios
|
|
|
14
37
|
|
|
15
38
|
---
|
|
16
39
|
|
|
17
|
-
##
|
|
40
|
+
## Why Axiomate?
|
|
41
|
+
|
|
42
|
+
This is what every developer writes without Axiomate — in every component, every time:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
// ❌ Without Axiomate — repeated everywhere
|
|
46
|
+
const token = localStorage.getItem("token");
|
|
47
|
+
|
|
48
|
+
const response = await axios.post("http://localhost:8080/auth/login",
|
|
49
|
+
{ username, password },
|
|
50
|
+
{
|
|
51
|
+
headers: {
|
|
52
|
+
"Authorization": `Bearer ${token}`,
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
```
|
|
18
58
|
|
|
19
|
-
|
|
59
|
+
Change the base URL? Update 50 files. Forget the token? Bug in production. New developer joins? Good luck finding which endpoint does what.
|
|
60
|
+
|
|
61
|
+
This is what the same thing looks like with Axiomate:
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
// ✅ With Axiomate — clean, simple, always consistent
|
|
65
|
+
const response = await api.login({ username, password });
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Token attached automatically. Base URL in one place. Every endpoint documented in one file.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Step-by-Step Setup
|
|
73
|
+
|
|
74
|
+
### Step 1 — One-time setup in `index.js` or `App.js`
|
|
20
75
|
|
|
21
76
|
```js
|
|
22
77
|
import { setTokenGetter, addErrorInterceptor } from "axiomate";
|
|
23
78
|
|
|
24
|
-
// Tell
|
|
79
|
+
// Tell Axiomate where your token lives
|
|
25
80
|
setTokenGetter(() => localStorage.getItem("token"));
|
|
26
81
|
|
|
27
|
-
//
|
|
82
|
+
// Handle errors globally — no try/catch needed in every component
|
|
28
83
|
addErrorInterceptor((error) => {
|
|
29
84
|
if (error.status === 401) {
|
|
85
|
+
localStorage.removeItem("token");
|
|
30
86
|
window.location.href = "/login";
|
|
31
87
|
}
|
|
88
|
+
if (error.status === 500) {
|
|
89
|
+
alert("Something went wrong. Please try again.");
|
|
90
|
+
}
|
|
32
91
|
});
|
|
33
92
|
```
|
|
34
93
|
|
|
35
|
-
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### Step 2 — Define your API in one file (`api.config.js`)
|
|
36
97
|
|
|
37
98
|
```js
|
|
38
99
|
import { createApi } from "axiomate";
|
|
39
100
|
|
|
40
|
-
export const api = createApi({
|
|
101
|
+
export const { api, mock, cache } = createApi({
|
|
41
102
|
baseUrl: "http://localhost:8080",
|
|
103
|
+
|
|
42
104
|
endpoints: {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
105
|
+
// Auth
|
|
106
|
+
login: { path: "/auth/login", method: "POST" },
|
|
107
|
+
register: { path: "/auth/register", method: "POST" },
|
|
108
|
+
logout: { path: "/auth/logout", method: "POST" },
|
|
109
|
+
|
|
110
|
+
// User — dynamic :id param + 60s cache
|
|
111
|
+
getUser: { path: "/user/:id", method: "GET", cache: 60000 },
|
|
112
|
+
updateUser: { path: "/user/:id", method: "PUT" },
|
|
113
|
+
deleteUser: { path: "/user/:id", method: "DELETE" },
|
|
114
|
+
|
|
115
|
+
// Products — paginated with cache
|
|
116
|
+
getProducts: { path: "/products", method: "GET", cache: 60000 },
|
|
117
|
+
|
|
118
|
+
// Dashboard — Stale While Revalidate
|
|
119
|
+
getDashboard: { path: "/dashboard", method: "GET", cache: 30000, swr: true },
|
|
120
|
+
|
|
121
|
+
// Stats — retry 3 times if server is flaky
|
|
122
|
+
getStats: { path: "/stats", method: "GET", retry: 3, retryDelay: 2000 },
|
|
123
|
+
|
|
124
|
+
// Heavy report — needs more time
|
|
125
|
+
getReport: { path: "/report/full", method: "GET", timeout: 60000 },
|
|
126
|
+
|
|
127
|
+
// File upload — FormData handled automatically
|
|
128
|
+
uploadAvatar: { path: "/user/avatar", method: "POST", upload: true },
|
|
129
|
+
|
|
130
|
+
// Payment — prevent double submit
|
|
131
|
+
createPayment: { path: "/payment", method: "POST", dedupePost: true },
|
|
47
132
|
},
|
|
48
133
|
});
|
|
49
134
|
```
|
|
50
135
|
|
|
51
|
-
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
### Step 3 — Use anywhere in your app
|
|
52
139
|
|
|
53
140
|
```js
|
|
54
141
|
import { api } from "./api.config";
|
|
55
142
|
|
|
56
|
-
//
|
|
143
|
+
// POST request
|
|
57
144
|
const res = await api.login({ username: "ali", password: "1234" });
|
|
58
145
|
|
|
59
|
-
// GET with
|
|
146
|
+
// GET with dynamic URL param — resolves to /user/5
|
|
147
|
+
const user = await api.getUser({ id: 5 });
|
|
148
|
+
|
|
149
|
+
// GET with query params — resolves to /products?page=1&limit=10
|
|
60
150
|
const products = await api.getProducts({ page: 1, limit: 10 });
|
|
61
|
-
|
|
151
|
+
|
|
152
|
+
// File upload — FormData and headers handled automatically
|
|
153
|
+
await api.uploadAvatar({ avatar: fileInput.files[0], userId: 5 });
|
|
62
154
|
```
|
|
63
155
|
|
|
64
|
-
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
### Step 4 — React Hook
|
|
65
159
|
|
|
66
160
|
```jsx
|
|
67
161
|
import { useApi } from "axiomate";
|
|
68
162
|
import { api } from "./api.config";
|
|
69
163
|
|
|
164
|
+
// Auto call on mount — loading, error, data all managed
|
|
70
165
|
function ProfilePage() {
|
|
71
|
-
const { data, loading, error } = useApi(api.getUser
|
|
166
|
+
const { data: user, loading, error } = useApi(api.getUser, {
|
|
167
|
+
params: { id: 5 },
|
|
168
|
+
});
|
|
72
169
|
|
|
73
170
|
if (loading) return <p>Loading...</p>;
|
|
74
|
-
if (error) return <p>{error.message}</p>;
|
|
75
|
-
return <h1>
|
|
171
|
+
if (error) return <p>Error: {error.message}</p>;
|
|
172
|
+
return <h1>Welcome, {user.name}</h1>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Manual call — for forms and buttons
|
|
176
|
+
function LoginPage() {
|
|
177
|
+
const [username, setUsername] = useState("");
|
|
178
|
+
const [password, setPassword] = useState("");
|
|
179
|
+
|
|
180
|
+
const { loading, error, execute: doLogin } = useApi(api.login, {
|
|
181
|
+
immediate: false, // don't call on mount
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const handleSubmit = async () => {
|
|
185
|
+
const res = await doLogin({ username, password });
|
|
186
|
+
localStorage.setItem("token", res.token);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<div>
|
|
191
|
+
<input onChange={(e) => setUsername(e.target.value)} placeholder="Username" />
|
|
192
|
+
<input onChange={(e) => setPassword(e.target.value)} placeholder="Password" type="password" />
|
|
193
|
+
<button onClick={handleSubmit} disabled={loading}>
|
|
194
|
+
{loading ? "Logging in..." : "Login"}
|
|
195
|
+
</button>
|
|
196
|
+
{error && <p style={{ color: "red" }}>{error.message}</p>}
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
76
199
|
}
|
|
77
200
|
```
|
|
78
201
|
|
|
79
202
|
---
|
|
80
203
|
|
|
81
|
-
##
|
|
204
|
+
## All Features Explained
|
|
82
205
|
|
|
83
206
|
### `createApi(config)`
|
|
84
207
|
|
|
208
|
+
The core function. Call it once per backend, get back `{ api, mock, cache }`.
|
|
209
|
+
|
|
85
210
|
| Option | Type | Required | Default | Description |
|
|
86
211
|
|--------|------|----------|---------|-------------|
|
|
87
212
|
| `baseUrl` | string | ✅ | — | Your backend base URL |
|
|
88
|
-
| `endpoints` | object | ✅ | — | Map of all endpoints |
|
|
213
|
+
| `endpoints` | object | ✅ | — | Map of all your endpoints |
|
|
89
214
|
| `headers` | object | ❌ | `{}` | Extra default headers |
|
|
90
|
-
| `timeout` | number | ❌ | `10000` |
|
|
215
|
+
| `timeout` | number | ❌ | `10000` | Global timeout in ms |
|
|
91
216
|
| `autoToken` | boolean | ❌ | `true` | Auto-attach Bearer token |
|
|
217
|
+
| `getToken` | function | ❌ | localStorage | Custom token getter for this instance |
|
|
218
|
+
| `maxCacheSize` | number | ❌ | `200` | Max cached entries before auto-eviction |
|
|
219
|
+
| `onSWRError` | function | ❌ | console.warn | Called when SWR background refetch fails |
|
|
220
|
+
|
|
221
|
+
**Endpoint options:**
|
|
222
|
+
|
|
223
|
+
| Option | Type | Default | Description |
|
|
224
|
+
|--------|------|---------|-------------|
|
|
225
|
+
| `path` | string | — | URL path, supports `:param` syntax |
|
|
226
|
+
| `method` | string | `"GET"` | HTTP method |
|
|
227
|
+
| `timeout` | number | global | Per-endpoint timeout override |
|
|
228
|
+
| `cache` | number | — | Cache TTL in ms (GET only) |
|
|
229
|
+
| `swr` | boolean | `false` | Stale While Revalidate |
|
|
230
|
+
| `retry` | number | `0` | Retry count on failure |
|
|
231
|
+
| `retryDelay` | number | `1000` | Delay between retries in ms |
|
|
232
|
+
| `upload` | boolean | `false` | File upload — sends multipart/form-data |
|
|
233
|
+
| `dedupePost` | boolean | `false` | Deduplicate POST requests |
|
|
234
|
+
|
|
235
|
+
---
|
|
92
236
|
|
|
93
237
|
### `setTokenGetter(fn)`
|
|
94
|
-
|
|
238
|
+
|
|
239
|
+
Tell Axiomate how to get your auth token. Called before every request automatically.
|
|
240
|
+
|
|
95
241
|
```js
|
|
242
|
+
// From localStorage
|
|
243
|
+
setTokenGetter(() => localStorage.getItem("token"));
|
|
244
|
+
|
|
245
|
+
// From Redux store
|
|
96
246
|
setTokenGetter(() => store.getState().auth.token);
|
|
247
|
+
|
|
248
|
+
// From a cookie
|
|
249
|
+
setTokenGetter(() => getCookie("auth_token"));
|
|
97
250
|
```
|
|
98
251
|
|
|
252
|
+
> For per-instance tokens (multiple backends), use `getToken` inside `createApi` instead.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
99
256
|
### `addRequestInterceptor(fn)`
|
|
100
|
-
|
|
257
|
+
|
|
258
|
+
Runs before every outgoing request across all instances. Use it to add headers, log requests, or modify config.
|
|
259
|
+
|
|
101
260
|
```js
|
|
261
|
+
// Add app version to every request
|
|
262
|
+
addRequestInterceptor((config) => {
|
|
263
|
+
config.headers["X-App-Version"] = "4.0.0";
|
|
264
|
+
return config; // must return config
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Log every request
|
|
102
268
|
addRequestInterceptor((config) => {
|
|
103
|
-
config.
|
|
269
|
+
console.log(`→ ${config.method} ${config.url}`);
|
|
104
270
|
return config;
|
|
105
271
|
});
|
|
106
272
|
```
|
|
107
273
|
|
|
274
|
+
---
|
|
275
|
+
|
|
108
276
|
### `addResponseInterceptor(fn)`
|
|
109
|
-
|
|
277
|
+
|
|
278
|
+
Runs after every successful response across all instances.
|
|
279
|
+
|
|
110
280
|
```js
|
|
281
|
+
// Save a new token if the server sends one
|
|
282
|
+
addResponseInterceptor((response) => {
|
|
283
|
+
if (response.data?.newToken) {
|
|
284
|
+
localStorage.setItem("token", response.data.newToken);
|
|
285
|
+
}
|
|
286
|
+
return response; // must return response
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Log response time
|
|
111
290
|
addResponseInterceptor((response) => {
|
|
112
|
-
console.log(
|
|
291
|
+
console.log(`← ${response.config.url} — ${response.status}`);
|
|
113
292
|
return response;
|
|
114
293
|
});
|
|
115
294
|
```
|
|
116
295
|
|
|
296
|
+
---
|
|
297
|
+
|
|
117
298
|
### `addErrorInterceptor(fn)`
|
|
118
|
-
|
|
299
|
+
|
|
300
|
+
Runs when any request fails. Use it for global error handling.
|
|
301
|
+
|
|
119
302
|
```js
|
|
120
303
|
addErrorInterceptor((error) => {
|
|
121
|
-
if (error.status ===
|
|
304
|
+
if (error.status === 401) {
|
|
305
|
+
// Token expired — redirect to login
|
|
306
|
+
window.location.href = "/login";
|
|
307
|
+
}
|
|
308
|
+
if (error.status === 403) {
|
|
309
|
+
alert("You don't have permission to do this.");
|
|
310
|
+
}
|
|
311
|
+
if (!navigator.onLine) {
|
|
312
|
+
alert("No internet connection.");
|
|
313
|
+
}
|
|
122
314
|
});
|
|
123
315
|
```
|
|
124
316
|
|
|
317
|
+
The `error` object always has:
|
|
318
|
+
- `error.status` — HTTP status code (401, 403, 500, etc.)
|
|
319
|
+
- `error.message` — clean error message from server
|
|
320
|
+
- `error.data` — full server response body
|
|
321
|
+
- `error.original` — original axios error
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
125
325
|
### `useApi(apiFn, options?)`
|
|
126
326
|
|
|
327
|
+
React hook that manages loading, error, and data state for you.
|
|
328
|
+
|
|
127
329
|
| Option | Type | Default | Description |
|
|
128
330
|
|--------|------|---------|-------------|
|
|
129
331
|
| `params` | object | `{}` | Default params passed to the function |
|
|
130
332
|
| `immediate` | boolean | `true` | Call automatically on mount |
|
|
131
|
-
| `initialData` | any | `null` | Initial value for
|
|
333
|
+
| `initialData` | any | `null` | Initial value for data |
|
|
132
334
|
|
|
133
335
|
**Returns:** `{ data, loading, error, execute, reset }`
|
|
134
336
|
|
|
135
337
|
```jsx
|
|
136
338
|
// Auto call on mount
|
|
137
|
-
const { data, loading, error } = useApi(api.getUser);
|
|
339
|
+
const { data, loading, error } = useApi(api.getUser, { params: { id: 5 } });
|
|
138
340
|
|
|
139
|
-
// Manual call
|
|
341
|
+
// Manual call — form submit, button click
|
|
140
342
|
const { execute, loading } = useApi(api.login, { immediate: false });
|
|
141
343
|
await execute({ username, password });
|
|
344
|
+
|
|
345
|
+
// Optimistic update — show change instantly, rollback if it fails
|
|
346
|
+
const { execute } = useApi(api.updateUser, { immediate: false });
|
|
347
|
+
await execute(
|
|
348
|
+
{ id: 1, name: "Ali New" },
|
|
349
|
+
{
|
|
350
|
+
optimisticData: { ...user, name: "Ali New" }, // show immediately
|
|
351
|
+
rollbackData: user, // revert on failure
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// Reset state back to initial
|
|
356
|
+
const { reset } = useApi(api.getUser);
|
|
357
|
+
reset(); // clears data, error, loading
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
### `createPaginator(apiFn, options?)`
|
|
363
|
+
|
|
364
|
+
Tracks page numbers for you. No more manually managing `page` state.
|
|
365
|
+
|
|
366
|
+
| Option | Type | Default | Description |
|
|
367
|
+
|--------|------|---------|-------------|
|
|
368
|
+
| `startPage` | number | `1` | First page number |
|
|
369
|
+
| `pageSize` | number | `10` | Items per page |
|
|
370
|
+
| `pageKey` | string | `"page"` | Query param name for page |
|
|
371
|
+
| `pageSizeKey` | string | `"limit"` | Query param name for size |
|
|
372
|
+
|
|
373
|
+
```js
|
|
374
|
+
import { createPaginator } from "axiomate";
|
|
375
|
+
import { api } from "./api.config";
|
|
376
|
+
|
|
377
|
+
const paginator = createPaginator(api.getProducts, { pageSize: 20 });
|
|
378
|
+
|
|
379
|
+
// Fetch pages
|
|
380
|
+
const page1 = await paginator.next(); // GET /products?page=1&limit=20
|
|
381
|
+
const page2 = await paginator.next(); // GET /products?page=2&limit=20
|
|
382
|
+
await paginator.goTo(5); // GET /products?page=5&limit=20
|
|
383
|
+
paginator.reset(); // back to page 1
|
|
384
|
+
|
|
385
|
+
// Check state
|
|
386
|
+
paginator.currentPage // current page number
|
|
387
|
+
paginator.hasMore // false when last page returned fewer items than pageSize
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
### Mock Mode
|
|
393
|
+
|
|
394
|
+
Develop and test your frontend without a real backend.
|
|
395
|
+
|
|
396
|
+
```js
|
|
397
|
+
const { api, mock } = createApi({ ... });
|
|
398
|
+
|
|
399
|
+
// Enable mock mode
|
|
400
|
+
mock.enable();
|
|
401
|
+
|
|
402
|
+
// Static mock response
|
|
403
|
+
mock.register("getUser", { id: 1, name: "Ali", email: "ali@test.com" });
|
|
404
|
+
|
|
405
|
+
// Dynamic mock — different responses based on request data
|
|
406
|
+
mock.register("login", (data) => {
|
|
407
|
+
if (data.username === "ali") return { token: "fake-token-123" };
|
|
408
|
+
throw new Error("Invalid credentials");
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// With custom delay (simulates slow network)
|
|
412
|
+
mock.register("getProducts", [{ id: 1 }, { id: 2 }], { delay: 1000 });
|
|
413
|
+
|
|
414
|
+
// Disable when done
|
|
415
|
+
mock.disable();
|
|
416
|
+
mock.clear(); // remove all registered mocks
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
### Cache Control
|
|
422
|
+
|
|
423
|
+
```js
|
|
424
|
+
const { cache } = createApi({ ... });
|
|
425
|
+
|
|
426
|
+
cache.clear(); // clear all cached responses
|
|
427
|
+
cache.clear("/user/5"); // clear one specific entry
|
|
428
|
+
cache.size(); // number of cached entries
|
|
429
|
+
cache.cleanup(); // remove only expired entries (keep fresh ones)
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
### Multiple Backends
|
|
435
|
+
|
|
436
|
+
Each `createApi` call is fully isolated — different base URLs, different tokens, no conflict.
|
|
437
|
+
|
|
438
|
+
```js
|
|
439
|
+
// Main API
|
|
440
|
+
const { api: mainApi } = createApi({
|
|
441
|
+
baseUrl: "http://api.myapp.com",
|
|
442
|
+
getToken: () => localStorage.getItem("token"),
|
|
443
|
+
endpoints: { getUser: { path: "/user/:id", method: "GET" } },
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Admin API — completely separate instance
|
|
447
|
+
const { api: adminApi } = createApi({
|
|
448
|
+
baseUrl: "http://admin.myapp.com",
|
|
449
|
+
getToken: () => localStorage.getItem("admin-token"),
|
|
450
|
+
endpoints: { getStats: { path: "/stats", method: "GET" } },
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Both work independently
|
|
454
|
+
await mainApi.getUser({ id: 1 });
|
|
455
|
+
await adminApi.getStats();
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
### TypeScript
|
|
461
|
+
|
|
462
|
+
Define response types once, get full type safety everywhere.
|
|
463
|
+
|
|
464
|
+
```ts
|
|
465
|
+
interface User { id: number; name: string; email: string; }
|
|
466
|
+
interface Product { id: number; name: string; price: number; }
|
|
467
|
+
interface LoginResp { token: string; user: User; }
|
|
468
|
+
|
|
469
|
+
const { api } = createApi<typeof endpoints, {
|
|
470
|
+
login: LoginResp;
|
|
471
|
+
getUser: User;
|
|
472
|
+
getProducts: Product[];
|
|
473
|
+
}>({
|
|
474
|
+
baseUrl: "http://localhost:8080",
|
|
475
|
+
endpoints: { ... },
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// Full type safety
|
|
479
|
+
const user = await api.getUser({ id: 1 });
|
|
480
|
+
user.name; // ✅ string — autocomplete works
|
|
481
|
+
user.xyz; // ❌ TypeScript error — property does not exist
|
|
482
|
+
|
|
483
|
+
// Typed React hook
|
|
484
|
+
const { data } = useApi<User>(api.getUser);
|
|
485
|
+
data?.name; // ✅ typed as string | null
|
|
486
|
+
|
|
487
|
+
// Typed paginator
|
|
488
|
+
const paginator = createPaginator<Product>(api.getProducts);
|
|
489
|
+
const items = await paginator.next();
|
|
490
|
+
items[0].price; // ✅ typed as number
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## Comparison
|
|
496
|
+
|
|
497
|
+
| | Without Axiomate | With Axiomate |
|
|
498
|
+
|---|---|---|
|
|
499
|
+
| Base URL change | Update 50+ files | Update 1 file |
|
|
500
|
+
| Auth token | Attach manually everywhere | Automatic |
|
|
501
|
+
| Error handling | Try/catch in every component | One global handler |
|
|
502
|
+
| Loading state | Manage manually | `useApi` handles it |
|
|
503
|
+
| Find an endpoint | Search the entire codebase | One config file |
|
|
504
|
+
| File upload | Write FormData manually | `upload: true` |
|
|
505
|
+
| Pagination | Track page number manually | `paginator.next()` |
|
|
506
|
+
| Retry logic | Write a while loop | `retry: 3` |
|
|
507
|
+
| Multiple backends | Risk of instance conflict | Fully isolated |
|
|
508
|
+
| Mock for testing | External library needed | Built-in |
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## Running Tests
|
|
513
|
+
|
|
514
|
+
```bash
|
|
515
|
+
npm install
|
|
516
|
+
npm test
|
|
142
517
|
```
|
|
143
518
|
|
|
144
519
|
---
|
package/package.json
CHANGED
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "axiomate",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Zero-boilerplate API client — define once, use everywhere.",
|
|
3
|
+
"version": "26.22.3",
|
|
4
|
+
"description": "Zero-boilerplate API client — define once, use everywhere. Production-ready.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
6
|
+
"types": "src/index.d.ts",
|
|
7
|
+
"files": ["src", "!src/**/*.test.js"],
|
|
8
|
+
"keywords": ["api", "axios", "react", "http-client", "rest", "typescript"],
|
|
8
9
|
"author": "Abdul Haseeb",
|
|
9
10
|
"license": "MIT",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "jest --coverage",
|
|
13
|
+
"test:watch": "jest --watch"
|
|
14
|
+
},
|
|
10
15
|
"peerDependencies": {
|
|
11
16
|
"axios": "^1.0.0",
|
|
12
17
|
"react": ">=16.8.0"
|
|
13
18
|
},
|
|
14
19
|
"devDependencies": {
|
|
15
20
|
"axios": "^1.6.0",
|
|
16
|
-
"react": "^18.0.0"
|
|
21
|
+
"react": "^18.0.0",
|
|
22
|
+
"@types/react": "^18.0.0",
|
|
23
|
+
"@types/jest": "^29.0.0",
|
|
24
|
+
"jest": "^29.0.0",
|
|
25
|
+
"jest-environment-jsdom": "^29.0.0",
|
|
26
|
+
"axios-mock-adapter": "^1.22.0"
|
|
17
27
|
},
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
"
|
|
28
|
+
"jest": {
|
|
29
|
+
"testEnvironment": "jsdom",
|
|
30
|
+
"collectCoverageFrom": ["src/**/*.js", "!src/index.js"],
|
|
31
|
+
"coverageThreshold": {
|
|
32
|
+
"global": { "branches": 80, "functions": 80, "lines": 80 }
|
|
33
|
+
}
|
|
21
34
|
}
|
|
22
35
|
}
|
package/src/_store.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* _store.js
|
|
3
|
-
* Internal shared state —
|
|
4
|
-
*
|
|
3
|
+
* Internal shared state — do not use directly
|
|
4
|
+
* Each createApi() call gets its OWN store — no global state conflicts
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const createStore = () => ({
|
|
8
8
|
instance: null,
|
|
9
|
-
interceptors: {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
interceptors: { request: [], response: [], error: [] },
|
|
10
|
+
pendingRequests: new Map(), // deduplication
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Global interceptors shared across all instances
|
|
14
|
+
const _globalInterceptors = {
|
|
15
|
+
request: [],
|
|
16
|
+
response: [],
|
|
17
|
+
error: [],
|
|
14
18
|
};
|
|
15
19
|
|
|
16
|
-
module.exports = {
|
|
20
|
+
module.exports = { createStore, _globalInterceptors };
|