@zimic/fetch 0.1.0-canary.20 → 0.1.0-canary.21

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.
@@ -4,6 +4,11 @@ import { PossiblePromise, RequiredByKey } from '@zimic/utils/types';
4
4
  import FetchResponseError from '../errors/FetchResponseError';
5
5
  import { FetchRequest, FetchRequestConstructor, FetchRequestInit, FetchResponse } from './requests';
6
6
 
7
+ /**
8
+ * The input to fetch a resource, either a path, a URL, or a {@link FetchRequest request}.
9
+ *
10
+ * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetch `fetch` API reference}
11
+ */
7
12
  export type FetchInput<
8
13
  Schema extends HttpSchema,
9
14
  Method extends HttpSchemaMethod<Schema>,
@@ -37,32 +42,478 @@ export namespace FetchFunction {
37
42
  ) => Promise<FetchResponse.Loose>;
38
43
  }
39
44
 
45
+ /**
46
+ * The options to create a {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetch fetch instance}.
47
+ *
48
+ * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#createfetch `createFetch(options)` API reference}
49
+ */
40
50
  export interface FetchOptions<Schema extends HttpSchema> extends Omit<FetchRequestInit.Defaults, 'method'> {
51
+ /**
52
+ * A listener function that is called for each request. It can modify the requests before they are sent.
53
+ *
54
+ * @example
55
+ * import { createFetch } from '@zimic/fetch';
56
+ * import { type HttpSchema } from '@zimic/http';
57
+ *
58
+ * interface User {
59
+ * id: string;
60
+ * username: string;
61
+ * }
62
+ *
63
+ * type Schema = HttpSchema<{
64
+ * '/users': {
65
+ * GET: {
66
+ * request: {
67
+ * searchParams: { page?: number; limit?: number };
68
+ * };
69
+ * response: {
70
+ * 200: { body: User[] };
71
+ * };
72
+ * };
73
+ * };
74
+ * }>;
75
+ *
76
+ * const fetch = createFetch<Schema>({
77
+ * baseURL: 'http://localhost:80',
78
+ *
79
+ * onRequest(request) {
80
+ * if (this.isRequest(request, 'GET', '/users')) {
81
+ * const url = new URL(request.url);
82
+ * url.searchParams.append('limit', '10');
83
+ *
84
+ * const updatedRequest = new Request(url, request);
85
+ * return updatedRequest;
86
+ * }
87
+ *
88
+ * return request;
89
+ * },
90
+ * });
91
+ *
92
+ * @param request The original request.
93
+ * @returns The request to be sent. It can be the original request or a modified version of it.
94
+ * @this {Fetch<Schema>} The fetch instance that is sending the request.
95
+ */
41
96
  onRequest?: (this: Fetch<Schema>, request: FetchRequest.Loose) => PossiblePromise<Request>;
97
+
98
+ /**
99
+ * A listener function that is called after each response is received. It can modify the responses before they are
100
+ * returned to the fetch caller.
101
+ *
102
+ * @example
103
+ * import { type HttpSchema } from '@zimic/http';
104
+ * import { createFetch } from '@zimic/fetch';
105
+ *
106
+ * interface User {
107
+ * id: string;
108
+ * username: string;
109
+ * }
110
+ *
111
+ * type Schema = HttpSchema<{
112
+ * '/users': {
113
+ * GET: {
114
+ * response: {
115
+ * 200: {
116
+ * headers: { 'content-encoding'?: string };
117
+ * body: User[];
118
+ * };
119
+ * };
120
+ * };
121
+ * };
122
+ * }>;
123
+ *
124
+ * const fetch = createFetch<Schema>({
125
+ * baseURL: 'http://localhost:80',
126
+ *
127
+ * onResponse(response) {
128
+ * if (this.isResponse(response, 'GET', '/users')) {
129
+ * console.log(response.headers.get('content-encoding'));
130
+ * }
131
+ * return response;
132
+ * },
133
+ * });
134
+ *
135
+ * @param response The original response.
136
+ * @returns The response to be returned.
137
+ * @this {Fetch<Schema>} The fetch instance that received the response.
138
+ */
42
139
  onResponse?: (this: Fetch<Schema>, response: FetchResponse.Loose) => PossiblePromise<Response>;
43
140
  }
44
141
 
142
+ /**
143
+ * The default options for each request sent by the fetch instance.
144
+ *
145
+ * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetchdefaults `fetch.defaults` API reference}
146
+ */
45
147
  export type FetchDefaults = RequiredByKey<FetchRequestInit.Defaults, 'headers' | 'searchParams'>;
