create-request 1.6.0 โ†’ 2.0.0-next.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
@@ -1,1409 +1,539 @@
1
1
  # create-request
2
2
 
3
- [![License](https://img.shields.io/npm/l/create-request.svg)](https://github.com/DanielAmenou/create-request/blob/main/LICENSE)
3
+ [![npm version](https://img.shields.io/npm/v/create-request.svg)](https://www.npmjs.com/package/create-request)
4
+ [![Bundle size](https://img.shields.io/bundlephobia/minzip/create-request)](https://bundlephobia.com/package/create-request)
4
5
  [![codecov](https://codecov.io/github/danielamenou/create-request/graph/badge.svg?token=OUBR6RNXZO)](https://codecov.io/github/danielamenou/create-request)
5
6
  [![npm downloads](https://img.shields.io/npm/dt/create-request.svg)](https://www.npmjs.com/package/create-request)
6
- [![npm version](https://img.shields.io/npm/v/create-request.svg)](https://www.npmjs.com/package/create-request)
7
- [![Bundle Size](https://img.shields.io/bundlephobia/minzip/create-request)](https://bundlephobia.com/package/create-request)
8
- [![TypeScript](https://img.shields.io/badge/TypeScript-4.7%2B-blue)](https://www.typescriptlang.org/)
9
- [![Snyk security report](https://img.shields.io/badge/Snyk-security%20report-4C1A51?logo=snyk)](https://security.snyk.io/package/npm/create-request)
10
-
11
- `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.
12
-
13
- ## Table of Contents
14
-
15
- - [Core Features](#core-features)
16
- - [Why create-request](#why-create-request)
17
- - [Mental Model](#mental-model)
18
- - [Installation](#installation)
19
- - [Named Exports](#named-exports)
20
- - [Tree-Shaking Guide](#tree-shaking-guide)
21
- - [Basic Usage](#basic-usage)
22
- - [URL Handling](#url-handling)
23
- - [Advanced Usage](#advanced-usage)
24
- - [API Builder](#api-builder)
25
- - [Automatic Retries with Delay](#automatic-retries-with-delay)
26
- - [Interceptors](#interceptors)
27
- - [Request Cancellation](#request-cancellation)
28
- - [Custom Fetch Injection](#custom-fetch-injection)
29
- - [Data Selection](#data-selection)
30
- - [TypeScript Support](#typescript-support)
31
- - [CSRF Protection](#csrf-protection)
32
- - [Subresource Integrity and Cache Control](#subresource-integrity-and-cache-control)
33
- - [Performance Considerations](#performance-considerations)
34
- - [Browser & Node.js Support](#browser--nodejs-support)
35
- - [Comparison of JavaScript HTTP Client Libraries](#comparison-of-javascript-http-client-libraries)
36
- - [License](#license)
37
- - [Website](#website)
38
- - [Sponsor](#sponsor)
39
-
40
- ## Core Features
41
-
42
- - ๐Ÿš€ **Performance** - Tiny bundle size with zero dependencies
43
- - ๐Ÿšง **Error Handling** - Detailed error info with custom error class
44
- - โ›“๏ธ **Chainable API** - Build and execute requests with a fluent interface
45
- - โฑ๏ธ **Timeout Support** - Set timeouts for requests with automatic aborts
46
- - ๐Ÿ›ก๏ธ **Type Safety** - Full TypeScript support with intelligent type inference
47
- - ๐Ÿ” **Auth Helpers** - Simple methods for common authentication patterns
48
- - ๐Ÿ” **Data Selection** - Extract and transform specific data from responses
49
- - ๐Ÿ” **Automatic Retries** - Retry failed requests with customizable settings
50
- - ๐Ÿ“‰ **Reduced Boilerplate** - Write 60% less code for common API operations
51
- - ๐Ÿ”’ **CSRF Protection** - Built-in safeguards against cross-site request forgery
52
- - ๐Ÿ—๏ธ **API Builder** - Create configured API instances with reusable default settings
53
- - ๐Ÿ›‘ **Request Cancellation** - Abort requests on demand with AbortController integration
54
- - ๐Ÿ”Œ **Interceptors** - Global and per-request interceptors for requests, responses, and errors
55
- - ๐Ÿงฉ **Custom Fetch** - Inject any fetch-compatible function for testing, undici agents, or Next.js caching
56
- - ๐Ÿ”ท **GraphQL Support** - Built-in GraphQL query and mutation helpers
57
-
58
- ## Why create-request?
59
-
60
- **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:
61
-
62
- ### With Regular Fetch
63
-
64
- ```typescript
65
- async function createUser(userData) {
66
- try {
67
- const response = await fetch("https://api.example.com/users", {
68
- method: "POST",
69
- headers: {
70
- "Content-Type": "application/json",
71
- Authorization: "Basic " + btoa("username:password"),
72
- },
73
- body: JSON.stringify(userData),
74
- });
75
-
76
- if (!response.ok) {
77
- throw new Error(`HTTP error! status: ${response.status}`);
78
- }
79
- const data = await response.json();
80
- return data;
81
- } catch (error) {
82
- console.error("Fetch error:", error);
83
- throw error;
84
- }
85
- }
86
- ```
7
+ [![License](https://img.shields.io/npm/l/create-request.svg)](https://github.com/DanielAmenou/create-request/blob/main/LICENSE)
87
8
 
88
- ### With create-request
9
+ A small, fully typed wrapper around `fetch`. Configure a request with `with*()` methods, then
10
+ send it and read the response with a `get*()` method.
89
11
 
90
12
  ```typescript
91
13
  import create from "create-request";
92
14
 
93
- function createUser(userData) {
94
- return create
95
- .post("https://api.example.com/users")
96
- .withBasicAuth("username", "password")
97
- .withBody(userData) // Content-Type automatically set to application/json
98
- .getData<User>() // Type-safe response handling
99
- .catch(error => {
100
- console.error("Fetch error:", error);
101
- throw error;
102
- });
103
- }
104
- ```
105
-
106
- ### Why Not Object-Based Configuration?
107
-
108
- Many HTTP client libraries (like Axios, Got, and even the native Fetch API) use object-based configuration where all options are passed in a single configuration object. While this approach works, it creates a poor developer experience:
109
-
110
- **The Developer Experience Problem:**
111
-
112
- With object-based configuration, you're constantly context-switching between your code and documentation. You need to:
113
-
114
- - Remember exact option names and their structure
115
- - Look up documentation to discover available options
116
- - Guess at nested object structures
117
- - Hope your IDE autocomplete works with complex nested types
118
-
119
- ```typescript
120
- // Object-based: What options are available? What's the structure? Need to check docs
121
- axios.post("https://api.example.com/users", userData, {
122
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
123
- timeout: 5000,
124
- params: { validate: true },
125
- withCredentials: true,
126
- });
127
- ```
128
-
129
- **Superior Developer Experience with Fluent API:**
130
-
131
- `create-request`'s fluent API is designed for an exceptional developer experience. Every method is discoverable, self-documenting, and provides rich IDE support:
132
-
133
- #### 1. Intelligent Autocomplete & IntelliSense
134
-
135
- As you type, your IDE suggests the exact methods you need. No guessing, no documentation lookup:
136
-
137
- ```typescript
138
- // Start typing and see all available methods
139
- create
140
- .post(url)
141
- .withBearerToken() // โ† IDE suggests: withBearerToken(token: string)
142
- .withTimeout() // โ† IDE suggests: withTimeout(ms: number)
143
- .withRetries(); // โ† IDE suggests: withRetries(count: number | RetryConfig)
144
- ```
145
-
146
- #### 2. Rich JSDoc Documentation in Your IDE
147
-
148
- Hover over any method to see comprehensive documentation, examples, and parameter details - all without leaving your editor:
149
-
150
- ```typescript
151
- // Hover over withRetries to see:
152
- // "Configures automatic retry behavior for failed requests.
153
- // @param retries - Number of retry attempts or retry configuration object
154
- // @example
155
- // .withRetries(3)
156
- // .withRetries({ attempts: 3, delay: 1000 })"
157
- create.get(url).withRetries(3);
158
- ```
159
-
160
- #### 3. Discoverability Through Method Chaining
161
-
162
- Each method reveals what's available next. Explore the API naturally through autocomplete:
163
-
164
- ```typescript
165
- // Discover available options as you chain
166
- create
167
- .post(url)
168
- .withHeaders() // โ† See all header methods
169
- .withBearerToken() // โ† See all auth methods
170
- .withTimeout() // โ† See all timeout/retry methods
171
- .withQueryParams(); // โ† See all query param methods
172
- ```
173
-
174
- #### 4. No Context Switching
175
-
176
- Stay in your flow. Everything you need is in your IDE - documentation, types, examples, and autocomplete. No alt-tabbing to documentation websites.
177
-
178
- This developer-first approach means you spend less time looking things up and more time writing code that works.
179
-
180
- ## Mental Model
181
-
182
- ### 1. **Separation of Building and Execution**
183
-
184
- Requests are built first, then executed. This separation allows you to:
185
-
186
- - Configure requests incrementally
187
- - Reuse request configurations
188
- - Pass requests around before executing them
189
- - Chain configuration methods fluently
190
-
191
- ```typescript
192
- // Building phase: configure the request
193
- const request = create
194
- .get("https://api.example.com/users")
195
- .withBearerToken(token)
196
- .withTimeout(5000);
197
-
198
- // Execution phase: actually make the HTTP call
199
- const data = await request.getJson();
200
- ```
201
-
202
- ### 2. **Fluent Chainable Interface**
203
-
204
- Every configuration method returns the request instance, enabling method chaining. This creates a readable, declarative API that reads like a sentence:
205
-
206
- ```typescript
207
- // Reads like: "Create a POST request to users endpoint, with auth, body, and timeout, then get JSON"
208
- const user = await create
209
- .post("https://api.example.com/users")
210
- .withBearerToken(token)
211
- .withBody(userData)
212
- .withTimeout(3000)
213
- .getJson();
214
- ```
215
-
216
- ### 3. **Configuration Layers**
217
-
218
- Configuration follows a layered approach, with more specific settings overriding general ones:
219
-
220
- 1. **Global Configuration** (via `create.config`) - Applies to all requests
221
- 2. **API Builder Defaults** (via `create.api()`) - Applies to requests from that API instance
222
- 3. **Per-Request Configuration** - Specific to individual requests
223
-
224
- ```typescript
225
- // Global: all requests get this
226
- create.config.setCsrfToken("global-token");
227
-
228
- // API instance: requests from this API get these defaults
229
- const api = create
230
- .api()
231
- .withBaseURL("https://api.example.com")
232
- .withBearerToken("default-token");
233
-
234
- // Per-request: this specific request overrides the default token
235
- const user = await api
236
- .get("/users")
237
- .withBearerToken("specific-token") // Overrides default-token
238
- .getJson();
239
- ```
240
-
241
- ### 4. **Request Definition with `with...` Functions**
242
-
243
- All request configuration is done through methods that start with `with...`. This consistent naming convention makes it immediately clear which methods are used for configuration:
244
-
245
- ```typescript
246
- // All configuration uses 'with...' prefix
247
- const request = create
15
+ const users = await create
248
16
  .get("https://api.example.com/users")
249
- .withHeaders({ "X-API-Key": "abc123" })
250
- .withBearerToken("token")
251
17
  .withTimeout(5000)
252
- .withRetries(3)
253
- .withQueryParams({ page: 1 })
254
- .withCookie("session", "abc123");
255
- ```
256
-
257
- This pattern makes the API self-documenting - any method starting with `with...` is a configuration method that returns the request instance for chaining.
258
-
259
- ### 5. **Request Lifecycle**
260
-
261
- The typical request lifecycle follows this pattern:
262
-
263
- ```text
264
- Build โ†’ Configure โ†’ Execute โ†’ Transform โ†’ Handle
265
- ```
266
-
267
- 1. **Build**: Create a request with a method and URL (`create.get(url)`)
268
- 2. **Configure**: Chain configuration methods using `with...` functions (`.withHeaders()`, `.withTimeout()`, etc.)
269
- 3. **Execute**: Call an execution method (`.getJson()`, `.getData()`, etc.)
270
- 4. **Transform**: Optionally transform the response (via `.getData()` selector or interceptors)
271
- 5. **Handle**: Process the result or catch errors
272
-
273
- ### 6. **Promise-Based Execution**
274
-
275
- All execution methods return Promises, making the library compatible with:
276
-
277
- - `async/await` syntax (recommended)
278
- - `.then()/.catch()` chains
279
- - Promise utilities like `Promise.all()`, `Promise.race()`, etc.
280
-
281
- ```typescript
282
- // All of these work:
283
- const data1 = await request.getJson();
284
-
285
- request.getJson().then(data => console.log(data));
286
-
287
- const results = await Promise.all([
288
- create.get("/users").getJson(),
289
- create.get("/posts").getJson(),
290
- ]);
18
+ .getJson<User[]>();
291
19
  ```
292
20
 
293
- ### 7. **Comprehensive JSDoc Documentation**
21
+ Retries, timeouts, interceptors and schema validation are built in. It has no dependencies, is
22
+ under 5 KB min+gzip, and runs in browsers and Node.js 22+.
294
23
 
295
- The library includes extensive JSDoc documentation throughout the codebase. This documentation is valuable for developers of all levels:
24
+ ## Table of contents
296
25
 
297
- - **For Junior Developers**: JSDoc provides clear explanations of what each method does, parameter types, return values, and usage examples directly in your IDE. This helps with learning and understanding the API without constantly referring to external documentation.
298
-
299
- - **For Senior Developers**: JSDoc offers detailed type information, edge cases, and implementation details that enable deeper understanding and more advanced usage patterns. The type definitions help with TypeScript inference and ensure type safety.
26
+ - [Installation](#installation)
27
+ - [60-second start](#60-second-start)
28
+ - [The mental model](#the-mental-model)
29
+ - [Requests](#requests) โ€” [creating](#creating), [configuring](#configuring),
30
+ [executing](#executing), [reusing](#reusing)
31
+ - [Errors](#errors)
32
+ - [Api instances](#api-instances)
33
+ - [Retries](#retries)
34
+ - [Timeouts and cancellation](#timeouts-and-cancellation)
35
+ - [Interceptors](#interceptors)
36
+ - [Schema validation](#schema-validation)
37
+ - [GraphQL](#graphql)
38
+ - [Streaming and downloads](#streaming-and-downloads)
39
+ - [Testing and custom fetch](#testing-and-custom-fetch)
40
+ - [Cookies and CSRF](#cookies-and-csrf)
41
+ - [TypeScript](#typescript)
42
+ - [Design principles](#design-principles)
43
+ - [Size](#size)
44
+ - [Migrating from v1](#migrating-from-v1)
300
45
 
301
46
  ## Installation
302
47
 
303
- ```bash
304
- # npm
48
+ ```sh
305
49
  npm install create-request
306
-
307
- # yarn
308
- yarn add create-request
309
-
310
- # pnpm
311
- pnpm add create-request
312
50
  ```
313
51
 
314
- ## Named Exports
315
-
316
- The library provides both default and named exports:
317
-
318
- **Default Export:**
52
+ | Runtime | Support |
53
+ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
54
+ | Node.js | 22 or newer |
55
+ | Browsers | Anything with `fetch`, `AbortSignal` and ES2022 โ€” Chrome/Edge 93+, Firefox 91+, Safari 15+ (2021 and later) |
56
+ | Bun, Deno, edge runtimes | Only standard `fetch` / `AbortSignal` / `URL` APIs are used, so they are expected to work |
57
+ | Module formats | ESM (`dist/index.js`) and CommonJS (`dist/index.cjs`). From CommonJS the entry point is the `default` export: `const { default: create, createApi } = require("create-request")` |
58
+ | TypeScript | 5.0 or newer; the declarations resolve with the DOM lib **or** with `@types/node` alone |
319
59
 
320
- - `create` - Main API object with factory methods (`get`, `post`, `put`, `del`, `patch`, `head`, `options`, `api`) and global `config`
321
-
322
- **Named Exports:**
323
-
324
- **Enums:**
325
-
326
- - `HttpMethod`, `RequestPriority`, `CredentialsPolicy`, `RequestMode`, `RedirectMode`, `SameSitePolicy`, `ReferrerPolicy`, `CacheMode`
327
-
328
- **Types:**
329
-
330
- - `RetryCallback`, `RetryConfig`, `CookiesRecord`, `CookieOptions`, `RequestConfig`, `GraphQLOptions`, `RequestOptions`, `ErrorInterceptor`, `RequestInterceptor`, `RetryDelayFunction`, `ResponseInterceptor`
331
-
332
- **Classes:**
333
-
334
- - `ResponseWrapper`, `CookieUtils`, `RequestError`
335
-
336
- **Request Classes:**
337
-
338
- - `GetRequest`, `PostRequest`, `PutRequest`, `DeleteRequest`, `PatchRequest`, `HeadRequest`, `OptionsRequest`
339
-
340
- **Factory Functions:**
341
-
342
- - `createGet`, `createPost`, `createPut`, `createDelete`, `createPatch`, `createHead`, `createOptions`, `createApi`
60
+ ## 60-second start
343
61
 
344
62
  ```typescript
345
- // Default export
346
- import create from "create-request";
63
+ import create, { createApi, isRequestError } from "create-request";
347
64
 
348
- // Named exports
349
- import { RequestError, CacheMode, createGet } from "create-request";
350
- ```
351
-
352
- ## Tree-Shaking Guide
353
-
354
- All named exports are tree-shakeable. The library is marked with `"sideEffects": false`, enabling bundlers to eliminate unused code.
355
-
356
- **Tree-shakeable:**
357
-
358
- - โœ… All named exports (enums, types, classes, factory functions)
359
- - โœ… Individual factory functions (`createGet`, `createPost`, etc.)
360
- - โœ… Individual request classes (`GetRequest`, `PostRequest`, etc.)
361
-
362
- **Not tree-shakeable:**
363
-
364
- - Default export (`create`) - imports the entire API object (library is small, so this is usually fine)
365
-
366
- **Tip:** For maximum tree-shaking, you can use named exports when you only need specific functionality:
367
-
368
- ```typescript
369
- // Tree-shakeable - only imports what you use
370
- import { createGet } from "create-request";
371
-
372
- // Imports entire library (recommended for most use cases)
373
- import create from "create-request";
374
- ```
65
+ // 1. A request is built with with*() and executed with get*()
66
+ const users = await create.get("https://api.example.com/users").getJson<User[]>();
375
67
 
376
- ## Basic Usage
377
-
378
- ### Creating Requests
379
-
380
- ```typescript
381
- import create from "create-request";
382
-
383
- // Create different request types with URL
384
- const getRequest = create.get("https://api.example.com/users"); // GET
385
- const putRequest = create.put("https://api.example.com/users/1"); // PUT
386
- const postRequest = create.post("https://api.example.com/users"); // POST
387
- const headRequest = create.head("https://api.example.com/users/1"); // HEAD
388
- const patchRequest = create.patch("https://api.example.com/users/1"); // PATCH
389
- const deleteRequest = create.del("https://api.example.com/users/1"); // DELETE
390
- const optionsRequest = create.options("https://api.example.com/users"); // OPTIONS
391
- ```
392
-
393
- ### Request Configuration
394
-
395
- The library provides a comprehensive set of configuration methods that can be chained together to customize your requests:
396
-
397
- ```typescript
398
- import create, {
399
- RequestPriority,
400
- CredentialsPolicy,
401
- RedirectMode,
402
- ReferrerPolicy,
403
- SameSitePolicy,
404
- CacheMode,
405
- } from "create-request";
406
-
407
- // Configure request options
408
- const request = create
409
- .get("https://api.example.com/users")
410
- // Basic headers
411
- .withHeaders({ "X-API-Key": "abc123", "Accept-Language": "en-US" })
412
- .withHeader("Custom-Header", "value") // Add a single header
413
-
414
- // Timeout settings
415
- .withTimeout(5000) // Request will abort after 5 seconds
416
-
417
- // Automatic retry configuration
418
- .withRetries(3) // Retry up to 3 times on failure
419
- // Or use a config object with delay support:
420
- .withRetries({ attempts: 3, delay: 1000 }) // Retry 3 times with 1 second delay between attempts
421
- .onRetry(({ attempt, error }) => {
422
- console.log(`Attempt ${attempt} failed: ${error.message}. Retrying...`);
423
- })
424
-
425
- // Authentication methods
426
- .withBearerToken("your-token") // Adds Authorization: Bearer your-token
427
- .withBasicAuth("username", "password") // HTTP Basic Authentication
428
- .withAuthorization("auth-scheme value") // Custom authorization header
429
-
430
- // Add a single cookie
431
- .withCookie("language", "en-US")
432
-
433
- // Add multiple cookies
434
- .withCookies({
435
- sessionId: "abc123",
436
- preferences: { value: "dark-mode", secure: true },
437
- tracking: { value: "enabled", sameSite: SameSitePolicy.STRICT },
438
- })
439
-
440
- // URL parameters (supports arrays, null/undefined filtering, and all types)
441
- .withQueryParams({ search: "term", page: 1, limit: 20, tags: ["js", "ts"] })
442
- .withQueryParam("filter", "active") // Add a single query parameter
443
- .withQueryParam("ids", [1, 2, 3]) // Array values create multiple query params
444
-
445
- // Request body configuration (for POST/PUT/PATCH)
446
- .withContentType("application/json") // Set specific content type
447
-
448
- // Fetch API options
449
- // Note: These methods support three styles - Fluent API (shown below), Enum-based (e.g., .withMode(RequestMode.CORS)), or String-based (e.g., .withMode("cors"))
450
- .withCredentials.INCLUDE() // Includes cookies with cross-origin requests (Fluent API)
451
- .withMode.CORS() // Controls CORS behavior
452
- .withRedirect.FOLLOW() // Controls redirect behavior
453
- .withReferrer("https://example.com") // Sets request referrer
454
- .withReferrerPolicy.NO_REFERRER_WHEN_DOWNGRADE() // Controls referrer policy
455
- .withPriority.HIGH() // Sets request priority
456
- .withKeepAlive(true) // Keeps connection alive after the page is unloaded
457
- .withIntegrity("sha256-abcdef1234567890...") // Sets subresource integrity hash
458
- .withCache("no-cache"); // Direct string value (or use .withCache.NO_CACHE() for Fluent API)
459
- ```
460
-
461
- 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:
462
-
463
- ```typescript
464
- // Simple example with just what's needed
465
- const users = await create
466
- .get("https://api.example.com/users")
467
- .withBearerToken(userToken)
468
- .withQueryParams({ q: searchTerm, limit: 20 })
469
- .withTimeout(3000)
470
- .getData();
471
- ```
472
-
473
- ### Request Bodies (POST/PUT/PATCH)
474
-
475
- ```typescript
476
- // JSON body (Content-Type automatically set to application/json)
477
- const jsonRequest = create
68
+ // 2. Everything you would configure lives on the chain
69
+ const created = await create
478
70
  .post("https://api.example.com/users")
479
- .withBody({ name: "John", age: 30 });
480
-
481
- // String body (Content-Type automatically set to text/plain)
482
- const textRequest = create
483
- .post("https://api.example.com/users")
484
- .withBody("Plain text content");
485
-
486
- // Form data
487
- const formData = new FormData();
488
- formData.append("name", "John");
489
- formData.append("file", fileBlob);
490
-
491
- const formRequest = create.post("https://api.example.com/users").withBody(formData);
492
-
493
- // URLSearchParams (typically used for application/x-www-form-urlencoded)
494
- const params = new URLSearchParams();
495
- params.append("username", "john");
496
- params.append("password", "secret");
497
-
498
- const formUrlEncodedRequest = create.post("https://api.example.com/login").withBody(params);
499
- ```
500
-
501
- ### GraphQL Requests
502
-
503
- The library provides built-in support for GraphQL queries and mutations:
504
-
505
- ```typescript
506
- // GraphQL query without variables
507
- const userQuery = "query { users { id name email } }";
508
- const users = await create
509
- .post("https://api.example.com/graphql")
510
- .withGraphQL(userQuery)
511
- .getJson();
512
-
513
- // GraphQL query with variables
514
- const userQuery = "query GetUser($id: ID!) { user(id: $id) { name email } }";
515
- const user = await create
516
- .post("https://api.example.com/graphql")
517
- .withGraphQL(userQuery, { id: "123" })
518
- .getJson();
519
- ```
520
-
521
- #### GraphQL Error Handling
71
+ .withBearerToken(token)
72
+ .withBody({ name: "Ada" }) // JSON-encoded, Content-Type set for you
73
+ .withTimeout(5000)
74
+ .withRetries(2)
75
+ .getJson<User>();
522
76
 
523
- GraphQL errors do not cause exceptions by default. Use the `throwOnError` option to make them throw exceptions:
77
+ // 3. Shared defaults live on an api instance โ€” create one, export it, use it everywhere
78
+ const api = createApi().withBaseURL("https://api.example.com").withBearerToken(token);
524
79
 
525
- ```typescript
526
- // Throw an error if the GraphQL response contains errors
527
- const userQuery = "query GetUser($id: ID!) { user(id: $id) { name email } }";
528
80
  try {
529
- const user = await create
530
- .post("https://api.example.com/graphql")
531
- .withGraphQL(userQuery, { id: "123" }, { throwOnError: true })
532
- .getJson();
81
+ await api.delete(`/users/${id}`).getResponse();
533
82
  } catch (error) {
534
- console.error(error.message);
83
+ if (isRequestError(error) && error.status === 404) return; // one error type, with a code and status
84
+ throw error;
535
85
  }
536
86
  ```
537
87
 
538
- The `withGraphQL` method automatically:
539
-
540
- - Formats the body as JSON with `query` and optional `variables` properties
541
- - Sets `Content-Type` to `application/json`
542
- - Validates the query is non-empty
543
- - Validates variables are a plain object (not arrays or null)
544
- - Optionally throws errors when GraphQL response contains errors (with `throwOnError: true`)
545
-
546
- ### Query Parameters Advanced Features
547
-
548
- The library supports advanced query parameter handling:
88
+ ## The mental model
549
89
 
550
- ```typescript
551
- // Array values create multiple query params with the same key
552
- const request = create.get("https://api.example.com/search").withQueryParams({
553
- tags: ["javascript", "typescript", "node"], // ?tags=javascript&tags=typescript&tags=node
554
- page: 1,
555
- active: true,
556
- });
557
-
558
- // Null and undefined values are automatically filtered out
559
- const filtered = create.get("https://api.example.com/users").withQueryParams({
560
- name: "John",
561
- age: null, // Ignored
562
- email: undefined, // Ignored
563
- });
564
-
565
- // Supports all JavaScript types (strings, numbers, booleans, arrays)
566
- const typed = create.get("https://api.example.com/data").withQueryParams({
567
- page: 1, // Number
568
- active: true, // Boolean
569
- tags: ["js", "ts"], // Array
570
- name: "John", // String
571
- });
90
+ 1. `create.get(url)` / `create.post(url)` / โ€ฆ (or `api.get(path)`) return a **request**.
91
+ 2. `with*()` methods configure it and return the same request, so calls chain. Nothing is sent
92
+ yet.
93
+ 3. A `get*()` method sends it and gives you the body in the format you ask for โ€” or the
94
+ `ResponseWrapper` with `getResponse()`, or `{ data, error }` with `getResult()`.
95
+ 4. Every failure โ€” HTTP status, network, timeout, abort, parsing, validation, interceptor โ€”
96
+ rejects with a **`RequestError`** whose `code` says which.
97
+ 5. An **api instance** holds defaults (base URL, auth, timeout, retries, interceptors, โ€ฆ) for the
98
+ requests it creates. It is immutable: `api.withHeader(โ€ฆ)` returns a new instance.
572
99
 
573
- // Merge with existing query params in URL
574
- const merged = create
575
- .get("https://api.example.com/users?existing=value")
576
- .withQueryParams({ new: "param" }); // Both existing and new params included
577
- ```
100
+ ## Requests
578
101
 
579
- ### Executing Requests
102
+ ### Creating
580
103
 
581
104
  ```typescript
582
- // Get the full response
583
- const response = await create.get("https://api.example.com/endpoint").getResponse();
584
-
585
- // With direct data extraction
586
- const jsonData = await create.get("https://api.example.com/endpoint").getJson();
587
- const textData = await create.get("https://api.example.com/endpoint").getText();
588
- const blobData = await create.get("https://api.example.com/endpoint").getBlob();
589
- const bodyStream = await create.get("https://api.example.com/endpoint").getBody();
590
- const arrayBuffer = await create.get("https://api.example.com/endpoint").getArrayBuffer();
591
-
592
- // Using the data selector API to extract specific data
593
- const userData = await create
594
- .get("https://api.example.com/users")
595
- .getData(data => data.results.users);
596
-
597
- // Using the data selector without a selector function just returns the full JSON response
598
- const fullData = await create.get("https://api.example.com/data").getData();
105
+ create.get(url); // also head, options, post, put, patch, delete (alias: del)
106
+ api.get("/users"); // joined to the api's base URL
107
+ api.get(); // no path โ†’ the base URL itself
108
+ api.get<User>("/me"); // declare the JSON type once; getJson() / getData() / getResult() use it
599
109
  ```
600
110
 
601
- ### ResponseWrapper Properties
111
+ Named factories exist as well: `createGet`, `createPost`, `createPut`, `createPatch`,
112
+ `createDelete`, `createHead`, `createOptions` โ€” the same functions as `create.get`, โ€ฆ โ€” and
113
+ `createApi()` is also available as `create.api()`.
602
114
 
603
- When you use `getResponse()`, you get a `ResponseWrapper` object that provides convenient access to response properties and methods:
115
+ ### Configuring
604
116
 
605
117
  ```typescript
606
- const response = await create.get("https://api.example.com/users").getResponse();
607
-
608
- // Access response properties directly
609
- console.log(response.status); // HTTP status code (e.g., 200)
610
- console.log(response.statusText); // Status text (e.g., "OK")
611
- console.log(response.ok); // Boolean: true if status is 200-299
612
- console.log(response.headers); // Headers object
613
- console.log(response.url); // Request URL
614
- console.log(response.method); // HTTP method
615
- console.log(response.raw); // Raw Response object from fetch
616
-
617
- // Use wrapper methods for body parsing
618
- const stream = response.getBody(); // ReadableStream or null
619
- const json = await response.getJson();
620
- const text = await response.getText();
621
- const blob = await response.getBlob();
622
- const arrayBuffer = await response.getArrayBuffer();
118
+ create
119
+ .post("https://api.example.com/items")
120
+ // headers & auth โ€” names are case-insensitive, null removes a header
121
+ .withHeaders({ Accept: "application/json", "X-Trace": id })
122
+ .withHeader("X-Feature", "beta")
123
+ .withContentType("application/json") // rarely needed: JSON and text bodies set it themselves
124
+ .withBearerToken(token) // or withBasicAuth(user, pass) / withAuthorization("Custom โ€ฆ")
125
+ // query string โ€” arrays repeat the key, Dates become ISO strings, null removes the key,
126
+ // and a key set twice keeps the last value (a request can override an api default)
127
+ .withQueryParams({ page: 2, tags: ["a", "b"], since: new Date() })
128
+ .withQueryParam("q", "search term")
129
+ // body (POST, PUT, PATCH, DELETE only): objects โ†’ JSON, strings โ†’ text/plain,
130
+ // FormData / Blob / URLSearchParams / ArrayBuffer / typed arrays / ReadableStream โ†’ sent as-is
131
+ .withBody({ name: "Ada" })
132
+ // resilience
133
+ .withTimeout(5000) // per attempt; covers the response and the body read
134
+ .withRetries(3) // see "Retries" below
135
+ .withSignal(signal) // an AbortSignal from anywhere; call it again to combine signals
136
+ // fetch options, typed with the DOM unions
137
+ .withCredentials("include")
138
+ .withMode("cors")
139
+ .withCache("no-store")
140
+ .withRedirect("follow")
141
+ .withReferrer("https://app.example.com/")
142
+ .withReferrerPolicy("no-referrer")
143
+ .withPriority("high")
144
+ .withKeepAlive()
145
+ .withIntegrity("sha256-โ€ฆ");
146
+ ```
147
+
148
+ Sending a `FormData`? Do not set a `Content-Type` โ€” `fetch` adds the multipart boundary itself
149
+ (the library removes one if an api default put it there). The remaining methods have sections of
150
+ their own: `withCookie(s)` and `withCsrf` ([Cookies and CSRF](#cookies-and-csrf)),
151
+ `withRequestInterceptor` / `withResponseInterceptor` / `withErrorInterceptor`
152
+ ([Interceptors](#interceptors)), `withGraphQL` ([GraphQL](#graphql)), `withFetch`
153
+ ([Testing and custom fetch](#testing-and-custom-fetch)).
154
+
155
+ ### Executing
156
+
157
+ | Method | Resolves with |
158
+ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
159
+ | `getJson<T>()` | The body parsed as JSON (`null` for an empty body such as a `204`); `getJson(schema)` validates it first |
160
+ | `getText()` | The body as text |
161
+ | `getBlob()` | The body as a `Blob` typed with the response `Content-Type` |
162
+ | `getArrayBuffer()` | The body as an `ArrayBuffer` |
163
+ | `getFormData()` | The body parsed as `multipart/form-data` or `application/x-www-form-urlencoded` |
164
+ | `getBody()` | The raw `ReadableStream` (not buffered โ€” for streaming), or `null` when there is no body (`HEAD`, `204`) |
165
+ | `getData(selector)` | `getJson()` followed by a selector, e.g. `getData(page => page.items)` |
166
+ | `getResult<T>()` | `{ data, error }` instead of throwing |
167
+ | `getResponse()` | A `ResponseWrapper`: `status`, `statusText`, `ok`, `headers`, `url`, `method`, the underlying `raw` `Response`, and every body reader above (`getJson` โ€ฆ `getData`) |
168
+
169
+ The body is buffered once, so on a `ResponseWrapper` you can call several readers, in any order,
170
+ even concurrently. `getBody()` is the exception: it hands you the live stream, so nothing else
171
+ can read the body afterwards.
172
+
173
+ ```typescript
174
+ const response = await api.get<User>("/me").getResponse();
175
+ console.log(response.status, response.headers.get("etag"));
176
+ const user = await response.getJson(); // User
177
+ const raw = await response.getText(); // still works โ€” same buffer
178
+ ```
179
+
180
+ ### Reusing
181
+
182
+ A request is a template until you execute it. `clone()` copies its configuration so one request
183
+ can serve many calls; interceptors, signals and body objects are shared by reference.
184
+
185
+ ```typescript
186
+ const search = api.get<Page<Post>>("/search").withTimeout(2000);
187
+ const [page1, page2] = await Promise.all([
188
+ search.clone().withQueryParam("page", 1).getJson(),
189
+ search.clone().withQueryParam("page", 2).getJson(),
190
+ ]);
623
191
  ```
624
192
 
625
- ### Error Handling
193
+ ## Errors
626
194
 
627
- All errors from requests are instances of `RequestError` with detailed information:
195
+ Every rejection is a `RequestError`. Check it with `isRequestError(error)` (or `instanceof`) and
196
+ switch on `code`:
628
197
 
629
- ```typescript
630
- try {
631
- const data = await create.get("https://api.example.com/data").getJson();
632
- } catch (error) {
633
- // error will always be a RequestError
634
- console.log(error.message); // Error message
635
- console.log(error.status); // HTTP status code (if available)
636
- console.log(error.url); // Request URL
637
- console.log(error.method); // HTTP method
638
- console.log(error.isTimeout); // Whether it was a timeout
639
- console.log(error.isAborted); // Whether it was aborted/cancelled
640
- console.log(error.body); // Raw response body as text (if available)
641
-
642
- // Access the original response if available
643
- if (error.response) {
644
- // Raw Response object is available
645
- console.log(error.response.status);
646
- }
647
- }
648
- ```
198
+ | `code` | When | Also set |
199
+ | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
200
+ | `"HTTP"` | The server answered with a non-2xx status (a 3xx under `withRedirect("manual")` and an opaque response under `withMode("no-cors")` resolve instead) | `status`, `response`, `body`, `data` |
201
+ | `"NETWORK"` | `fetch` itself failed: DNS, connection refused, CORS, offline | `cause` (the error `fetch` threw) |
202
+ | `"TIMEOUT"` | `withTimeout()` fired, or a signal from `AbortSignal.timeout()` aborted | `cause`; plus `status`, `response` when it fired while the body was being read |
203
+ | `"ABORTED"` | A signal passed with `withSignal()` / `withAbortController()` aborted | `cause` (the abort reason); plus `status`, `response` when it fired while the body was being read |
204
+ | `"PARSE"` | The body could not be read or parsed, or a `getData` selector threw | `status`, `response`, `cause`, `body` (for invalid JSON) |
205
+ | `"VALIDATION"` | The response failed the schema, or the request could not be built (empty or unparsable absolute URL, invalid header value, non-serialisable body) | `issues` and `body` for schema failures; `cause` otherwise |
206
+ | `"INTERCEPTOR"` | A request/response interceptor or a callback (retry, CSRF token) threw | `cause`; plus `status`, `response` (and `body`) when a response existed |
207
+ | `"GRAPHQL"` | The GraphQL response had `errors` and `throwOnError` was on | `status`, `response`, `body`, `data` |
649
208
 
650
- #### Error Response Body
209
+ `url` and `method` are always set. `data` is `body` parsed as JSON โ€” the shape most APIs use for
210
+ error details โ€” and never throws. `body` holds at most 1 MB: a response that announces more is
211
+ left unread on `error.response`; a longer chunked or compressed one is cut off and `body` is
212
+ `undefined`. `isTimeout` / `isAborted` are shorthands for the two codes. In Node.js a relative
213
+ URL is a `"NETWORK"` failure, because `fetch` there has no page to resolve it against.
651
214
 
652
- When a request fails with an HTTP error (e.g., 400, 404, 500), the response body is automatically captured and available directly on the error - no need to read it from `error.response` manually:
215
+ Three mistakes are reported earlier, synchronously, by the `with*` call itself rather than by the
216
+ execution: an invalid timeout (`withTimeout(-1)`), an invalid retry count (`withRetries(-1)`) and
217
+ a body that cannot be JSON-serialised. They are `RequestError`s with code `"VALIDATION"` too.
653
218
 
654
219
  ```typescript
655
220
  try {
656
- await create.post("https://api.example.com/users").withBody(newUser).getJson();
221
+ await api.get<User>("/users/42").getJson();
657
222
  } catch (error) {
658
- // Raw body as text (undefined for network errors, timeouts, and aborts)
659
- console.log(error.body); // '{"message":"Email already taken","code":"DUPLICATE_EMAIL"}'
660
-
661
- // Body parsed as JSON - never throws, returns undefined if the body isn't valid JSON
662
- const details = error.getJson<{ message: string; code: string }>();
663
- if (details) {
664
- showToast(details.message);
223
+ if (!isRequestError(error)) throw error;
224
+ switch (error.code) {
225
+ case "HTTP":
226
+ console.log(error.status, error.data); // e.g. 404, { message: "No such user" }
227
+ break;
228
+ case "TIMEOUT":
229
+ case "NETWORK":
230
+ showError("Please try again");
231
+ break;
232
+ case "ABORTED":
233
+ break; // the user navigated away
234
+ default:
235
+ console.error(error.message, error.cause);
665
236
  }
666
237
  }
667
238
  ```
668
239
 
669
- Notes on the captured body:
670
-
671
- - `error.body` contains the raw body text whenever a response was received (HTTP errors, JSON parsing errors, GraphQL errors with `throwOnError`). For errors without a response (network failures, timeouts, aborts) it's `undefined`.
672
- - `error.getJson()` lazily parses `error.body` as JSON and caches the result. It never throws - it returns `undefined` when there's no body or the body isn't valid JSON.
673
- - The body is captured from a clone of the response, so `error.response` remains fully readable for backward compatibility.
674
- - The captured body is also available in retry callbacks (`onRetry`, `delay`) and error interceptors.
675
-
676
- ## URL Handling
677
-
678
- The library handles both absolute and relative URLs, and automatically merges query parameters:
679
-
680
- ```typescript
681
- // Relative URLs (preserved as-is)
682
- const relative = await create.get("/api/users").getJson();
683
-
684
- // Absolute URLs
685
- const absolute = await create.get("https://api.example.com/users").getJson();
686
-
687
- // Merging query params with existing URL params
688
- const merged = await create
689
- .get("https://api.example.com/users?page=1")
690
- .withQueryParams({ limit: 20, sort: "name" })
691
- .getJson();
692
- // Result: https://api.example.com/users?page=1&limit=20&sort=name
693
-
694
- // Special characters and unicode are properly encoded
695
- const encoded = await create
696
- .get("https://api.example.com/search")
697
- .withQueryParams({ name: "็”จๆˆทๅ", filter: "status:active" })
698
- .getJson();
699
- ```
700
-
701
- ## Advanced Usage
702
-
703
- ### API Builder
704
-
705
- The API builder allows you to create configured API instances with default settings that can be reused across your application. This is perfect for setting up a base URL, default headers, timeout values, and other request configurations once and using them for all requests.
706
-
707
- #### Creating an API Instance
240
+ Prefer errors as values? `getResult()` resolves with the error instead of rejecting:
708
241
 
709
242
  ```typescript
710
- import create from "create-request";
711
-
712
- // Create a configured API instance
713
- const api = create.api().withBaseURL("https://api.example.com").withTimeout(20000);
714
-
715
- // Use it with relative URLs
716
- const users = await api.get("/users").getJson();
717
-
718
- // Or without URL (uses baseURL)
719
- const users = await api.get().getJson();
720
- const newUser = await api.post().withBody({ name: "John" }).getJson();
243
+ const { data, error } = await api.get<User>("/me").getResult();
244
+ if (error) showError(error.message);
245
+ else render(data);
721
246
  ```
722
247
 
723
- #### Core API Builder Method
724
-
725
- - **`.withBaseURL(baseURL: string)`** - Set the base URL for all requests. Relative URLs will be resolved against this base URL.
726
-
727
- #### Available Request Methods
728
-
729
- The API builder provides access to most request configuration methods from `BaseRequest` that can be used as defaults. These methods will apply to all requests made through the API instance.
730
-
731
- **Important limitations:**
732
-
733
- - Methods that are request-specific (like `withBody`, `withGraphQL`, `withAbortController`) are not available in ApiBuilder
734
- - The Fluent API (e.g., `.withCache.NO_STORE()`) is not supported - use direct calls with string or enum values instead
735
-
736
- Available methods:
737
-
738
- **Authentication & Headers:**
739
-
740
- - `withHeaders(headers)` - Set default headers for all requests
741
- - `withHeader(key, value)` - Add a single default header
742
- - `withAuthorization(authValue)` - Set Authorization header
743
- - `withBasicAuth(username, password)` - Add Basic Authentication
744
- - `withBearerToken(token)` - Add Bearer token authentication
745
- - `withContentType(contentType)` - Set default Content-Type header
746
-
747
- **Cookies:**
748
-
749
- - `withCookies(cookies)` - Add cookies to all requests
750
- - `withCookie(name, value)` - Add a single cookie
751
-
752
- **Query Parameters:**
753
-
754
- - `withQueryParams(params)` - Add default query parameters to all requests
755
- - `withQueryParam(key, value)` - Add a single default query parameter
756
-
757
- **Request Configuration:**
758
-
759
- - `withTimeout(timeout)` - Set default timeout for all requests
760
- - `withRetries(retries)` - Configure default retry behavior
761
- - `withReferrer(referrer)` - Set default referrer
762
- - `withReferrerPolicy(policy)` - Set default referrer policy (use string or enum)
763
- - `withKeepAlive(keepalive)` - Configure keep-alive
764
- - `withIntegrity(integrity)` - Set integrity check
765
- - `withMode(mode)` - Set request mode (use string or enum)
766
- - `withCredentials(credentials)` - Set credentials policy (use string or enum)
767
- - `withRedirect(redirect)` - Set redirect behavior (use string or enum)
768
- - `withPriority(priority)` - Set request priority (use string or enum)
769
- - `withCache(cache)` - Set cache mode (use string or enum)
770
-
771
- **CSRF Protection:**
772
-
773
- - `withCsrfToken(token, headerName?)` - Set CSRF token
774
- - `withoutCsrfProtection()` - Disable CSRF protection
775
- - `withAntiCsrfHeaders()` - Enable anti-CSRF headers
776
-
777
- **Interceptors:**
248
+ ## Api instances
778
249
 
779
- - `withRequestInterceptor(interceptor)` - Add default request interceptor
780
- - `withResponseInterceptor(interceptor)` - Add default response interceptor
781
- - `withErrorInterceptor(interceptor)` - Add default error interceptor
782
-
783
- These methods can be chained together and will apply to all requests made through the API instance:
784
-
785
- ```typescript
786
- import { CacheMode, CredentialsPolicy, RequestMode } from "create-request";
787
-
788
- const api = create
789
- .api()
790
- .withBaseURL("https://api.example.com")
791
- .withBearerToken("token123")
792
- .withCookies({ session: "abc123" })
793
- .withTimeout(5000)
794
- .withHeaders({ "X-Custom": "value" })
795
- .withQueryParams({ apiVersion: "v2" }) // Default query params
796
- .withCache("no-store") // Direct string value
797
- // Or use enum: .withCache(CacheMode.NO_STORE)
798
- .withCredentials("include"); // Direct string value
799
- // Or use enum: .withCredentials(CredentialsPolicy.INCLUDE)
800
- // Note: Fluent API like .withCache.NO_STORE() is NOT supported in ApiBuilder
801
-
802
- // All requests will include the Bearer token, cookies, timeout, headers, query params, and cache settings
803
- await api.get("/users").getJson();
804
- await api.post("/posts").withBody({ title: "Hello" }).getJson();
805
- ```
806
-
807
- #### URL Resolution
808
-
809
- The API builder intelligently resolves URLs:
810
-
811
- ```typescript
812
- const api = create.api().withBaseURL("https://api.example.com");
813
-
814
- // Relative URLs are resolved against baseURL
815
- await api.get("users").getJson(); // โ†’ https://api.example.com/users
816
- await api.get("/users").getJson(); // โ†’ https://api.example.com/users
817
- await api.get("./users").getJson(); // โ†’ https://api.example.com/users
818
-
819
- // Absolute URLs are used as-is
820
- await api.get("https://other-api.com/data").getJson(); // โ†’ https://other-api.com/data
821
-
822
- // No URL uses baseURL directly
823
- await api.get().getJson(); // โ†’ https://api.example.com
824
- ```
825
-
826
- #### Overriding Defaults
827
-
828
- You can override default settings on individual requests:
250
+ An api instance is a bundle of defaults for the requests it creates. It has every `with*` method
251
+ a request has, except the body and signal ones (those belong to a single request), plus
252
+ `withBaseURL()`. A timeout or retry count is validated when you set it on the api; URLs and
253
+ header values are checked when a request runs.
829
254
 
830
255
  ```typescript
831
- const api = create
832
- .api()
256
+ // src/lib/api.ts
257
+ export const api = createApi()
833
258
  .withBaseURL("https://api.example.com")
259
+ .withBearerToken(token)
834
260
  .withTimeout(5000)
835
- .withBearerToken("token123");
836
-
837
- // Override timeout for this specific request
838
- await api.get("/slow-endpoint").withTimeout(30000).getJson();
839
-
840
- // Override headers (merges with defaults)
841
- await api
842
- .get("/users")
843
- .withBearerToken("newtoken")
844
- .withHeaders({ "X-Custom": "value" })
845
- .getJson();
846
- // Result: Authorization: "Bearer newtoken", X-Custom: "value"
847
- ```
848
-
849
- #### All HTTP Methods Supported
850
-
851
- The API instance supports all HTTP methods:
852
-
853
- ```typescript
854
- const api = create.api().withBaseURL("https://api.example.com");
855
-
856
- await api.get("/users").getJson();
857
- await api.post("/users").withBody({ name: "John" }).getJson();
858
- await api.put("/users/1").withBody({ name: "Jane" }).getJson();
859
- await api.patch("/users/1").withBody({ status: "active" }).getJson();
860
- await api.del("/users/1").getJson();
861
- await api.head("/users").getResponse();
862
- await api.options("/users").getResponse();
863
- ```
864
-
865
- #### Merging Default Headers
866
-
867
- Multiple calls to `withHeaders` will merge headers, with later calls taking precedence:
868
-
869
- ```typescript
870
- const api = create
871
- .api()
872
- .withBaseURL("https://api.example.com")
873
- .withBearerToken("token123")
874
- .withHeaders({ "X-Custom": "value1" })
875
- .withHeaders({ "X-Other": "value2" })
876
- .withBearerToken("newtoken");
261
+ .withRetries(2)
262
+ .withRequestInterceptor(config => {
263
+ config.headers["x-trace-id"] = crypto.randomUUID();
264
+ });
877
265
 
878
- // Result: Authorization: "Bearer newtoken", X-Custom: "value1", X-Other: "value2"
266
+ // anywhere
267
+ const posts = await api.get<Post[]>("/posts").getJson();
268
+ const post = await api.post<Post>("/posts").withBody({ title: "Hello" }).getJson();
269
+ const admin = api.withHeader("X-Role", "admin"); // a NEW instance; `api` is unchanged
879
270
  ```
880
271
 
881
- #### Complete Example
272
+ Requests inherit the defaults and can override any of them: `api.get("/slow").withTimeout(30_000)`,
273
+ `api.get("/public").withHeaders({ Authorization: null })`, `api.get("/live").withTimeout(0)` (no
274
+ timeout at all).
882
275
 
883
- ```typescript
884
- // Set up your API once
885
- import { CacheMode } from "create-request";
886
-
887
- const api = create
888
- .api()
889
- .withBaseURL("https://api.example.com/v1")
890
- .withHeaders({ "Content-Type": "application/json" })
891
- .withCookies({ session: "abc123" })
892
- .withBearerToken("token123")
893
- .withTimeout(20000)
894
- .withQueryParams({ format: "json" })
895
- .withCache(CacheMode.NO_CACHE);
896
-
897
- // Use throughout your application
898
- async function getUsers() {
899
- return api.get("/users").getJson();
900
- }
901
-
902
- async function createUser(userData: User) {
903
- return api.post("/users").withBody(userData).getJson();
904
- }
276
+ Paths are **joined** to the base URL, not resolved: `/v1` + `/users` โ†’ `/v1/users`; `users` and
277
+ `./users` work the same; `api.get()` without a path requests the base URL; absolute URLs
278
+ (`https://โ€ฆ`, `//โ€ฆ`) are used as-is โ€” and carry the api's headers with them, so never build a
279
+ path from untrusted input.
905
280
 
906
- async function updateUser(id: string, userData: Partial<User>) {
907
- return api.put(`/users/${id}`).withBody(userData).getJson();
908
- }
909
-
910
- async function deleteUser(id: string) {
911
- return api.del(`/users/${id}`).getJson();
912
- }
913
- ```
914
-
915
- ### Automatic Retries with Delay
916
-
917
- The `withRetries()` method supports both simple number-based retries and object-based configuration with customizable delays:
281
+ ## Retries
918
282
 
919
283
  ```typescript
920
- // Simple number
921
- const request1 = create.get("https://api.example.com/data").withRetries(3);
922
-
923
- // With fixed delay between retries
924
- const request2 = create
925
- .get("https://api.example.com/data")
926
- .withRetries({ attempts: 3, delay: 1000 }); // Wait 1 second between retries
927
-
928
- // With exponential backoff function
929
- const request3 = create.get("https://api.example.com/data").withRetries({
930
- attempts: 5,
931
- delay: ({ attempt }) => Math.min(1000 * Math.pow(2, attempt - 1), 10000), // Exponential backoff capped at 10s
932
- });
933
-
934
- // With error-aware delay (e.g., longer delay for rate limits)
935
- const request4 = create.get("https://api.example.com/data").withRetries({
284
+ api.get("/status").withRetries(3); // default policy
285
+ api.get("/status").withRetries({
936
286
  attempts: 3,
937
- delay: ({ attempt, error }) => {
938
- if (error.status === 429) {
939
- return 5000; // Wait 5 seconds for rate limit errors
940
- }
941
- return attempt * 1000; // Linear backoff for other errors
942
- },
287
+ delay: ({ attempt }) => attempt * 500, // ms, or a number; default: exponential backoff
288
+ statuses: [503], // default: 408, 425, 429, 500, 502, 503, 504
289
+ methods: ["GET", "HEAD", "PUT", "DELETE"], // default: every method โ€” including POST and PATCH
290
+ maxDelay: 10_000, // caps the backoff (default 30 s); a longer Retry-After gives up instead
291
+ shouldRetry: ({ error }) => error.code === "NETWORK", // full override of the decision
292
+ onRetry: ({ attempt, error, delay }) =>
293
+ console.warn(`retry ${attempt} in ${delay}ms: ${error.message}`),
943
294
  });
944
295
  ```
945
296
 
946
- **Rate Limit Aware:**
947
-
948
- ```typescript
949
- .withRetries({
950
- attempts: 3,
951
- delay: ({ error }) => {
952
- if (error.status === 429) {
953
- // Check Retry-After header if available
954
- const retryAfter = error.response?.headers.get("Retry-After");
955
- if (retryAfter) return parseInt(retryAfter) * 1000;
956
-
957
- // Or read the delay from the error response body
958
- const details = error.getJson<{ retryAfterMs?: number }>();
959
- return details?.retryAfterMs ?? 5000;
960
- }
961
- return 1000; // Default delay
962
- },
963
- })
964
- ```
965
-
966
- ### Interceptors
967
-
968
- 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.
297
+ Defaults that keep you out of trouble: network errors, timeouts and the statuses above are retried
298
+ with exponential backoff (300 ms, 600 ms, 1.2 s, โ€ฆ plus up to 100 ms of jitter, capped at
299
+ `maxDelay`); a `Retry-After` header is honoured unless you set `delay`, and one longer than
300
+ `maxDelay` cancels the retry so you can react yourself; validation errors are not retried by the
301
+ default policy; aborted requests and requests with a stream body are never retried, whatever the
302
+ policy โ€” that includes a timeout coming from your own `AbortSignal.timeout()` signal, which stays
303
+ aborted (only `withTimeout()` timeouts are retried); the timeout applies to each attempt; error
304
+ interceptors run once, after the last attempt.
305
+ Every method is retried by default โ€” pass `methods: ["GET", "HEAD", "OPTIONS", "PUT", "DELETE"]`
306
+ if a repeated `POST` could duplicate work. `onRetry(callback)` also exists as a method; it does
307
+ not enable retries by itself.
969
308
 
970
- #### Global Interceptors
971
-
972
- Global interceptors apply to all requests:
309
+ ## Timeouts and cancellation
973
310
 
974
311
  ```typescript
975
- // Add a global request interceptor (modify all requests)
976
- const requestInterceptorId = create.config.addRequestInterceptor(config => {
977
- // Add auth token to all requests
978
- config.headers["Authorization"] = `Bearer ${getToken()}`;
979
- // Modify URL, headers, body, etc.
980
- return config;
981
- });
312
+ api.get("/slow").withTimeout(2000); // rejects with code "TIMEOUT"
313
+ api.get("/stream").withTimeout(0); // removes a timeout inherited from the api
982
314
 
983
- // Add a global response interceptor (transform all responses)
984
- const responseInterceptorId = create.config.addResponseInterceptor(response => {
985
- console.log(`Response received: ${response.status}`);
986
- // Transform or modify the response
987
- return response;
988
- });
315
+ const controller = new AbortController();
316
+ const download = api.get("/big").withAbortController(controller).getBlob();
317
+ controller.abort(); // `download` rejects with code "ABORTED"
989
318
 
990
- // Add a global error interceptor (handle all errors)
991
- const errorInterceptorId = create.config.addErrorInterceptor(error => {
992
- console.error("Request failed:", error.message);
993
- // Can throw to propagate error, or return ResponseWrapper to recover
994
- throw error;
319
+ // Data-fetching libraries hand you a signal โ€” pass it straight through
320
+ useQuery({
321
+ queryKey: ["user", id],
322
+ queryFn: ({ signal }) => api.get<User>(`/users/${id}`).withSignal(signal).getJson(),
995
323
  });
996
-
997
- // Remove interceptors when no longer needed
998
- create.config.removeRequestInterceptor(requestInterceptorId);
999
- create.config.removeResponseInterceptor(responseInterceptorId);
1000
- create.config.removeErrorInterceptor(errorInterceptorId);
1001
-
1002
- // Clear all interceptors at once
1003
- create.config.clearInterceptors();
1004
- ```
1005
-
1006
- #### Per-Request Interceptors
1007
-
1008
- Per-request interceptors apply only to a specific request:
1009
-
1010
- ```typescript
1011
- // Request interceptor - modify request configuration
1012
- const data = await create
1013
- .get("https://api.example.com/users")
1014
- .withRequestInterceptor(config => {
1015
- config.headers["X-Custom-Header"] = "value";
1016
- config.url = "https://api.example.com/modified-url"; // Can modify URL
1017
- return config;
1018
- })
1019
- .getJson();
1020
-
1021
- // Response interceptor - transform response
1022
- const transformed = await create
1023
- .get("https://api.example.com/users")
1024
- .withResponseInterceptor(response => {
1025
- console.log(`Got response with status ${response.status}`);
1026
- return response;
1027
- })
1028
- .getJson();
1029
-
1030
- // Error interceptor - handle or recover from errors
1031
- const recovered = await create
1032
- .get("https://api.example.com/users")
1033
- .withErrorInterceptor(error => {
1034
- // Option 1: Throw to propagate error
1035
- throw error;
1036
-
1037
- // Option 2: Return a ResponseWrapper to recover from error
1038
- // return new ResponseWrapper(fallbackResponse, error.url, error.method);
1039
- })
1040
- .getJson();
1041
324
  ```
1042
325
 
1043
- #### Interceptor Execution Order
326
+ The timeout covers the whole exchange โ€” waiting for the response _and_ reading its body โ€” and
327
+ starts after request interceptors ran. Taking the stream with `getBody()` ends it. A signal
328
+ created with `AbortSignal.timeout()` is reported as `"TIMEOUT"` too (but, unlike `withTimeout()`,
329
+ is not retried โ€” the signal stays aborted); any other abort is `"ABORTED"`. A request whose
330
+ signal is already aborted fails before anything is sent.
1044
331
 
1045
- Interceptors execute in a specific order:
332
+ ## Interceptors
1046
333
 
1047
- 1. **Request interceptors**: Global interceptors run first (in registration order), then per-request interceptors (in registration order)
1048
- 2. **Response interceptors**: Per-request interceptors run first (in registration order), then global interceptors (in reverse registration order)
1049
- 3. **Error interceptors**: Per-request interceptors run first (in registration order), then global interceptors (in reverse registration order)
334
+ Interceptors run in registration order โ€” api-level ones first, then request-level ones.
335
+ Returning nothing keeps your in-place changes.
1050
336
 
1051
337
  ```typescript
1052
- // Request: Global 1 โ†’ Global 2 โ†’ Per-request 1 โ†’ Per-request 2
1053
- // Response: Per-request 1 โ†’ Per-request 2 โ†’ Global 2 โ†’ Global 1
1054
- const data = await create
1055
- .get("https://api.example.com/users")
1056
- .withRequestInterceptor(() => console.log("Per-request 1"))
1057
- .withRequestInterceptor(() => console.log("Per-request 2"))
1058
- .getJson();
1059
- ```
1060
-
1061
- #### Advanced Interceptor Patterns
1062
-
1063
- ```typescript
1064
- // Short-circuit request with early response
1065
- const cached = await create
1066
- .get("https://api.example.com/users")
1067
- .withRequestInterceptor(() => {
1068
- // Return early response from cache
1069
- return new Response(JSON.stringify(cachedData), {
1070
- status: 200,
1071
- headers: { "Content-Type": "application/json" },
1072
- });
1073
- })
1074
- .getJson();
1075
-
1076
- // Recover from error with fallback
1077
- const fallback = await create
1078
- .get("https://api.example.com/users")
1079
- .withErrorInterceptor(error => {
1080
- // Return fallback response instead of throwing
1081
- const fallbackResponse = new Response(JSON.stringify({ users: [] }), {
1082
- status: 200,
1083
- headers: { "Content-Type": "application/json" },
1084
- });
1085
- return new ResponseWrapper(fallbackResponse, error.url, error.method);
338
+ let accessToken = token;
339
+ const replayed = new WeakSet<HttpRequest>(); // requests already replayed after a 401
340
+ const authed = api
341
+ // before the request: mutate the config, return a new one, or return a Response to skip the network
342
+ .withRequestInterceptor(config => {
343
+ config.headers["authorization"] = `Bearer ${accessToken}`;
1086
344
  })
1087
- .getJson();
1088
-
1089
- // Async interceptors
1090
- const asyncData = await create
1091
- .get("https://api.example.com/users")
1092
- .withRequestInterceptor(async config => {
1093
- const token = await getTokenAsync();
1094
- config.headers["Authorization"] = `Bearer ${token}`;
1095
- return config;
345
+ // after a successful response, with the request that produced it
346
+ .withResponseInterceptor((response, request) => {
347
+ console.debug(request.method, response.url, response.status);
1096
348
  })
1097
- .getJson();
1098
- ```
1099
-
1100
- ### Request Cancellation
1101
-
1102
- ```typescript
1103
- const controller = new AbortController();
1104
-
1105
- const request = create
1106
- .get("https://api.example.com/slow-endpoint")
1107
- .withTimeout(10000)
1108
- .withAbortController(controller);
1109
-
1110
- // Later, cancel the request if needed
1111
- setTimeout(() => controller.abort(), 2000);
1112
-
1113
- try {
1114
- const data = await request.getJson();
1115
- } catch (error) {
1116
- if (error.name === "AbortError") {
1117
- console.log("Request was cancelled by user");
1118
- } else if (error.isTimeout) {
1119
- console.log("Request timed out");
1120
- } else {
1121
- console.log("Other error:", error.message);
1122
- }
1123
- }
1124
- ```
1125
-
1126
- ### Custom Fetch Injection
1127
-
1128
- By default, requests run through the global `fetch`. With `withFetch` you can inject any fetch-compatible function โ€” per request or for a whole API instance. This unlocks testing without global mocks, custom undici agents/dispatchers in Node.js, and framework-patched fetch features like Next.js caching.
1129
-
1130
- ```typescript
1131
- import create, { createApi } from "create-request";
1132
- import type { FetchFunction } from "create-request";
1133
-
1134
- // Testing: inject a stub instead of monkey-patching globalThis.fetch
1135
- const stubFetch: FetchFunction = async () =>
1136
- new Response(JSON.stringify({ id: 1 }), {
1137
- status: 200,
1138
- headers: { "content-type": "application/json" },
349
+ // once the request failed for good: replace the error, or recover by returning a ResponseWrapper
350
+ .withErrorInterceptor(async (error, request) => {
351
+ if (error.status !== 401 || replayed.has(request)) return;
352
+ accessToken = await refreshToken(); // the request interceptor above picks it up
353
+ const retry = request.clone(); // same method, body, headers and query
354
+ replayed.add(retry); // the clone runs this interceptor too โ€” never loop on a persistent 401
355
+ return retry.getResponse();
1139
356
  });
1140
-
1141
- const user = await create.get("/api/users/1").withFetch(stubFetch).getJson();
1142
- ```
1143
-
1144
- ```typescript
1145
- // Node.js: route requests through a custom undici Agent (proxies, keep-alive tuning, mTLS, ...)
1146
- import { fetch as undiciFetch, Agent } from "undici";
1147
-
1148
- const agent = new Agent({ keepAliveTimeout: 30_000, connections: 10 });
1149
-
1150
- const api = createApi()
1151
- .withBaseURL("https://api.example.com")
1152
- .withFetch((url, init) => undiciFetch(url, { ...init, dispatcher: agent }));
1153
-
1154
- const users = await api.get("/users").getJson();
1155
- ```
1156
-
1157
- ```typescript
1158
- // Next.js: pass caching hints through to the framework's patched fetch
1159
- const revalidatingFetch: FetchFunction = (url, init) =>
1160
- fetch(url, { ...init, next: { revalidate: 60 } });
1161
-
1162
- const posts = await create
1163
- .get("https://api.example.com/posts")
1164
- .withFetch(revalidatingFetch)
1165
- .getJson();
1166
357
  ```
1167
358
 
1168
- Notes:
1169
-
1170
- - The custom function receives the final URL and `RequestInit` after query params, headers, and request interceptors have been applied, and it is called once per attempt when retries are configured.
1171
- - It should honor `init.signal`, otherwise `withTimeout` and `withAbortController` cannot cancel the underlying work.
1172
- - A per-request `withFetch` overrides one set on an API builder.
359
+ The `config` a request interceptor receives is the `RequestInit`-shaped object about to be sent:
360
+ `url`, `method`, lower-case `headers`, the serialised `body` (JSON bodies are already strings) and
361
+ the combined `signal` โ€” before the CSRF header (`withCsrf()`) and the timeout are added. A
362
+ `Response` returned by a request interceptor skips the network โ€” and the remaining request
363
+ interceptors, URL/header validation and the CSRF header โ€” but its status is checked and response
364
+ interceptors run like for a fetched one; `withTimeout()` does not apply to it. A request or
365
+ response interceptor that throws fails the request with code `"INTERCEPTOR"`; an error
366
+ interceptor that throws replaces the error (a thrown `RequestError` is kept as-is).
1173
367
 
1174
- ### Data Selection
368
+ ## Schema validation
1175
369
 
1176
- The `getData` method provides a powerful way to extract and transform specific data from API responses:
370
+ Pass any [Standard Schema](https://standardschema.dev) โ€” zod 3.24+, valibot 1+, arktype 2+,
371
+ effect (via `Schema.standardSchemaV1`) and more โ€” to `getJson`, `getData` or `getResult`. The
372
+ body is validated at runtime and the result is typed from the schema; this library adds no
373
+ dependency for it.
1177
374
 
1178
375
  ```typescript
1179
- // Extract specific properties from nested structures
1180
- const posts = await create
1181
- .get("https://api.example.com/feed")
1182
- .getData(data => data.feed.posts);
376
+ import { z } from "zod";
1183
377
 
1184
- // Transform data in the selector function
1185
- const usernames = await create
1186
- .get("https://api.example.com/users")
1187
- .getData(data => data.users.map(user => user.username));
378
+ const User = z.object({ id: z.number(), name: z.string() });
1188
379
 
1189
- // Apply filtering in the selector
1190
- const activeUsers = await create
1191
- .get("https://api.example.com/users")
1192
- .getData(data => data.users.filter(user => user.isActive));
1193
-
1194
- // Combine data from complex nested structures
1195
- const combinedData = await create.get("https://api.example.com/dashboard").getData(data => ({
1196
- userCount: data.stats.users.total,
1197
- recentPosts: data.content.recent.slice(0, 5),
1198
- notifications: data.user.notifications.unread,
1199
- }));
380
+ const user = await api.get("/me").getJson(User); // { id: number; name: string }
381
+ const names = await api.get("/users").getData(z.array(User), users => users.map(u => u.name));
382
+ const { data, error } = await api.get("/me").getResult(User);
1200
383
  ```
1201
384
 
1202
- When a selector fails, the error message will contain helpful context to diagnose the issue:
1203
-
1204
- ```typescript
1205
- try {
1206
- // This will fail if the response structure doesn't match expectations
1207
- const result = await create
1208
- .get("https://api.example.com/data")
1209
- .getData(data => data.results.items);
1210
- } catch (error) {
1211
- console.error(error);
1212
- // Error message includes context for debugging
1213
- }
1214
- ```
385
+ A mismatch rejects with `code: "VALIDATION"`; `error.issues` lists every problem and
386
+ `error.message` names the first one (e.g. `Response validation failed: Invalid input: expected
387
+ number, received string at id`).
1215
388
 
1216
- ### TypeScript Support
389
+ ## GraphQL
1217
390
 
1218
391
  ```typescript
1219
- interface User {
1220
- id: number;
1221
- name: string;
1222
- email: string;
1223
- isActive: boolean;
1224
- }
1225
-
1226
- interface ApiResponse<T> {
1227
- data: T;
1228
- meta: {
1229
- total: number;
1230
- page: number;
1231
- };
1232
- }
1233
-
1234
- // Type the full response
1235
- const response = await create
1236
- .get("https://api.example.com/users")
1237
- .getJson<ApiResponse<User[]>>();
1238
-
1239
- // Or use getData with type parameters
1240
- const users = await create
1241
- .get("https://api.example.com/users")
1242
- .getData<ApiResponse<User[]>, User[]>(data => data.data);
1243
-
1244
- // TypeScript knows the types
1245
- users.forEach(user => {
1246
- console.log(`${user.name} (${user.email}): ${user.isActive ? "Active" : "Inactive"}`);
1247
- });
1248
-
1249
- // Function with proper types
1250
- async function getUserById(id: number): Promise<User> {
1251
- return create
1252
- .get("https://api.example.com/users")
1253
- .withQueryParam("id", id)
1254
- .getData<ApiResponse<User[]>, User>(data => {
1255
- const user = data.data[0];
1256
- if (!user) throw new Error(`User with ID ${id} not found`);
1257
- return user;
1258
- });
1259
- }
1260
- ```
1261
-
1262
- ### CSRF Protection
1263
-
1264
- Cross-Site Request Forgery (CSRF) is a type of security vulnerability where unauthorized commands are executed on behalf of an authenticated user. `create-request` provides built-in protection mechanisms to help prevent CSRF attacks.
1265
-
1266
- ### How CSRF Protection Works
1267
-
1268
- The library employs multiple strategies to protect against CSRF attacks:
1269
-
1270
- 1. **Automatic X-Requested-With Header**: By default, all requests include the `X-Requested-With: XMLHttpRequest` header, which helps servers identify legitimate AJAX requests.
1271
-
1272
- 2. **CSRF Token Support**: The library can automatically include CSRF tokens in request headers, which servers can validate to ensure the request came from your application.
1273
-
1274
- 3. **XSRF Cookie Reading**: For frameworks that use the double-submit cookie pattern (like Laravel, Rails, or Django), the library can automatically read XSRF tokens from cookies and include them in request headers.
1275
-
1276
- ### Global CSRF Configuration
1277
-
1278
- You can configure CSRF settings globally for all requests:
1279
-
1280
- ```typescript
1281
- // Configure CSRF settings for all requests
1282
- create.config.setCsrfToken("your-csrf-token");
1283
- create.config.setCsrfHeaderName("X-CSRF-Token"); // Default header name for CSRF token
1284
- create.config.setXsrfCookieName("XSRF-TOKEN"); // Default cookie name to read from
1285
- create.config.setXsrfHeaderName("X-XSRF-TOKEN"); // Default header name for XSRF token from cookie
1286
- create.config.setEnableAntiCsrf(true); // Enable/disable X-Requested-With header
1287
- create.config.setEnableAutoXsrf(true); // Enable/disable automatic cookie-to-header token
1288
-
1289
- // Reset all configuration to defaults
1290
- create.config.reset();
392
+ const result = await api
393
+ .post("/graphql")
394
+ .withGraphQL("query ($id: ID!) { user(id: $id) { name } }", { id }, { throwOnError: true })
395
+ .getJson<{ data: { user: User } }>();
1291
396
  ```
1292
397
 
1293
- ### Per-Request CSRF Settings
1294
-
1295
- You can also configure CSRF protection on individual requests:
1296
-
1297
- ```typescript
1298
- // Configure CSRF for a specific request
1299
- const request = create
1300
- .post("https://api.example.com/users")
1301
- .withCsrfToken("request-specific-token") // Set a specific token
1302
- .withAntiCsrfHeaders() // Explicitly add X-Requested-With header
1303
- .withoutCsrfProtection(); // Or disable all automatic CSRF protection
1304
- ```
398
+ `withGraphQL` sends `{ query, variables }` as JSON. With `throwOnError`, a response whose `errors`
399
+ array is non-empty rejects with `code: "GRAPHQL"` (GraphQL servers answer `200` for those).
1305
400
 
1306
- ### Subresource Integrity and Cache Control
401
+ ## Streaming and downloads
1307
402
 
1308
- The library supports subresource integrity verification and cache control options:
403
+ `getBody()` returns the raw stream; everything else is available on the wrapper.
1309
404
 
1310
405
  ```typescript
1311
- // Subresource Integrity - ensures the fetched resource hasn't been tampered with
1312
- const secureRequest = create
1313
- .get("https://cdn.example.com/script.js")
1314
- .withIntegrity("sha256-abcdef1234567890..."); // Browser will verify the hash
1315
-
1316
- // Cache Control - supports all cache modes via fluent API or string values
1317
- const cachedRequest = create.get("https://api.example.com/data").withCache("no-cache"); // Direct string value
1318
-
1319
- // Using fluent API for cache modes
1320
- const fluentCache = create.get("https://api.example.com/data").withCache.NO_CACHE(); // Fluent API method
1321
-
1322
- // All available cache modes:
1323
- create
1324
- .get("https://api.example.com/data")
1325
- .withCache.DEFAULT() // Default cache behavior
1326
- .withCache.NO_STORE() // Don't store in cache
1327
- .withCache.RELOAD() // Reload from server
1328
- .withCache.NO_CACHE() // Validate with server before using cache
1329
- .withCache.FORCE_CACHE() // Use cache even if stale
1330
- .withCache.ONLY_IF_CACHED(); // Only use cache, don't fetch from server
1331
-
1332
- // Using enum values (import from create-request)
1333
- import { CacheMode } from "create-request";
1334
-
1335
- const enumCache = create.get("https://api.example.com/data").withCache(CacheMode.NO_CACHE);
1336
-
1337
- // Combining integrity and cache
1338
- const secureCached = create
1339
- .get("https://cdn.example.com/resource.js")
1340
- .withIntegrity("sha256-abcdef1234567890...")
1341
- .withCache("no-store"); // Ensure no caching for sensitive resources
406
+ const response = await api.get("/export.csv").getResponse();
407
+ const total = Number(response.headers.get("content-length")) || undefined;
408
+ const decoder = new TextDecoder();
409
+ let loaded = 0;
410
+ const reader = response.getBody()!.getReader(); // ends the withTimeout() deadline: the stream is yours
411
+ for (;;) {
412
+ const { done, value } = await reader.read();
413
+ if (done) break;
414
+ loaded += value.length;
415
+ if (total) onProgress(loaded / total);
416
+ process(decoder.decode(value, { stream: true }));
417
+ }
1342
418
  ```
1343
419
 
1344
- ## Performance Considerations
1345
-
1346
- create-request is designed to be lightweight and efficient:
1347
-
1348
- - **Zero Dependencies**: No extra libraries to load
1349
- - **Tree-Shakable**: Only import what you need
1350
- - **Minimal Overhead**: Thin wrapper around the native Fetch API
1351
- - **Memory Efficient**: Doesn't create unnecessary objects
1352
- - **Clean API**: Simple and intuitive interface
1353
-
1354
- ## Browser & Node.js Support
1355
-
1356
- This library works with all browsers that support the Fetch API:
1357
-
1358
- - Chrome 42+
1359
- - Firefox 39+
1360
- - Safari 10.1+
1361
- - Edge 14+
1362
- - Opera 29+
1363
-
1364
- For Node.js:
1365
-
1366
- - Node.js 18.3.0+ (native fetch support required)
1367
-
1368
- ## Comparison of JavaScript HTTP Client Libraries
1369
-
1370
- | Feature | create-request | Fetch | Axios | SuperAgent | Got | Ky | node-fetch | Redaxios |
1371
- | --------------------- | -------------- | ------ | ------- | ---------- | ------- | ------ | ---------- | -------- |
1372
- | **Size (min+gzip)** | ~6.4KB | Native | ~13.6KB | ~17.8KB | ~17.8KB | ~3.4KB | ~7.7KB | ~1KB |
1373
- | **Browser** | Modern | Modern | IE11+ | IE9+ | โŒ No | Modern | โŒ No | Modern |
1374
- | **Node.js** | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… |
1375
- | **HTTP/2** | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… | โŒ | โŒ |
1376
- | **Auto Retries** | โœ… | โŒ | ๐Ÿ› ๏ธ | โœ… | โœ… | โœ… | โŒ | โŒ |
1377
- | **Cancellation** | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… |
1378
- | **Auto JSON** | โœ… | โŒ | โœ… | โœ… | โœ… | โœ… | โŒ | โœ… |
1379
- | **Timeout** | โœ… | โŒ | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… |
1380
- | **TypeScript** | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… |
1381
- | **Streaming** | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… | โœ… | โŒ |
1382
- | **Progress** | โŒ | โŒ | โœ… | โœ… | โœ… | โœ… | โŒ | โŒ |
1383
- | **Cookies** | โœ… | โœ… | ๐Ÿ› ๏ธ | โœ… | โœ… | โŒ | โŒ | โŒ |
1384
- | **Pagination API** | โŒ | โŒ | โŒ | โŒ | โœ… | โŒ | โŒ | โŒ |
1385
- | **Zero Deps** | โœ… | โœ… | โŒ | โŒ | โŒ | โœ… | โœ… | โœ… |
1386
- | **Chainable API** | โœ… | โŒ | โŒ | โœ… | โœ… | โœ… | โŒ | โŒ |
1387
- | **CSRF Protection** | โœ… | โŒ | โœ… | โŒ | โŒ | โŒ | โŒ | โŒ |
1388
- | **GraphQL Support** | โœ… | โŒ | โŒ | โŒ | โŒ | โŒ | โŒ | โŒ |
1389
- | **Interceptors** | โœ… | โŒ | โœ… | โœ… | โœ… | โœ… | โŒ | โŒ |
1390
- | **Instance Creation** | โœ… | โŒ | โœ… | โœ… | โœ… | โœ… | โŒ | โŒ |
1391
-
1392
- **Notes:**
1393
-
1394
- - "Modern" browser support: Chrome 42+, Firefox 39+, Safari 10.1+, Edge 14+, Opera 29+
1395
- - ๐Ÿ› ๏ธ Feature requires additional plugins or adapters (not available out-of-the-box)
420
+ Request bodies can be streams too (`withBody(readableStream)`), sent with `duplex: "half"` โ€”
421
+ Node.js and Chromium support that, Firefox and Safari do not. Stream bodies are never retried.
422
+ Upload _progress_ is not something `fetch` exposes in browsers, so there is no API for it.
423
+
424
+ ## Testing and custom fetch
425
+
426
+ `withFetch()` replaces the global `fetch` for a request or an api: inject a stub in tests, an
427
+ undici `Agent` for proxies and keep-alive tuning in Node.js, or a framework's patched fetch. The
428
+ function receives the final URL and `RequestInit`; it must honour `init.signal` for timeouts and
429
+ cancellation to keep working.
430
+
431
+ ```typescript
432
+ // Tests: no global mocks
433
+ const stubbed = api.withFetch(
434
+ async () => new Response('{"id":1}', { headers: { "content-type": "application/json" } })
435
+ );
436
+
437
+ // Node.js: an undici Agent (proxy, mTLS, keep-alive)
438
+ const viaAgent = api.withFetch(
439
+ (url, init) =>
440
+ undiciFetch(url, {
441
+ ...(init as object),
442
+ dispatcher: agent,
443
+ }) as unknown as Promise<Response>
444
+ );
445
+
446
+ // Next.js: pass caching hints to the framework's fetch
447
+ const cached = api.withFetch((url, init) =>
448
+ fetch(url, { ...init, next: { revalidate: 60 } } as RequestInit)
449
+ );
450
+ ```
451
+
452
+ ## Cookies and CSRF
453
+
454
+ - `withCookie(name, value)` / `withCookies({ โ€ฆ })` add a `Cookie` header โ€” **server-side only**.
455
+ Browsers ignore a `Cookie` header on `fetch` and send their own cookies; use
456
+ `withCredentials("include")` there. Names and values are sent verbatim, so never pass untrusted
457
+ input (a `;` in a value would smuggle in another cookie).
458
+ - `withCsrf()` copies the `XSRF-TOKEN` cookie (URL-decoded) into an `X-XSRF-TOKEN` header unless
459
+ the request already has one (the Angular, Laravel and Spring convention), **only for
460
+ same-origin URLs** โ€” judged on the final request URL, after
461
+ interceptors and before any redirect (`fetch` forwards custom headers across redirects, so use
462
+ `withRedirect("error")` for endpoints that may redirect elsewhere). Outside a browser there is
463
+ no page origin and every URL counts as same-origin. Configure other setups with
464
+ `withCsrf({ cookie: "csrftoken", header: "X-CSRFToken" })` (Django),
465
+ `withCsrf({ token: () => readMetaTag() })` (Rails; the token is then sent as `X-CSRF-Token`) or
466
+ `withCsrf({ crossOrigin: true })`.
467
+ - `withCsrfToken(token)` sends a token you already hold, wherever you send the request โ€” it is a
468
+ plain header; prefer `withCsrf({ token })` to keep the same-origin check.
469
+ - Nothing is sent unless you ask for it: there is no automatic `X-Requested-With` header. Add
470
+ `withHeader("X-Requested-With", "XMLHttpRequest")` if a framework still checks it.
471
+
472
+ Custom headers (including these) make cross-origin requests CORS-preflighted; that is standard
473
+ browser behaviour, not something the library adds on its own.
474
+
475
+ ## TypeScript
476
+
477
+ - `api.get<User>("/me")` (or `create.get<User>(url)`) declares the response type once; every
478
+ reader uses it: `getJson()`, `getData(user => user.name)`, `getResult()`, `getResponse()`.
479
+ - Per-call overrides still work: `getJson<Other>()`. An endpoint that can answer `204` is typed as
480
+ `getJson<User | null>()` โ€” an empty body yields `null` at runtime.
481
+ - `withBody` / `withGraphQL` are a compile error on `GET`, `HEAD` and `OPTIONS` requests (and on
482
+ a `BaseRequest`, whose method is unknown โ€” narrow it with `as BodyRequest`).
483
+ - Method-typed aliases are exported for annotations: `GetRequest<T>`, `PostRequest<T>`, โ€ฆ,
484
+ `BaseRequest<T>` (any method), `BodyRequest<T>`; the class itself is `HttpRequest<Method, T>`.
485
+ - `ApiBuilder` (the api instance type) is derived from `HttpRequest`, so the two can never drift,
486
+ and hovering an api method shows the request method's documentation.
487
+ - `RequestError<TData>` types `error.data`; `error.code` is the `RequestErrorCode` union.
488
+ - Exported types: `Method`, `BodyMethod`, `Body`, `QueryValue`, `QueryParams`, `HeadersRecord`,
489
+ `CookiesRecord`, `RetryConfig`, `RetryContext`, `RequestConfig`, `RequestInterceptor`,
490
+ `ResponseInterceptor`, `ErrorInterceptor`, `FetchFunction`, `CsrfOptions`, `GraphQLOptions`,
491
+ `RequestResult`, `RequestErrorCode`, `RequestErrorOptions`, `ApiBuilder`, `StandardSchemaV1`.
492
+ - The fetch options (`withCache`, `withCredentials`, `withMode`, `withRedirect`, `withPriority`,
493
+ `withReferrerPolicy`) are typed with the DOM unions, spelled so that they also resolve in a
494
+ Node-only project (`@types/node`, no `dom` lib).
495
+ - `withQueryParams`, `withHeaders`, `withCookies` and `withGraphQL` variables accept
496
+ interface-typed objects (no index signature needed).
497
+
498
+ ## Design principles
499
+
500
+ - **One thing at a time.** Every option is a method your editor autocompletes, so the whole API
501
+ is discoverable from the chain.
502
+ - **Correct by default.** Only retriable failures are retried (with backoff and `Retry-After`),
503
+ aborted requests are never retried, CSRF tokens only go to same-origin URLs, and a body can be
504
+ read in any format, in any order.
505
+ - **Types you can trust.** `getJson<User>()` is a `Promise<User>`, `withBody` does not exist on
506
+ a `GET`, api instances share the request's configuration methods (derived, not copied), and
507
+ `error.code` narrows.
508
+ - **Nothing global.** Defaults live on immutable api instances that you create and export
509
+ yourself.
510
+
511
+ ## Size
512
+
513
+ Measured with `size-limit` on the published build of this version (`npm run size`):
514
+
515
+ | Import | min + gzip | min + brotli |
516
+ | ----------------------------------- | ---------: | -----------: |
517
+ | everything (`import create from โ€ฆ`) | 4.86 KB | 4.40 KB |
518
+ | `import { createGet }` only | 4.25 KB | |
519
+ | `import { RequestError }` only | 0.18 KB | |
520
+
521
+ The package is one module with no side effects, so bundlers drop whatever you do not import. The
522
+ JavaScript ships without JSDoc comments; the documentation lives in the declaration files, where
523
+ your editor reads it.
524
+
525
+ ## Migrating from v1
526
+
527
+ v2 keeps the fluent API and most method names, and removes the global `create.config`, the
528
+ `.withCache.NO_CACHE()`-style enum getters and the runtime enums. See [MIGRATION.md](MIGRATION.md)
529
+ for the complete v1 โ†’ v2 map and the list of behaviour changes.
530
+
531
+ ## Contributing
532
+
533
+ `npm run check` runs lint, format, type-check, the type tests (including every code block of
534
+ this README), the test suite at 100% coverage, the build, package linting and the size gate.
535
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
1396
536
 
1397
537
  ## License
1398
538
 
1399
- MIT
1400
-
1401
- ---
1402
-
1403
- ## Website
1404
-
1405
- Visit [create-request.com](https://create-request.com) for documentation, examples, and more resources.
1406
-
1407
- ## Sponsor
1408
-
1409
- If `create-request` helps your work, you can support ongoing development through [GitHub Sponsors](https://github.com/sponsors/DanielAmenou).
539
+ [MIT](LICENSE) ยท [Website](https://create-request.com) ยท [Sponsor](https://github.com/sponsors/DanielAmenou)