@alexfalconflores/safe-fetch 2.0.0 → 2.0.1
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/README.md +468 -94
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<h1 align="center">
|
|
2
|
-
|
|
2
|
+
safeFetch
|
|
3
3
|
<br />
|
|
4
4
|
<img src="https://github.com/alexfalconflores/safe-fetch/blob/4fd0a9af158b69fa3bab5861ce13c552203845d9/logo.svg" alt="safeFetch logo" width="150"/>
|
|
5
5
|
</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
<strong>Typed, production‑ready HTTP client for modern TypeScript apps.</strong><br />
|
|
9
|
+
A lightweight, zero‑dependency wrapper around <code>fetch</code> with retries, timeouts, interceptors, abortAll and strong typing.
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
@@ -25,15 +25,26 @@
|
|
|
25
25
|
|
|
26
26
|
## 🚀 Why safeFetch?
|
|
27
27
|
|
|
28
|
-
Native
|
|
28
|
+
Native `fetch` is great, but in real apps we always end up writing the same things:
|
|
29
29
|
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
- Repetitive error handling.
|
|
31
|
+
- Converting responses to JSON.
|
|
32
|
+
- Adding common headers.
|
|
33
|
+
- Dealing with timeouts, retries, AbortController, etc.
|
|
34
|
+
|
|
35
|
+
safeFetch solves all of this without the heaviness of Axios.
|
|
36
|
+
|
|
37
|
+
- ✨ Strong typing with `<T>` generics on responses.
|
|
38
|
+
- 🔄 Automatic retries on network errors or 5xx responses.
|
|
39
|
+
- ⏱️ Per‑request configurable timeouts.
|
|
40
|
+
- 🛑 `abortAll()` to cancel all pending requests for an instance.
|
|
41
|
+
- 🪝 Interceptors (`onRequest`, `onResponse`, `onResponseError`).
|
|
42
|
+
- 🧠 HTTP status handlers (on200, on401, on500, etc).
|
|
43
|
+
- 🔍 Debug mode with ready‑to‑paste cURL commands.
|
|
44
|
+
- 🍬 Sugar syntax: `.get()`, `.post()`, `.put()`, `.delete()`, `.patch()`.
|
|
45
|
+
- 📦 Typed headers for common cases (`Content-Type`, `Authorization`, `Accept`, etc).
|
|
46
|
+
|
|
47
|
+
---
|
|
37
48
|
|
|
38
49
|
## 📦 Installation
|
|
39
50
|
|
|
@@ -43,11 +54,13 @@ npm install @alexfalconflores/safe-fetch
|
|
|
43
54
|
bun add @alexfalconflores/safe-fetch
|
|
44
55
|
```
|
|
45
56
|
|
|
46
|
-
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 💻 Basic usage
|
|
47
60
|
|
|
48
|
-
### 1.
|
|
61
|
+
### 1. Default singleton
|
|
49
62
|
|
|
50
|
-
|
|
63
|
+
For simple cases, use the default `safeFetch` instance:
|
|
51
64
|
|
|
52
65
|
```ts
|
|
53
66
|
import safeFetch from "@alexfalconflores/safe-fetch";
|
|
@@ -57,161 +70,522 @@ interface User {
|
|
|
57
70
|
name: string;
|
|
58
71
|
}
|
|
59
72
|
|
|
60
|
-
|
|
73
|
+
/**
|
|
74
|
+
* 1) Sugar syntax (get/post)
|
|
75
|
+
* Great for most day‑to‑day use cases.
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
// Typed GET + automatic JSON parsing
|
|
61
79
|
const users = await safeFetch.get<User[]>("/api/users");
|
|
62
80
|
|
|
63
|
-
//
|
|
81
|
+
// POST with body automatically serialized to JSON
|
|
64
82
|
await safeFetch.post("/api/users", { name: "Alex" });
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 2) Core usage (no sugar)
|
|
86
|
+
* Same instance, but calling the underlying request function directly.
|
|
87
|
+
* Useful if you want full control over method, body and options.
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
// Equivalent GET without .get()
|
|
91
|
+
const usersCore = await safeFetch("/api/users", {
|
|
92
|
+
method: "GET",
|
|
93
|
+
timeout: 5000, // 5 segundos
|
|
94
|
+
// You can also add headers, params, timeout, etc. here
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Equivalent POST without .post()
|
|
98
|
+
await safeFetch("/api/users", {
|
|
99
|
+
method: "POST",
|
|
100
|
+
body: { name: "Alex" }, // Will be JSON‑stringified automatically
|
|
101
|
+
headers: {
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
},
|
|
104
|
+
});
|
|
65
105
|
```
|
|
66
106
|
|
|
67
|
-
|
|
107
|
+
Under the hood:
|
|
108
|
+
|
|
109
|
+
- If the method is not `GET`/`HEAD` and the body is an object, it will be serialized as JSON.
|
|
110
|
+
- If you don’t define `Content-Type`, `"application/json"` is used by default.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 🏭 Recommended pattern: `createSafeFetch
|
|
68
115
|
|
|
69
|
-
|
|
116
|
+
The ideal setup is to create **isolated instances per API** (backend, third‑party services, etc).
|
|
70
117
|
|
|
71
118
|
```ts
|
|
72
119
|
import { createSafeFetch } from "@alexfalconflores/safe-fetch";
|
|
73
120
|
|
|
74
|
-
const api = createSafeFetch({
|
|
75
|
-
|
|
121
|
+
export const api = createSafeFetch({
|
|
122
|
+
debug: true, // Imprime logs y cURL cuando algo falla
|
|
123
|
+
baseUrl: "https://api.myapp.com/v1",
|
|
76
124
|
headers: {
|
|
77
|
-
"
|
|
78
|
-
|
|
125
|
+
"Content-Type": "application/json",
|
|
126
|
+
},
|
|
127
|
+
onRequest: async (url, config) => {
|
|
128
|
+
const session = await getSession();
|
|
129
|
+
config.headers = {
|
|
130
|
+
...config.headers,
|
|
131
|
+
Authorization: `Bearer ${session?.backend_token}`,
|
|
132
|
+
...(await buildAuditHeaders()),
|
|
133
|
+
};
|
|
134
|
+
return config;
|
|
135
|
+
},
|
|
136
|
+
on401: async () => {
|
|
137
|
+
console.error("Unauthorized: Redirecting to login page.");
|
|
79
138
|
},
|
|
80
|
-
debug: true, // Prints cURL on error
|
|
81
139
|
});
|
|
82
140
|
|
|
83
|
-
//
|
|
84
|
-
|
|
141
|
+
// Ejemplo de uso
|
|
142
|
+
interface DashboardResponse {
|
|
143
|
+
stats: {
|
|
144
|
+
users: number;
|
|
145
|
+
sales: number;
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Sin Azucar
|
|
150
|
+
const res = await api("/dashboard", {
|
|
151
|
+
method: "GET",
|
|
152
|
+
timeout: TIMEOUT,
|
|
153
|
+
next: { tags: [GET_BRANDS_TAG], revalidate: CRUD_REVALIDATE_SECONDS }, // Si usas Next.js
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const data = await api.get<DashboardResponse>("/dashboard");
|
|
157
|
+
console.log(data.stats.users);
|
|
85
158
|
```
|
|
86
159
|
|
|
87
|
-
###
|
|
160
|
+
### Update configuration on the fly
|
|
88
161
|
|
|
89
|
-
|
|
162
|
+
You can update `baseUrl`, `headers` and `handlers` without creating a new instance.
|
|
90
163
|
|
|
91
|
-
|
|
164
|
+
A common pattern (especially in frameworks like Next.js) is to configure `safeFetch` once and then simply import it wherever you need it.
|
|
92
165
|
|
|
93
166
|
```ts
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
167
|
+
// config.ts
|
|
168
|
+
// Global configuration for the default safeFetch instance
|
|
169
|
+
|
|
170
|
+
import safeFetch from "@alexfalconflores/safe-fetch";
|
|
171
|
+
import { getSession } from "./auth";
|
|
172
|
+
import { buildAuditHeaders } from "./audit";
|
|
173
|
+
|
|
174
|
+
safeFetch.configure({
|
|
175
|
+
baseUrl: "http://localhost:8000/api/v1",
|
|
176
|
+
headers: {
|
|
177
|
+
"Content-Type": "application/json",
|
|
178
|
+
},
|
|
179
|
+
onRequest: async (url, config) => {
|
|
180
|
+
const session = await getSession();
|
|
181
|
+
config.headers = {
|
|
182
|
+
...config.headers,
|
|
183
|
+
Authorization: session?.backend_token
|
|
184
|
+
? `Bearer ${session.backend_token}`
|
|
185
|
+
: undefined,
|
|
186
|
+
...(await buildAuditHeaders()),
|
|
187
|
+
};
|
|
188
|
+
return config;
|
|
189
|
+
},
|
|
190
|
+
on401: async () => {
|
|
191
|
+
console.error("Unauthorized: Redirecting to login page.");
|
|
192
|
+
// redirectToLogin(); // your logic here
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Then import this configuration once at the top level of your app (so it runs before any requests):
|
|
198
|
+
|
|
199
|
+
> `app/layout.tsx` (or your main entry file)
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import "./config.ts";
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
From that point on, you can use the already configured `safeFetch` anywhere in your code:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import safeFetch from "@alexfalconflores/safe-fetch";
|
|
209
|
+
|
|
210
|
+
const usersCore = await safeFetch("/api/users", {
|
|
211
|
+
method: "GET",
|
|
212
|
+
timeout: 5000, // 5 segundos
|
|
213
|
+
// You can also add headers, params, timeout, etc. here
|
|
98
214
|
});
|
|
99
215
|
```
|
|
100
216
|
|
|
101
|
-
|
|
217
|
+
---
|
|
102
218
|
|
|
103
|
-
|
|
219
|
+
If you create a custom instance with `createSafeFetch`, you can also re‑configure it:
|
|
104
220
|
|
|
105
221
|
```ts
|
|
222
|
+
import { createSafeFetch } from "@alexfalconflores/safe-fetch";
|
|
223
|
+
|
|
106
224
|
const api = createSafeFetch({
|
|
107
|
-
|
|
108
|
-
|
|
225
|
+
baseUrl: "https://api.myapp.com",
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
api.configure({
|
|
229
|
+
headers: {
|
|
230
|
+
Authorization: `Bearer ${token}`,
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
> New configuration is merged with the existing one (headers are merged, not fully replaced).
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## 🪝 Interceptores y manejo global
|
|
240
|
+
|
|
241
|
+
### `onRequest`: inject token, tweak URL, etc.
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
const api = createSafeFetch({
|
|
245
|
+
baseUrl: "https://api.myapp.com",
|
|
246
|
+
async onRequest(url, config) {
|
|
109
247
|
const token = localStorage.getItem("token");
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
248
|
+
return {
|
|
249
|
+
...config,
|
|
250
|
+
headers: {
|
|
251
|
+
...config.headers,
|
|
252
|
+
Authorization: token ? `Bearer ${token}` : undefined,
|
|
253
|
+
},
|
|
254
|
+
};
|
|
114
255
|
},
|
|
256
|
+
});
|
|
257
|
+
```
|
|
115
258
|
|
|
116
|
-
|
|
117
|
-
onError: (err) => console.error("Network died:", err),
|
|
259
|
+
### `onResponse`: global logging
|
|
118
260
|
|
|
119
|
-
|
|
120
|
-
|
|
261
|
+
```ts
|
|
262
|
+
const api = createSafeFetch({
|
|
263
|
+
baseUrl: "https://api.myapp.com",
|
|
264
|
+
onResponse(response) {
|
|
265
|
+
console.log("[HTTP]", response.status, response.url);
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### `onResponseError`: refresh token / transparent retry
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
const api = createSafeFetch({
|
|
274
|
+
baseUrl: "https://api.myapp.com",
|
|
275
|
+
async onResponseError(response, attempt) {
|
|
276
|
+
// First 401 ➜ try to refresh token and retry
|
|
121
277
|
if (response.status === 401 && attempt === 0) {
|
|
122
|
-
await refreshToken(); //
|
|
123
|
-
// Retry the
|
|
124
|
-
return api.request(response.url, {
|
|
278
|
+
await refreshToken(); // Tu lógica
|
|
279
|
+
// Retry the same URL with the new configuration
|
|
280
|
+
return api.request(response.url, {
|
|
281
|
+
method: response.request?.method ?? "GET",
|
|
282
|
+
// You can pass the original init here if you store it yourself
|
|
283
|
+
responseType: "response",
|
|
284
|
+
});
|
|
125
285
|
}
|
|
126
|
-
}
|
|
286
|
+
},
|
|
127
287
|
});
|
|
128
288
|
```
|
|
129
289
|
|
|
130
|
-
|
|
290
|
+
> Note: in a real app, it’s usually better to store the original `init` so you can reuse it when retrying.
|
|
291
|
+
|
|
292
|
+
---
|
|
131
293
|
|
|
132
|
-
|
|
294
|
+
## 🧠 HTTP status handlers
|
|
295
|
+
|
|
296
|
+
safeFetch lets you attach callbacks to specific HTTP status codes.
|
|
297
|
+
|
|
298
|
+
Instead of doing `console.log`, it’s more realistic to delegate to shared services (auth, notifications, monitoring, etc.).
|
|
299
|
+
|
|
300
|
+
Example: centralize expired session handling, notifications and reporting.
|
|
301
|
+
|
|
302
|
+
Assume you have three simple utilities:
|
|
133
303
|
|
|
134
304
|
```ts
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
305
|
+
// auth.ts
|
|
306
|
+
export function forceLogout() {
|
|
307
|
+
// Clear tokens, global state, etc.
|
|
308
|
+
localStorage.removeItem("token");
|
|
309
|
+
// You can use your router here
|
|
310
|
+
window.location.href = "/login";
|
|
311
|
+
}
|
|
138
312
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
313
|
+
// notifications.ts
|
|
314
|
+
export function notifyError(message: string) {
|
|
315
|
+
// Here you could integrate Sonner, Toastify, Radix, etc.
|
|
316
|
+
// Por ejemplo:
|
|
317
|
+
// toast.error(message);
|
|
318
|
+
console.error("[UI ERROR]", message);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// monitoring.ts
|
|
322
|
+
export function reportError(error: unknown) {
|
|
323
|
+
// In a real project: send to Sentry, Datadog, LogRocket, etc.
|
|
324
|
+
console.error("[REPORT ERROR]", error);
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Ahora conectas todo desde `createSafeFetch`:
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
import { createSafeFetch } from "@alexfalconflores/safe-fetch";
|
|
332
|
+
import { forceLogout } from "./auth";
|
|
333
|
+
import { notifyError } from "./notifications";
|
|
334
|
+
import { reportError } from "./monitoring";
|
|
335
|
+
|
|
336
|
+
const api = createSafeFetch({
|
|
337
|
+
baseUrl: "https://api.myapp.com",
|
|
338
|
+
// 4xx: errores de cliente
|
|
339
|
+
async on400(response) {
|
|
340
|
+
// Example: extract validation errors and show them in the UI
|
|
341
|
+
try {
|
|
342
|
+
const data = await response.json();
|
|
343
|
+
const message =
|
|
344
|
+
data?.message ?? "Tu solicitud contiene datos inválidos (400).";
|
|
345
|
+
notifyError(message);
|
|
346
|
+
} catch {
|
|
347
|
+
notifyError("Tu solicitud contiene datos inválidos (400).");
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
async on401() {
|
|
351
|
+
// Session expired ➜ force logout and redirect to login
|
|
352
|
+
notifyError("Tu sesión ha expirado. Ingresa nuevamente.");
|
|
353
|
+
forceLogout();
|
|
354
|
+
},
|
|
355
|
+
on403() {
|
|
356
|
+
// No permissions ➜ show generic message
|
|
357
|
+
notifyError("No tienes permisos para realizar esta acción (403).");
|
|
358
|
+
},
|
|
359
|
+
on404() {
|
|
360
|
+
// Resource not found ➜ you may update a routing store, etc.
|
|
361
|
+
notifyError("Recurso no encontrado (404).");
|
|
362
|
+
},
|
|
363
|
+
// 5xx: server errors
|
|
364
|
+
async on500(response) {
|
|
365
|
+
notifyError("Estamos teniendo problemas en el servidor. Inténtalo más tarde.");
|
|
366
|
+
reportError({
|
|
367
|
+
status: response.status,
|
|
368
|
+
url: response.url,
|
|
369
|
+
});
|
|
370
|
+
},
|
|
371
|
+
// Network error (no HTTP response)
|
|
372
|
+
onError(error) {
|
|
373
|
+
notifyError("Parece que no tienes conexión a Internet.");
|
|
374
|
+
reportError(error);
|
|
375
|
+
},
|
|
376
|
+
});
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
In your business code you just use `api` normally:
|
|
380
|
+
```ts
|
|
381
|
+
// Example in a domain service / React hook
|
|
382
|
+
export async function fetchCurrentUser() {
|
|
383
|
+
const user = await api.get<User>("/me");
|
|
384
|
+
return user;
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
>💡 This keeps all network logic in one place (safeFetch + handlers)
|
|
388
|
+
and your UI only talks to utilities like `notifyError`, `forceLogout`, etc.
|
|
389
|
+
|
|
390
|
+
Besides specific handlers (`on200`, `on404`, `on503`, etc.), you also have:
|
|
391
|
+
|
|
392
|
+
- `onError(error)`: when the network fails (no Internet, DNS, CORS, timeout, abort).
|
|
393
|
+
- `onResponse(response)`: runs whenever there is a response (2xx, 3xx, 4xx, 5xx).
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## ⏱️ Timeouts and retries
|
|
398
|
+
|
|
399
|
+
### Per‑request timeout
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
const user = await api.get<User>("/users/1", {
|
|
403
|
+
timeout: 5000, // 5 segundos
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
If the timeout is exceeded:
|
|
408
|
+
|
|
409
|
+
- The request is aborted.
|
|
410
|
+
- An error is thrown with the message `Request timeout after 5000ms`.
|
|
411
|
+
|
|
412
|
+
### Retries on network / 5xx errors
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
const data = await api.get("/flaky-endpoint", {
|
|
416
|
+
retries: 3, // 3 retries
|
|
417
|
+
retryDelay: 1000, // 1s between retries
|
|
418
|
+
timeout: 4000, // Max 4s per attempt
|
|
419
|
+
});
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Execution flow:
|
|
423
|
+
|
|
424
|
+
1. Attempt 1 → fails (network or 5xx) → wait 1s.
|
|
425
|
+
2. Attempt 2 → fails → wait 1s.
|
|
426
|
+
3. Attempt 3 → fails → throw the last captured error.
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
## 🛑 Cancel all requests (`abortAll`)
|
|
431
|
+
|
|
432
|
+
Each `safeFetch` instance keeps its own set of `AbortController`s.
|
|
433
|
+
|
|
434
|
+
This is perfect for React when unmounting components:
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
import { useEffect, useState } from "react";
|
|
438
|
+
import { createSafeFetch } from "@alexfalconflores/safe-fetch";
|
|
439
|
+
|
|
440
|
+
const api = createSafeFetch({
|
|
441
|
+
baseUrl: "https://api.myapp.com",
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
function HeavyComponent() {
|
|
445
|
+
const [data, setData] = useState<any>(null);
|
|
446
|
+
|
|
447
|
+
useEffect(() => {
|
|
448
|
+
api
|
|
449
|
+
.get("/heavy-data")
|
|
450
|
+
.then(setData)
|
|
451
|
+
.catch((err) => {
|
|
452
|
+
if (err.message === "Request aborted by safeFetch.abortAll()") return;
|
|
453
|
+
console.error(err);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
return () => {
|
|
457
|
+
api.abortAll(); // Cancels ALL pending requests for this instance
|
|
458
|
+
};
|
|
459
|
+
}, []);
|
|
460
|
+
|
|
461
|
+
return <pre>{JSON.stringify(data, null, 2)}</pre>;
|
|
462
|
+
}
|
|
143
463
|
```
|
|
144
464
|
|
|
145
|
-
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## 🔍 Easy query params
|
|
146
468
|
|
|
147
|
-
Forget about URLSearchParams
|
|
469
|
+
Forget about building `URLSearchParams` manually:
|
|
148
470
|
|
|
149
471
|
```ts
|
|
150
|
-
// GET /search?q=books&page=
|
|
472
|
+
// Genera: GET /search?q=books&page=2&tags=a&tags=b
|
|
151
473
|
await api.get("/search", {
|
|
152
474
|
params: {
|
|
153
475
|
q: "books",
|
|
154
|
-
page:
|
|
155
|
-
tags: ["a", "b"]
|
|
156
|
-
}
|
|
476
|
+
page: 2,
|
|
477
|
+
tags: ["a", "b"],
|
|
478
|
+
},
|
|
157
479
|
});
|
|
158
480
|
```
|
|
159
481
|
|
|
160
|
-
|
|
482
|
+
- `undefined` and `null` are ignored.
|
|
483
|
+
- Arrays become multiple query params.
|
|
484
|
+
|
|
485
|
+
---
|
|
486
|
+
|
|
487
|
+
## 📥 Response types (`responseType`)
|
|
488
|
+
|
|
489
|
+
By default, safeFetch tries to parse the response as JSON (`responseType: "json"`).
|
|
490
|
+
If the backend does not return valid JSON, it falls back to returning the raw `text`.
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
// Plain text (HTML, CSV, etc.)
|
|
494
|
+
const html = await api.get<string>("/page", {
|
|
495
|
+
responseType: "text",
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// Blob (archivos, imágenes, PDFs)
|
|
499
|
+
const file = await api.get<Blob>("/report.pdf", {
|
|
500
|
+
responseType: "blob",
|
|
501
|
+
});
|
|
161
502
|
|
|
162
|
-
|
|
503
|
+
// ArrayBuffer (binary data for low‑level processing)
|
|
504
|
+
const buffer = await api.get<ArrayBuffer>("/binary", {
|
|
505
|
+
responseType: "arrayBuffer",
|
|
506
|
+
});
|
|
163
507
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|onRequest |function |`undefined` |Hook previo para modificar la configuración.|
|
|
170
|
-
|onResponse |function |`undefined` |Hook posterior para logging/analytics. |
|
|
171
|
-
|onResponseError|function |`undefined` |Hook de recuperación para errores 4xx/5xx. |
|
|
508
|
+
// Raw native Response (you handle parsing yourself)
|
|
509
|
+
const response = await api.get<Response>("/raw", {
|
|
510
|
+
responseType: "response",
|
|
511
|
+
});
|
|
512
|
+
```
|
|
172
513
|
|
|
173
|
-
|
|
174
|
-
Extends standard RequestInit with:
|
|
514
|
+
---
|
|
175
515
|
|
|
176
|
-
|
|
177
|
-
- params: Object for query string generation.
|
|
178
|
-
- timeout: Number in ms.
|
|
179
|
-
- retries: Number of retry attempts.
|
|
180
|
-
- responseType: 'json' | 'text' | 'blob' | 'arrayBuffer' | 'response'.
|
|
516
|
+
## 🧱 Typed headers (quality‑of‑life)
|
|
181
517
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
518
|
+
`HeadersType` gives you autocomplete and type safety for most common HTTP headers:
|
|
519
|
+
|
|
520
|
+
```ts
|
|
521
|
+
import {
|
|
522
|
+
createSafeFetch,
|
|
523
|
+
type HeadersType,
|
|
524
|
+
} from "@alexfalconflores/safe-fetch";
|
|
525
|
+
|
|
526
|
+
const defaultHeaders: HeadersType = {
|
|
527
|
+
"Content-Type": "application/json",
|
|
528
|
+
Authorization: `Bearer ${myToken}`,
|
|
529
|
+
Accept: "application/json",
|
|
530
|
+
"Cache-Control": "no-cache",
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
const api = createSafeFetch({
|
|
534
|
+
baseUrl: "https://api.myapp.com",
|
|
535
|
+
headers: {
|
|
536
|
+
"Content-Type": "application/json",
|
|
537
|
+
// Headers custom siguen siendo válidos
|
|
538
|
+
"X-Request-Source": "dashboard-web",
|
|
539
|
+
},
|
|
540
|
+
});
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
You still keep full flexibility for custom headers, while getting strong typing and autocomplete for the standard ones.
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
## 🛠 Utility: `Join`
|
|
548
|
+
|
|
549
|
+
Small helper to join strings or arrays:
|
|
185
550
|
|
|
186
551
|
```ts
|
|
187
552
|
import { Join } from "@alexfalconflores/safe-fetch";
|
|
188
|
-
|
|
553
|
+
|
|
554
|
+
const date = Join("-", "2025", "04", "19"); // "2025-04-19"
|
|
555
|
+
const classes = Join(" ", "btn", ["btn-primary", "btn-lg"]); // "btn btn-primary btn-lg"
|
|
189
556
|
```
|
|
190
557
|
|
|
558
|
+
---
|
|
559
|
+
|
|
191
560
|
## 🧩 Compatibility
|
|
192
561
|
|
|
193
562
|
Works in all modern runtimes:
|
|
194
563
|
|
|
195
564
|
- ✅ Browser (Chrome, Firefox, Safari, Edge)
|
|
196
|
-
- ✅ Node.js (v18+ or with polyfill)
|
|
565
|
+
- ✅ Node.js (v18+ or with a `fetch` polyfill)
|
|
197
566
|
- ✅ Bun
|
|
198
567
|
- ✅ Deno
|
|
199
568
|
|
|
200
|
-
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
## 👤 Author
|
|
201
572
|
|
|
202
573
|
Alex Stefano Falcon Flores
|
|
203
574
|
|
|
204
|
-
- 🐙 GitHub: [
|
|
205
|
-
- 💼 LinkedIn: [
|
|
575
|
+
- 🐙 GitHub: [alexfalconflores](https://github.com/alexfalconflores)
|
|
576
|
+
- 💼 LinkedIn: [alexfalconflores](https://www.linkedin.com/in/alexfalconflores/)
|
|
577
|
+
- 🌐 Website: [alexfalconflores](https://www.alexfalconflores.com/)
|
|
206
578
|
|
|
207
|
-
|
|
579
|
+
---
|
|
208
580
|
|
|
209
|
-
|
|
581
|
+
## 📄 Licencia
|
|
582
|
+
|
|
583
|
+
This project is licensed under the MIT license. See the LICENSE ↗ file for more details.
|
|
210
584
|
|
|
211
585
|
<p align="center">
|
|
212
|
-
|
|
586
|
+
|
|
587
|
+
⭐ <strong>Find it useful?</strong> Give it a star on GitHub.<br />
|
|
588
|
+
|
|
213
589
|
Built with ❤️ in Peru 🇵🇪
|
|
214
|
-
</p>
|
|
215
590
|
|
|
216
|
-
|
|
217
|
-
And if you use it in your projects, I'd love to see it! 🎉
|
|
591
|
+
</p>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexfalconflores/safe-fetch",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "The production-ready, typed HTTP client with built-in retries, timeouts, interceptors, and cancellation.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|