46
148
 
47
149
  export interface FetchClient<Schema extends HttpSchema> extends Pick<FetchOptions<Schema>, 'onRequest' | 'onResponse'> {
150
+ /**
151
+ * The default options for each request sent by the fetch instance. The available options are the same as the
152
+ * {@link https://developer.mozilla.org/docs/Web/API/RequestInit `RequestInit`} options, plus `baseURL`.
153
+ *
154
+ * @example
155
+ * import { type HttpSchema } from '@zimic/http';
156
+ * import { createFetch } from '@zimic/fetch';
157
+ *
158
+ * interface Post {
159
+ * id: string;
160
+ * title: string;
161
+ * }
162
+ *
163
+ * type Schema = HttpSchema<{
164
+ * '/posts': {
165
+ * POST: {
166
+ * request: {
167
+ * headers: { 'content-type': 'application/json' };
168
+ * body: { title: string };
169
+ * };
170
+ * response: {
171
+ * 201: { body: Post };
172
+ * };
173
+ * };
174
+ * };
175
+ * }>;
176
+ *
177
+ * const fetch = createFetch<Schema>({
178
+ * baseURL: 'http://localhost:3000',
179
+ * headers: { 'accept-language': 'en' },
180
+ * });
181
+ *
182
+ * // Set the authorization header for all requests
183
+ * const { accessToken } = await authenticate();
184
+ *
185
+ * fetch.defaults.headers.authorization = `Bearer ${accessToken}`;
186
+ * console.log(fetch.defaults.headers);
187
+ *
188
+ * const response = await fetch('/posts', {
189
+ * method: 'POST',
190
+ * headers: { 'content-type': 'application/json' },
191
+ * body: JSON.stringify({ title: 'My post' }),
192
+ * });
193
+ *
194
+ * const post = await response.json(); // Post
195
+ */
48
196
  defaults: FetchDefaults;
49
197
 
198
+ /**
199
+ * A loosely-typed version of {@link Fetch `fetch`}. This can be useful to make requests with fewer type constraints,
200
+ * such as in {@link onRequest `onRequest`} and {@link onResponse `onResponse`} listeners.
201
+ *
202
+ * @example
203
+ * import { type HttpSchema } from '@zimic/http';
204
+ * import { createFetch } from '@zimic/fetch';
205
+ *
206
+ * interface User {
207
+ * id: string;
208
+ * username: string;
209
+ * }
210
+ *
211
+ * type Schema = HttpSchema<{
212
+ * '/auth/login': {
213
+ * POST: {
214
+ * request: {
215
+ * headers: { 'content-type': 'application/json' };
216
+ * body: { username: string; password: string };
217
+ * };
218
+ * response: {
219
+ * 201: { body: { accessToken: string } };
220
+ * };
221
+ * };
222
+ * };
223
+ *
224
+ * '/auth/refresh': {
225
+ * POST: {
226
+ * response: {
227
+ * 201: { body: { accessToken: string } };
228
+ * };
229
+ * };
230
+ * };
231
+ *
232
+ * '/users': {
233
+ * GET: {
234
+ * request: {
235
+ * headers: { authorization: string };
236
+ * };
237
+ * response: {
238
+ * 200: { body: User[] };
239
+ * 401: { body: { message: string } };
240
+ * 403: { body: { message: string } };
241
+ * };
242
+ * };
243
+ * };
244
+ * }>;
245
+ *
246
+ * const fetch = createFetch<Schema>({
247
+ * baseURL,
248
+ *
249
+ * async onResponse(response) {
250
+ * if (response.status === 401) {
251
+ * const body = await response.clone().json();
252
+ *
253
+ * if (body.message === 'Access token expired') {
254
+ * // Refresh the access token
255
+ * const refreshResponse = await this('/auth/refresh', { method: 'POST' });
256
+ * const { accessToken } = await refreshResponse.json();
257
+ *
258
+ * // Clone the original request and update its headers
259
+ * const updatedRequest = response.request.clone();
260
+ * updatedRequest.headers.set('authorization', `Bearer ${accessToken}`);
261
+ *
262
+ * // Retry the original request with the updated headers
263
+ * return this.loose(updatedRequest);
264
+ * }
265
+ * }
266
+ *
267
+ * return response;
268
+ * },
269
+ * });
270
+ *
271
+ * // Authenticate to your service before requests
272
+ * const loginRequest = await fetch('/auth/login', {
273
+ * method: 'POST',
274
+ * headers: { 'content-type': 'application/json' },
275
+ * body: JSON.stringify({ username: 'me', password: 'password' }),
276
+ * });
277
+ * const { accessToken } = await loginRequest.json();
278
+ *
279
+ * // Set the authorization header for all requests
280
+ * fetch.defaults.headers.authorization = `Bearer ${accessToken}`;
281
+ *
282
+ * const request = await fetch('/users', {
283
+ * method: 'GET',
284
+ * searchParams: { query: 'u' },
285
+ * });
286
+ *
287
+ * const users = await request.json(); // User[]
288
+ *
289
+ * @param input The resource to fetch, either a path, a URL, or a {@link FetchRequest request}. If a path is provided,
290
+ * it is automatically prefixed with the base URL of the fetch instance when the request is sent. If a URL or a
291
+ * request is provided, it is used as is.
292
+ * @param init The request options. If a path or a URL is provided as the first argument, this argument is required
293
+ * and should contain at least the method of the request. If the first argument is a {@link FetchRequest request},
294
+ * this argument is optional.
295
+ * @returns A promise that resolves to the response to the request.
296
+ * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetchloose `fetch.loose` API reference}
297
+ * @see {@link https://developer.mozilla.org/docs/Web/API/Fetch_API}
298
+ * @see {@link https://developer.mozilla.org/docs/Web/API/Request}
299
+ * @see {@link https://developer.mozilla.org/docs/Web/API/RequestInit}
300
+ * @see {@link https://developer.mozilla.org/docs/Web/API/Response}
301
+ */
50
302
  loose: FetchFunction.Loose;
51
303
 
304
+ /**
305
+ * A constructor for creating
306
+ * {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetchrequest-1 `FetchRequest`}, closely compatible with
307
+ * the native {@link https://developer.mozilla.org/docs/Web/API/Request Request} constructor.
308
+ *
309
+ * @example
310
+ * import { type HttpSchema } from '@zimic/http';
311
+ * import { createFetch } from '@zimic/fetch';
312
+ *
313
+ * interface User {
314
+ * id: string;
315
+ * username: string;
316
+ * }
317
+ *
318
+ * type Schema = HttpSchema<{
319
+ * '/users': {
320
+ * POST: {
321
+ * request: {
322
+ * headers: { 'content-type': 'application/json' };
323
+ * body: { username: string };
324
+ * };
325
+ * response: {
326
+ * 201: { body: User };
327
+ * };
328
+ * };
329
+ * };
330
+ * }>;
331
+ *
332
+ * const fetch = createFetch<Schema>({
333
+ * baseURL: 'http://localhost:3000',
334
+ * });
335
+ *
336
+ * const request = new fetch.Request('/users', {
337
+ * method: 'POST',
338
+ * headers: { 'content-type': 'application/json' },
339
+ * body: JSON.stringify({ username: 'me' }),
340
+ * });
341
+ *
342
+ * console.log(request); // FetchRequest<Schema, 'POST', '/users'>
343
+ * console.log(request.path); // '/users'
344
+ *
345
+ * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetchrequest-1 `FetchRequest`}
346
+ */
52
347
  Request: FetchRequestConstructor<Schema>;
53
348
 
349
+ /**
350
+ * A type guard that checks if a request is a {@link FetchRequest}, was created by the fetch instance, and has a
351
+ * specific method and path. This is useful to narrow down the type of a request before using it.
352
+ *
353
+ * @example
354
+ * import { type HttpSchema } from '@zimic/http';
355
+ * import { createFetch } from '@zimic/fetch';
356
+ *
357
+ * interface User {
358
+ * id: string;
359
+ * username: string;
360
+ * }
361
+ *
362
+ * type Schema = HttpSchema<{
363
+ * '/users': {
364
+ * POST: {
365
+ * request: {
366
+ * headers: { 'content-type': 'application/json' };
367
+ * body: { username: string };
368
+ * };
369
+ * response: {
370
+ * 201: { body: User };
371
+ * };
372
+ * };
373
+ * };
374
+ * }>;
375
+ *
376
+ * const fetch = createFetch<Schema>({
377
+ * baseURL: 'http://localhost:3000',
378
+ * });
379
+ *
380
+ * const request = new fetch.Request('/users', {
381
+ * method: 'POST',
382
+ * headers: { 'content-type': 'application/json' },
383
+ * body: JSON.stringify({ username: 'me' }),
384
+ * });
385
+ *
386
+ * if (fetch.isRequest(request, 'POST', '/users')) {
387
+ * // request is a FetchRequest<Schema, 'POST', '/users'>
388
+ *
389
+ * const contentType = request.headers.get('content-type'); // 'application/json'
390
+ * const body = await request.json(); // { username: string }
391
+ * }
392
+ *
393
+ * @param request The request to check.
394
+ * @param method The method to check.
395
+ * @param path The path to check.
396
+ * @returns `true` if the request was created by the fetch instance and has the specified method and path; `false`
397
+ * otherwise.
398
+ */
54
399
  isRequest: <Method extends HttpSchemaMethod<Schema>, Path extends HttpSchemaPath.Literal<Schema, Method>>(
55
400
  request: unknown,
56
401
  method: Method,
57
402
  path: Path,
58
403
  ) => request is FetchRequest<Schema, Method, Path>;
59
404
 
405
+ /**
406
+ * A type guard that checks if a response is a {@link FetchResponse}, was received by the fetch instance, and has a
407
+ * specific method and path. This is useful to narrow down the type of a response before using it.
408
+ *
409
+ * @example
410
+ * import { type HttpSchema } from '@zimic/http';
411
+ * import { createFetch } from '@zimic/fetch';
412
+ *
413
+ * interface User {
414
+ * id: string;
415
+ * username: string;
416
+ * }
417
+ *
418
+ * type Schema = HttpSchema<{
419
+ * '/users': {
420
+ * GET: {
421
+ * request: {
422
+ * searchParams: { query?: string };
423
+ * };
424
+ * response: {
425
+ * 200: { body: User[] };
426
+ * };
427
+ * };
428
+ * };
429
+ * }>;
430
+ *
431
+ * const fetch = createFetch<Schema>({
432
+ * baseURL: 'http://localhost:3000',
433
+ * });
434
+ *
435
+ * const response = await fetch('/users', {
436
+ * method: 'GET',
437
+ * searchParams: { query: 'u' },
438
+ * });
439
+ *
440
+ * if (fetch.isResponse(response, 'GET', '/users')) {
441
+ * // response is a FetchResponse<Schema, 'GET', '/users'>
442
+ *
443
+ * const users = await response.json(); // User[]
444
+ * }
445
+ *
446
+ * @param response The response to check.
447
+ * @param method The method to check.
448
+ * @param path The path to check.
449
+ * @returns `true` if the response was received by the fetch instance and has the specified method and path; `false`
450
+ * otherwise.
451
+ */
60
452
  isResponse: <Method extends HttpSchemaMethod<Schema>, Path extends HttpSchemaPath.Literal<Schema, Method>>(
61
453
  response: unknown,
62
454
  method: Method,
63
455
  path: Path,
64
456
  ) => response is FetchResponse<Schema, Method, Path>;
65
457
 
458
+ /**
459
+ * A type guard that checks if an error is a {@link FetchResponseError} related to a {@link FetchResponse response}
460
+ * received by the fetch instance with a specific method and path. This is useful to narrow down the type of an error
461
+ * before handling it.
462
+ *
463
+ * @example
464
+ * import { type HttpSchema } from '@zimic/http';
465
+ * import { createFetch } from '@zimic/fetch';
466
+ *
467
+ * interface User {
468
+ * id: string;
469
+ * username: string;
470
+ * }
471
+ *
472
+ * type Schema = HttpSchema<{
473
+ * '/users': {
474
+ * GET: {
475
+ * request: {
476
+ * searchParams: { query?: string };
477
+ * };
478
+ * response: {
479
+ * 200: { body: User[] };
480
+ * 400: { body: { message: string } };
481
+ * 500: { body: { message: string } };
482
+ * };
483
+ * };
484
+ * };
485
+ * }>;
486
+ *
487
+ * const fetch = createFetch<Schema>({
488
+ * baseURL: 'http://localhost:3000',
489
+ * });
490
+ *
491
+ * try {
492
+ * const response = await fetch('/users', {
493
+ * method: 'GET',
494
+ * searchParams: { query: 'u' },
495
+ * });
496
+ *
497
+ * if (!response.ok) {
498
+ * throw response.error; // FetchResponseError<Schema, 'GET', '/users'>
499
+ * }
500
+ * } catch (error) {
501
+ * if (fetch.isResponseError(error, 'GET', '/users')) {
502
+ * // error is a FetchResponseError<Schema, 'GET', '/users'>
503
+ *
504
+ * const status = error.response.status; // 400 | 500
505
+ * const { message } = await error.response.json(); // { message: string }
506
+ *
507
+ * console.error('Could not fetch users:', { status, message });
508
+ * }
509
+ * }
510
+ *
511
+ * @param error The error to check.
512
+ * @param method The method to check.
513
+ * @param path The path to check.
514
+ * @returns `true` if the error is a response error received by the fetch instance and has the specified method and
515
+ * path; `false` otherwise.
516
+ */
66
517
  isResponseError: <Method extends HttpSchemaMethod<Schema>, Path extends HttpSchemaPath.Literal<Schema, Method>>(
67
518
  error: unknown,
68
519
  method: Method,
@@ -70,6 +521,99 @@ export interface FetchClient<Schema extends HttpSchema> extends Pick<FetchOption
70
521
  ) => error is FetchResponseError<Schema, Method, Path>;
71
522
  }
