create-request 0.0.1-beta.9 → 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,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,189 @@ 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();
191
+ .withTimeout(3000)
192
+ .getData();
147
193
  ```
148
194
 
149
- You can also create reusable base requests with common settings:
195
+ ### Request Bodies (POST/PUT/PATCH)
150
196
 
151
197
  ```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);
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 });
161
202
 
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();
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);
165
221
  ```
166
222
 
167
- ### Request Bodies (POST/PUT/PATCH)
223
+ ### GraphQL Requests
224
+
225
+ The library provides built-in support for GraphQL queries and mutations:
168
226
 
169
227
  ```typescript
170
- // JSON body
171
- const jsonRequest = create.post()
172
- .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
+ ```
173
249
 
174
- // String body
175
- const textRequest = create.post()
176
- .withBody('Plain text content');
250
+ The `withGraphQL` method automatically:
177
251
 
178
- // Form data
179
- const formData = new FormData();
180
- formData.append('name', 'John');
181
- 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:
182
261
 
183
- const formRequest = create.post()
184
- .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
185
289
  ```
186
290
 
187
291
  ### Executing Requests
188
292
 
189
293
  ```typescript
190
- // Simple execution
191
- 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();
192
296
 
193
297
  // 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();
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();
199
302
 
200
303
  // Using the data selector API to extract specific data
201
- const userData = await request.sendTo('https://api.example.com/users')
304
+ const userData = await create
305
+ .get("https://api.example.com/users")
202
306
  .getData(data => data.results.users);
203
307
 
204
308
  // 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();
309
+ const fullData = await create.get("https://api.example.com/data").getData();
206
310
  ```
207
311
 
208
- ### Data Selection
312
+ ### ResponseWrapper Properties
209
313
 
210
- 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:
211
315
 
212
316
  ```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);
217
-
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));
222
-
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));
227
-
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
- }));
236
- ```
237
-
238
- When a selector fails, the error message will contain the original response data to help diagnose the issue:
239
-
240
- ```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
- }
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
249
333
  ```
250
334
 
251
335
  ### Error Handling
@@ -254,14 +338,15 @@ All errors from requests are instances of `RequestError` with detailed informati
254
338
 
255
339
  ```typescript
256
340
  try {
257
- const data = await request.sendTo('https://api.example.com/data').getJson();
341
+ const data = await create.get("https://api.example.com/data").getJson();
258
342
  } catch (error) {
259
343
  // 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
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
265
350
 
266
351
  // Access the original response if available
267
352
  if (error.response) {
@@ -271,53 +356,186 @@ try {
271
356
  }
272
357
  ```
273
358
 
274
- ### Caching Requests
359
+ ## Advanced Usage
360
+
361
+ ### GraphQL Support
362
+
363
+ `create-request` provides first-class support for GraphQL with the `withGraphQL` method:
275
364
 
276
365
  ```typescript
277
- 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:
278
450
 
279
- // Simple in-memory caching
280
- const request = create.get()
281
- .withCache({
282
- storage: createMemoryStorage(),
283
- 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;
284
459
  })
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'
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;
294
468
  })
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']
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);
303
480
  })
304
- .sendTo('https://api.example.com/data');
481
+ .getJson();
305
482
  ```
306
483
 
307
- ## Advanced Usage
484
+ #### Interceptor Execution Order
485
+
486
+ Interceptors execute in a specific order:
308
487
 
309
- ### 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)
310
491
 
311
492
  ```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();
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();
321
539
  ```
322
540
 
323
541
  ### Request Cancellation
@@ -325,7 +543,8 @@ const products = await authRequest.sendTo('https://api.example.com/products').ge
325
543
  ```typescript
326
544
  const controller = new AbortController();
327
545
 
328
- const request = create.get()
546
+ const request = create
547
+ .get("https://api.example.com/slow-endpoint")
329
548
  .withTimeout(10000)
330
549
  .withAbortController(controller);
331
550
 
@@ -333,151 +552,130 @@ const request = create.get()
333
552
  setTimeout(() => controller.abort(), 2000);
334
553
 
335
554
  try {
336
- const data = await request.sendTo('https://api.example.com/slow-endpoint').getJson();
555
+ const data = await request.getJson();
337
556
  } catch (error) {
338
- // Check if request was cancelled
339
- 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
+ }
340
564
  }
341
565
  ```
