create-request 0.0.1-beta.9 → 1.1.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/README.md CHANGED
@@ -3,36 +3,58 @@
3
3
  [![npm version](https://img.shields.io/npm/v/create-request.svg)](https://www.npmjs.com/package/create-request)
4
4
  [![License](https://img.shields.io/npm/l/create-request.svg)](https://github.com/DanielAmenou/create-request/blob/main/LICENSE)
5
5
  [![Bundle Size](https://img.shields.io/bundlephobia/minzip/create-request)](https://bundlephobia.com/package/create-request)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-4.7%2B-blue)](https://www.typescriptlang.org/)
6
7
 
7
8
  `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
 
10
+ ## Table of Contents
11
+
12
+ - [Core Features](#core-features)
13
+ - [Why create-request](#why-create-request)
14
+ - [Installation](#installation)
15
+ - [Basic Usage](#basic-usage)
16
+ - [Advanced Usage](#advanced-usage)
17
+ - [GraphQL Support](#graphql-support)
18
+ - [Interceptors](#interceptors)
19
+ - [TypeScript Support](#typescript-support)
20
+ - [CSRF Protection](#csrf-protection)
21
+ - [Performance Considerations](#performance-considerations)
22
+ - [Browser Support](#browser-support)
23
+ - [Comparison of JavaScript HTTP Client Libraries](#comparison-of-javascript-http-client-libraries)
24
+ - [License](#license)
25
+
9
26
  ## Core Features
10
27
 
11
- - ⏱️ **Timeout Support** - Set timeouts for any request
12
28
  - 🚀 **Performance** - Tiny bundle size with zero dependencies
13
29
  - 🚧 **Error Handling** - Detailed error info with custom error class
14
- - 🔐 **Auth Helpers** - Simple methods for common authentication patterns
15
30
  - ⛓️ **Chainable API** - Build and execute requests with a fluent interface
16
- - 📉 **Reduced Boilerplate** - Write 60% less code for common API operations
17
- - 🔁 **Automatic Retries** - Retry failed requests with customizable settings
31
+ - ⏱️ **Timeout Support** - Set timeouts for requests with automatic aborts
18
32
  - 🛡️ **Type Safety** - Full TypeScript support with intelligent type inference
19
- - 📝 **Response Caching** - Flexible caching system with multiple storage options
33
+ - 🔐 **Auth Helpers** - Simple methods for common authentication patterns
34
+ - 🔍 **Data Selection** - Extract and transform specific data from responses
35
+ - 🔁 **Automatic Retries** - Retry failed requests with customizable settings
36
+ - 📉 **Reduced Boilerplate** - Write 60% less code for common API operations
37
+ - 🔒 **CSRF Protection** - Built-in safeguards against cross-site request forgery
38
+ - 🛑 **Request Cancellation** - Abort requests on demand with AbortController integration
39
+ - 🔌 **Interceptors** - Global and per-request interceptors for requests, responses, and errors
40
+ - 🔷 **GraphQL Support** - Built-in GraphQL query and mutation helpers
20
41
 
21
42
  ## Why create-request?
22
43
 
23
44
  **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:
24
45
 
46
+ ### With Regular Fetch
47
+
25
48
  ```typescript
26
- // Before: Regular fetch
27
49
  async function createUser(userData) {
28
50
  try {
29
- const response = await fetch('https://api.example.com/users', {
30
- method: 'POST',
51
+ const response = await fetch("https://api.example.com/users", {
52
+ method: "POST",
31
53
  headers: {
32
- 'Content-Type': 'application/json',
33
- 'Authorization': 'Basic ' + btoa('username:password')
54
+ "Content-Type": "application/json",
55
+ Authorization: "Basic " + btoa("username:password"),
34
56
  },
35
- body: JSON.stringify(userData)
57
+ body: JSON.stringify(userData),
36
58
  });
37
59
 
38
60
  if (!response.ok) {
@@ -41,22 +63,25 @@ async function createUser(userData) {
41
63
  const data = await response.json();
42
64
  return data;
43
65
  } catch (error) {
44
- console.error('Fetch error:', error);
66
+ console.error("Fetch error:", error);
45
67
  throw error;
46
68
  }
47
69
  }
70
+ ```
48
71
 
49
- // After: With create-request
50
- import create from 'create-request';
72
+ ### With create-request
73
+
74
+ ```typescript
75
+ import create from "create-request";
51
76
 
52
77
  function createUser(userData) {
53
- return create.post()
54
- .withBasicAuth('username', 'password')
55
- .withBody(userData)
56
- .sendTo('https://api.example.com/users')
57
- .getData<User>()
78
+ return create
79
+ .post("https://api.example.com/users")
80
+ .withBasicAuth("username", "password")
81
+ .withBody(userData) // Content-Type automatically set to application/json
82
+ .getData<User>() // Type-safe response handling
58
83
  .catch(error => {
59
- console.error('Fetch error:', error);
84
+ console.error("Fetch error:", error);
60
85
  throw error;
61
86
  });
62
87
  }
@@ -65,13 +90,14 @@ function createUser(userData) {
65
90
  ## Installation
66
91
 
67
92
  ```bash
93
+ # npm
68
94
  npm install create-request
69
- ```
70
95
 
71
- Or with yarn:
72
-
73
- ```bash
96
+ # yarn
74
97
  yarn add create-request
98
+
99
+ # pnpm
100
+ pnpm add create-request
75
101
  ```
76
102
 
77
103
  ## Basic Usage
@@ -79,16 +105,16 @@ yarn add create-request
79
105
  ### Creating Requests
80
106
 
81
107
  ```typescript
82
- import create from 'create-request';
83
-
84
- // Create different request types
85
- const getRequest = create.get();
86
- const postRequest = create.post();
87
- const putRequest = create.put();
88
- const headRequest = create.head();
89
- const deleteRequest = create.del();
90
- const patchRequest = create.patch();
91
- const optionsRequest = create.options();
108
+ import create from "create-request";
109
+
110
+ // Create different request types with URL
111
+ const getRequest = create.get("https://api.example.com/users"); // GET
112
+ const putRequest = create.put("https://api.example.com/users/1"); // PUT
113
+ const postRequest = create.post("https://api.example.com/users"); // POST
114
+ const headRequest = create.head("https://api.example.com/users/1"); // HEAD
115
+ const patchRequest = create.patch("https://api.example.com/users/1"); // PATCH
116
+ const deleteRequest = create.del("https://api.example.com/users/1"); // DELETE
117
+ const optionsRequest = create.options("https://api.example.com/users"); // OPTIONS
92
118
  ```
93
119
 
94
120
  ### Request Configuration
@@ -96,13 +122,20 @@ const optionsRequest = create.options();
96
122
  The library provides a comprehensive set of configuration methods that can be chained together to customize your requests:
97
123
 
98
124
  ```typescript
99
- import create, { RequestPriority, CredentialsPolicy, RequestMode, CacheMode, RedirectMode, ReferrerPolicy } from 'create-request';
125
+ import create, {
126
+ RequestPriority,
127
+ CredentialsPolicy,
128
+ RedirectMode,
129
+ ReferrerPolicy,
130
+ SameSitePolicy,
131
+ } from "create-request";
100
132
 
101
133
  // Configure request options
102
- const request = create.get()
134
+ const request = create
135
+ .get("https://api.example.com/users")
103
136
  // Basic headers
104
- .withHeaders({ 'X-API-Key': 'abc123', 'Accept-Language': 'en-US' })
105
- .withHeader('Custom-Header', 'value') // Add a single header
137
+ .withHeaders({ "X-API-Key": "abc123", "Accept-Language": "en-US" })
138
+ .withHeader("Custom-Header", "value") // Add a single header
106
139
 
107
140
  // Timeout settings
108
141
  .withTimeout(5000) // Request will abort after 5 seconds
@@ -114,138 +147,209 @@ const request = create.get()
114
147
  })
115
148
 
116
149
  // Authentication methods
117
- .withBearerToken('your-token') // Adds Authorization: Bearer your-token
118
- .withBasicAuth('username', 'password') // HTTP Basic Authentication
150
+ .withBearerToken("your-token") // Adds Authorization: Bearer your-token
151
+ .withBasicAuth("username", "password") // HTTP Basic Authentication
152
+ .withAuthorization("auth-scheme value") // Custom authorization header
153
+
154
+ // Add a single cookie
155
+ .withCookie("language", "en-US")
156
+
157
+ // Add multiple cookies
158
+ .withCookies({
159
+ sessionId: "abc123",
160
+ preferences: { value: "dark-mode", secure: true },
161
+ tracking: { value: "enabled", sameSite: SameSitePolicy.STRICT },
162
+ })
119
163
 
120
- // URL parameters
121
- .withQueryParams({ search: 'term', page: 1, limit: 20 })
122
- .withQueryParam('filter', 'active') // Add a single query parameter
164
+ // URL parameters (supports arrays, null/undefined filtering, and all types)
165
+ .withQueryParams({ search: "term", page: 1, limit: 20, tags: ["js", "ts"] })
166
+ .withQueryParam("filter", "active") // Add a single query parameter
167
+ .withQueryParam("ids", [1, 2, 3]) // Array values create multiple query params
123
168
 
124
169
  // Request body configuration (for POST/PUT/PATCH)
125
- .withContentType('application/json') // Set specific content type
170
+ .withContentType("application/json") // Set specific content type
126
171
 
127
172
  // Fetch API options
128
- .withCredentials(CredentialsPolicy.INCLUDE) // Includes cookies with cross-origin requests
129
- .withMode(RequestMode.CORS) // Controls CORS behavior
130
- .withRedirect(RedirectMode.FOLLOW) // Controls redirect behavior (follow, error, manual)
131
- .withReferrer('https://example.com') // Sets request referrer
132
- .withReferrerPolicy(ReferrerPolicy.NO_REFERRER_WHEN_DOWNGRADE) // Controls referrer policy
133
- .withPriority(RequestPriority.HIGH) // Sets request priority
173
+ // Note: These methods support three styles - Fluent API (shown below), Enum-based (e.g., .withMode(RequestMode.CORS)), or String-based (e.g., .withMode("cors"))
174
+ .withCredentials.INCLUDE() // Includes cookies with cross-origin requests
175
+ .withMode.CORS() // Controls CORS behavior
176
+ .withRedirect.FOLLOW() // Controls redirect behavior (follow, error, manual)
177
+ .withReferrer("https://example.com") // Sets request referrer
178
+ .withReferrerPolicy.NO_REFERRER_WHEN_DOWNGRADE() // Controls referrer policy
179
+ .withPriority.HIGH() // Sets request priority
180
+ .withKeepAlive(true); // Keeps connection alive after the page is unloaded
134
181
  ```
135
182
 
136
183
  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:
137
184
 
138
185
  ```typescript
139
186
  // Simple example with just what's needed
140
- const searchUsers = create.get()
187
+ const users = await create
188
+ .get("https://api.example.com/users")
141
189
  .withBearerToken(userToken)
142
190
  .withQueryParams({ q: searchTerm, limit: 20 })
143
- .withTimeout(3000);
144
-
145
- // Now execute the request
146
- const users = await searchUsers.sendTo('https://api.example.com/users').getData();
147
- ```
148
-
149
- You can also create reusable base requests with common settings:
150
-
151
- ```typescript
152
- // Create base authenticated request
153
- const apiBase = create.get()
154
- .withHeaders({
155
- 'X-API-Version': '1.2',
156
- 'Accept-Language': 'en-US'
157
- })
158
- .withBearerToken(authToken)
159
- .withTimeout(5000)
160
- .withRetries(2);
161
-
162
- // Use the base request for different endpoints
163
- const users = await apiBase.sendTo('https://api.example.com/users').getData();
164
- const products = await apiBase.sendTo('https://api.example.com/products').getData();
191
+ .withTimeout(3000)
192
+ .getData();
165
193
  ```
166
194
 
167
195
  ### Request Bodies (POST/PUT/PATCH)
168
196
 
169
197
  ```typescript
170
- // JSON body
171
- const jsonRequest = create.post()
172
- .withBody({ name: 'John', age: 30 });
198
+ // JSON body (Content-Type automatically set to application/json)
199
+ const jsonRequest = create
200
+ .post("https://api.example.com/users")
201
+ .withBody({ name: "John", age: 30 });
173
202
 
174
- // String body
175
- const textRequest = create.post()
176
- .withBody('Plain text content');
203
+ // String body (Content-Type automatically set to text/plain)
204
+ const textRequest = create
205
+ .post("https://api.example.com/users")
206
+ .withBody("Plain text content");
177
207
 
178
208
  // Form data
179
209
  const formData = new FormData();
180
- formData.append('name', 'John');
181
- formData.append('file', fileBlob);
210
+ formData.append("name", "John");
211
+ formData.append("file", fileBlob);
182
212
 
183
- const formRequest = create.post()
184
- .withBody(formData);
213
+ const formRequest = create.post("https://api.example.com/users").withBody(formData);
214
+
215
+ // URLSearchParams (typically used for application/x-www-form-urlencoded)
216
+ const params = new URLSearchParams();
217
+ params.append("username", "john");
218
+ params.append("password", "secret");
219
+
220
+ const formUrlEncodedRequest = create.post("https://api.example.com/login").withBody(params);
185
221
  ```
186
222
 
187
- ### Executing Requests
223
+ ### GraphQL Requests
224
+
225
+ The library provides built-in support for GraphQL queries and mutations:
188
226
 
189
227
  ```typescript
190
- // Simple execution
191
- const response = await request.sendTo('https://api.example.com/endpoint');
228
+ // GraphQL query without variables
229
+ const users = await create
230
+ .post("https://api.example.com/graphql")
231
+ .withGraphQL("query { users { id name email } }")
232
+ .getJson();
233
+
234
+ // GraphQL query with variables
235
+ const user = await create
236
+ .post("https://api.example.com/graphql")
237
+ .withGraphQL("query GetUser($id: ID!) { user(id: $id) { name email } }", { id: "123" })
238
+ .getJson();
239
+
240
+ // GraphQL mutation
241
+ const result = await create
242
+ .post("https://api.example.com/graphql")
243
+ .withGraphQL(
244
+ "mutation CreateUser($name: String!) { createUser(name: $name) { id name } }",
245
+ { name: "John Doe" }
246
+ )
247
+ .getJson();
248
+ ```
192
249
 
193
- // With direct data extraction
194
- const jsonData = await request.sendTo('https://api.example.com/endpoint').getJson();
195
- const textData = await request.sendTo('https://api.example.com/endpoint').getText();
196
- const blobData = await request.sendTo('https://api.example.com/endpoint').getBlob();
197
- const bodyStream = await request.sendTo('https://api.example.com/endpoint').getBody();
198
- const arrayBuffer = await request.sendTo('https://api.example.com/endpoint').getArrayBuffer();
250
+ #### GraphQL Error Handling
199
251
 
200
- // Using the data selector API to extract specific data
201
- const userData = await request.sendTo('https://api.example.com/users')
202
- .getData(data => data.results.users);
252
+ GraphQL errors do not cause exceptions by default. Use the `throwOnError` option to make them throw exceptions:
203
253
 
204
- // Using the data selector without a selector function just returns the full JSON response
205
- const fullData = await request.sendTo('https://api.example.com/data').getData();
254
+ ```typescript
255
+ // Throw an error if the GraphQL response contains errors
256
+ try {
257
+ const user = await create
258
+ .post("https://api.example.com/graphql")
259
+ .withGraphQL(
260
+ "query GetUser($id: ID!) { user(id: $id) { name email } }",
261
+ { id: "123" },
262
+ { throwOnError: true }
263
+ )
264
+ .getJson();
265
+ } catch (error) {
266
+ console.error(error.message);
267
+ }
206
268
  ```
207
269
 
208
- ### Data Selection
270
+ The `withGraphQL` method automatically:
209
271
 
210
- The `getData` method provides a powerful way to extract and transform specific data from API responses:
272
+ - Formats the body as JSON with `query` and optional `variables` properties
273
+ - Sets `Content-Type` to `application/json`
274
+ - Validates the query is non-empty
275
+ - Validates variables are a plain object (not arrays or null)
276
+ - Optionally throws errors when GraphQL response contains errors (with `throwOnError: true`)
277
+
278
+ ### Query Parameters Advanced Features
279
+
280
+ The library supports advanced query parameter handling:
211
281
 
212
282
  ```typescript
213
- // Extract specific properties from nested structures
214
- const posts = await request
215
- .sendTo('https://api.example.com/feed')
216
- .getData(data => data.feed.posts);
283
+ // Array values create multiple query params with the same key
284
+ const request = create.get("https://api.example.com/search").withQueryParams({
285
+ tags: ["javascript", "typescript", "node"], // ?tags=javascript&tags=typescript&tags=node
286
+ page: 1,
287
+ active: true,
288
+ });
289
+
290
+ // Null and undefined values are automatically filtered out
291
+ const filtered = create.get("https://api.example.com/users").withQueryParams({
292
+ name: "John",
293
+ age: null, // Ignored
294
+ email: undefined, // Ignored
295
+ });
296
+
297
+ // Supports all JavaScript types (strings, numbers, booleans, arrays)
298
+ const typed = create.get("https://api.example.com/data").withQueryParams({
299
+ page: 1, // Number
300
+ active: true, // Boolean
301
+ tags: ["js", "ts"], // Array
302
+ name: "John", // String
303
+ });
304
+
305
+ // Merge with existing query params in URL
306
+ const merged = create
307
+ .get("https://api.example.com/users?existing=value")
308
+ .withQueryParams({ new: "param" }); // Both existing and new params included
309
+ ```
217
310
 
218
- // Transform data in the selector function
219
- const usernames = await request
220
- .sendTo('https://api.example.com/users')
221
- .getData(data => data.users.map(user => user.username));
311
+ ### Executing Requests
222
312
 
223
- // Apply filtering in the selector
224
- const activeUsers = await request
225
- .sendTo('https://api.example.com/users')
226
- .getData(data => data.users.filter(user => user.isActive));
313
+ ```typescript
314
+ // Get the full response
315
+ const response = await create.get("https://api.example.com/endpoint").getResponse();
227
316
 
228
- // Combine data from complex nested structures
229
- const combinedData = await request
230
- .sendTo('https://api.example.com/dashboard')
231
- .getData(data => ({
232
- userCount: data.stats.users.total,
233
- recentPosts: data.content.recent.slice(0, 5),
234
- notifications: data.user.notifications.unread
235
- }));
317
+ // With direct data extraction
318
+ const jsonData = await create.get("https://api.example.com/endpoint").getJson();
319
+ const textData = await create.get("https://api.example.com/endpoint").getText();
320
+ const blobData = await create.get("https://api.example.com/endpoint").getBlob();
321
+ const bodyStream = await create.get("https://api.example.com/endpoint").getBody();
322
+
323
+ // Using the data selector API to extract specific data
324
+ const userData = await create
325
+ .get("https://api.example.com/users")
326
+ .getData(data => data.results.users);
327
+
328
+ // Using the data selector without a selector function just returns the full JSON response
329
+ const fullData = await create.get("https://api.example.com/data").getData();
236
330
  ```
237
331
 
238
- When a selector fails, the error message will contain the original response data to help diagnose the issue:
332
+ ### ResponseWrapper Properties
333
+
334
+ When you use `getResponse()`, you get a `ResponseWrapper` object that provides convenient access to response properties and methods:
239
335
 
240
336
  ```typescript
241
- try {
242
- const result = await request
243
- .sendTo('https://api.example.com/data')
244
- .getData(data => data.results.items); // Will fail if structure is different
245
- } catch (error) {
246
- console.error(error);
247
- // Error message includes the original response data
248
- }
337
+ const response = await create.get("https://api.example.com/users").getResponse();
338
+
339
+ // Access response properties directly
340
+ console.log(response.status); // HTTP status code (e.g., 200)
341
+ console.log(response.statusText); // Status text (e.g., "OK")
342
+ console.log(response.ok); // Boolean: true if status is 200-299
343
+ console.log(response.headers); // Headers object
344
+ console.log(response.url); // Request URL
345
+ console.log(response.method); // HTTP method
346
+ console.log(response.raw); // Raw Response object from fetch
347
+
348
+ // Use wrapper methods for body parsing
349
+ const json = await response.getJson();
350
+ const text = await response.getText();
351
+ const blob = await response.getBlob();
352
+ const stream = response.getBody(); // ReadableStream or null
249
353
  ```
250
354
 
251
355
  ### Error Handling
@@ -254,14 +358,15 @@ All errors from requests are instances of `RequestError` with detailed informati
254
358
 
255
359
  ```typescript
256
360
  try {
257
- const data = await request.sendTo('https://api.example.com/data').getJson();
361
+ const data = await create.get("https://api.example.com/data").getJson();
258
362
  } catch (error) {
259
363
  // error will always be a RequestError
260
- console.log(error.message); // Error message
261
- console.log(error.status); // HTTP status code (if available)
262
- console.log(error.url); // Request URL
263
- console.log(error.method); // HTTP method
264
- console.log(error.timeoutError); // Whether it was a timeout
364
+ console.log(error.message); // Error message
365
+ console.log(error.status); // HTTP status code (if available)
366
+ console.log(error.url); // Request URL
367
+ console.log(error.method); // HTTP method
368
+ console.log(error.isTimeout); // Whether it was a timeout
369
+ console.log(error.isAborted); // Whether it was aborted/cancelled
265
370
 
266
371
  // Access the original response if available
267
372
  if (error.response) {
@@ -271,53 +376,140 @@ try {
271
376
  }
272
377
  ```
273
378
 
274
- ### Caching Requests
379
+ ## Advanced Usage
380
+
381
+ ### Interceptors
382
+
383
+ Interceptors allow you to modify requests, transform responses, or handle errors globally or per-request. This is perfect for adding authentication tokens, logging, error recovery, and more.
384
+
385
+ #### Global Interceptors
386
+
387
+ Global interceptors apply to all requests:
275
388
 
276
389
  ```typescript
277
- import create, { createMemoryStorage, createLocalStorageStorage, createSessionStorageStorage } from 'create-request';
390
+ // Add a global request interceptor (modify all requests)
391
+ const requestInterceptorId = create.config.addRequestInterceptor(config => {
392
+ // Add auth token to all requests
393
+ config.headers["Authorization"] = `Bearer ${getToken()}`;
394
+ // Modify URL, headers, body, etc.
395
+ return config;
396
+ });
397
+
398
+ // Add a global response interceptor (transform all responses)
399
+ const responseInterceptorId = create.config.addResponseInterceptor(response => {
400
+ console.log(`Response received: ${response.status}`);
401
+ // Transform or modify the response
402
+ return response;
403
+ });
404
+
405
+ // Add a global error interceptor (handle all errors)
406
+ const errorInterceptorId = create.config.addErrorInterceptor(error => {
407
+ console.error("Request failed:", error.message);
408
+ // Can throw to propagate error, or return ResponseWrapper to recover
409
+ throw error;
410
+ });
411
+
412
+ // Remove interceptors when no longer needed
413
+ create.config.removeRequestInterceptor(requestInterceptorId);
414
+ create.config.removeResponseInterceptor(responseInterceptorId);
415
+ create.config.removeErrorInterceptor(errorInterceptorId);
416
+
417
+ // Clear all interceptors at once
418
+ create.config.clearInterceptors();
419
+ ```
278
420
 
279
- // Simple in-memory caching
280
- const request = create.get()
281
- .withCache({
282
- storage: createMemoryStorage(),
283
- ttl: 60000 // 1 minute in milliseconds
421
+ #### Per-Request Interceptors
422
+
423
+ Per-request interceptors apply only to a specific request:
424
+
425
+ ```typescript
426
+ // Request interceptor - modify request configuration
427
+ const data = await create
428
+ .get("https://api.example.com/users")
429
+ .withRequestInterceptor(config => {
430
+ config.headers["X-Custom-Header"] = "value";
431
+ config.url = "https://api.example.com/modified-url"; // Can modify URL
432
+ return config;
284
433
  })
285
- .sendTo('https://api.example.com/data');
286
-
287
- // Using localStorage storage
288
- const request = create.get()
289
- .withCache({
290
- storage: createLocalStorageStorage(),
291
- ttl: 5 * 60 * 1000, // 5 minutes
292
- maxSize: '1MB',
293
- keyPrefix: 'user-data'
434
+ .getJson();
435
+
436
+ // Response interceptor - transform response
437
+ const transformed = await create
438
+ .get("https://api.example.com/users")
439
+ .withResponseInterceptor(response => {
440
+ console.log(`Got response with status ${response.status}`);
441
+ return response;
294
442
  })
295
- .sendTo('https://api.example.com/users');
296
-
297
- // Using sessionStorage for per-session caching
298
- const request = create.get()
299
- .withCache({
300
- storage: createSessionStorageStorage(),
301
- maxEntries: 50,
302
- varyByHeaders: ['x-api-version']
443
+ .getJson();
444
+
445
+ // Error interceptor - handle or recover from errors
446
+ const recovered = await create
447
+ .get("https://api.example.com/users")
448
+ .withErrorInterceptor(error => {
449
+ // Option 1: Throw to propagate error
450
+ throw error;
451
+
452
+ // Option 2: Return a ResponseWrapper to recover from error
453
+ // return new ResponseWrapper(fallbackResponse, error.url, error.method);
303
454
  })
304
- .sendTo('https://api.example.com/data');
455
+ .getJson();
305
456
  ```
306
457
 
307
- ## Advanced Usage
458
+ #### Interceptor Execution Order
308
459
 
309
- ### Request Reuse
460
+ Interceptors execute in a specific order:
461
+
462
+ 1. **Request interceptors**: Global interceptors run first (in registration order), then per-request interceptors (in registration order)
463
+ 2. **Response interceptors**: Per-request interceptors run first (in registration order), then global interceptors (in reverse registration order)
464
+ 3. **Error interceptors**: Per-request interceptors run first (in registration order), then global interceptors (in reverse registration order)
310
465
 
311
466
  ```typescript
312
- // Create a base authenticated request
313
- const authRequest = create.get()
314
- .withBearerToken('token123')
315
- .withTimeout(5000)
316
- .withRetries(2);
317
-
318
- // Reuse for different endpoints
319
- const users = await authRequest.sendTo('https://api.example.com/users').getJson();
320
- const products = await authRequest.sendTo('https://api.example.com/products').getJson();
467
+ // Request: Global 1 → Global 2 → Per-request 1 → Per-request 2
468
+ // Response: Per-request 1 → Per-request 2 → Global 2 → Global 1
469
+ const data = await create
470
+ .get("https://api.example.com/users")
471
+ .withRequestInterceptor(() => console.log("Per-request 1"))
472
+ .withRequestInterceptor(() => console.log("Per-request 2"))
473
+ .getJson();
474
+ ```
475
+
476
+ #### Advanced Interceptor Patterns
477
+
478
+ ```typescript
479
+ // Short-circuit request with early response
480
+ const cached = await create
481
+ .get("https://api.example.com/users")
482
+ .withRequestInterceptor(() => {
483
+ // Return early response from cache
484
+ return new Response(JSON.stringify(cachedData), {
485
+ status: 200,
486
+ headers: { "Content-Type": "application/json" },
487
+ });
488
+ })
489
+ .getJson();
490
+
491
+ // Recover from error with fallback
492
+ const fallback = await create
493
+ .get("https://api.example.com/users")
494
+ .withErrorInterceptor(error => {
495
+ // Return fallback response instead of throwing
496
+ const fallbackResponse = new Response(JSON.stringify({ users: [] }), {
497
+ status: 200,
498
+ headers: { "Content-Type": "application/json" },
499
+ });
500
+ return new ResponseWrapper(fallbackResponse, error.url, error.method);
501
+ })
502
+ .getJson();
503
+
504
+ // Async interceptors
505
+ const asyncData = await create
506
+ .get("https://api.example.com/users")
507
+ .withRequestInterceptor(async config => {
508
+ const token = await getTokenAsync();
509
+ config.headers["Authorization"] = `Bearer ${token}`;
510
+ return config;
511
+ })
512
+ .getJson();
321
513
  ```
322
514
 
323
515
  ### Request Cancellation
@@ -325,7 +517,8 @@ const products = await authRequest.sendTo('https://api.example.com/products').ge
325
517
  ```typescript
326
518
  const controller = new AbortController();
327
519
 
328
- const request = create.get()
520
+ const request = create
521
+ .get("https://api.example.com/slow-endpoint")
329
522
  .withTimeout(10000)
330
523
  .withAbortController(controller);
331
524
 
@@ -333,151 +526,130 @@ const request = create.get()
333
526
  setTimeout(() => controller.abort(), 2000);
334
527
 
335
528
  try {
336
- const data = await request.sendTo('https://api.example.com/slow-endpoint').getJson();
529
+ const data = await request.getJson();
337
530
  } catch (error) {
338
- // Check if request was cancelled
339
- console.log('Request was cancelled:', error.name === 'AbortError');
531
+ if (error.name === "AbortError") {
532
+ console.log("Request was cancelled by user");
533
+ } else if (error.isTimeout) {
534
+ console.log("Request timed out");
535
+ } else {
536
+ console.log("Other error:", error.message);
537
+ }
340
538
  }
341
539
  ```
342
540
 
343
- ### Working with Typed Responses
541
+ ### URL Handling
542
+
543
+ The library handles both absolute and relative URLs, and automatically merges query parameters:
344
544
 
345
545
  ```typescript
346
- interface User {
347
- id: number;
348
- name: string;
349
- email: string;
350
- }
546
+ // Relative URLs (preserved as-is)
547
+ const relative = await create.get("/api/users").getJson();
548
+
549
+ // Absolute URLs
550
+ const absolute = await create.get("https://api.example.com/users").getJson();
551
+
552
+ // Merging query params with existing URL params
553
+ const merged = await create
554
+ .get("https://api.example.com/users?page=1")
555
+ .withQueryParams({ limit: 20, sort: "name" })
556
+ .getJson();
557
+ // Result: https://api.example.com/users?page=1&limit=20&sort=name
558
+
559
+ // Special characters and unicode are properly encoded
560
+ const encoded = await create
561
+ .get("https://api.example.com/search")
562
+ .withQueryParams({ name: "用户名", filter: "status:active" })
563
+ .getJson();
564
+ ```
351
565
 
352
- interface ApiResponse {
353
- users: User[];
354
- pagination: {
355
- total: number;
356
- page: number;
357
- }
358
- }
566
+ ### Data Selection
359
567
 
360
- const userRequest = create.get();
568
+ The `getData` method provides a powerful way to extract and transform specific data from API responses:
361
569
 
362
- // Type the full response
363
- const response = await userRequest
364
- .sendTo('https://api.example.com/users')
365
- .getJson<ApiResponse>();
570
+ ```typescript
571
+ // Extract specific properties from nested structures
572
+ const posts = await create
573
+ .get("https://api.example.com/feed")
574
+ .getData(data => data.feed.posts);
366
575
 
367
- // Or use getData with type parameters
368
- const users = await userRequest
369
- .sendTo('https://api.example.com/users')
370
- .getData<ApiResponse, User[]>(data => data.users);
576
+ // Transform data in the selector function
577
+ const usernames = await create
578
+ .get("https://api.example.com/users")
579
+ .getData(data => data.users.map(user => user.username));
371
580
 
372
- // Or just get the full typed response
373
- const fullData = await userRequest
374
- .sendTo('https://api.example.com/users')
375
- .getData<ApiResponse>();
581
+ // Apply filtering in the selector
582
+ const activeUsers = await create
583
+ .get("https://api.example.com/users")
584
+ .getData(data => data.users.filter(user => user.isActive));
376
585
 
377
- // TypeScript knows the types
378
- console.log(users[0].name);
586
+ // Combine data from complex nested structures
587
+ const combinedData = await create.get("https://api.example.com/dashboard").getData(data => ({
588
+ userCount: data.stats.users.total,
589
+ recentPosts: data.content.recent.slice(0, 5),
590
+ notifications: data.user.notifications.unread,
591
+ }));
379
592
  ```
380
593
 
381
- ### Promise Chaining
594
+ When a selector fails, the error message will contain helpful context to diagnose the issue:
382
595
 
383
596
  ```typescript
384
- const request = create.get();
385
-
386
- request.sendTo('https://api.example.com/data')
387
- .then(response => {
388
- console.log('Status:', response.status);
389
- return response.getJson();
390
- })
391
- .then(data => {
392
- console.log('Data:', data);
393
- })
394
- .catch(error => {
395
- console.error('Error:', error.message);
396
- });
597
+ try {
598
+ // This will fail if the response structure doesn't match expectations
599
+ const result = await create
600
+ .get("https://api.example.com/data")
601
+ .getData(data => data.results.items);
602
+ } catch (error) {
603
+ console.error(error);
604
+ // Error message includes context for debugging
605
+ }
397
606
  ```
398
607
 
399
- ### Advanced Caching
608
+ ## TypeScript Support
400
609
 
401
610
  ```typescript
402
- import create, { createMemoryStorage, StorageProvider } from 'create-request';
403
-
404
- // Custom key generation
405
- const request = create.get()
406
- .withCache({
407
- storage: createMemoryStorage(),
408
- keyGenerator: (url, method, headers) => {
409
- // Include user ID from authorization header in the cache key
410
- const authHeader = headers?.['Authorization'] || '';
411
- const userMatch = authHeader.match(/User-(\d+)/);
412
- const userId = userMatch ? userMatch[1] : 'anonymous';
413
- return `${method}:${userId}:${url}`;
414
- }
415
- })
416
- .sendTo('https://api.example.com/data');
417
- ```
418
-
419
- ### Implementing Custom Storage Providers
420
-
421
- You can create your own storage provider by implementing the `StorageProvider` interface:
611
+ interface User {
612
+ id: number;
613
+ name: string;
614
+ email: string;
615
+ isActive: boolean;
616
+ }
422
617
 
423
- ```typescript
424
- import { StorageProvider } from 'create-request';
425
-
426
- // Create a simple custom storage provider
427
- export function createNamespacedStorage(namespace: string): StorageProvider {
428
- return {
429
- get(key: string): string | null {
430
- return localStorage.getItem(`${namespace}:${key}`);
431
- },
432
-
433
- set(key: string, value: string): void {
434
- try {
435
- localStorage.setItem(`${namespace}:${key}`, value);
436
- } catch (e) {
437
- // Handle quota exceeded errors
438
- console.warn("Storage quota exceeded, clearing cache");
439
- this.clear();
440
- try {
441
- localStorage.setItem(`${namespace}:${key}`, value);
442
- } catch (e) {
443
- console.error("Failed to set cache item even after clearing cache");
444
- }
445
- }
446
- },
447
-
448
- has(key: string): boolean {
449
- return localStorage.getItem(`${namespace}:${key}`) !== null;
450
- },
451
-
452
- delete(key: string): void {
453
- localStorage.removeItem(`${namespace}:${key}`);
454
- },
455
-
456
- clear(): void {
457
- // Only clear items in our namespace
458
- Object.keys(localStorage)
459
- .filter(key => key.startsWith(`${namespace}:`))
460
- .forEach(key => localStorage.removeItem(key));
461
- }
618
+ interface ApiResponse<T> {
619
+ data: T;
620
+ meta: {
621
+ total: number;
622
+ page: number;
462
623
  };
463
624
  }
464
625
 
465
- // Usage:
466
- const request = create.get()
467
- .withCache({
468
- storage: createNamespacedStorage('api-cache'),
469
- ttl: 24 * 60 * 60 * 1000 // 24 hours
470
- })
471
- .sendTo('https://api.example.com/data');
472
- ```
626
+ // Type the full response
627
+ const response = await create
628
+ .get("https://api.example.com/users")
629
+ .getJson<ApiResponse<User[]>>();
473
630
 
474
- With custom storage providers, you can integrate with any storage system, such as:
631
+ // Or use getData with type parameters
632
+ const users = await create
633
+ .get("https://api.example.com/users")
634
+ .getData<ApiResponse<User[]>, User[]>(data => data.data);
475
635
 
476
- - Custom browser storage solutions
477
- - Memory caches with expiration policies
478
- - Session-based storage mechanisms
479
- - Encrypted storage providers
480
- - Third-party storage libraries
636
+ // TypeScript knows the types
637
+ users.forEach(user => {
638
+ console.log(`${user.name} (${user.email}): ${user.isActive ? "Active" : "Inactive"}`);
639
+ });
640
+
641
+ // Function with proper types
642
+ async function getUserById(id: number): Promise<User> {
643
+ return create
644
+ .get("https://api.example.com/users")
645
+ .withQueryParam("id", id)
646
+ .getData<ApiResponse<User[]>, User>(data => {
647
+ const user = data.data[0];
648
+ if (!user) throw new Error(`User with ID ${id} not found`);
649
+ return user;
650
+ });
651
+ }
652
+ ```
481
653
 
482
654
  ## CSRF Protection
483
655
 
@@ -499,11 +671,15 @@ You can configure CSRF settings globally for all requests:
499
671
 
500
672
  ```typescript
501
673
  // Configure CSRF settings for all requests
502
- create.config.setCsrfToken('your-csrf-token');
503
- create.config.setCsrfHeaderName('X-CSRF-Token'); // Default header name
504
- create.config.setXsrfCookieName('XSRF-TOKEN'); // Default cookie name
674
+ create.config.setCsrfToken("your-csrf-token");
675
+ create.config.setCsrfHeaderName("X-CSRF-Token"); // Default header name for CSRF token
676
+ create.config.setXsrfCookieName("XSRF-TOKEN"); // Default cookie name to read from
677
+ create.config.setXsrfHeaderName("X-XSRF-TOKEN"); // Default header name for XSRF token from cookie
505
678
  create.config.setEnableAntiCsrf(true); // Enable/disable X-Requested-With header
506
679
  create.config.setEnableAutoXsrf(true); // Enable/disable automatic cookie-to-header token
680
+
681
+ // Reset all configuration to defaults
682
+ create.config.reset();
507
683
  ```
508
684
 
509
685
  ### Per-Request CSRF Settings
@@ -512,22 +688,22 @@ You can also configure CSRF protection on individual requests:
512
688
 
513
689
  ```typescript
514
690
  // Configure CSRF for a specific request
515
- const request = create.post()
516
- .withCsrfToken('request-specific-token') // Set a specific token
691
+ const request = create
692
+ .post("https://api.example.com/users")
693
+ .withCsrfToken("request-specific-token") // Set a specific token
517
694
  .withAntiCsrfHeaders() // Explicitly add X-Requested-With header
518
695
  .withoutCsrfProtection(); // Or disable all automatic CSRF protection
519
696
  ```
520
697
 
521
- ### Integrating with Backend Frameworks
698
+ ## Performance Considerations
522
699
 
523
- Most modern frameworks support CSRF protection out of the box. The library works seamlessly with:
700
+ create-request is designed to be lightweight and efficient:
524
701
 
525
- - **Laravel**: Automatically reads XSRF-TOKEN cookie and sends X-XSRF-TOKEN header
526
- - **Rails**: Works with the Rails CSRF token system
527
- - **Django**: Compatible with Django's CSRF middleware
528
- - **Express.js + csurf**: Works with the csurf middleware token pattern
529
-
530
- When your server sends a CSRF token in a cookie or response header, `create-request` can automatically extract and include it in subsequent requests.
702
+ - **Zero Dependencies**: No extra libraries to load
703
+ - **Tree-Shakable**: Only import what you need
704
+ - **Minimal Overhead**: Thin wrapper around the native Fetch API
705
+ - **Memory Efficient**: Doesn't create unnecessary objects
706
+ - **Clean API**: Simple and intuitive interface
531
707
 
532
708
  ## Browser Support
533
709
 
@@ -539,6 +715,35 @@ This library works with all browsers that support the Fetch API:
539
715
  - Edge 14+
540
716
  - Opera 29+
541
717
 
718
+ ## Comparison of JavaScript HTTP Client Libraries
719
+
720
+ | Feature | create-request | Fetch | Axios | SuperAgent | Got | Ky | node-fetch | Redaxios |
721
+ | ------------------- | -------------- | ------ | ------- | ---------- | ------- | ------ | ---------- | -------- |
722
+ | **Size (min+gzip)** | ~5.8KB | Native | ~13.6KB | ~17.8KB | ~17.8KB | ~3.4KB | ~7.7KB | ~1KB |
723
+ | **Browser** | Modern | Modern | IE11+ | IE9+ | ❌ No | Modern | ❌ No | Modern |
724
+ | **Node.js** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
725
+ | **HTTP/2** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
726
+ | **Auto Retries** | ✅ | ❌ | 🛠️ | ✅ | ✅ | ✅ | ❌ | ❌ |
727
+ | **Cancellation** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
728
+ | **Auto JSON** | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
729
+ | **Timeout** | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
730
+ | **TypeScript** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
731
+ | **Streaming** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
732
+ | **Progress** | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
733
+ | **Middleware** | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
734
+ | **Cookies** | ✅ | ✅ | 🛠️ | ✅ | ✅ | ❌ | ❌ | ❌ |
735
+ | **Pagination API** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
736
+ | **Zero Deps** | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
737
+ | **Chainable API** | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
738
+ | **CSRF Protection** | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
739
+ | **GraphQL Support** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
740
+ | **Interceptors** | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
741
+
742
+ **Notes:**
743
+
744
+ - "Modern" browser support: Chrome 42+, Firefox 39+, Safari 10.1+, Edge 14+, Opera 29+
745
+ - 🛠️ Feature requires additional plugins or adapters (not available out-of-the-box)
746
+
542
747
  ## License
543
748
 
544
749
  MIT