create-request 1.4.2 → 1.4.3-rc.1

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
@@ -16,9 +16,13 @@
16
16
  - [Why create-request](#why-create-request)
17
17
  - [Installation](#installation)
18
18
  - [Basic Usage](#basic-usage)
19
- - [Advanced Usage](#advanced-usage)
20
- - [GraphQL Support](#graphql-requests)
19
+ - [API Builder](#api-builder)
20
+ - [Automatic Retries with Delay](#automatic-retries-with-delay)
21
21
  - [Interceptors](#interceptors)
22
+ - [Request Cancellation](#request-cancellation)
23
+ - [URL Handling](#url-handling)
24
+ - [Data Selection](#data-selection)
25
+ - [GraphQL Support](#graphql-requests)
22
26
  - [TypeScript Support](#typescript-support)
23
27
  - [CSRF Protection](#csrf-protection)
24
28
  - [Performance Considerations](#performance-considerations)
@@ -414,9 +418,211 @@ try {
414
418
  }
415
419
  ```
416
420
 
417
- ## Advanced Usage
421
+ ## API Builder
422
+
423
+ 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.
424
+
425
+ #### Creating an API Instance
426
+
427
+ ```typescript
428
+ import create from "create-request";
429
+
430
+ // Create a configured API instance
431
+ const api = create.api().withBaseURL("https://api.example.com").withTimeout(20000);
432
+
433
+ // Use it with relative URLs
434
+ const users = await api.get("/users").getJson();
435
+
436
+ // Or without URL (uses baseURL)
437
+ const users = await api.get().getJson();
438
+ const newUser = await api.post().withBody({ name: "John" }).getJson();
439
+ ```
440
+
441
+ #### Core API Builder Method
442
+
443
+ - **`.withBaseURL(baseURL: string)`** - Set the base URL for all requests. Relative URLs will be resolved against this base URL.
444
+
445
+ #### Available Request Methods
446
+
447
+ The API builder provides access to all request configuration methods from `BaseRequest` that can be used as defaults. These methods will apply to all requests made through the API instance:
448
+
449
+ **Authentication & Headers:**
450
+
451
+ - `withHeaders(headers)` - Set default headers for all requests
452
+ - `withHeader(key, value)` - Add a single default header
453
+ - `withAuthorization(authValue)` - Set Authorization header
454
+ - `withBasicAuth(username, password)` - Add Basic Authentication
455
+ - `withBearerToken(token)` - Add Bearer token authentication
456
+ - `withContentType(contentType)` - Set default Content-Type header
457
+
458
+ **Cookies:**
459
+
460
+ - `withCookies(cookies)` - Add cookies to all requests
461
+ - `withCookie(name, value)` - Add a single cookie
462
+
463
+ **Request Configuration:**
464
+
465
+ - `withTimeout(timeout)` - Set default timeout for all requests
466
+ - `withRetries(retries)` - Configure default retry behavior
467
+ - `withReferrer(referrer)` - Set default referrer
468
+ - `withKeepAlive(keepalive)` - Configure keep-alive
469
+ - `withIntegrity(integrity)` - Set integrity check
470
+ - `withQueryParams(params)` - Add default query parameters
471
+ - `withQueryParam(key, value)` - Add a single default query parameter
472
+
473
+ **CSRF Protection:**
474
+
475
+ - `withCsrfToken(token, headerName?)` - Set CSRF token
476
+ - `withoutCsrfProtection()` - Disable CSRF protection
477
+ - `withAntiCsrfHeaders()` - Enable anti-CSRF headers
478
+
479
+ **Interceptors:**
480
+
481
+ - `withRequestInterceptor(interceptor)` - Add default request interceptor
482
+ - `withResponseInterceptor(interceptor)` - Add default response interceptor
483
+ - `withErrorInterceptor(interceptor)` - Add default error interceptor
484
+
485
+ These methods can be chained together and will apply to all requests made through the API instance:
486
+
487
+ ```typescript
488
+ const api = create
489
+ .api()
490
+ .withBaseURL("https://api.example.com")
491
+ .withBearerToken("token123")
492
+ .withCookies({ session: "abc123" })
493
+ .withTimeout(5000)
494
+ .withHeaders({ "X-Custom": "value" });
495
+
496
+ // All requests will include the Bearer token, cookies, timeout, and headers
497
+ await api.get("/users").getJson();
498
+ await api.post("/posts").withBody({ title: "Hello" }).getJson();
499
+ ```
500
+
501
+ #### Methods NOT Available on API Builder
502
+
503
+ The following methods are **not available** on the API builder because they are request-specific and don't make sense as defaults:
504
+
505
+ - **`withAbortController(controller)`** - AbortController is per-request, not a default
506
+ - **`withBody(body)`** - Request bodies are different for each request
507
+ - **`withGraphQL(query, variables, options)`** - GraphQL queries are request-specific
508
+
509
+ These methods should be called directly on individual request instances:
510
+
511
+ ```typescript
512
+ const api = create.api().withBaseURL("https://api.example.com");
513
+
514
+ // ✅ Good: Use withBody on individual requests
515
+ await api.post("/users").withBody({ name: "John" }).getJson();
516
+
517
+ // ❌ Bad: withBody is not available on the API builder
518
+ // api.withBody({ name: "John" }); // This will be undefined
519
+ ```
520
+
521
+ #### URL Resolution
522
+
523
+ The API builder intelligently resolves URLs:
524
+
525
+ ```typescript
526
+ const api = create.api().withBaseURL("https://api.example.com");
527
+
528
+ // Relative URLs are resolved against baseURL
529
+ await api.get("users").getJson(); // → https://api.example.com/users
530
+ await api.get("/users").getJson(); // → https://api.example.com/users
531
+ await api.get("./users").getJson(); // → https://api.example.com/users
532
+
533
+ // Absolute URLs are used as-is
534
+ await api.get("https://other-api.com/data").getJson(); // → https://other-api.com/data
535
+
536
+ // No URL uses baseURL directly
537
+ await api.get().getJson(); // → https://api.example.com
538
+ ```
539
+
540
+ #### Overriding Defaults
541
+
542
+ You can override default settings on individual requests:
543
+
544
+ ```typescript
545
+ const api = create
546
+ .api()
547
+ .withBaseURL("https://api.example.com")
548
+ .withTimeout(5000)
549
+ .withBearerToken("token123");
550
+
551
+ // Override timeout for this specific request
552
+ await api.get("/slow-endpoint").withTimeout(30000).getJson();
553
+
554
+ // Override headers (merges with defaults)
555
+ await api
556
+ .get("/users")
557
+ .withBearerToken("newtoken")
558
+ .withHeaders({ "X-Custom": "value" })
559
+ .getJson();
560
+ // Result: Authorization: "Bearer newtoken", X-Custom: "value"
561
+ ```
562
+
563
+ #### All HTTP Methods Supported
564
+
565
+ The API instance supports all HTTP methods:
566
+
567
+ ```typescript
568
+ const api = create.api().withBaseURL("https://api.example.com");
569
+
570
+ await api.get("/users").getJson();
571
+ await api.post("/users").withBody({ name: "John" }).getJson();
572
+ await api.put("/users/1").withBody({ name: "Jane" }).getJson();
573
+ await api.patch("/users/1").withBody({ status: "active" }).getJson();
574
+ await api.del("/users/1").getJson();
575
+ await api.head("/users").getResponse();
576
+ await api.options("/users").getResponse();
577
+ ```
578
+
579
+ #### Merging Default Headers
580
+
581
+ Multiple calls to `withHeaders` will merge headers, with later calls taking precedence:
582
+
583
+ ```typescript
584
+ const api = create
585
+ .api()
586
+ .withBaseURL("https://api.example.com")
587
+ .withBearerToken("token123")
588
+ .withHeaders({ "X-Custom": "value1" })
589
+ .withHeaders({ "X-Other": "value2" })
590
+ .withBearerToken("newtoken");
591
+
592
+ // Result: Authorization: "Bearer newtoken", X-Custom: "value1", X-Other: "value2"
593
+ ```
594
+
595
+ #### Complete Example
596
+
597
+ ```typescript
598
+ // Set up your API once
599
+ const api = create
600
+ .api()
601
+ .withBaseURL("https://api.example.com/v1")
602
+ .withHeaders({ "Content-Type": "application/json" })
603
+ .withCookies({ session: "abc123" })
604
+ .withBearerToken("token123")
605
+ .withTimeout(20000);
606
+
607
+ // Use throughout your application
608
+ async function getUsers() {
609
+ return api.get("/users").getJson();
610
+ }
611
+
612
+ async function createUser(userData: User) {
613
+ return api.post("/users").withBody(userData).getJson();
614
+ }
615
+
616
+ async function updateUser(id: string, userData: Partial<User>) {
617
+ return api.put(`/users/${id}`).withBody(userData).getJson();
618
+ }
619
+
620
+ async function deleteUser(id: string) {
621
+ return api.del(`/users/${id}`).getJson();
622
+ }
623
+ ```
418
624
 
419
- ### Automatic Retries with Delay
625
+ ## Automatic Retries with Delay
420
626
 
421
627
  The `withRetries()` method supports both simple number-based retries and object-based configuration with customizable delays:
422
628
 
@@ -463,7 +669,7 @@ const request4 = create.get("https://api.example.com/data").withRetries({
463
669
  })
464
670
  ```
465
671
 
466
- ### Interceptors
672
+ ## Interceptors
467
673
 
468
674
  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.
469
675
 
@@ -597,7 +803,7 @@ const asyncData = await create
597
803
  .getJson();
598
804
  ```
599
805
 
600
- ### Request Cancellation
806
+ ## Request Cancellation
601
807
 
602
808
  ```typescript
603
809
  const controller = new AbortController();
@@ -623,7 +829,7 @@ try {
623
829
  }
624
830
  ```
625
831
 
626
- ### URL Handling
832
+ ## URL Handling
627
833
 
628
834
  The library handles both absolute and relative URLs, and automatically merges query parameters:
629
835
 
@@ -648,7 +854,7 @@ const encoded = await create
648
854
  .getJson();
649
855
  ```
650
856
 
651
- ### Data Selection
857
+ ## Data Selection
652
858
 
653
859
  The `getData` method provides a powerful way to extract and transform specific data from API responses:
654
860
 
@@ -815,7 +1021,6 @@ This library works with all browsers that support the Fetch API:
815
1021
  | **TypeScript** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
816
1022
  | **Streaming** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
817
1023
  | **Progress** | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
818
- | **Middleware** | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
819
1024
  | **Cookies** | ✅ | ✅ | 🛠️ | ✅ | ✅ | ❌ | ❌ | ❌ |
820
1025
  | **Pagination API** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
821
1026
  | **Zero Deps** | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ |
@@ -10,6 +10,7 @@ export declare class RequestError extends Error {
10
10
  response?: Response;
11
11
  isTimeout?: boolean;
12
12
  isAborted?: boolean;
13
+ cause?: Error;
13
14
  });
14
15
  /**
15
16
  * Static methods for creating specific types of RequestError
@@ -3,16 +3,25 @@ import type { GraphQLOptions } from "./types.js";
3
3
  * Wrapper for HTTP responses with methods to transform the response data
4
4
  */
5
5
  export declare class ResponseWrapper {
6
- private readonly response;
7
6
  readonly url?: string;
8
7
  readonly method?: string;
8
+ private readonly response;
9
9
  private graphQLOptions?;
10
+ private cachedBlob?;
11
+ private cachedText?;
12
+ private cachedJson?;
13
+ private cachedArrayBuffer?;
10
14
  constructor(response: Response, url?: string, method?: string, graphQLOptions?: GraphQLOptions);
11
15
  get status(): number;
12
16
  get statusText(): string;
13
17
  get headers(): Headers;
14
18
  get ok(): boolean;
15
19
  get raw(): Response;
20
+ /**
21
+ * Check if the response body has already been consumed and throw an error if so
22
+ * @throws RequestError if the body has already been consumed
23
+ */
24
+ private checkBodyNotConsumed;
16
25
  /**
17
26
  * Check for GraphQL errors and throw if throwOnError is enabled
18
27
  * @param data - The parsed JSON data
@@ -21,11 +30,10 @@ export declare class ResponseWrapper {
21
30
  private checkGraphQLErrors;
22
31
  /**
23
32
  * Parse the response body as JSON
24
- * Note: This consumes the response body and can only be called once.
25
33
  * If GraphQL options are set with throwOnError=true, will check for GraphQL errors and throw.
26
34
  *
27
35
  * @returns The parsed JSON data
28
- * @throws {RequestError} When the request fails, JSON parsing fails, GraphQL errors occur (if throwOnError enabled), or body is already consumed
36
+ * @throws {RequestError} When the request fails, JSON parsing fails, or GraphQL errors occur (if throwOnError enabled).
29
37
  *
30
38
  * @example
31
39
  * const data = await response.getJson();
@@ -44,10 +52,9 @@ export declare class ResponseWrapper {
44
52
  getJson<T = unknown>(): Promise<T>;
45
53
  /**
46
54
  * Get the response body as text
47
- * Note: This consumes the response body and can only be called once.
48
55
  *
49
56
  * @returns The response text
50
- * @throws {RequestError} When reading fails or the response has already been consumed
57
+ * @throws {RequestError} When reading fails
51
58
  *
52
59
  * @example
53
60
  * const text = await response.getText();
@@ -55,10 +62,9 @@ export declare class ResponseWrapper {
55
62
  getText(): Promise<string>;
56
63
  /**
57
64
  * Get the response body as a Blob
58
- * Note: This consumes the response body and can only be called once.
59
65
  *
60
66
  * @returns The response as a Blob
61
- * @throws {RequestError} When reading fails or the response has already been consumed
67
+ * @throws {RequestError} When reading fails
62
68
  *
63
69
  * @example
64
70
  * const blob = await response.getBlob();
@@ -67,10 +73,9 @@ export declare class ResponseWrapper {
67
73
  getBlob(): Promise<Blob>;
68
74
  /**
69
75
  * Get the response body as an ArrayBuffer
70
- * Note: This consumes the response body and can only be called once.
71
76
  *
72
77
  * @returns The response as an ArrayBuffer
73
- * @throws {RequestError} When reading fails or the response has already been consumed
78
+ * @throws {RequestError} When reading fails
74
79
  *
75
80
  * @example
76
81
  * const buffer = await response.getArrayBuffer();
@@ -80,6 +85,7 @@ export declare class ResponseWrapper {
80
85
  /**
81
86
  * Get the raw response body as a ReadableStream
82
87
  * Note: This consumes the response body and should only be called once.
88
+ * Unlike other methods, streams cannot be cached, so this will throw if the body is already consumed.
83
89
  *
84
90
  * @returns The response body as a ReadableStream or null
85
91
  * @throws {RequestError} When the response body has already been consumed
@@ -0,0 +1,182 @@
1
+ import type { CookiesRecord, CookieOptions, RetryConfig, RequestInterceptor, ResponseInterceptor, ErrorInterceptor } from "./types.js";
2
+ import type { GetRequest, PostRequest, PutRequest, DeleteRequest, PatchRequest, HeadRequest, OptionsRequest } from "./requestMethods.js";
3
+ interface ApiBuilderRequestMethods {
4
+ withoutCsrfProtection(): ApiBuilder;
5
+ withAntiCsrfHeaders(): ApiBuilder;
6
+ withTimeout(timeout: number): ApiBuilder;
7
+ withReferrer(referrer: string): ApiBuilder;
8
+ withKeepAlive(keepalive: boolean): ApiBuilder;
9
+ withIntegrity(integrity: string): ApiBuilder;
10
+ withHeader(key: string, value: string): ApiBuilder;
11
+ withHeaders(headers: Record<string, string>): ApiBuilder;
12
+ withRetries(retries: number | RetryConfig): ApiBuilder;
13
+ withBearerToken(token: string): ApiBuilder;
14
+ withCookies(cookies: CookiesRecord): ApiBuilder;
15
+ withContentType(contentType: string): ApiBuilder;
16
+ withAuthorization(authValue: string): ApiBuilder;
17
+ withBasicAuth(username: string, password: string): ApiBuilder;
18
+ withCsrfToken(token: string, headerName?: string): ApiBuilder;
19
+ withErrorInterceptor(interceptor: ErrorInterceptor): ApiBuilder;
20
+ withCookie(name: string, value: string | CookieOptions): ApiBuilder;
21
+ withRequestInterceptor(interceptor: RequestInterceptor): ApiBuilder;
22
+ withResponseInterceptor(interceptor: ResponseInterceptor): ApiBuilder;
23
+ withQueryParam(key: string, value: string | string[] | number | boolean | null | undefined): ApiBuilder;
24
+ withQueryParams(params: Record<string, string | string[] | number | boolean | null | undefined>): ApiBuilder;
25
+ }
26
+ /**
27
+ * API builder for creating configured API instances with default settings.
28
+ * Allows you to set default headers, timeouts, authentication, and other options
29
+ * that will be applied to all requests created through this builder.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * const api = create.api()
34
+ * .withBaseURL("https://api.example.com")
35
+ * .withBearerToken("token123")
36
+ * .withTimeout(5000);
37
+ *
38
+ * // All requests will use the base URL, bearer token, and timeout
39
+ * await api.get("/users").getJson();
40
+ * await api.post("/posts").withBody({ title: "Hello" }).getJson();
41
+ * ```
42
+ */
43
+ export declare class ApiBuilder {
44
+ private baseURL?;
45
+ private modifiers?;
46
+ /**
47
+ * Sets the base URL for all requests created through this API builder.
48
+ * Relative URLs will be resolved against this base URL.
49
+ *
50
+ * @param baseURL - The base URL to use for all requests
51
+ * @returns The API builder instance for method chaining
52
+ * @example
53
+ * ```typescript
54
+ * const api = create.api().withBaseURL("https://api.example.com");
55
+ * await api.get("/users").getJson(); // Requests https://api.example.com/users
56
+ * ```
57
+ */
58
+ withBaseURL(baseURL: string): this;
59
+ /**
60
+ * Adds a modifier function that will be applied to all requests created through this builder.
61
+ *
62
+ * @private
63
+ * @param modifier - A function that modifies a request and returns it
64
+ * @returns The API builder instance for method chaining
65
+ */
66
+ private addModifier;
67
+ /**
68
+ * Creates a GET request with the configured default settings.
69
+ *
70
+ * @param url - Optional URL path. If not provided, uses the base URL. If relative, resolves against base URL.
71
+ * @returns A GetRequest instance ready to be executed
72
+ * @example
73
+ * ```typescript
74
+ * const api = create.api().withBaseURL("https://api.example.com");
75
+ * await api.get("/users").getJson();
76
+ * await api.get("https://other.com/data").getJson(); // Absolute URL overrides base
77
+ * ```
78
+ */
79
+ get: (url?: string) => GetRequest;
80
+ /**
81
+ * Creates a POST request with the configured default settings.
82
+ *
83
+ * @param url - Optional URL path. If not provided, uses the base URL. If relative, resolves against base URL.
84
+ * @returns A PostRequest instance ready to be executed
85
+ * @example
86
+ * ```typescript
87
+ * const api = create.api().withBaseURL("https://api.example.com");
88
+ * await api.post("/users").withBody({ name: "John" }).getJson();
89
+ * ```
90
+ */
91
+ post: (url?: string) => PostRequest;
92
+ /**
93
+ * Creates a PUT request with the configured default settings.
94
+ *
95
+ * @param url - Optional URL path. If not provided, uses the base URL. If relative, resolves against base URL.
96
+ * @returns A PutRequest instance ready to be executed
97
+ * @example
98
+ * ```typescript
99
+ * const api = create.api().withBaseURL("https://api.example.com");
100
+ * await api.put("/users/123").withBody({ name: "Jane" }).getJson();
101
+ * ```
102
+ */
103
+ put: (url?: string) => PutRequest;
104
+ /**
105
+ * Creates a DELETE request with the configured default settings.
106
+ *
107
+ * @param url - Optional URL path. If not provided, uses the base URL. If relative, resolves against base URL.
108
+ * @returns A DeleteRequest instance ready to be executed
109
+ * @example
110
+ * ```typescript
111
+ * const api = create.api().withBaseURL("https://api.example.com");
112
+ * await api.del("/users/123").getResponse();
113
+ * ```
114
+ */
115
+ del: (url?: string) => DeleteRequest;
116
+ /**
117
+ * Creates a PATCH request with the configured default settings.
118
+ *
119
+ * @param url - Optional URL path. If not provided, uses the base URL. If relative, resolves against base URL.
120
+ * @returns A PatchRequest instance ready to be executed
121
+ * @example
122
+ * ```typescript
123
+ * const api = create.api().withBaseURL("https://api.example.com");
124
+ * await api.patch("/users/123").withBody({ name: "Updated" }).getJson();
125
+ * ```
126
+ */
127
+ patch: (url?: string) => PatchRequest;
128
+ /**
129
+ * Creates a HEAD request with the configured default settings.
130
+ *
131
+ * @param url - Optional URL path. If not provided, uses the base URL. If relative, resolves against base URL.
132
+ * @returns A HeadRequest instance ready to be executed
133
+ * @example
134
+ * ```typescript
135
+ * const api = create.api().withBaseURL("https://api.example.com");
136
+ * const response = await api.head("/users").getResponse();
137
+ * ```
138
+ */
139
+ head: (url?: string) => HeadRequest;
140
+ /**
141
+ * Creates an OPTIONS request with the configured default settings.
142
+ *
143
+ * @param url - Optional URL path. If not provided, uses the base URL. If relative, resolves against base URL.
144
+ * @returns An OptionsRequest instance ready to be executed
145
+ * @example
146
+ * ```typescript
147
+ * const api = create.api().withBaseURL("https://api.example.com");
148
+ * await api.options("/users").getResponse();
149
+ * ```
150
+ */
151
+ options: (url?: string) => OptionsRequest;
152
+ /**
153
+ * Creates a new ApiBuilder instance with a Proxy that enables dynamic method forwarding.
154
+ * The Proxy allows calling any `with*` method from BaseRequest on the API builder,
155
+ * which will apply that configuration to all requests created through this builder.
156
+ *
157
+ * @internal
158
+ */
159
+ constructor();
160
+ }
161
+ /**
162
+ * Creates a new API builder instance for configuring default request settings.
163
+ * The builder allows you to set base URLs, authentication, headers, timeouts,
164
+ * and other options that will be applied to all requests created through it.
165
+ *
166
+ * @returns A new ApiBuilder instance with all configuration methods available
167
+ * @example
168
+ * ```typescript
169
+ * // Create an API instance with default configuration
170
+ * const api = create.api()
171
+ * .withBaseURL("https://api.example.com")
172
+ * .withBearerToken("your-token")
173
+ * .withTimeout(5000)
174
+ * .withHeaders({ "X-Custom": "value" });
175
+ *
176
+ * // All requests will use these defaults
177
+ * const users = await api.get("/users").getJson();
178
+ * const newUser = await api.post("/users").withBody({ name: "John" }).getJson();
179
+ * ```
180
+ */
181
+ export declare function api(): ApiBuilder & ApiBuilderRequestMethods;
182
+ export {};