342
566
 
343
- ### Working with Typed Responses
344
-
345
- ```typescript
346
- interface User {
347
- id: number;
348
- name: string;
349
- email: string;
350
- }
351
-
352
- interface ApiResponse {
353
- users: User[];
354
- pagination: {
355
- total: number;
356
- page: number;
357
- }
358
- }
567
+ ### URL Handling
359
568
 
360
- const userRequest = create.get();
569
+ The library handles both absolute and relative URLs, and automatically merges query parameters:
361
570
 
362
- // Type the full response
363
- const response = await userRequest
364
- .sendTo('https://api.example.com/users')
365
- .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
+ ```
366
591
 
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);
592
+ ### Data Selection
371
593
 
372
- // Or just get the full typed response
373
- const fullData = await userRequest
374
- .sendTo('https://api.example.com/users')
375
- .getData<ApiResponse>();
594
+ The `getData` method provides a powerful way to extract and transform specific data from API responses:
376
595
 
377
- // TypeScript knows the types
378
- console.log(users[0].name);
379
- ```
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);
380
601
 
381
- ### 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));
382
606
 
383
- ```typescript
384
- 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));
385
611
 
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
- });
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
+ }));
397
618
  ```
398
619
 
399
- ### Advanced Caching
620
+ When a selector fails, the error message will contain helpful context to diagnose the issue:
400
621
 
401
622
  ```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');
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
+ }
417
632
  ```
418
633
 
419
- ### Implementing Custom Storage Providers
420
-
421
- You can create your own storage provider by implementing the `StorageProvider` interface:
634
+ ## TypeScript Support
422
635
 
423
636
  ```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
- },
637
+ interface User {
638
+ id: number;
639
+ name: string;
640
+ email: string;
641
+ isActive: boolean;
642
+ }
455
643
 
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
- }
644
+ interface ApiResponse<T> {
645
+ data: T;
646
+ meta: {
647
+ total: number;
648
+ page: number;
462
649
  };
463
650
  }
464
651
 
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
- ```
652
+ // Type the full response
653
+ const response = await create
654
+ .get("https://api.example.com/users")
655
+ .getJson<ApiResponse<User[]>>();
473
656
 
474
- 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);
475
661
 
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
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
+ ```
481
679
 
482
680
  ## CSRF Protection
483
681
 
@@ -499,11 +697,15 @@ You can configure CSRF settings globally for all requests:
499
697
 
500
698
  ```typescript
501
699
  // 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
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
505
704
  create.config.setEnableAntiCsrf(true); // Enable/disable X-Requested-With header
506
705
  create.config.setEnableAutoXsrf(true); // Enable/disable automatic cookie-to-header token
706
+
707
+ // Reset all configuration to defaults
708
+ create.config.reset();
507
709
  ```
508
710
 
509
711
  ### Per-Request CSRF Settings
@@ -512,22 +714,22 @@ You can also configure CSRF protection on individual requests:
512
714
 
513
715
  ```typescript
514
716
  // Configure CSRF for a specific request
515
- const request = create.post()
516
- .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
517
720
  .withAntiCsrfHeaders() // Explicitly add X-Requested-With header
518
721
  .withoutCsrfProtection(); // Or disable all automatic CSRF protection
519
722
  ```
520
723
 
521
- ### Integrating with Backend Frameworks
724
+ ## Performance Considerations
522
725
 
523
- 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:
524
727
 
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.
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
531
733
 
532
734
  ## Browser Support
533
735
 
@@ -539,6 +741,35 @@ This library works with all browsers that support the Fetch API:
539
741
  - Edge 14+
540
742
  - Opera 29+
541
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
+
542
773
  ## License
543
774
 
544
775
  MIT