aspi 1.3.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,109 +1,227 @@
1
- # aspi
1
+ # Aspi
2
2
 
3
- I made this project because I am not happy with any of the Rest API clients available in eco system. Sure, Axios is great but it feels so bloated and I am never going to use interceptors or any of the other features it provides. I just want to make a simple request and get the response. That's it. So, I made this project. It is a simple Rest API client that is built on top of native fetch API. It is very simple to use and has a very small bundle size. It is perfect for small projects where you don't want to bloat your project with unnecessary features.
3
+ A tiny, type‑safe wrapper around the native **fetch** API that gives you a clean, monadic interface for HTTP requests.
4
+ It ships with **zero runtime dependencies**, a **tiny bundle size**, and full **TypeScript** support out of the box.
4
5
 
5
- ## Why Aspi?
6
+ **Why use Aspi?**
7
+
8
+ - End‑to‑end TypeScript typings (request + response)
9
+ - No extra weight – only a thin wrapper around `fetch`
10
+ - Chain‑of‑responsibility middleware support via `use`
11
+ - Result‑based error handling (values as errors)
12
+ - Built‑in retry, header helpers, query‑string handling, and schema validation (Zod, Arktype, Valibot)
13
+ - Flexible error mapping with `error` and convenience shortcuts
14
+
15
+ ---
16
+
17
+ ## Installation
18
+
19
+ npm
20
+ `bash
21
+ npm install aspi
22
+ `
23
+
24
+ yarn
25
+ `bash
26
+ yarn add aspi
27
+ `
28
+
29
+ pnpm
30
+ `bash
31
+ pnpm add aspi
32
+ `
6
33
 
7
- - 🔷 End to end TypeScript support
8
- - 📦 Very small bundle size
9
- - 🚀 Built on top of native fetch API
10
- - 📦 No dependencies
11
- - ⛓️ Chain of responsibility pattern
12
- - 🧮 Monadic API
13
- - ⚠️ Errors as values with Result type
14
- - 🔍 Errors comes with support for pattern matching
15
- - 🔄 Retry support
16
- - 📜 Schema validation support - Zod, Arktype etc.
34
+ ---
17
35
 
18
- ## Example
36
+ ## Quick start
19
37
 
20
- ```typescript
21
- import { aspi, Result } from 'aspi';
38
+ ```ts
39
+ import { Aspi, Result } from 'aspi';
22
40
 
