create-request 1.6.1 โ 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/CHANGELOG.md +58 -0
- package/MIGRATION.md +158 -0
- package/README.md +422 -1292
- package/dist/index.cjs +730 -0
- package/dist/index.d.cts +851 -0
- package/dist/index.d.ts +851 -0
- package/dist/index.js +717 -0
- package/package.json +63 -67
- package/dist/library/BaseRequest.d.ts +0 -870
- package/dist/library/BodyRequest.d.ts +0 -100
- package/dist/library/RequestError.d.ts +0 -181
- package/dist/library/ResponseWrapper.d.ts +0 -193
- package/dist/library/apiBuilder.d.ts +0 -560
- package/dist/library/enums.d.ts +0 -94
- package/dist/library/index.cjs +0 -2994
- package/dist/library/index.cjs.map +0 -1
- package/dist/library/index.d.ts +0 -47
- package/dist/library/index.esm.js +0 -2966
- package/dist/library/index.esm.js.map +0 -1
- package/dist/library/index.esm.min.js +0 -1
- package/dist/library/index.esm.min.js.map +0 -1
- package/dist/library/index.min.cjs +0 -1
- package/dist/library/index.min.cjs.map +0 -1
- package/dist/library/requestFactories.d.ts +0 -91
- package/dist/library/requestMethods.d.ts +0 -84
- package/dist/library/types.d.ts +0 -329
- package/dist/library/utils/Config.d.ts +0 -221
- package/dist/library/utils/CookieUtils.d.ts +0 -9
- package/dist/library/utils/CsrfUtils.d.ts +0 -24
package/README.md
CHANGED
|
@@ -1,1409 +1,539 @@
|
|
|
1
1
|
# create-request
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/create-request)
|
|
4
|
+
[](https://bundlephobia.com/package/create-request)
|
|
4
5
|
[](https://codecov.io/github/danielamenou/create-request)
|
|
5
6
|
[](https://www.npmjs.com/package/create-request)
|
|
6
|
-
[](https://bundlephobia.com/package/create-request)
|
|
8
|
-
[](https://www.typescriptlang.org/)
|
|
9
|
-
[](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
|
+
[](https://github.com/DanielAmenou/create-request/blob/main/LICENSE)
|
|
87
8
|
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
24
|
+
## Table of contents
|
|
296
25
|
|
|
297
|
-
-
|
|
298
|
-
|
|
299
|
-
-
|
|
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
|
-
```
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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
|
-
|
|
346
|
-
import create from "create-request";
|
|
63
|
+
import create, { createApi, isRequestError } from "create-request";
|
|
347
64
|
|
|
348
|
-
//
|
|
349
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
102
|
+
### Creating
|
|
580
103
|
|
|
581
104
|
```typescript
|
|
582
|
-
//
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
115
|
+
### Configuring
|
|
604
116
|
|
|
605
117
|
```typescript
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
//
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
//
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
193
|
+
## Errors
|
|
626
194
|
|
|
627
|
-
|
|
195
|
+
Every rejection is a `RequestError`. Check it with `isRequestError(error)` (or `instanceof`) and
|
|
196
|
+
switch on `code`:
|
|
628
197
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
221
|
+
await api.get<User>("/users/42").getJson();
|
|
657
222
|
} catch (error) {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
832
|
-
|
|
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
|
-
.
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
921
|
-
|
|
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
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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
|
-
|
|
971
|
-
|
|
972
|
-
Global interceptors apply to all requests:
|
|
309
|
+
## Timeouts and cancellation
|
|
973
310
|
|
|
974
311
|
```typescript
|
|
975
|
-
//
|
|
976
|
-
|
|
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
|
-
|
|
984
|
-
const
|
|
985
|
-
|
|
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
|
-
//
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
-
|
|
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
|
|
332
|
+
## Interceptors
|
|
1046
333
|
|
|
1047
|
-
|
|
1048
|
-
|
|
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
|
-
|
|
1053
|
-
|
|
1054
|
-
const
|
|
1055
|
-
|
|
1056
|
-
.withRequestInterceptor(
|
|
1057
|
-
|
|
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
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
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
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
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
|
-
|
|
368
|
+
## Schema validation
|
|
1175
369
|
|
|
1176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1190
|
-
const
|
|
1191
|
-
|
|
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
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
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
|
-
|
|
389
|
+
## GraphQL
|
|
1217
390
|
|
|
1218
391
|
```typescript
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
name:
|
|
1222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
401
|
+
## Streaming and downloads
|
|
1307
402
|
|
|
1308
|
-
|
|
403
|
+
`getBody()` returns the raw stream; everything else is available on the wrapper.
|
|
1309
404
|
|
|
1310
405
|
```typescript
|
|
1311
|
-
|
|
1312
|
-
const
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
- "
|
|
1395
|
-
|
|
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)
|