create-request 0.0.1-beta.10

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) 2025 Daniel Amenou
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,547 @@
1
+ # create-request
2
+
3
+ [![npm version](https://img.shields.io/npm/v/create-request.svg)](https://www.npmjs.com/package/create-request)
4
+ [![License](https://img.shields.io/npm/l/create-request.svg)](https://github.com/DanielAmenou/create-request/blob/main/LICENSE)
5
+ [![Bundle Size](https://img.shields.io/bundlephobia/minzip/create-request)](https://bundlephobia.com/package/create-request)
6
+
7
+ `create-request` is a modern TypeScript library that transforms how you make API calls. Built as an elegant wrapper around the native Fetch API, it provides a chainable, fluent interface that dramatically reduces boilerplate while adding powerful features like automatic retries, timeout handling, and comprehensive error management.
8
+
9
+ ## Core Features
10
+
11
+ - 🚀 **Performance** - Tiny bundle size with zero dependencies
12
+ - 🚧 **Error Handling** - Detailed error info with custom error class
13
+ - ⛓️ **Chainable API** - Build and execute requests with a fluent interface
14
+ - ⏱️ **Timeout Support** - Set timeouts for requests with automatic aborts
15
+ - 🛡️ **Type Safety** - Full TypeScript support with intelligent type inference
16
+ - 🔐 **Auth Helpers** - Simple methods for common authentication patterns
17
+ - 🔍 **Data Selection** - Extract and transform specific data from responses
18
+ - 🔁 **Automatic Retries** - Retry failed requests with customizable settings
19
+ - 📉 **Reduced Boilerplate** - Write 60% less code for common API operations
20
+ - 🔒 **CSRF Protection** - Built-in safeguards against cross-site request forgery
21
+ - 📝 **Response Caching** - Flexible caching system with multiple storage options
22
+ - 🛑 **Request Cancellation** - Abort requests on demand with AbortController integration
23
+
24
+ ## Why create-request?
25
+
26
+ **API interactions often require repetitive code patterns** - handling HTTP status checks, parsing responses, managing errors, and dealing with TypeScript types. `create-request` provides a clean, efficient solution with an elegant API that separates request building from execution:
27
+
28
+ ```typescript
29
+ // Before: Regular fetch
30
+ async function createUser(userData) {
31
+ try {
32
+ const response = await fetch('https://api.example.com/users', {
33
+ method: 'POST',
34
+ headers: {
35
+ 'Content-Type': 'application/json',
36
+ 'Authorization': 'Basic ' + btoa('username:password')
37
+ },
38
+ body: JSON.stringify(userData)
39
+ });
40
+
41
+ if (!response.ok) {
42
+ throw new Error(`HTTP error! status: ${response.status}`);
43
+ }
44
+ const data = await response.json();
45
+ return data;
46
+ } catch (error) {
47
+ console.error('Fetch error:', error);
48
+ throw error;
49
+ }
50
+ }
51
+
52
+ // After: With create-request
53
+ import create from 'create-request';
54
+
55
+ function createUser(userData) {
56
+ return create.post()
57
+ .withBasicAuth('username', 'password')
58
+ .withBody(userData)
59
+ .sendTo('https://api.example.com/users')
60
+ .getData<User>()
61
+ .catch(error => {
62
+ console.error('Fetch error:', error);
63
+ throw error;
64
+ });
65
+ }
66
+ ```
67
+
68
+ ## Installation
69
+
70
+ ```bash
71
+ npm install create-request
72
+ ```
73
+
74
+ Or with yarn:
75
+
76
+ ```bash
77
+ yarn add create-request
78
+ ```
79
+
80
+ ## Basic Usage
81
+
82
+ ### Creating Requests
83
+
84
+ ```typescript
85
+ import create from 'create-request';
86
+
87
+ // Create different request types
88
+ const getRequest = create.get();
89
+ const postRequest = create.post();
90
+ const putRequest = create.put();
91
+ const headRequest = create.head();
92
+ const deleteRequest = create.del();
93
+ const patchRequest = create.patch();
94
+ const optionsRequest = create.options();
95
+ ```
96
+
97
+ ### Request Configuration
98
+
99
+ The library provides a comprehensive set of configuration methods that can be chained together to customize your requests:
100
+
101
+ ```typescript
102
+ import create, { RequestPriority, CredentialsPolicy, RequestMode, CacheMode, RedirectMode, ReferrerPolicy } from 'create-request';
103
+
104
+ // Configure request options
105
+ const request = create.get()
106
+ // Basic headers
107
+ .withHeaders({ 'X-API-Key': 'abc123', 'Accept-Language': 'en-US' })
108
+ .withHeader('Custom-Header', 'value') // Add a single header
109
+
110
+ // Timeout settings
111
+ .withTimeout(5000) // Request will abort after 5 seconds
112
+
113
+ // Automatic retry configuration
114
+ .withRetries(3) // Retry up to 3 times on failure
115
+ .onRetry(({ attempt, error }) => {
116
+ console.log(`Attempt ${attempt} failed: ${error.message}. Retrying...`);
117
+ })
118
+
119
+ // Authentication methods
120
+ .withBearerToken('your-token') // Adds Authorization: Bearer your-token
121
+ .withBasicAuth('username', 'password') // HTTP Basic Authentication
122
+
123
+ // URL parameters
124
+ .withQueryParams({ search: 'term', page: 1, limit: 20 })
125
+ .withQueryParam('filter', 'active') // Add a single query parameter
126
+
127
+ // Request body configuration (for POST/PUT/PATCH)
128
+ .withContentType('application/json') // Set specific content type
129
+
130
+ // Fetch API options
131
+ .withCredentials(CredentialsPolicy.INCLUDE) // Includes cookies with cross-origin requests
132
+ .withMode(RequestMode.CORS) // Controls CORS behavior
133
+ .withRedirect(RedirectMode.FOLLOW) // Controls redirect behavior (follow, error, manual)
134
+ .withReferrer('https://example.com') // Sets request referrer
135
+ .withReferrerPolicy(ReferrerPolicy.NO_REFERRER_WHEN_DOWNGRADE) // Controls referrer policy
136
+ .withPriority(RequestPriority.HIGH) // Sets request priority
137
+ ```
138
+
139
+ Each configuration method returns the request object, allowing for a fluent interface where methods can be chained together. You can configure only what you need for a specific request:
140
+
141
+ ```typescript
142
+ // Simple example with just what's needed
143
+ const searchUsers = create.get()
144
+ .withBearerToken(userToken)
145
+ .withQueryParams({ q: searchTerm, limit: 20 })
146
+ .withTimeout(3000);
147
+
148
+ // Now execute the request
149
+ const users = await searchUsers.sendTo('https://api.example.com/users').getData();
150
+ ```
151
+
152
+ You can also create reusable base requests with common settings:
153
+
154
+ ```typescript
155
+ // Create base authenticated request
156
+ const apiBase = create.get()
157
+ .withHeaders({
158
+ 'X-API-Version': '1.2',
159
+ 'Accept-Language': 'en-US'
160
+ })
161
+ .withBearerToken(authToken)
162
+ .withTimeout(5000)
163
+ .withRetries(2);
164
+
165
+ // Use the base request for different endpoints
166
+ const users = await apiBase.sendTo('https://api.example.com/users').getData();
167
+ const products = await apiBase.sendTo('https://api.example.com/products').getData();
168
+ ```
169
+
170
+ ### Request Bodies (POST/PUT/PATCH)
171
+
172
+ ```typescript
173
+ // JSON body
174
+ const jsonRequest = create.post()
175
+ .withBody({ name: 'John', age: 30 });
176
+
177
+ // String body
178
+ const textRequest = create.post()
179
+ .withBody('Plain text content');
180
+
181
+ // Form data
182
+ const formData = new FormData();
183
+ formData.append('name', 'John');
184
+ formData.append('file', fileBlob);
185
+
186
+ const formRequest = create.post()
187
+ .withBody(formData);
188
+ ```
189
+
190
+ ### Executing Requests
191
+
192
+ ```typescript
193
+ // Simple execution
194
+ const response = await request.sendTo('https://api.example.com/endpoint');
195
+
196
+ // With direct data extraction
197
+ const jsonData = await request.sendTo('https://api.example.com/endpoint').getJson();
198
+ const textData = await request.sendTo('https://api.example.com/endpoint').getText();
199
+ const blobData = await request.sendTo('https://api.example.com/endpoint').getBlob();
200
+ const bodyStream = await request.sendTo('https://api.example.com/endpoint').getBody();
201
+ const arrayBuffer = await request.sendTo('https://api.example.com/endpoint').getArrayBuffer();
202
+
203
+ // Using the data selector API to extract specific data
204
+ const userData = await request.sendTo('https://api.example.com/users')
205
+ .getData(data => data.results.users);
206
+
207
+ // Using the data selector without a selector function just returns the full JSON response
208
+ const fullData = await request.sendTo('https://api.example.com/data').getData();
209
+ ```
210
+
211
+ ### Data Selection
212
+
213
+ The `getData` method provides a powerful way to extract and transform specific data from API responses:
214
+
215
+ ```typescript
216
+ // Extract specific properties from nested structures
217
+ const posts = await request
218
+ .sendTo('https://api.example.com/feed')
219
+ .getData(data => data.feed.posts);
220
+
221
+ // Transform data in the selector function
222
+ const usernames = await request
223
+ .sendTo('https://api.example.com/users')
224
+ .getData(data => data.users.map(user => user.username));
225
+
226
+ // Apply filtering in the selector
227
+ const activeUsers = await request
228
+ .sendTo('https://api.example.com/users')
229
+ .getData(data => data.users.filter(user => user.isActive));
230
+
231
+ // Combine data from complex nested structures
232
+ const combinedData = await request
233
+ .sendTo('https://api.example.com/dashboard')
234
+ .getData(data => ({
235
+ userCount: data.stats.users.total,
236
+ recentPosts: data.content.recent.slice(0, 5),
237
+ notifications: data.user.notifications.unread
238
+ }));
239
+ ```
240
+
241
+ When a selector fails, the error message will contain the original response data to help diagnose the issue:
242
+
243
+ ```typescript
244
+ try {
245
+ const result = await request
246
+ .sendTo('https://api.example.com/data')
247
+ .getData(data => data.results.items); // Will fail if structure is different
248
+ } catch (error) {
249
+ console.error(error);
250
+ // Error message includes the original response data
251
+ }
252
+ ```
253
+
254
+ ### Error Handling
255
+
256
+ All errors from requests are instances of `RequestError` with detailed information:
257
+
258
+ ```typescript
259
+ try {
260
+ const data = await request.sendTo('https://api.example.com/data').getJson();
261
+ } catch (error) {
262
+ // error will always be a RequestError
263
+ console.log(error.message); // Error message
264
+ console.log(error.status); // HTTP status code (if available)
265
+ console.log(error.url); // Request URL
266
+ console.log(error.method); // HTTP method
267
+ console.log(error.timeoutError); // Whether it was a timeout
268
+
269
+ // Access the original response if available
270
+ if (error.response) {
271
+ // Raw Response object is available
272
+ console.log(error.response.status);
273
+ }
274
+ }
275
+ ```
276
+
277
+ ### Caching Requests
278
+
279
+ ```typescript
280
+ import create, { createMemoryStorage, createLocalStorageStorage, createSessionStorageStorage } from 'create-request';
281
+
282
+ // Simple in-memory caching
283
+ const request = create.get()
284
+ .withCache({
285
+ storage: createMemoryStorage(),
286
+ ttl: 60000 // 1 minute in milliseconds
287
+ })
288
+ .sendTo('https://api.example.com/data');
289
+
290
+ // Using localStorage storage
291
+ const request = create.get()
292
+ .withCache({
293
+ storage: createLocalStorageStorage(),
294
+ ttl: 5 * 60 * 1000, // 5 minutes
295
+ maxSize: '1MB',
296
+ keyPrefix: 'user-data'
297
+ })
298
+ .sendTo('https://api.example.com/users');
299
+
300
+ // Using sessionStorage for per-session caching
301
+ const request = create.get()
302
+ .withCache({
303
+ storage: createSessionStorageStorage(),
304
+ maxEntries: 50,
305
+ varyByHeaders: ['x-api-version']
306
+ })
307
+ .sendTo('https://api.example.com/data');
308
+ ```
309
+
310
+ ## Advanced Usage
311
+
312
+ ### Request Reuse
313
+
314
+ ```typescript
315
+ // Create a base authenticated request
316
+ const authRequest = create.get()
317
+ .withBearerToken('token123')
318
+ .withTimeout(5000)
319
+ .withRetries(2);
320
+
321
+ // Reuse for different endpoints
322
+ const users = await authRequest.sendTo('https://api.example.com/users').getJson();
323
+ const products = await authRequest.sendTo('https://api.example.com/products').getJson();
324
+ ```
325
+
326
+ ### Request Cancellation
327
+
328
+ ```typescript
329
+ const controller = new AbortController();
330
+
331
+ const request = create.get()
332
+ .withTimeout(10000)
333
+ .withAbortController(controller);
334
+
335
+ // Later, cancel the request if needed
336
+ setTimeout(() => controller.abort(), 2000);
337
+
338
+ try {
339
+ const data = await request.sendTo('https://api.example.com/slow-endpoint').getJson();
340
+ } catch (error) {
341
+ // Check if request was cancelled
342
+ console.log('Request was cancelled:', error.name === 'AbortError');
343
+ }
344
+ ```
345
+
346
+ ### Working with Typed Responses
347
+
348
+ ```typescript
349
+ interface User {
350
+ id: number;
351
+ name: string;
352
+ email: string;
353
+ }
354
+
355
+ interface ApiResponse {
356
+ users: User[];
357
+ pagination: {
358
+ total: number;
359
+ page: number;
360
+ }
361
+ }
362
+
363
+ const userRequest = create.get();
364
+
365
+ // Type the full response
366
+ const response = await userRequest
367
+ .sendTo('https://api.example.com/users')
368
+ .getJson<ApiResponse>();
369
+
370
+ // Or use getData with type parameters
371
+ const users = await userRequest
372
+ .sendTo('https://api.example.com/users')
373
+ .getData<ApiResponse, User[]>(data => data.users);
374
+
375
+ // Or just get the full typed response
376
+ const fullData = await userRequest
377
+ .sendTo('https://api.example.com/users')
378
+ .getData<ApiResponse>();
379
+
380
+ // TypeScript knows the types
381
+ console.log(users[0].name);
382
+ ```
383
+
384
+ ### Promise Chaining
385
+
386
+ ```typescript
387
+ const request = create.get();
388
+
389
+ request.sendTo('https://api.example.com/data')
390
+ .then(response => {
391
+ console.log('Status:', response.status);
392
+ return response.getJson();
393
+ })
394
+ .then(data => {
395
+ console.log('Data:', data);
396
+ })
397
+ .catch(error => {
398
+ console.error('Error:', error.message);
399
+ });
400
+ ```
401
+
402
+ ### Advanced Caching
403
+
404
+ ```typescript
405
+ import create, { createMemoryStorage, StorageProvider } from 'create-request';
406
+
407
+ // Custom key generation
408
+ const request = create.get()
409
+ .withCache({
410
+ storage: createMemoryStorage(),
411
+ keyGenerator: (url, method, headers) => {
412
+ // Include user ID from authorization header in the cache key
413
+ const authHeader = headers?.['Authorization'] || '';
414
+ const userMatch = authHeader.match(/User-(\d+)/);
415
+ const userId = userMatch ? userMatch[1] : 'anonymous';
416
+ return `${method}:${userId}:${url}`;
417
+ }
418
+ })
419
+ .sendTo('https://api.example.com/data');
420
+ ```
421
+
422
+ ### Implementing Custom Storage Providers
423
+
424
+ You can create your own storage provider by implementing the `StorageProvider` interface:
425
+
426
+ ```typescript
427
+ import { StorageProvider } from 'create-request';
428
+
429
+ // Create a simple custom storage provider
430
+ export function createNamespacedStorage(namespace: string): StorageProvider {
431
+ return {
432
+ get(key: string): string | null {
433
+ return localStorage.getItem(`${namespace}:${key}`);
434
+ },
435
+
436
+ set(key: string, value: string): void {
437
+ try {
438
+ localStorage.setItem(`${namespace}:${key}`, value);
439
+ } catch (e) {
440
+ // Handle quota exceeded errors
441
+ console.warn("Storage quota exceeded, clearing cache");
442
+ this.clear();
443
+ try {
444
+ localStorage.setItem(`${namespace}:${key}`, value);
445
+ } catch (e) {
446
+ console.error("Failed to set cache item even after clearing cache");
447
+ }
448
+ }
449
+ },
450
+
451
+ has(key: string): boolean {
452
+ return localStorage.getItem(`${namespace}:${key}`) !== null;
453
+ },
454
+
455
+ delete(key: string): void {
456
+ localStorage.removeItem(`${namespace}:${key}`);
457
+ },
458
+
459
+ clear(): void {
460
+ // Only clear items in our namespace
461
+ Object.keys(localStorage)
462
+ .filter(key => key.startsWith(`${namespace}:`))
463
+ .forEach(key => localStorage.removeItem(key));
464
+ }
465
+ };
466
+ }
467
+
468
+ // Usage:
469
+ const request = create.get()
470
+ .withCache({
471
+ storage: createNamespacedStorage('api-cache'),
472
+ ttl: 24 * 60 * 60 * 1000 // 24 hours
473
+ })
474
+ .sendTo('https://api.example.com/data');
475
+ ```
476
+
477
+ With custom storage providers, you can integrate with any storage system, such as:
478
+
479
+ - Custom browser storage solutions
480
+ - Memory caches with expiration policies
481
+ - Session-based storage mechanisms
482
+ - Encrypted storage providers
483
+ - Third-party storage libraries
484
+
485
+ ## CSRF Protection
486
+
487
+ Cross-Site Request Forgery (CSRF) is a type of security vulnerability where unauthorized commands are executed on behalf of an authenticated user. `create-request` provides built-in protection mechanisms to help prevent CSRF attacks.
488
+
489
+ ### How CSRF Protection Works
490
+
491
+ The library employs multiple strategies to protect against CSRF attacks:
492
+
493
+ 1. **Automatic X-Requested-With Header**: By default, all requests include the `X-Requested-With: XMLHttpRequest` header, which helps servers identify legitimate AJAX requests.
494
+
495
+ 2. **CSRF Token Support**: The library can automatically include CSRF tokens in request headers, which servers can validate to ensure the request came from your application.
496
+
497
+ 3. **XSRF Cookie Reading**: For frameworks that use the double-submit cookie pattern (like Laravel, Rails, or Django), the library can automatically read XSRF tokens from cookies and include them in request headers.
498
+
499
+ ### Global CSRF Configuration
500
+
501
+ You can configure CSRF settings globally for all requests:
502
+
503
+ ```typescript
504
+ // Configure CSRF settings for all requests
505
+ create.config.setCsrfToken('your-csrf-token');
506
+ create.config.setCsrfHeaderName('X-CSRF-Token'); // Default header name
507
+ create.config.setXsrfCookieName('XSRF-TOKEN'); // Default cookie name
508
+ create.config.setEnableAntiCsrf(true); // Enable/disable X-Requested-With header
509
+ create.config.setEnableAutoXsrf(true); // Enable/disable automatic cookie-to-header token
510
+ ```
511
+
512
+ ### Per-Request CSRF Settings
513
+
514
+ You can also configure CSRF protection on individual requests:
515
+
516
+ ```typescript
517
+ // Configure CSRF for a specific request
518
+ const request = create.post()
519
+ .withCsrfToken('request-specific-token') // Set a specific token
520
+ .withAntiCsrfHeaders() // Explicitly add X-Requested-With header
521
+ .withoutCsrfProtection(); // Or disable all automatic CSRF protection
522
+ ```
523
+
524
+ ### Integrating with Backend Frameworks
525
+
526
+ Most modern frameworks support CSRF protection out of the box. The library works seamlessly with:
527
+
528
+ - **Laravel**: Automatically reads XSRF-TOKEN cookie and sends X-XSRF-TOKEN header
529
+ - **Rails**: Works with the Rails CSRF token system
530
+ - **Django**: Compatible with Django's CSRF middleware
531
+ - **Express.js + csurf**: Works with the csurf middleware token pattern
532
+
533
+ When your server sends a CSRF token in a cookie or response header, `create-request` can automatically extract and include it in subsequent requests.
534
+
535
+ ## Browser Support
536
+
537
+ This library works with all browsers that support the Fetch API:
538
+
539
+ - Chrome 42+
540
+ - Firefox 39+
541
+ - Safari 10.1+
542
+ - Edge 14+
543
+ - Opera 29+
544
+
545
+ ## License
546
+
547
+ MIT