23
- const apiClient = new Aspi({
41
+ // Create a client with a base URL and default headers
42
+ const api = new Aspi({
24
43
  baseUrl: 'https://api.example.com',
25
44
  headers: {
26
45
  'Content-Type': 'application/json',
27
46
  },
28
47
  });
29
48
 
30
- const getTodos = async (id: number) => {
31
- const [value, error] = await apiClient
49
+ // Simple GET request returns a tuple [value, error]
50
+ async function getTodo(id: number) {
51
+ const [value, error] = await api
32
52
  .get(`/todos/${id}`)
33
- .notFound(() => ({
34
- message: 'Todo not found',
35
- }))
36
- .json<{
37
- id: number;
38
- title: string;
39
- completed: boolean;
40
- }>();
41
-
42
- if (value) {
43
- console.log(value);
44
- }
53
+ .setQueryParams({ include: 'details' }) // optional query string
54
+ .notFound(() => ({ message: 'Todo not found' }))
55
+ .json<{ id: number; title: string; completed: boolean }>();
45
56
 
57
+ if (value) console.log('Todo:', value);
46
58
  if (error) {
47
- if (error.tag === 'aspiError') {
48
- console.error(error.response.status);
49
- } else if (error.tag === 'notFoundError') {
50
- console.log(error.data.message);
51
- }
59
+ if (error.tag === 'aspiError') console.error(error.response.status);
60
+ if (error.tag === 'notFoundError') console.warn(error.data.message);
61
+ if (error.tag === 'jsonParseError') console.error(error.data.message);
52
62
  }
53
- };
63
+ }
54
64
 
55
- getTodos(1);
65
+ getTodo(1);
56
66
  ```
57
67
 
58
- ## With Result type
68
+ ---
69
+
70
+ ## Why Aspi?
71
+
72
+ Most real‑world codebases end up with one or more of these issues:
73
+
74
+ 1. **Inconsistent error handling**
75
+ - Some utilities throw raw `Error`/`AxiosError`.
76
+ - Others return `{ ok: false, error }` or `null` or a custom union.
77
+ - Callers don’t know whether to use `try/catch`, check `ok`, or both.
78
+
79
+ 2. **Retry logic duplicated everywhere**
80
+ - Each service rolls its own `while (attempt <= retries)` loop.
81
+ - Status codes, backoff strategies, and retry limits slowly diverge over time.
82
+ - There is no single place to see “how do we retry HTTP calls in this app?”.
83
+
84
+ 3. **Validation pushed far from the network boundary**
85
+ - Request payloads are sometimes validated, sometimes not.
86
+ - Response validation happens deep in the business logic (if at all).
87
+ - JSON parse errors leak as raw `SyntaxError`, not structured errors.
88
+
89
+ 4. **Configuration scattered across factories and interceptors**
90
+ - Base URL helpers, auth decorators, error mappers, retry plugins, and logging interceptors all live in different files.
91
+ - Global state / interceptors can make it hard to tell what a given request will actually do.
92
+
93
+ 5. **Type systems are bolted on, not designed in**
94
+ - Generic HTTP clients often expose `any` for responses.
95
+ - Error flows are not encoded in the type system, forcing manual guards and casting.
96
+
97
+ ## How Aspi fixes them
98
+
99
+ Aspi’s design centers around three things:
100
+
101
+ 1. **Mode‑driven responses**
102
+
103
+ You decide at call‑site how you want to consume responses:
104
+ - `withResult()` → `json/text/blob` return a `Result.Result<Ok, ErrorUnion>`.
105
+ - `throwable()` → `json/text/blob` return `AspiPlainResponse` and throw on failure.
106
+ - Default → `json/text/blob` return `[ok, err]` tuples.
107
+
108
+ All error variants are **tagged** so they can be safely narrowed by `error.tag`.
109
+
110
+ 2. **Centralized, configurable retry layer**
111
+
112
+ Retry behavior is described declaratively:
113
+ - `retries`: max attempts.
114
+ - `retryDelay`: number or function `(attempt, maxAttempts, request, response) => delayMs`.
115
+ - `retryOn`: list of HTTP status codes that should trigger a retry.
116
+ - `retryWhile`: predicate `(request, response) => boolean` for custom retry conditions.
117
+ - `onRetry`: hook invoked after each retry attempt.
118
+
119
+ This configuration can be applied globally (`Aspi.setRetry`) and overridden per request (`Request.setRetry`).
120
+
121
+ 3. **Validation at the transport boundary**
122
+
123
+ Using a `StandardSchemaV1` interface, Aspi integrates with schema libraries (e.g. Zod, Valibot) to:
124
+ - Validate request bodies with `bodySchema` + `bodyJson` **before** the network call.
125
+ - Validate responses with `schema()` + `json()` **after** JSON parsing.
126
+
127
+ These failures appear as tagged `parseError` values with structured issue lists, not random runtime exceptions.
59
128
 
60
- ```typescript
61
- const getTodos = async (id: number) => {
62
- const [value, error] = await apiClient
129
+ ---
130
+
131
+ ## Using the `Result` monad
132
+
133
+ If you prefer a single `Result` value instead of a tuple, call **`.withResult()`** before a body‑parser method.
134
+
135
+ ```ts
136
+ async function getTodoResult(id: number) {
137
+ const response = await api
63
138
  .get(`/todos/${id}`)
64
- .notFound(() => ({
65
- message: 'Todo not found',
66
- }))
67
- .withResult()
68
- .json<{
69
- id: number;
70
- title: string;
71
- completed: boolean;
72
- }>();
139
+ .notFound(() => ({ message: 'Todo not found' }))
140
+ .withResult() // enable Result mode
141
+ .json<{ id: number; title: string; completed: boolean }>();
73
142
 
74
143
  Result.match(response, {
75
- onOk: (data) => {
76
- console.log(data);
77
- },
78
- onErr: (error) => {
79
- if (error.tag === 'aspiError') {
80
- console.error(error.response.status);
81
- } else if (error.tag === 'notFoundError') {
82
- console.log(error.data.message);
83
- }
144
+ onOk: (data) => console.log('✅', data),
145
+ onErr: (err) => {
146
+ if (err.tag === 'aspiError') console.error(err.response.status);
147
+ if (err.tag === 'notFoundError') console.warn(err.data.message);
84
148
  },
85
149
  });
150
+ }
151
+ ```
152
+
153
+ ---
154
+
155
+ ## Throwable
156
+
157
+ The `throwable()` toggle makes a request **throw** on any non‑2xx HTTP response, allowing you to use the familiar `try / catch` pattern instead of dealing with tuples or `Result` objects.
158
+
159
+ When a request is in _throwable_ mode, the body‑parser methods (`json()`, `text()`, `blob()`) resolve with the parsed value directly. If the response status indicates an error, the promise is rejected with a typed Aspi error (e.g., `aspiError`, `unauthorisedError`, `jsonParseError`, …).
160
+
161
+ #### Basic usage
86
162
 
87
- getTodos(1);
88
- };
163
+ ```ts
164
+ // Using throwable with async/await + try/catch
165
+ try {
166
+ const todo = await api
167
+ .get('/todos/1')
168
+ .throwable() // <─ enable throwable mode
169
+ .json<{ id: number; title: string; completed: boolean }>(); // returns the parsed JSON
170
+
171
+ console.log('✅ Todo:', todo);
172
+ } catch (err) {
173
+ // `err` is a typed Aspi error
174
+ if (err.tag === 'aspiError') {
175
+ console.error('HTTP error:', err.response.status);
176
+ } else if (err.tag === 'jsonParseError') {
177
+ console.error('Invalid JSON:', err.data.message);
178
+ } else {
179
+ console.error('Unexpected error:', err);
180
+ }
181
+ }
182
+ ```
183
+
184
+ #### Interaction with `withResult()`
185
+
186
+ `throwable()` and `withResult()` are _mutually exclusive_ – the last toggle applied wins.
187
+
188
+ ```ts
189
+ // Result mode wins (throwable is ignored)
190
+ const result = await api
191
+ .post('/login')
192
+ .withResult() // enables Result mode
193
+ .throwable() // ignored because withResult was called later
194
+ .json<{ token: string }>();
195
+
196
+ // Throwable mode wins (Result is ignored)
197
+ const data = await api
198
+ .get('/profile')
199
+ .throwable() // enables throwable mode
200
+ .withResult() // ignored because throwable was called later
201
+ .json();
89
202
  ```
90
203
 
91
- ## Example with Schema Validation (with Zod)
204
+ #### When to use `throwable()`
92
205
 
93
- ```typescript
94
- import { aspi, Result } from 'aspi';
95
- import { z, ZodError } from 'zod';
206
+ - You prefer native `try / catch` flow over tuple/result handling.
207
+ - You want the request to **reject** automatically on HTTP errors, keeping the success path clean.
208
+ - You are integrating Aspi into existing codebases that already rely on exception handling.
96
209
 
97
- // JSON Placeholder API Client
98
- const apiClient = new Aspi({
210
+ `throwable()` gives you the flexibility to choose the error‑handling style that best fits your project.
211
+
212
+ ## Schema validation (Zod example)
213
+
214
+ ```ts
215
+ import { z } from 'zod';
216
+ import { Aspi, Result } from 'aspi';
217
+
218
+ const api = new Aspi({
99
219
  baseUrl: 'https://jsonplaceholder.typicode.com',
100
- headers: {
101
- 'Content-Type': 'application/json',
102
- },
220
+ headers: { 'Content-Type': 'application/json' },
103
221
  });
104
222
 
105
- const getTodo = async (id: number) => {
106
- const response = await apiClient
223
+ async function getValidatedTodo(id: number) {
224
+ const response = await api
107
225
  .get(`/todos/${id}`)
108
226
  .withResult()
109
227
  .schema(
@@ -113,175 +231,312 @@ const getTodo = async (id: number) => {
113
231
  completed: z.boolean(),
114
232
  }),
115
233
  )
116
- .json();
234
+ .json(); // type inferred from the schema
117
235
 
118
236
  Result.match(response, {
119
- onOk: (data) => {
120
- console.log(data);
121
- },
237
+ onOk: (data) => console.log('Todo ✅', data),
122
238
  onErr: (err) => {
123
239
  if (err.tag === 'parseError') {
124
- const error = err.data as ZodError;
125
- console.error(error.errors);
240
+ const parseErr = err.data as z.ZodError;
241
+ console.error('Validation failed:', parseErr.errors);
126
242
  } else {
127
- // do something else
243
+ console.error('Other error', err);
128
244
  }
129
245
  },
130
246
  });
131
- };
247
+ }
132
248
  ```
133
249
 
134
- ## Example with retry
250
+ ---
135
251
 
136
- ```typescript
137
- import { aspi, Result } from 'aspi';
252
+ ## Retry & back‑off
138
253
 
139
- const apiClient = new Aspi({
254
+ ```ts
255
+ const api = new Aspi({
140
256
  baseUrl: 'https://example.com',
141
- headers: {
142
- 'Content-Type': 'application/json',
143
- },
257
+ headers: { 'Content-Type': 'application/json' },
144
258
  }).setRetry({
145
259
  retries: 3,
146
- retryDelay: 1000,
147
- // retry on 404 error
148
- retryOn: [404],
260
+ retryDelay: 1000, // simple fixed delay
261
+ retryOn: [404, 500], // retry on specific status codes
149
262
  });
150
263
 
151
- // the given GET endpoint does not exist
152
- apiClient
264
+ // Override retry options for a single request
265
+ api
153
266
  .get('/todos/1')
154
- .setHeader('Content-Type', 'application/json')
155
- // Updating retry options for this request
267
+ .setHeader('Accept', 'application/json')
156
268
  .setRetry({
157
- // Exponential backoff
158
- retryDelay: (attempts) => Math.pow(2, attempts) * 1000,
269
+ // exponential back‑off for this call only
270
+ retryDelay: (attempt) => Math.pow(2, attempt) * 1000,
159
271
  })
160
272
  .withResult()
161
273
  .json()
162
- .then((response) => {
163
- Result.match(response, {
164
- onOk: (data) => {
165
- console.log(data);
166
- },
167
- onErr: (error) => {
168
- if (error.tag === 'aspiError') {
169
- console.error(error.response);
170
- } else if (error.tag === 'notFoundError') {
171
- console.log(error.data.message);
172
- }
173
- },
174
- });
175
- });
274
+ .then((res) =>
275
+ Result.match(res, {
276
+ onOk: (data) => console.log('Got data', data),
277
+ onErr: (err) => console.error('Failed', err),
278
+ }),
279
+ );
176
280
  ```
177
281
 
178
- ### Installation
179
-
180
- ```bash
181
- npm install aspi
282
+ ---
283
+
284
+ ## Global configuration helpers
285
+
286
+ | Method | Description |
287
+ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
288
+ | `setBaseUrl(url)` | Change the base URL for all subsequent requests. |
289
+ | `setHeaders(headers)` | Merge an object of headers with any existing ones. |
290
+ | `setHeader(key, value)` | Set a single header. |
291
+ | `setBearer(token)` | Shortcut for `Authorization: Bearer <token>`. |
292
+ | `setRetry(retryConfig)` | Define a global retry strategy (overridable per request). |
293
+ | `setQueryParams(params)` | Replace the request’s query string – accepts object, `URLSearchParams`, array of tuples, or raw string. |
294
+ | `schema(schema)` | Attach a `StandardSchemaV1` validator for the response body. |
295
+ | `use(fn)` | Register a request‑transformer middleware that receives the current `RequestInit` and returns a new one. Returns a new `Aspi` instance typed with the transformed config. |
296
+ | `withResult()` | Switch the request into Result mode (returns a `Result` instead of a tuple). |
297
+ | `throwable()` | Make the request throw on non‑2xx responses (useful for `try / catch` patterns). |
298
+ | `url()` | Get the fully‑qualified URL that will be used for the request. |
299
+
300
+ ---
301
+
302
+ ## Custom error handling
303
+
304
+ Aspi lets you map **any HTTP status** to a typed error object that can be pattern‑matched later.
305
+
306
+ ```ts
307
+ api
308
+ .error('badRequestError', 'BAD_REQUEST', (req, res) => ({
309
+ message: 'The request payload is invalid',
310
+ payload: res.body,
311
+ }))
312
+ .error('unauthorisedError', 'UNAUTHORIZED', () => ({
313
+ message: 'You must log in first',
314
+ }));
182
315
  ```
183
316
 
184
- ### Features
185
-
186
- #### Result type
187
-
188
- - `Result` type is a union type of `Ok` and `Err` type.
189
- - When you call a method that returns a `Result` type, you can use methods on `Result` to handle the result.
190
- - When the api succeeds, It will yield an `Ok` type with the data.
191
- - When the api fails, It will yield an `Err` type with the error.
192
-
193
- When succeded with OK, the data comes in the `AspiSuccessOk` type, where additional information about the request and response is also provided.
194
-
195
- #### Error handling
196
-
197
- - The error handling is done using the `Result` type, which is a union type of `Ok` and `Err` type.
198
- - When called `json` method on the response, it will return either the AspiSuccessOk with the data or AspiError with the error as well as JSON parsing error.
199
- - Additionally, user can define custom errors to handle specific http status codes, those errors can be pattern matched using any pattern matching library.
200
-
201
- #### API Descriptions
202
-
203
- ##### WithResult
204
-
205
- By default, the response is not wrapped in the Result type. It will be a tuple of the value and error. both can be null but only one will be non-null at a time. If you want the response to be wrapped in the Result type, you can call `withResult` method on the response.
206
-
207
- ```typescript
208
- const response = await new Aspi({ baseUrl: '...' })
209
- .get('...')
210
- .json<{ data: any }>();
211
-
212
- // [AspiResultOk<AspiRequestInit, { data: any; }> | null, JSONParseError | AspiError<AspiRequestInit> | null]
317
+ Convenient shortcuts are provided for the most common statuses (each forwards to `error` internally and augments the generic `Opts['error']` type):
318
+
319
+ ```ts
320
+ api.notFound(cb); // 404
321
+ api.tooManyRequests(cb); // 429
322
+ api.conflict(cb); // 409
323
+ api.badRequest(cb); // 400
324
+ api.unauthorised(cb); // 401 (British spelling, matches the Request API)
325
+ api.forbidden(cb); // 403
326
+ api.notImplemented(cb); // 501
327
+ api.internalServerError(cb); // 500
213
328
  ```
214
329
 
215
- The above response is a tuple of the value and error. The value itself is wrapped in the AspiResultOk type. It contains the request and response information as well as the data. If you want the response to be wrapped in the Result type, you can call `withResult` method on the response.
330
+ These helpers allow you to write:
216
331
 
217
- ```typescript
218
- const response = await new Aspi({ baseUrl: '...' })
219
- .get('...')
332
+ ```ts
333
+ api
334
+ .get('/secret')
335
+ .unauthorised(() => ({ message: 'You need a token' }))
220
336
  .withResult()
221
- .json<{ data: any }>();
222
-
223
- // Result<AspiResultOk<AspiRequestInit, { data: any; }>, JSONParseError | AspiError<AspiRequestInit>>
337
+ .json()
338
+ .then((res) =>
339
+ Result.match(res, {
340
+ onOk: (data) => console.log(data),
341
+ onErr: (err) => {
342
+ if (err.tag === 'unauthorisedError') {
343
+ console.warn(err.data.message);
344
+ }
345
+ },
346
+ }),
347
+ );
224
348
  ```
225
349
 
226
- The above response is a Result type. It can be pattern matched using any pattern matching library. We also pack one custom Result implementation that can be used to pattern match the response.
227
-
228
- ```typescript
229
- // handling all the errors
230
- const resultWithoutError = Result.pipe(
231
- response,
232
- Result.map((data) => data.data),
233
- Result.catchError('aspiError', () => {
234
- console.log('aspi error');
235
- }),
236
- Result.catchError('jsonParseError', () =>
237
- console.log('failed to parse json error'),
238
- ),
239
- );
240
-
241
- // Result<AspiResultOk<AspiRequestInit, { data: any; }>, never>
350
+ ---
351
+
352
+ ## API reference (selected)
353
+
354
+ ```ts
355
+ class Request<
356
+ Method extends HttpMethods,
357
+ TRequest extends AspiRequestInitWithBody = AspiRequestInit,
358
+ Opts extends Record<any, any> = { error: {} },
359
+ > {
360
+ // core request factories
361
+ get(path: string): Request<'GET', TRequest, Opts>;
362
+ post(path: string): Request<'POST', TRequest, Opts>;
363
+ put(path: string): Request<'PUT', TRequest, Opts>;
364
+ patch(path: string): Request<'PATCH', TRequest, Opts>;
365
+ delete(path: string): Request<'DELETE', TRequest, Opts>;
366
+ head(path: string): Request<'HEAD', TRequest, Opts>;
367
+ options(path: string): Request<'OPTIONS', TRequest, Opts>;
368
+
369
+ // configuration
370
+ setBaseUrl(url: BaseURL): this;
371
+ setHeaders(headers: HeadersInit): this;
372
+ setHeader(key: string, value: string): this;
373
+ setBearer(token: string): this;
374
+ setRetry(cfg: AspiRetryConfig<TRequest>): this;
375
+ setQueryParams(
376
+ params: Record<string, string> | string[][] | string | URLSearchParams,
377
+ ): this;
378
+ use<T extends TRequest, U extends TRequest>(
379
+ fn: RequestTransformer<T, U>,
380
+ ): Request<U>;
381
+
382
+ // schema validation
383
+ schema<TSchema extends StandardSchemaV1>(
384
+ schema: TSchema,
385
+ ): Request<
386
+ Method,
387
+ TRequest,
388
+ Merge<
389
+ Omit<Opts, 'schema'>,
390
+ {
391
+ schema: TSchema;
392
+ error: Merge<
393
+ Opts['error'],
394
+ {
395
+ parseError: CustomError<
396
+ 'parseError',
397
+ StandardSchemaV1.FailureResult['issues']
398
+ >;
399
+ }
400
+ >;
401
+ }
402
+ >
403
+ >;
404
+
405
+ // result / throwable toggles
406
+ withResult(): Request<
407
+ Method,
408
+ TRequest,
409
+ Merge<
410
+ Omit<Opts, 'withResult' | 'throwable'>,
411
+ {
412
+ withResult: true;
413
+ throwable: false;
414
+ }
415
+ >
416
+ >;
417
+ throwable(): Request<
418
+ Method,
419
+ TRequest,
420
+ Merge<
421
+ Omit<Opts, 'withResult' | 'throwable'>,
422
+ {
423
+ withResult: false;
424
+ throwable: true;
425
+ }
426
+ >
427
+ >;
428
+
429
+ // custom error handling
430
+ error<Tag extends string, A extends {}>(
431
+ tag: Tag,
432
+ status: HttpErrorStatus,
433
+ cb: CustomErrorCb<TRequest, A>,
434
+ ): Request<
435
+ Method,
436
+ TRequest,
437
+ Merge<
438
+ Omit<Opts, 'error'>,
439
+ {
440
+ error: {
441
+ [K in Tag | keyof Opts['error']]: K extends Tag
442
+ ? CustomError<Tag, A>
443
+ : Opts['error'][K];
444
+ };
445
+ }
446
+ >
447
+ >;
448
+ notFound<A>(cb: CustomErrorCb<TRequest, A>): this;
449
+ tooManyRequests<A>(cb: CustomErrorCb<TRequest, A>): this;
450
+ conflict<A>(cb: CustomErrorCb<TRequest, A>): this;
451
+ badRequest<A>(cb: CustomErrorCb<TRequest, A>): this;
452
+ unauthorised<A>(cb: CustomErrorCb<TRequest, A>): this;
453
+ forbidden<A>(cb: CustomErrorCb<TRequest, A>): this;
454
+ notImplemented<A>(cb: CustomErrorCb<TRequest, A>): this;
455
+ internalServerError<A>(cb: CustomErrorCb<TRequest, A>): this;
456
+
457
+ // helpers
458
+ url(): string;
459
+
460
+ // response parsers
461
+ json<T extends StandardSchemaV1.InferOutput<Opts['schema']>>(): Promise<
462
+ Opts['withResult'] extends true
463
+ ? Result.Result<
464
+ AspiResultOk<TRequest, T>,
465
+ | AspiError<TRequest>
466
+ | (Opts extends { error: any }
467
+ ? Opts['error'][keyof Opts['error']]
468
+ : never)
469
+ | JSONParseError
470
+ >
471
+ : Opts['throwable'] extends true
472
+ ? AspiPlainResponse<TRequest, T>
473
+ : [
474
+ AspiResultOk<TRequest, T> | null,
475
+ (
476
+ | (
477
+ | AspiError<TRequest>
478
+ | (Opts extends { error: any }
479
+ ? Opts['error'][keyof Opts['error']]
480
+ : never)
481
+ | JSONParseError
482
+ )
483
+ | null
484
+ ),
485
+ ]
486
+ >;
487
+ text(): Promise<
488
+ Opts['withResult'] extends true
489
+ ? Result.Result<
490
+ AspiResultOk<TRequest, string>,
491
+ | AspiError<TRequest>
492
+ | (Opts extends { error: any }
493
+ ? Opts['error'][keyof Opts['error']]
494
+ : never)
495
+ >
496
+ : Opts['throwable'] extends true
497
+ ? AspiPlainResponse<TRequest, string>
498
+ : [
499
+ AspiResultOk<TRequest, string> | null,
500
+ (
501
+ | (
502
+ | AspiError<TRequest>
503
+ | (Opts extends { error: any }
504
+ ? Opts['error'][keyof Opts['error']]
505
+ : never)
506
+ )
507
+ | null
508
+ ),
509
+ ]
510
+ >;
511
+ blob(): Promise<
512
+ Opts['withResult'] extends true
513
+ ? Result.Result<
514
+ AspiResultOk<TRequest, Blob>,
515
+ | AspiError<TRequest>
516
+ | (Opts extends { error: any }
517
+ ? Opts['error'][keyof Opts['error']]
518
+ : never)
519
+ >
520
+ : Opts['throwable'] extends true
521
+ ? AspiPlainResponse<TRequest, Blob>
522
+ : [
523
+ AspiResultOk<TRequest, Blob> | null,
524
+ (
525
+ | (
526
+ | AspiError<TRequest>
527
+ | (Opts extends { error: any }
528
+ ? Opts['error'][keyof Opts['error']]
529
+ : never)
530
+ )
531
+ | null
532
+ ),
533
+ ]
534
+ >;
535
+ }
242
536
  ```
243
537
 
244
- ##### Schema Validation
538
+ ---
245
539
 
246
- Aspi by default implements schema validation using StandardSchemaV1. It means, as of now, it only supports Zod, Arktype and Valibot. If you want to use schema validation, you can call the `schema` method on the response.
540
+ ## License
247
541
 
248
- ```typescript
249
- import { aspi, Result } from 'aspi';
250
- import { z, ZodError } from 'zod';
251
-
252
- // JSON Placeholder API Client
253
- const apiClient = new Aspi({
254
- baseUrl: 'https://jsonplaceholder.typicode.com',
255
- headers: {
256
- 'Content-Type': 'application/json',
257
- },
258
- });
259
-
260
- const getTodo = async (id: number) => {
261
- const response = await apiClient
262
- .get(`/todos/${id}`)
263
- .withResult()
264
- .schema(
265
- z.object({
266
- id: z.number(),
267
- title: z.string(),
268
- completed: z.boolean(),
269
- }),
270
- )
271
- .json();
272
-
273
- Result.match(response, {
274
- onOk: (data) => {
275
- console.log(data);
276
- },
277
- onErr: (err) => {
278
- if (err.tag === 'parseError') {
279
- const error = err.data as ZodError;
280
- console.error(error.errors);
281
- } else {
282
- // do something else
283
- }
284
- },
285
- });
286
- };
287
- ```
542
+ MIT © Aspi contributors