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 CHANGED
@@ -1,8 +1,31 @@
1
- # axiomate
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
- Stop writing `axios.get(...)` with headers in every component. Define your entire backend API in one file, then call it anywhere with a single line.
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
- ## Quick Start
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
- ### 1. Setup (once in `index.js` or `App.js`)
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 axiomate where to get the auth token
79
+ // Tell Axiomate where your token lives
25
80
  setTokenGetter(() => localStorage.getItem("token"));
26
81
 
27
- // Global error handling
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
- ### 2. Define your API (one file — `api.config.js`)
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
- login: { path: "/auth/login", method: "POST" },
44
- getUser: { path: "/user/profile", method: "GET" },
45
- updateUser: { path: "/user/update", method: "PUT" },
46
- getProducts: { path: "/products", method: "GET" },
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
- ### 3. Use anywhere
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
- // Simple call
143
+ // POST request
57
144
  const res = await api.login({ username: "ali", password: "1234" });
58
145
 
59
- // GET with query params
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
- // → GET /products?page=1&limit=10
151
+
152
+ // File upload — FormData and headers handled automatically
153
+ await api.uploadAvatar({ avatar: fileInput.files[0], userId: 5 });
62
154
  ```
63
155
 
64
- ### 4. React Hook
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>Hello, {data.name}</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
- ## API Reference
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` | Request timeout in ms |
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
- Define how axiomate should retrieve the auth token.
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
- Run a function before every outgoing request.
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.headers["X-App-Version"] = "2.0";
269
+ console.log(`→ ${config.method} ${config.url}`);
104
270
  return config;
105
271
  });
106
272
  ```
107
273
 
274
+ ---
275
+
108
276
  ### `addResponseInterceptor(fn)`
109
- Run a function after every successful response.
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("Response:", response.config.url);
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
- Handle failed requests globally.
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 === 403) alert("Access denied!");
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 `data` |
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 (e.g. on form submit)
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": "1.0.2",
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
- "module": "src/index.js",
7
- "keywords": ["api", "axios", "react", "http-client", "rest"],
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
- "files": ["src"],
19
- "scripts": {
20
- "test": "node tests/test.js"
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 — library ke andar use hota hai
4
- * Developer directly use na kare
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 _store = {
7
+ const createStore = () => ({
8
8
  instance: null,
9
- interceptors: {
10
- request: [],
11
- response: [],
12
- error: [],
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 = { _store };
20
+ module.exports = { createStore, _globalInterceptors };