create-request 0.0.1-beta.8 → 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/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
-
71
- Or with yarn:
72
95
 
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,161 +122,214 @@ 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, CachePolicy, 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
109
142
 
110
143
  // Automatic retry configuration
111
144
  .withRetries(3) // Retry up to 3 times on failure
112
- .withRetryDelay(1000) // Wait 1 second between retries
113
- .withExponentialBackoff(true) // Use exponential backoff (increases wait time between retries)
114
- .onRetry(({ attempt, error, delay }) => {
115
- console.log(`Attempt ${attempt} failed: ${error.message}. Retrying in ${delay}ms`);
145
+ .onRetry(({ attempt, error }) => {
146
+ console.log(`Attempt ${attempt} failed: ${error.message}. Retrying...`);
116
147
  })
117
148
 
118
149
  // Authentication methods
119
- .withBearerToken('your-token') // Adds Authorization: Bearer your-token
120
- .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
+ })
121
163
 
122
- // URL parameters
123
- .withQueryParams({ search: 'term', page: 1, limit: 20 })
124
- .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
125
168
 
126
169
  // Request body configuration (for POST/PUT/PATCH)
127
- .withContentType('application/json') // Set specific content type
170
+ .withContentType("application/json") // Set specific content type
128
171
 
129
172
  // Fetch API options
130
- .withCredentials(CredentialsPolicy.INCLUDE) // Includes cookies with cross-origin requests
131
- .withMode(RequestMode.CORS) // Controls CORS behavior
132
- .withCache(CachePolicy.NO_STORE) // Controls how request interacts with browser cache
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
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
137
181
  ```
138
182
 
139
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:
140
184
 
141
185
  ```typescript
142
186
  // Simple example with just what's needed
143
- const searchUsers = create.get()
187
+ const users = await create
188
+ .get("https://api.example.com/users")
144
189
  .withBearerToken(userToken)
145
190
  .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();
191
+ .withTimeout(3000)
192
+ .getData();
150
193
  ```
151
194
 
152
- You can also create reusable base requests with common settings:
195
+ ### Request Bodies (POST/PUT/PATCH)
153
196
 
154
197
  ```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);
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 });
164
202
 
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();
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");
207
+
208
+ // Form data
209
+ const formData = new FormData();
210
+ formData.append("name", "John");
211
+ formData.append("file", fileBlob);
212
+
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);
168
221
  ```
169
222
 
170
- ### Request Bodies (POST/PUT/PATCH)
223
+ ### GraphQL Requests
224
+
225
+ The library provides built-in support for GraphQL queries and mutations:
171
226
 
172
227
  ```typescript
173
- // JSON body
174
- const jsonRequest = create.post()
175
- .withBody({ name: 'John', age: 30 });
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
+ ```
176
249
 
177
- // String body
178
- const textRequest = create.post()
179
- .withBody('Plain text content');
250
+ The `withGraphQL` method automatically:
180
251
 
181
- // Form data
182
- const formData = new FormData();
183
- formData.append('name', 'John');
184
- formData.append('file', fileBlob);
252
+ - Formats the body as JSON with `query` and optional `variables` properties
253
+ - Sets `Content-Type` to `application/json`
254
+ - Trims whitespace from the query string
255
+ - Validates the query is non-empty
256
+ - Validates variables are a plain object (not arrays or null)
257
+
258
+ ### Query Parameters Advanced Features
259
+
260
+ The library supports advanced query parameter handling:
185
261
 
186
- const formRequest = create.post()
187
- .withBody(formData);
262
+ ```typescript
263
+ // Array values create multiple query params with the same key
264
+ const request = create.get("https://api.example.com/search").withQueryParams({
265
+ tags: ["javascript", "typescript", "node"], // ?tags=javascript&tags=typescript&tags=node
266
+ page: 1,
267
+ active: true,
268
+ });
269
+
270
+ // Null and undefined values are automatically filtered out
271
+ const filtered = create.get("https://api.example.com/users").withQueryParams({
272
+ name: "John",
273
+ age: null, // Ignored
274
+ email: undefined, // Ignored
275
+ });
276
+
277
+ // Supports all JavaScript types (strings, numbers, booleans, arrays)
278
+ const typed = create.get("https://api.example.com/data").withQueryParams({
279
+ page: 1, // Number
280
+ active: true, // Boolean
281
+ tags: ["js", "ts"], // Array
282
+ name: "John", // String
283
+ });
284
+
285
+ // Merge with existing query params in URL
286
+ const merged = create
287
+ .get("https://api.example.com/users?existing=value")
288
+ .withQueryParams({ new: "param" }); // Both existing and new params included
188
289
  ```
189
290
 
190
291
  ### Executing Requests
191
292
 
192
293
  ```typescript