72
523
 
524
+ /**
525
+ * A fetch instance typed with an HTTP schema, closely compatible with the
526
+ * {@link https://developer.mozilla.org/docs/Web/API/Fetch_API native Fetch API}. All requests and responses are typed by
527
+ * default with the schema, including methods, paths, status codes, parameters, and bodies.
528
+ *
529
+ * Requests sent by the fetch instance have their URL automatically prefixed with the base URL of the instance. Default
530
+ * options are also applied to the requests, if present in the instance.
531
+ *
532
+ * @example
533
+ * import { type HttpSchema } from '@zimic/http';
534
+ * import { createFetch } from '@zimic/fetch';
535
+ *
536
+ * interface User {
537
+ * id: string;
538
+ * username: string;
539
+ * }
540
+ *
541
+ * type Schema = HttpSchema<{
542
+ * '/users': {
543
+ * GET: {
544
+ * request: {
545
+ * searchParams: { query?: string };
546
+ * };
547
+ * response: {
548
+ * 200: { body: User[] };
549
+ * 404: { body: { message: string } };
550
+ * 500: { body: { message: string } };
551
+ * };
552
+ * };
553
+ * };
554
+ * }>;
555
+ *
556
+ * const fetch = createFetch<Schema>({
557
+ * baseURL: 'http://localhost:3000',
558
+ * headers: { 'accept-language': 'en' },
559
+ * });
560
+ *
561
+ * const response = await fetch('/users', {
562
+ * method: 'GET',
563
+ * searchParams: { query: 'u' },
564
+ * });
565
+ *
566
+ * if (response.status === 404) {
567
+ * return null; // User not found
568
+ * }
569
+ *
570
+ * if (!response.ok) {
571
+ * throw response.error;
572
+ * }
573
+ *
574
+ * const users = await response.json();
575
+ * return users; // User[]
576
+ *
577
+ * @param input The resource to fetch, either a path, a URL, or a {@link FetchRequest request}. If a path is provided, it
578
+ * is automatically prefixed with the base URL of the fetch instance when the request is sent. If a URL or a request
579
+ * is provided, it is used as is.
580
+ * @param init The request options. If a path or a URL is provided as the first argument, this argument is required and
581
+ * should contain at least the method of the request. If the first argument is a {@link FetchRequest request}, this
582
+ * argument is optional.
583
+ * @returns A promise that resolves to the response to the request.
584
+ * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetch `fetch` API reference}
585
+ * @see {@link https://developer.mozilla.org/docs/Web/API/Fetch_API}
586
+ * @see {@link https://developer.mozilla.org/docs/Web/API/Request}
587
+ * @see {@link https://developer.mozilla.org/docs/Web/API/RequestInit}
588
+ * @see {@link https://developer.mozilla.org/docs/Web/API/Response}
589
+ */
73
590
  export type Fetch<Schema extends HttpSchema> = FetchFunction<Schema> & FetchClient<Schema>;
74
591
 
592
+ /**
593
+ * Infers the schema of a {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetch fetch instance}.
594
+ *
595
+ * @example
596
+ * import { type HttpSchema } from '@zimic/http';
597
+ * import { createFetch, InferFetchSchema } from '@zimic/fetch';
598
+ *
599
+ * const fetch = createFetch<{
600
+ * '/users': {
601
+ * GET: {
602
+ * response: { 200: { body: User[] } };
603
+ * };
604
+ * };
605
+ * }>({
606
+ * baseURL: 'http://localhost:3000',
607
+ * });
608
+ *
609
+ * type Schema = InferFetchSchema<typeof fetch>;
610
+ * // {
611
+ * // '/users': {
612
+ * // GET: {
613
+ * // response: { 200: { body: User[] } };
614
+ * // };
615
+ * // };
616
+ *
617
+ * @see {@link https://github.com/zimicjs/zimic/wiki/api‐zimic‐fetch#fetch `fetch` API reference}
618
+ */
75
619
  export type InferFetchSchema<FetchInstance> = FetchInstance extends Fetch<infer Schema> ? Schema : never;