fetchwire 1.0.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Doan Vinh Phu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,568 @@
1
+ # fetchwire
2
+
3
+ A lightweight, focused API fetching library for React and React Native applications.
4
+
5
+ **fetchwire** wraps the native `fetch` API in a global configuration layer. It is designed to make it easy to:
6
+
7
+ - Centralize your API base URL, auth token, and common headers.
8
+ - Handle errors consistently through a single `ApiError` type.
9
+
10
+ ### When to use fetchwire
11
+ - **React / React Native apps** that:
12
+ - Want a **simple**, centralized way to call HTTP APIs.
13
+ - Prefer plain hooks over a heavier state management or query library.
14
+ - Need basic tag-based invalidation without a full cache layer.
15
+ - Handle errors in a consistent way across screens.
16
+
17
+ ### When not to use fetchwire
18
+ - Consider a more full-featured solution (e.g. TanStack Query / React Query, SWR, RTK Query) if:
19
+ - You need advanced, automatic caching strategies.
20
+ - You need built-in pagination helpers, infinite queries.
21
+ - You need a more powerful data-fetching library and you want to avoid overlap.
22
+
23
+ ## Support
24
+ If you find **fetchwire** helpful and want to support its development, you can buy me a coffee via:
25
+
26
+ [![Ko-fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/doanvinhphu)
27
+ [![PayPal](https://img.shields.io/badge/PayPal-004595?style=for-the-badge&logo=paypal&logoColor=white)](https://paypal.me/doanvinhphu)
28
+
29
+ Your support helps maintain the library and keep it up to date!
30
+
31
+ ## Features
32
+
33
+ - **Global configuration with `initWire`**
34
+ - Configure `baseUrl`, default headers, and how to read the auth token.
35
+ - Optionally register global interceptors for 401/403/other errors.
36
+ - Customize which HTTP status codes are treated as **unauthorized** or **forbidden** via `unauthorizedStatusCodes` and `forbiddenStatusCodes` (defaults to `[401]` and `[403]`).
37
+
38
+ - **Typed HTTP wrapper with `wireApi`**
39
+ - Thin wrapper around `fetch` that:
40
+ - Appends the endpoint to a global `baseUrl`.
41
+ - Adds auth headers from `getToken`.
42
+ - Merges default and per-request headers.
43
+ - Converts server/network errors into a typed `ApiError`.
44
+
45
+ - **React hooks for requests**
46
+ - **`useFetchFn<T>`** for data fetching
47
+ - **`useMutationFn<T>`** for mutations
48
+
49
+ - **Tag-based invalidation**
50
+ - `useFetchFn` can subscribe to **tags**.
51
+ - `useMutationFn` can **invalidate tags** after a successful mutation.
52
+ - This gives you a simple, explicit way to refetch related data without a complex cache layer.
53
+
54
+ ---
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ npm install fetchwire
60
+ # or
61
+ yarn add fetchwire
62
+ # or
63
+ pnpm add fetchwire
64
+ ```
65
+
66
+ ### Peer expectations
67
+
68
+ - React or React Native project using function components and hooks.
69
+ - TypeScript is recommended but not required.
70
+ - For React Native / Expo, make sure the global `fetch` is available (default in modern RN/Expo).
71
+
72
+ ---
73
+
74
+ ## Getting Started
75
+
76
+ ### 1. Initialize fetchwire once at app startup
77
+
78
+ Call `initWire` once, as early as possible in your app lifecycle.
79
+
80
+ #### Simple React example
81
+
82
+ ```ts
83
+ // src/api/wire.ts
84
+ import { initWire } from 'fetchwire';
85
+
86
+ export function setupWire() {
87
+ initWire({
88
+ baseUrl: 'https://api.example.com',
89
+ headers: {
90
+ 'x-client': 'web',
91
+ },
92
+ getToken: async () => {
93
+ // Read token from localStorage (or any storage you prefer)
94
+ return localStorage.getItem('access_token');
95
+ },
96
+ // Optional: customize which status codes should trigger auth interceptors
97
+ unauthorizedStatusCodes: [401, 419], // defaults to [401] if omitted
98
+ forbiddenStatusCodes: [403], // defaults to [403] if omitted
99
+ interceptors: {
100
+ onUnauthorized: (error) => {
101
+ // e.g. redirect to login, clear token, show toast, etc.
102
+ },
103
+ onForbidden: (error) => {
104
+ // e.g. show "no permission" message
105
+ },
106
+ onError: (error) => {
107
+ // fallback handler for other error statuses
108
+ },
109
+ },
110
+ });
111
+ }
112
+ ```
113
+
114
+ ```tsx
115
+ // src/main.tsx or src/index.tsx
116
+ import React from 'react';
117
+ import ReactDOM from 'react-dom/client';
118
+ import App from './App';
119
+ import { setupWire } from './api/wire';
120
+
121
+ setupWire();
122
+
123
+ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
124
+ <React.StrictMode>
125
+ <App />
126
+ </React.StrictMode>,
127
+ );
128
+ ```
129
+
130
+ You **must** call `initWire` (directly or via a helper like `setupWire`) before using `wireApi`, `useFetchFn`, or `useMutationFn`.
131
+
132
+ ---
133
+
134
+ ## Usage
135
+
136
+ ### 1. Define API helpers with `wireApi`
137
+
138
+ A common pattern is to define small API helper functions in `src/api/*` that wrap your backend endpoints. For example, a simple CRUD helper for `Todo`:
139
+
140
+ ```ts
141
+ // src/api/todo-api.ts
142
+ import { wireApi } from 'fetchwire';
143
+
144
+ export type Todo = {
145
+ id: string;
146
+ title: string;
147
+ completed: boolean;
148
+ };
149
+
150
+ export async function getTodosApi() {
151
+ return wireApi<Todo[]>('/todos', { method: 'GET' });
152
+ }
153
+
154
+ export async function createTodoApi(input: { title: string }) {
155
+ return wireApi<Todo>('/todos', {
156
+ method: 'POST',
157
+ body: JSON.stringify(input),
158
+ });
159
+ }
160
+
161
+ export async function toggleTodoApi(id: string) {
162
+ return wireApi<Todo>(`/todos/${id}/toggle`, {
163
+ method: 'POST',
164
+ });
165
+ }
166
+
167
+ export async function deleteTodoApi(id: string) {
168
+ return wireApi<null>(`/todos/${id}`, {
169
+ method: 'DELETE',
170
+ });
171
+ }
172
+ ```
173
+
174
+ You can organize similar helpers for users, invoices, organizations, uploads, etc., all using `wireApi`.
175
+
176
+ ---
177
+
178
+ ### 2. Fetch data with `useFetchFn`
179
+
180
+ `useFetchFn<T>` is a generic hook that manages state for running an async function returning `{ data: T }`.
181
+
182
+ **Key ideas:**
183
+
184
+ - You **do not** pass the async function into the hook directly.
185
+ - Instead, the hook returns `executeFetchFn`, which you call with a function that performs your API request.
186
+ - The hook tracks:
187
+ - `data: T | null`
188
+ - `isLoading: boolean`
189
+ - `isRefreshing: boolean`
190
+ - `error: ApiError | null`
191
+ - `executeFetchFn(fetchFn)`
192
+ - `refreshFetchFn()`
193
+
194
+ Example: loading and refreshing a todo list in a React component:
195
+
196
+ ```tsx
197
+ // src/components/TodoList.tsx
198
+ import { useEffect } from 'react';
199
+ import { useFetchFn } from 'fetchwire';
200
+ import { getTodosApi, type Todo } from '../api/todo-api';
201
+
202
+ export function TodoList() {
203
+ const {
204
+ data: todos,
205
+ isLoading,
206
+ isRefreshing,
207
+ error,
208
+ executeFetchFn: fetchTodos,
209
+ refreshFetchFn: refreshTodos,
210
+ } = useFetchFn<Todo[]>({
211
+ tags: ['todos'],
212
+ });
213
+
214
+ useEffect(() => {
215
+ fetchTodos(() => getTodosApi());
216
+ }, [fetchTodos]);
217
+
218
+ if (isLoading) return <div>Loading...</div>;
219
+ if (error) return <div>Error: {error.message}</div>;
220
+
221
+ return (
222
+ <div>
223
+ <button onClick={() => refreshTodos()} disabled={isRefreshing}>
224
+ {isRefreshing ? 'Refreshing...' : 'Refresh'}
225
+ </button>
226
+
227
+ <ul>
228
+ {(todos ?? []).map((todo) => (
229
+ <li key={todo.id}>
230
+ {todo.title} {todo.completed ? '(done)' : ''}
231
+ </li>
232
+ ))}
233
+ </ul>
234
+ </div>
235
+ );
236
+ }
237
+ ```
238
+
239
+ ---
240
+
241
+ ### 3. Mutate data with `useMutationFn`
242
+
243
+ `useMutationFn<T>` is a hook for mutations (create/update/delete). It:
244
+
245
+ - Tracks `data` and `isMutating`.
246
+ - Lets you invalidate **tags** after a successful mutation.
247
+ - Accepts per-call `onSuccess` and `onError` callbacks.
248
+
249
+ Signature:
250
+
251
+ ```ts
252
+ const {
253
+ data,
254
+ isMutating,
255
+ executeMutationFn,
256
+ reset,
257
+ } = useMutationFn<T>({ invalidatesTags?: string[] });
258
+ ```
259
+
260
+ Example: creating and toggling todos with `useMutationFn`:
261
+
262
+ ```tsx
263
+ // src/components/TodoActions.tsx
264
+ import { FormEvent, useState } from 'react';
265
+ import { useMutationFn } from 'fetchwire';
266
+ import {
267
+ createTodoApi,
268
+ toggleTodoApi,
269
+ deleteTodoApi,
270
+ type Todo,
271
+ } from '../api/todo-api';
272
+
273
+ export function TodoActions() {
274
+ const [title, setTitle] = useState('');
275
+
276
+ const {
277
+ isMutating: isCreating,
278
+ executeMutationFn: createTodo,
279
+ } = useMutationFn<Todo>({
280
+ invalidatesTags: ['todos'],
281
+ });
282
+
283
+ const {
284
+ isMutating: isToggling,
285
+ executeMutationFn: toggleTodo,
286
+ } = useMutationFn<Todo>({
287
+ invalidatesTags: ['todos'],
288
+ });
289
+
290
+ const {
291
+ isMutating: isDeleting,
292
+ executeMutationFn: deleteTodo,
293
+ } = useMutationFn<null>({
294
+ invalidatesTags: ['todos'],
295
+ });
296
+
297
+ const handleCreate = (e: FormEvent) => {
298
+ e.preventDefault();
299
+ if (!title.trim()) return;
300
+
301
+ createTodo(() => createTodoApi({ title }), {
302
+ onSuccess: () => setTitle(''),
303
+ });
304
+ };
305
+
306
+ // Example usage of toggleTodo and deleteTodo in your UI:
307
+ // toggleTodo(() => toggleTodoApi(todoId))
308
+ // deleteTodo(() => deleteTodoApi(todoId))
309
+
310
+ return (
311
+ <form onSubmit={handleCreate}>
312
+ <input
313
+ value={title}
314
+ onChange={(e) => setTitle(e.target.value)}
315
+ placeholder="New todo"
316
+ />
317
+ <button type="submit" disabled={isCreating}>
318
+ {isCreating ? 'Adding...' : 'Add'}
319
+ </button>
320
+ </form>
321
+ );
322
+ }
323
+ ```
324
+
325
+ ---
326
+
327
+ ### 4. Tag-based invalidation and auto-refresh
328
+
329
+ Tags provide a simple way to coordinate refetches across your app:
330
+
331
+ - `useFetchFn({ tags: [...] })` subscribes the hook to one or more **tags**.
332
+ - `useMutationFn({ invalidatesTags: [...] })` emits those tags after a **successful** mutation.
333
+ - When a tag is emitted, all subscribed fetch hooks will automatically **call `refreshFetchFn`**.
334
+
335
+ This pattern keeps your code explicit and small, without introducing a full query cache library.
336
+
337
+ ---
338
+
339
+ ## Error Handling
340
+
341
+ ### Response object shape
342
+
343
+ By default, `wireApi` assumes your backend returns an object compatible with:
344
+
345
+ ```ts
346
+ type HttpResponse<T> = {
347
+ data?: T;
348
+ message?: string;
349
+ status?: number;
350
+ };
351
+ ```
352
+
353
+ **Successful response example:**
354
+
355
+ ```json
356
+ {
357
+ "data": {
358
+ "id": "123",
359
+ "email": "user@example.com"
360
+ },
361
+ "message": "OK",
362
+ "status": 200
363
+ }
364
+ ```
365
+
366
+ **Error response example (from server):**
367
+
368
+ ```json
369
+ {
370
+ "message": "Something went wrong",
371
+ "error": "ERROR_CODE"
372
+ }
373
+ ```
374
+
375
+ If the response body cannot be parsed as JSON or a network error occurs, fetchwire falls back to a synthetic error with:
376
+
377
+ - `message`: from the thrown `Error` or `"Network error"`
378
+ - `errorCode`: `"NETWORK_ERROR"`
379
+ - `statusCode`: `520`
380
+
381
+ ### ApiError
382
+
383
+ All errors are normalized to an `ApiError` instance. It extends `Error` and typically includes:
384
+
385
+ - `message: string`
386
+ - `errorCode: string | undefined` (e.g. from server `error` field or `'NETWORK_ERROR'`)
387
+ - `statusCode: number | undefined` (e.g. 401, 403, 500, 520, etc.)
388
+
389
+ ### Using ApiError in components
390
+
391
+ With `useMutationFn`, you commonly handle errors with `onError`:
392
+
393
+ ```tsx
394
+ import { ApiError } from 'fetchwire';
395
+
396
+ executeMutationFn(() => someMutationApi(), {
397
+ onSuccess: () => {
398
+ // success logic
399
+ },
400
+ onError: (error: ApiError) => {
401
+ Alert.alert('Login failed', error.message || 'Unexpected error');
402
+ },
403
+ });
404
+ ```
405
+
406
+ You can also read `error` directly from `useFetchFn` state if you want to render error messages in your UI.
407
+
408
+ ---
409
+
410
+ ## Configuration Reference
411
+
412
+ ### `initWire(config)`
413
+
414
+ ```ts
415
+ type WireInterceptors = {
416
+ onUnauthorized?: (error: ApiError) => void;
417
+ onForbidden?: (error: ApiError) => void;
418
+ onError?: (error: ApiError) => void;
419
+ };
420
+
421
+ type WireConfig = {
422
+ baseUrl: string;
423
+ headers?: Record<string, string>;
424
+ getToken: () => Promise<string | null>;
425
+ interceptors?: WireInterceptors;
426
+ unauthorizedStatusCodes?: number[];
427
+ forbiddenStatusCodes?: number[];
428
+ };
429
+
430
+ function initWire(config: WireConfig): void;
431
+ ```
432
+
433
+ - **`baseUrl`**: Base API URL (e.g. `'https://api.example.com'`).
434
+ - **`headers`**: Global headers to apply to every request.
435
+ - **`getToken`**: Async function that returns a bearer token or `null`. If present, fetchwire adds `Authorization: Bearer <token>`.
436
+ - **`interceptors`** (optional):
437
+ - `onUnauthorized(error)`: Called when a 401 is returned.
438
+ - `onForbidden(error)`: Called when a 403 is returned.
439
+ - `onError(error)`: Called for other error statuses.
440
+ - **`unauthorizedStatusCodes`** (optional): List of HTTP status codes that should be treated as unauthorized (defaults to `[401]`).
441
+ - **`forbiddenStatusCodes`** (optional): List of HTTP status codes that should be treated as forbidden (defaults to `[403]`).
442
+
443
+ ### `updateWireConfig(configPartial)`
444
+
445
+ ```ts
446
+ function updateWireConfig(config: Partial<WireConfig>): void;
447
+ ```
448
+
449
+ - Merges new configuration into the existing global config.
450
+ - Merges header objects deeply, so you can safely add new headers at runtime.
451
+ - Throws if called before `initWire`.
452
+
453
+ Use this if you need to adjust base URL, headers, or interceptors after startup.
454
+
455
+ ### `getWireConfig()`
456
+
457
+ ```ts
458
+ function getWireConfig(): WireConfig;
459
+ ```
460
+
461
+ - Returns the current configuration.
462
+ - Throws if called before `initWire`.
463
+ - Intended for advanced usage (e.g. custom hooks or libraries that build on top of fetchwire).
464
+
465
+ ---
466
+
467
+ ## API Reference
468
+
469
+ ### `wireApi<T>(endpoint, options?)`
470
+
471
+ ```ts
472
+ async function wireApi<T>(
473
+ endpoint: string,
474
+ options?: RequestInit & { headers?: Record<string, string> },
475
+ ): Promise<HttpResponse<T>>;
476
+ ```
477
+
478
+ - **`endpoint`**: Path relative to `baseUrl`, e.g. `'/invoice'`.
479
+ - **`options`**: Standard `fetch` options (method, body, headers, etc).
480
+ - **Return value**: Resolves to the parsed JSON body in the standard shape `{ data?: T; message?: string; status?: number }`.
481
+ - **Errors**: Throws `ApiError` on non-OK responses or network issues.
482
+
483
+ Usage:
484
+
485
+ ```ts
486
+ const result = await wireApi<UserResponse>('/user/me', { method: 'GET' });
487
+ // result.data is your typed data
488
+ // result.message and result.status are available if your backend provides them
489
+ ```
490
+
491
+ ---
492
+
493
+ ### `useFetchFn<T>(options?)`
494
+
495
+ ```ts
496
+ type FetchOptions = {
497
+ tags?: string[];
498
+ };
499
+
500
+ function useFetchFn<T>(options?: FetchOptions): {
501
+ data: T | null;
502
+ isLoading: boolean;
503
+ isRefreshing: boolean;
504
+ error: ApiError | null;
505
+ executeFetchFn: (fetchFn: () => Promise<{ data: T }>) => Promise<{ data: T } | null>;
506
+ refreshFetchFn: () => Promise<{ data: T } | null> | null;
507
+ };
508
+ ```
509
+
510
+ - **`options.tags`**: Optional array of tag strings to subscribe to. When a mutation invalidates these tags, `refreshFetchFn` is called automatically.
511
+ - **`executeFetchFn`**:
512
+ - Executes the provided async function.
513
+ - Updates `data`, `isLoading`, `error`.
514
+ - Stores the last function so it can be used by `refreshFetchFn`.
515
+ - **`refreshFetchFn`**:
516
+ - Re-runs the last `executeFetchFn` call, setting `isRefreshing` during the call.
517
+
518
+ ---
519
+
520
+ ### `useMutationFn<T>(options?)`
521
+
522
+ ```ts
523
+ type MutationOptions = {
524
+ invalidatesTags?: string[];
525
+ };
526
+
527
+ type ExecuteOptions<T> = {
528
+ onSuccess?: (data: T | null) => void;
529
+ onError?: (error: ApiError) => void;
530
+ };
531
+
532
+ function useMutationFn<T>(options?: MutationOptions): {
533
+ data: T | null;
534
+ isMutating: boolean;
535
+ executeMutationFn: (
536
+ mutationFn: () => Promise<{ data: T }>,
537
+ executeOptions?: ExecuteOptions<T>,
538
+ ) => Promise<{ data: T } | null>;
539
+ reset: () => void;
540
+ };
541
+ ```
542
+
543
+ - **`options.invalidatesTags`**:
544
+ - List of tags to emit after a **successful** mutation.
545
+ - All `useFetchFn` hooks that subscribed to any of these tags will be refreshed.
546
+ - **`executeMutationFn`**:
547
+ - Executes the provided `mutationFn`.
548
+ - Sets `isMutating` while running.
549
+ - On success:
550
+ - Updates `data`.
551
+ - Emits all `invalidatesTags`.
552
+ - Calls `onSuccess` with `response.data` (or `null`).
553
+ - On error:
554
+ - Resets `isMutating`.
555
+ - Calls `onError` with an `ApiError` instance.
556
+ - **`reset`**:
557
+ - Resets `data` and `isMutating` to initial values.
558
+
559
+ ---
560
+
561
+ ## License
562
+
563
+ **MIT License**
564
+
565
+ Copyright (c) Doanvinhphu
566
+
567
+ See the `LICENSE` file for details (or include the standard MIT text directly in your repository).
568
+