193
- // Simple execution
194
- const response = await request.sendTo('https://api.example.com/endpoint');
294
+ // Get the full response
295
+ const response = await create.get("https://api.example.com/endpoint").getResponse();
195
296
 
196
297
  // 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();
298
+ const jsonData = await create.get("https://api.example.com/endpoint").getJson();
299
+ const textData = await create.get("https://api.example.com/endpoint").getText();
300
+ const blobData = await create.get("https://api.example.com/endpoint").getBlob();
301
+ const bodyStream = await create.get("https://api.example.com/endpoint").getBody();
202
302
 
203
303
  // Using the data selector API to extract specific data
204
- const userData = await request.sendTo('https://api.example.com/users')
304
+ const userData = await create
305
+ .get("https://api.example.com/users")
205
306
  .getData(data => data.results.users);
206
307
 
207
308
  // 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();
309
+ const fullData = await create.get("https://api.example.com/data").getData();
209
310
  ```
210
311
 
211
- ### Data Selection
312
+ ### ResponseWrapper Properties
212
313
 
213
- The `getData` method provides a powerful way to extract and transform specific data from API responses:
314
+ When you use `getResponse()`, you get a `ResponseWrapper` object that provides convenient access to response properties and methods:
214
315
 
215
316
  ```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
- // "Data selector failed: Cannot read properties of undefined (reading 'items').
252
- // Original data: { "status": "error", "message": "Invalid request" }"
253
- }
317
+ const response = await create.get("https://api.example.com/users").getResponse();
318
+
319
+ // Access response properties directly
320
+ console.log(response.status); // HTTP status code (e.g., 200)
321
+ console.log(response.statusText); // Status text (e.g., "OK")
322
+ console.log(response.ok); // Boolean: true if status is 200-299
323
+ console.log(response.headers); // Headers object
324
+ console.log(response.url); // Request URL
325
+ console.log(response.method); // HTTP method
326
+ console.log(response.raw); // Raw Response object from fetch
327
+
328
+ // Use wrapper methods for body parsing
329
+ const json = await response.getJson();
330
+ const text = await response.getText();
331
+ const blob = await response.getBlob();
332
+ const stream = response.getBody(); // ReadableStream or null
254
333
  ```
255
334
 
256
335
  ### Error Handling
@@ -259,14 +338,15 @@ All errors from requests are instances of `RequestError` with detailed informati
259
338
 
260
339
  ```typescript
261
340
  try {
262
- const data = await request.sendTo('https://api.example.com/data').getJson();
341
+ const data = await create.get("https://api.example.com/data").getJson();
263
342
  } catch (error) {
264
343
  // error will always be a RequestError
265
- console.log(error.message); // Error message
266
- console.log(error.status); // HTTP status code (if available)
267
- console.log(error.url); // Request URL
268
- console.log(error.method); // HTTP method
269
- console.log(error.timeoutError); // Whether it was a timeout
344
+ console.log(error.message); // Error message
345
+ console.log(error.status); // HTTP status code (if available)
346
+ console.log(error.url); // Request URL
347
+ console.log(error.method); // HTTP method
348
+ console.log(error.isTimeout); // Whether it was a timeout
349
+ console.log(error.isAborted); // Whether it was aborted/cancelled
270
350
 
271
351
  // Access the original response if available
272
352
  if (error.response) {
@@ -276,53 +356,186 @@ try {
276
356
  }
277
357
  ```
278
358
 
279
- ### Caching Requests
359
+ ## Advanced Usage
360
+
361
+ ### GraphQL Support
362
+
363
+ `create-request` provides first-class support for GraphQL with the `withGraphQL` method:
280
364
 
281
365
  ```typescript
282
- import create, { createMemoryStorage, createLocalStorageStorage, createSessionStorageStorage } from 'create-request';
366
+ // Simple query
367
+ const query = "query { user { name email } }";
368
+ const data = await create
369
+ .post("https://api.example.com/graphql")
370
+ .withGraphQL(query)
371
+ .getJson();
372
+
373
+ // Query with variables
374
+ const queryWithVars = "query GetUser($id: ID!) { user(id: $id) { name email } }";
375
+ const user = await create
376
+ .post("https://api.example.com/graphql")
377
+ .withGraphQL(queryWithVars, { id: "123" })
378
+ .getJson();
379
+
380
+ // Mutation
381
+ const mutation =
382
+ "mutation CreateUser($name: String!) { createUser(name: $name) { id name } }";
383
+ const result = await create
384
+ .post("https://api.example.com/graphql")
385
+ .withGraphQL(mutation, { name: "John Doe" })
386
+ .getJson();
387
+
388
+ // Complex variables with nested objects
389
+ const complexQuery =
390
+ "query SearchUsers($filters: UserFilters!) { users(filters: $filters) { id name } }";
391
+ const results = await create
392
+ .post("https://api.example.com/graphql")
393
+ .withGraphQL(complexQuery, {
394
+ filters: {
395
+ name: "John",
396
+ age: 30,
397
+ tags: ["active", "verified"],
398
+ metadata: {
399
+ source: "web",
400
+ verified: true,
401
+ },
402
+ },
403
+ })
404
+ .getJson();
405
+ ```
406
+
407
+ ### Interceptors
408
+
409
+ 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.
410
+
411
+ #### Global Interceptors
412
+
413
+ Global interceptors apply to all requests:
414
+
415
+ ```typescript
416
+ // Add a global request interceptor (modify all requests)
417
+ const requestInterceptorId = create.config.addRequestInterceptor(config => {
418
+ // Add auth token to all requests
419
+ config.headers["Authorization"] = `Bearer ${getToken()}`;
420
+ // Modify URL, headers, body, etc.
421
+ return config;
422
+ });
423
+
424
+ // Add a global response interceptor (transform all responses)
425
+ const responseInterceptorId = create.config.addResponseInterceptor(response => {
426
+ console.log(`Response received: ${response.status}`);
427
+ // Transform or modify the response
428
+ return response;
429
+ });
430
+
431
+ // Add a global error interceptor (handle all errors)
432
+ const errorInterceptorId = create.config.addErrorInterceptor(error => {
433
+ console.error("Request failed:", error.message);
434
+ // Can throw to propagate error, or return ResponseWrapper to recover
435
+ throw error;
436
+ });
437
+
438
+ // Remove interceptors when no longer needed
439
+ create.config.removeRequestInterceptor(requestInterceptorId);
440
+ create.config.removeResponseInterceptor(responseInterceptorId);
441
+ create.config.removeErrorInterceptor(errorInterceptorId);
442
+
443
+ // Clear all interceptors at once
444
+ create.config.clearInterceptors();
445
+ ```
446
+
447
+ #### Per-Request Interceptors
448
+
449
+ Per-request interceptors apply only to a specific request:
283
450
 
284
- // Simple in-memory caching
285
- const request = create.get()
286
- .withCache({
287
- storage: createMemoryStorage(),
288
- ttl: 60000 // 1 minute in milliseconds
451
+ ```typescript
452
+ // Request interceptor - modify request configuration
453
+ const data = await create
454
+ .get("https://api.example.com/users")
455
+ .withRequestInterceptor(config => {
456
+ config.headers["X-Custom-Header"] = "value";
457
+ config.url = "https://api.example.com/modified-url"; // Can modify URL
458
+ return config;
289
459
  })
290
- .sendTo('https://api.example.com/data');
291
-
292
- // Using localStorage storage
293
- const request = create.get()
294
- .withCache({
295
- storage: createLocalStorageStorage(),
296
- ttl: 5 * 60 * 1000, // 5 minutes
297
- maxSize: '1MB',
298
- keyPrefix: 'user-data'
460
+ .getJson();
461
+
462
+ // Response interceptor - transform response
463
+ const transformed = await create
464
+ .get("https://api.example.com/users")
465
+ .withResponseInterceptor(response => {
466
+ console.log(`Got response with status ${response.status}`);
467
+ return response;
299
468
  })
300
- .sendTo('https://api.example.com/users');
301
-
302
- // Using sessionStorage for per-session caching
303
- const request = create.get()
304
- .withCache({
305
- storage: createSessionStorageStorage(),
306
- maxEntries: 50,
307
- varyByHeaders: ['x-api-version']
469
+ .getJson();
470
+
471
+ // Error interceptor - handle or recover from errors
472
+ const recovered = await create
473
+ .get("https://api.example.com/users")
474
+ .withErrorInterceptor(error => {
475
+ // Option 1: Throw to propagate error
476
+ throw error;
477
+
478
+ // Option 2: Return a ResponseWrapper to recover from error
479
+ // return new ResponseWrapper(fallbackResponse, error.url, error.method);
308
480
  })
309
- .sendTo('https://api.example.com/data');
481
+ .getJson();
310
482
  ```
311
483
 
312
- ## Advanced Usage
484
+ #### Interceptor Execution Order
485
+
486
+ Interceptors execute in a specific order:
313
487
 
314
- ### Request Reuse
488
+ 1. **Request interceptors**: Global interceptors run first (in registration order), then per-request interceptors (in registration order)
489
+ 2. **Response interceptors**: Per-request interceptors run first (in registration order), then global interceptors (in reverse registration order)
490
+ 3. **Error interceptors**: Per-request interceptors run first (in registration order), then global interceptors (in reverse registration order)
315
491
 
316
492
  ```typescript
317
- // Create a base authenticated request
318
- const authRequest = create.get()
319
- .withBearerToken('token123')
320
- .withTimeout(5000)
321
- .withRetries(2);
322
-
323
- // Reuse for different endpoints
324
- const users = await authRequest.sendTo('https://api.example.com/users').getJson();
325
- const products = await authRequest.sendTo('https://api.example.com/products').getJson();
493
+ // Request: Global 1 → Global 2 → Per-request 1 → Per-request 2
494
+ // Response: Per-request 1 → Per-request 2 → Global 2 → Global 1
495
+ const data = await create
496
+ .get("https://api.example.com/users")
497
+ .withRequestInterceptor(() => console.log("Per-request 1"))
498
+ .withRequestInterceptor(() => console.log("Per-request 2"))
499
+ .getJson();
500
+ ```
501
+
502
+ #### Advanced Interceptor Patterns
503
+
504
+ ```typescript
505
+ // Short-circuit request with early response
506
+ const cached = await create
507
+ .get("https://api.example.com/users")
508
+ .withRequestInterceptor(() => {
509
+ // Return early response from cache
510
+ return new Response(JSON.stringify(cachedData), {
511
+ status: 200,
512
+ headers: { "Content-Type": "application/json" },
513
+ });
514
+ })
515
+ .getJson();
516
+
517
+ // Recover from error with fallback
518
+ const fallback = await create
519
+ .get("https://api.example.com/users")
520
+ .withErrorInterceptor(error => {
521
+ // Return fallback response instead of throwing
522
+ const fallbackResponse = new Response(JSON.stringify({ users: [] }), {
523
+ status: 200,
524
+ headers: { "Content-Type": "application/json" },
525
+ });
526
+ return new ResponseWrapper(fallbackResponse, error.url, error.method);
527
+ })
528
+ .getJson();
529
+
530
+ // Async interceptors
531
+ const asyncData = await create
532
+ .get("https://api.example.com/users")
533
+ .withRequestInterceptor(async config => {
534
+ const token = await getTokenAsync();
535
+ config.headers["Authorization"] = `Bearer ${token}`;
536
+ return config;
537
+ })
538
+ .getJson();
326
539
  ```
327
540
 
328
541
  ### Request Cancellation
@@ -330,7 +543,8 @@ const products = await authRequest.sendTo('https://api.example.com/products').ge
330
543
  ```typescript
331
544
  const controller = new AbortController();
332
545
 
333
- const request = create.get()
546
+ const request = create
547
+ .get("https://api.example.com/slow-endpoint")
334
548
  .withTimeout(10000)
335
549
  .withAbortController(controller);
336
550
 
@@ -338,170 +552,130 @@ const request = create.get()
338
552
  setTimeout(() => controller.abort(), 2000);
339
553
 
340
554
  try {
341
- const data = await request.sendTo('https://api.example.com/slow-endpoint').getJson();
555
+ const data = await request.getJson();
342
556
  } catch (error) {
343
- // Check if request was cancelled
344
- console.log('Request was cancelled:', error.name === 'AbortError');
557
+ if (error.name === "AbortError") {
558
+ console.log("Request was cancelled by user");
559
+ } else if (error.isTimeout) {
560
+ console.log("Request timed out");
561
+ } else {
562
+ console.log("Other error:", error.message);
563
+ }
345
564
  }
346
565
  ```
347
566
 
348
- ### Working with Typed Responses
567
+ ### URL Handling
349
568
 
350
- ```typescript
351
- interface User {
352
- id: number;
353
- name: string;
354
- email: string;
355
- }
569
+ The library handles both absolute and relative URLs, and automatically merges query parameters:
356
570
 
357
- interface ApiResponse {
358
- users: User[];
359
- pagination: {
360
- total: number;
361
- page: number;
362
- }
363
- }
364
-
365
- const userRequest = create.get();
366
-
367
- // Type the full response
368
- const response = await userRequest
369
- .sendTo('https://api.example.com/users')
370
- .getJson<ApiResponse>();
571
+ ```typescript
572
+ // Relative URLs (preserved as-is)
573
+ const relative = await create.get("/api/users").getJson();
574
+
575
+ // Absolute URLs
576
+ const absolute = await create.get("https://api.example.com/users").getJson();
577
+
578
+ // Merging query params with existing URL params
579
+ const merged = await create
580
+ .get("https://api.example.com/users?page=1")
581
+ .withQueryParams({ limit: 20, sort: "name" })
582
+ .getJson();
583
+ // Result: https://api.example.com/users?page=1&limit=20&sort=name
584
+
585
+ // Special characters and unicode are properly encoded
586
+ const encoded = await create
587
+ .get("https://api.example.com/search")
588
+ .withQueryParams({ name: "用户名", filter: "status:active" })
589
+ .getJson();
590
+ ```
371
591
 
372
- // Or use getData with type parameters
373
- const users = await userRequest
374
- .sendTo('https://api.example.com/users')
375
- .getData<ApiResponse, User[]>(data => data.users);
592
+ ### Data Selection
376
593
 
377
- // Or just get the full typed response
378
- const fullData = await userRequest
379
- .sendTo('https://api.example.com/users')
380
- .getData<ApiResponse>();
594
+ The `getData` method provides a powerful way to extract and transform specific data from API responses:
381
595
 
382
- // TypeScript knows the types
383
- console.log(users[0].name);
384
- ```
596
+ ```typescript
597
+ // Extract specific properties from nested structures
598
+ const posts = await create
599
+ .get("https://api.example.com/feed")
600
+ .getData(data => data.feed.posts);
385
601
 
386
- ### Promise Chaining
602
+ // Transform data in the selector function
603
+ const usernames = await create
604
+ .get("https://api.example.com/users")
605
+ .getData(data => data.users.map(user => user.username));
387
606
 
388
- ```typescript
389
- const request = create.get();
607
+ // Apply filtering in the selector
608
+ const activeUsers = await create
609
+ .get("https://api.example.com/users")
610
+ .getData(data => data.users.filter(user => user.isActive));
390
611
 
391
- request.sendTo('https://api.example.com/data')
392
- .then(response => {
393
- console.log('Status:', response.status);
394
- return response.getJson();
395
- })
396
- .then(data => {
397
- console.log('Data:', data);
398
- })
399
- .catch(error => {
400
- console.error('Error:', error.message);
401
- });
612
+ // Combine data from complex nested structures
613
+ const combinedData = await create.get("https://api.example.com/dashboard").getData(data => ({
614
+ userCount: data.stats.users.total,
615
+ recentPosts: data.content.recent.slice(0, 5),
616
+ notifications: data.user.notifications.unread,
617
+ }));
402
618
  ```
403
619
 
404
- ### Advanced Caching
620
+ When a selector fails, the error message will contain helpful context to diagnose the issue:
405
621
 
406
622
  ```typescript
407
- import create, { createMemoryStorage, StorageProvider } from 'create-request';
408
-
409
- // Custom key generation
410
- const request = create.get()
411
- .withCache({
412
- storage: createMemoryStorage(),
413
- keyGenerator: (url, method, headers) => {
414
- // Include user ID from authorization header in the cache key
415
- // Get from your custom storage
416
- return localStorage.getItem(`my-app:${key}`);
417
- },
418
- set(key: string, value: string): void {
419
- // Set in your custom storage
420
- localStorage.setItem(`my-app:${key}`, value);
421
- },
422
- has(key: string): boolean {
423
- // Check if exists in your custom storage
424
- return localStorage.getItem(`my-app:${key}`) !== null;
425
- },
426
- delete(key: string): void {
427
- // Remove from your custom storage
428
- localStorage.removeItem(`my-app:${key}`);
429
- },
430
- clear(): void {
431
- // Clear all items from your namespace
432
- Object.keys(localStorage)
433
- .filter(key => key.startsWith('my-app:'))
434
- .forEach(key => localStorage.removeItem(key));
435
- }
436
- };
437
-
438
- const request = create.get()
439
- .withCache({ storage: customStorage })
440
- .sendTo('https://api.example.com/data');
623
+ try {
624
+ // This will fail if the response structure doesn't match expectations
625
+ const result = await create
626
+ .get("https://api.example.com/data")
627
+ .getData(data => data.results.items);
628
+ } catch (error) {
629
+ console.error(error);
630
+ // Error message includes context for debugging
631
+ }
441
632
  ```
442
633
 
443
- ### Implementing Custom Storage Providers
444
-
445
- You can create your own storage provider by implementing the `StorageProvider` interface:
634
+ ## TypeScript Support
446
635
 
447
636
  ```typescript
448
- import { StorageProvider } from 'create-request';
449
-
450
- // Create a simple custom storage provider
451
- export function createNamespacedStorage(namespace: string): StorageProvider {
452
- return {
453
- get(key: string): string | null {
454
- return localStorage.getItem(`${namespace}:${key}`);
455
- },
456
-
457
- set(key: string, value: string): void {
458
- try {
459
- localStorage.setItem(`${namespace}:${key}`, value);
460
- } catch (e) {
461
- // Handle quota exceeded errors
462
- console.warn("Storage quota exceeded, clearing cache");
463
- this.clear();
464
- try {
465
- localStorage.setItem(`${namespace}:${key}`, value);
466
- } catch (e) {
467
- console.error("Failed to set cache item even after clearing cache");
468
- }
469
- }
470
- },
471
-
472
- has(key: string): boolean {
473
- return localStorage.getItem(`${namespace}:${key}`) !== null;
474
- },
475
-
476
- delete(key: string): void {
477
- localStorage.removeItem(`${namespace}:${key}`);
478
- },
637
+ interface User {
638
+ id: number;
639
+ name: string;
640
+ email: string;
641
+ isActive: boolean;
642
+ }
479
643
 
480
- clear(): void {
481
- // Only clear items in our namespace
482
- Object.keys(localStorage)
483
- .filter(key => key.startsWith(`${namespace}:`))
484
- .forEach(key => localStorage.removeItem(key));
485
- }
644
+ interface ApiResponse<T> {
645
+ data: T;
646
+ meta: {
647
+ total: number;
648
+ page: number;
486
649
  };
487
650
  }
488
651
 
489
- // Usage:
490
- const request = create.get()
491
- .withCache({
492
- storage: createNamespacedStorage('api-cache'),
493
- ttl: 24 * 60 * 60 * 1000 // 24 hours
494
- })
495
- .sendTo('https://api.example.com/data');
496
- ```
652
+ // Type the full response
653
+ const response = await create
654
+ .get("https://api.example.com/users")
655
+ .getJson<ApiResponse<User[]>>();
497
656
 
498
- With custom storage providers, you can integrate with any storage system, such as:
657
+ // Or use getData with type parameters
658
+ const users = await create
659
+ .get("https://api.example.com/users")
660
+ .getData<ApiResponse<User[]>, User[]>(data => data.data);
499
661
 
500
- - Custom browser storage solutions
501
- - Memory caches with expiration policies
502
- - Session-based storage mechanisms
503
- - Encrypted storage providers
504
- - Third-party storage libraries
662
+ // TypeScript knows the types
663
+ users.forEach(user => {
664
+ console.log(`${user.name} (${user.email}): ${user.isActive ? "Active" : "Inactive"}`);
665
+ });
666
+
667
+ // Function with proper types
668
+ async function getUserById(id: number): Promise<User> {
669
+ return create
670
+ .get("https://api.example.com/users")
671
+ .withQueryParam("id", id)
672
+ .getData<ApiResponse<User[]>, User>(data => {
673
+ const user = data.data[0];
674
+ if (!user) throw new Error(`User with ID ${id} not found`);
675
+ return user;
676
+ });
677
+ }
678
+ ```
505
679
 
506
680
  ## CSRF Protection
507
681
 
@@ -523,11 +697,15 @@ You can configure CSRF settings globally for all requests:
523
697
 
524
698
  ```typescript
525
699
  // Configure CSRF settings for all requests
526
- create.config.setCsrfToken('your-csrf-token');
527
- create.config.setCsrfHeaderName('X-CSRF-Token'); // Default header name
528
- create.config.setXsrfCookieName('XSRF-TOKEN'); // Default cookie name
529
- create.config.enableAntiCsrf(true); // Enable/disable X-Requested-With header
530
- create.config.enableAutoXsrf(true); // Enable/disable automatic cookie-to-header token
700
+ create.config.setCsrfToken("your-csrf-token");
701
+ create.config.setCsrfHeaderName("X-CSRF-Token"); // Default header name for CSRF token
702
+ create.config.setXsrfCookieName("XSRF-TOKEN"); // Default cookie name to read from
703
+ create.config.setXsrfHeaderName("X-XSRF-TOKEN"); // Default header name for XSRF token from cookie
704
+ create.config.setEnableAntiCsrf(true); // Enable/disable X-Requested-With header
705
+ create.config.setEnableAutoXsrf(true); // Enable/disable automatic cookie-to-header token
706
+
707
+ // Reset all configuration to defaults
708
+ create.config.reset();
531
709
  ```
532
710
 
533
711
  ### Per-Request CSRF Settings
@@ -536,22 +714,22 @@ You can also configure CSRF protection on individual requests:
536
714
 
537
715
  ```typescript
538
716
  // Configure CSRF for a specific request
539
- const request = create.post()
540
- .withCsrfToken('request-specific-token') // Set a specific token
717
+ const request = create
718
+ .post("https://api.example.com/users")
719
+ .withCsrfToken("request-specific-token") // Set a specific token
541
720
  .withAntiCsrfHeaders() // Explicitly add X-Requested-With header
542
721
  .withoutCsrfProtection(); // Or disable all automatic CSRF protection
543
722
  ```
544
723
 
545
- ### Integrating with Backend Frameworks
724
+ ## Performance Considerations
546
725
 
547
- Most modern frameworks support CSRF protection out of the box. The library works seamlessly with:
726
+ create-request is designed to be lightweight and efficient:
548
727
 
549
- - **Laravel**: Automatically reads XSRF-TOKEN cookie and sends X-XSRF-TOKEN header
550
- - **Rails**: Works with the Rails CSRF token system
551
- - **Django**: Compatible with Django's CSRF middleware
552
- - **Express.js + csurf**: Works with the csurf middleware token pattern
553
-
554
- When your server sends a CSRF token in a cookie or response header, `create-request` can automatically extract and include it in subsequent requests.
728
+ - **Zero Dependencies**: No extra libraries to load
729
+ - **Tree-Shakable**: Only import what you need
730
+ - **Minimal Overhead**: Thin wrapper around the native Fetch API
731
+ - **Memory Efficient**: Doesn't create unnecessary objects
732
+ - **Clean API**: Simple and intuitive interface
555
733
 
556
734
  ## Browser Support
557
735
 
@@ -563,6 +741,35 @@ This library works with all browsers that support the Fetch API:
563
741
  - Edge 14+
564
742
  - Opera 29+
565
743
 
744
+ ## Comparison of JavaScript HTTP Client Libraries
745
+
746
+ | Feature | create-request | Fetch | Axios | SuperAgent | Got | Ky | node-fetch | Redaxios |
747
+ | ------------------- | -------------- | ------ | ------- | ---------- | ------- | ------ | ---------- | -------- |
748
+ | **Size (min+gzip)** | ~5.5KB | Native | ~13.6KB | ~17.8KB | ~17.8KB | ~3.4KB | ~7.7KB | ~1KB |
749
+ | **Browser** | Modern | Modern | IE11+ | IE9+ | ❌ No | Modern | ❌ No | Modern |
750
+ | **Node.js** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
751
+ | **HTTP/2** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
752
+ | **Auto Retries** | ✅ | ❌ | 🛠️ | ✅ | ✅ | ✅ | ❌ | ❌ |
753
+ | **Cancellation** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
754
+ | **Auto JSON** | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
755
+ | **Timeout** | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
756
+ | **TypeScript** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
757
+ | **Streaming** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
758
+ | **Progress** | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
759
+ | **Middleware** | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
760
+ | **Cookies** | ✅ | ✅ | 🛠️ | ✅ | ✅ | ❌ | ❌ | ❌ |
761
+ | **Pagination API** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
762
+ | **Zero Deps** | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
763
+ | **Chainable API** | ✅ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
764
+ | **CSRF Protection** | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
765
+ | **GraphQL Support** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
766
+ | **Interceptors** | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
767
+
768
+ **Notes:**
769
+
770
+ - "Modern" browser support: Chrome 42+, Firefox 39+, Safari 10.1+, Edge 14+, Opera 29+
771
+ - 🛠️ Feature requires additional plugins or adapters (not available out-of-the-box)
772
+
566
773
  ## License
567
774
 
568
775
  MIT