aspi 1.3.0 → 2.0.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
@@ -1,109 +1,165 @@
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
+ > • End‑to‑end TypeScript typings (request + response)
8
+ > • No extra weight – only a thin wrapper around `fetch`
9
+ > • Chain‑of‑responsibility middleware support via `use`
10
+ > • Result‑based error handling (values as errors)
11
+ > • Built‑in retry, header helpers, query‑string handling, and schema validation (Zod, Arktype, Valibot)
12
+ > • Flexible error mapping with `error` and convenience shortcuts
6
13
 
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.
14
+ ---
17
15
 
18
- ## Example
16
+ ## Installation
19
17
 
20
- ```typescript
21
- import { aspi, Result } from 'aspi';
18
+ npm
19
+ `bash
20
+ npm install aspi
21
+ `
22
22
 
23
- const apiClient = new Aspi({
23
+ yarn
24
+ `bash
25
+ yarn add aspi
26
+ `
27
+
28
+ pnpm
29
+ `bash
30
+ pnpm add aspi
31
+ `
32
+
33
+ ---
34
+
35
+ ## Quick start
36
+
37
+ ```ts
38
+ import { Aspi, Result } from 'aspi';
39
+
40
+ // Create a client with a base URL and default headers
41
+ const api = new Aspi({
24
42
  baseUrl: 'https://api.example.com',
25
43
  headers: {
26
44
  'Content-Type': 'application/json',
27
45
  },
28
46
  });
29
47
 
30
- const getTodos = async (id: number) => {
31
- const [value, error] = await apiClient
48
+ // Simple GET request returns a tuple [value, error]
49
+ async function getTodo(id: number) {
50
+ const [value, error] = await api
32
51
  .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
- }
52
+ .setQueryParams({ include: 'details' }) // optional query string
53
+ .notFound(() => ({ message: 'Todo not found' }))
54
+ .json<{ id: number; title: string; completed: boolean }>();
45
55
 
56
+ if (value) console.log('Todo:', value);
46
57
  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
- }
58
+ if (error.tag === 'aspiError') console.error(error.response.status);
59
+ if (error.tag === 'notFoundError') console.warn(error.data.message);
60
+ if (error.tag === 'jsonParseError') console.error(error.data.message);
52
61
  }
53
- };
62
+ }
54
63
 
55
- getTodos(1);
64
+ getTodo(1);
56
65
  ```
57
66
 
58
- ## With Result type
67
+ ---
59
68
 
60
- ```typescript
61
- const getTodos = async (id: number) => {
62
- const [value, error] = await apiClient
69
+ ## Using the `Result` monad
70
+
71
+ If you prefer a single `Result` value instead of a tuple, call **`.withResult()`** before a body‑parser method.
72
+
73
+ ```ts
74
+ async function getTodoResult(id: number) {
75
+ const response = await api
63
76
  .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
- }>();
77
+ .notFound(() => ({ message: 'Todo not found' }))
78
+ .withResult() // enable Result mode
79
+ .json<{ id: number; title: string; completed: boolean }>();
73
80
 
74
81
  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
- }
82
+ onOk: (data) => console.log('✅', data),
83
+ onErr: (err) => {
84
+ if (err.tag === 'aspiError') console.error(err.response.status);
85
+ if (err.tag === 'notFoundError') console.warn(err.data.message);
84
86
  },
85
87
  });
88
+ }
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Throwable
94
+
95
+ 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.
96
+
97
+ 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`, …).
98
+
99
+ #### Basic usage
100
+
101
+ ```ts
102
+ // Using throwable with async/await + try/catch
103
+ try {
104
+ const todo = await api
105
+ .get('/todos/1')
106
+ .throwable() // <─ enable throwable mode
107
+ .json<{ id: number; title: string; completed: boolean }>(); // returns the parsed JSON
108
+
109
+ console.log('✅ Todo:', todo);
110
+ } catch (err) {
111
+ // `err` is a typed Aspi error
112
+ if (err.tag === 'aspiError') {
113
+ console.error('HTTP error:', err.response.status);
114
+ } else if (err.tag === 'jsonParseError') {
115
+ console.error('Invalid JSON:', err.data.message);
116
+ } else {
117
+ console.error('Unexpected error:', err);
118
+ }
119
+ }
120
+ ```
121
+
122
+ #### Interaction with `withResult()`
123
+
124
+ `throwable()` and `withResult()` are _mutually exclusive_ – the last toggle applied wins.
86
125
 
87
- getTodos(1);
88
- };
126
+ ```ts
127
+ // Result mode wins (throwable is ignored)
128
+ const result = await api
129
+ .post('/login')
130
+ .withResult() // enables Result mode
131
+ .throwable() // ignored because withResult was called later
132
+ .json<{ token: string }>();
133
+
134
+ // Throwable mode wins (Result is ignored)
135
+ const data = await api
136
+ .get('/profile')
137
+ .throwable() // enables throwable mode
138
+ .withResult() // ignored because throwable was called later
139
+ .json();
89
140
  ```
90
141
 
91
- ## Example with Schema Validation (with Zod)
142
+ #### When to use `throwable()`
143
+
144
+ - You prefer native `try / catch` flow over tuple/result handling.
145
+ - You want the request to **reject** automatically on HTTP errors, keeping the success path clean.
146
+ - You are integrating Aspi into existing codebases that already rely on exception handling.
147
+
148
+ `throwable()` gives you the flexibility to choose the error‑handling style that best fits your project.
92
149
 
93
- ```typescript
94
- import { aspi, Result } from 'aspi';
95
- import { z, ZodError } from 'zod';
150
+ ## Schema validation (Zod example)
96
151
 
97
- // JSON Placeholder API Client
98
- const apiClient = new Aspi({
152
+ ```ts
153
+ import { z } from 'zod';
154
+ import { Aspi, Result } from 'aspi';
155
+
156
+ const api = new Aspi({
99
157
  baseUrl: 'https://jsonplaceholder.typicode.com',
100
- headers: {
101
- 'Content-Type': 'application/json',
102
- },
158
+ headers: { 'Content-Type': 'application/json' },
103
159
  });
104
160
 
105
- const getTodo = async (id: number) => {
106
- const response = await apiClient
161
+ async function getValidatedTodo(id: number) {
162
+ const response = await api
107
163
  .get(`/todos/${id}`)
108
164
  .withResult()
109
165
  .schema(
@@ -113,175 +169,312 @@ const getTodo = async (id: number) => {
113
169
  completed: z.boolean(),
114
170
  }),
115
171
  )
116
- .json();
172
+ .json(); // type inferred from the schema
117
173
 
118
174
  Result.match(response, {
119
- onOk: (data) => {
120
- console.log(data);
121
- },
175
+ onOk: (data) => console.log('Todo ✅', data),
122
176
  onErr: (err) => {
123
177
  if (err.tag === 'parseError') {
124
- const error = err.data as ZodError;
125
- console.error(error.errors);
178
+ const parseErr = err.data as z.ZodError;
179
+ console.error('Validation failed:', parseErr.errors);
126
180
  } else {
127
- // do something else
181
+ console.error('Other error', err);
128
182
  }
129
183
  },
130
184
  });
131
- };
185
+ }
132
186
  ```
133
187
 
134
- ## Example with retry
188
+ ---
135
189
 
136
- ```typescript
137
- import { aspi, Result } from 'aspi';
190
+ ## Retry & back‑off
138
191
 
139
- const apiClient = new Aspi({
192
+ ```ts
193
+ const api = new Aspi({
140
194
  baseUrl: 'https://example.com',
141
- headers: {
142
- 'Content-Type': 'application/json',
143
- },
195
+ headers: { 'Content-Type': 'application/json' },
144
196
  }).setRetry({
145
197
  retries: 3,
146
- retryDelay: 1000,
147
- // retry on 404 error
148
- retryOn: [404],
198
+ retryDelay: 1000, // simple fixed delay
199
+ retryOn: [404, 500], // retry on specific status codes
149
200
  });
150
201
 
151
- // the given GET endpoint does not exist
152
- apiClient
202
+ // Override retry options for a single request
203
+ api
153
204
  .get('/todos/1')
154
- .setHeader('Content-Type', 'application/json')
155
- // Updating retry options for this request
205
+ .setHeader('Accept', 'application/json')
156
206
  .setRetry({
157
- // Exponential backoff
158
- retryDelay: (attempts) => Math.pow(2, attempts) * 1000,
207
+ // exponential back‑off for this call only
208
+ retryDelay: (attempt) => Math.pow(2, attempt) * 1000,
159
209
  })
160
210
  .withResult()
161
211
  .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
- });
212
+ .then((res) =>
213
+ Result.match(res, {
214
+ onOk: (data) => console.log('Got data', data),
215
+ onErr: (err) => console.error('Failed', err),
216
+ }),
217
+ );
176
218
  ```
177
219
 
178
- ### Installation
179
-
180
- ```bash
181
- npm install aspi
220
+ ---
221
+
222
+ ## Global configuration helpers
223
+
224
+ | Method | Description |
225
+ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
226
+ | `setBaseUrl(url)` | Change the base URL for all subsequent requests. |
227
+ | `setHeaders(headers)` | Merge an object of headers with any existing ones. |
228
+ | `setHeader(key, value)` | Set a single header. |
229
+ | `setBearer(token)` | Shortcut for `Authorization: Bearer <token>`. |
230
+ | `setRetry(retryConfig)` | Define a global retry strategy (overridable per request). |
231
+ | `setQueryParams(params)` | Replace the request’s query string – accepts object, `URLSearchParams`, array of tuples, or raw string. |
232
+ | `schema(schema)` | Attach a `StandardSchemaV1` validator for the response body. |
233
+ | `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. |
234
+ | `withResult()` | Switch the request into Result mode (returns a `Result` instead of a tuple). |
235
+ | `throwable()` | Make the request throw on non‑2xx responses (useful for `try / catch` patterns). |
236
+ | `url()` | Get the fully‑qualified URL that will be used for the request. |
237
+
238
+ ---
239
+
240
+ ## Custom error handling
241
+
242
+ Aspi lets you map **any HTTP status** to a typed error object that can be pattern‑matched later.
243
+
244
+ ```ts
245
+ api
246
+ .error('badRequestError', 'BAD_REQUEST', (req, res) => ({
247
+ message: 'The request payload is invalid',
248
+ payload: res.body,
249
+ }))
250
+ .error('unauthorisedError', 'UNAUTHORIZED', () => ({
251
+ message: 'You must log in first',
252
+ }));
182
253
  ```
183
254
 
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]
255
+ Convenient shortcuts are provided for the most common statuses (each forwards to `error` internally and augments the generic `Opts['error']` type):
256
+
257
+ ```ts
258
+ api.notFound(cb); // 404
259
+ api.tooManyRequests(cb); // 429
260
+ api.conflict(cb); // 409
261
+ api.badRequest(cb); // 400
262
+ api.unauthorised(cb); // 401 (British spelling, matches the Request API)
263
+ api.forbidden(cb); // 403
264
+ api.notImplemented(cb); // 501
265
+ api.internalServerError(cb); // 500
213
266
  ```
214
267
 
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.
268
+ These helpers allow you to write:
216
269
 
217
- ```typescript
218
- const response = await new Aspi({ baseUrl: '...' })
219
- .get('...')
270
+ ```ts
271
+ api
272
+ .get('/secret')
273
+ .unauthorised(() => ({ message: 'You need a token' }))
220
274
  .withResult()
221
- .json<{ data: any }>();
222
-
223
- // Result<AspiResultOk<AspiRequestInit, { data: any; }>, JSONParseError | AspiError<AspiRequestInit>>
275
+ .json()
276
+ .then((res) =>
277
+ Result.match(res, {
278
+ onOk: (data) => console.log(data),
279
+ onErr: (err) => {
280
+ if (err.tag === 'unauthorisedError') {
281
+ console.warn(err.data.message);
282
+ }
283
+ },
284
+ }),
285
+ );
224
286
  ```
225
287
 
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>
288
+ ---
289
+
290
+ ## API reference (selected)
291
+
292
+ ```ts
293
+ class Request<
294
+ Method extends HttpMethods,
295
+ TRequest extends AspiRequestInitWithBody = AspiRequestInit,
296
+ Opts extends Record<any, any> = { error: {} },
297
+ > {
298
+ // core request factories
299
+ get(path: string): Request<'GET', TRequest, Opts>;
300
+ post(path: string): Request<'POST', TRequest, Opts>;
301
+ put(path: string): Request<'PUT', TRequest, Opts>;
302
+ patch(path: string): Request<'PATCH', TRequest, Opts>;
303
+ delete(path: string): Request<'DELETE', TRequest, Opts>;
304
+ head(path: string): Request<'HEAD', TRequest, Opts>;
305
+ options(path: string): Request<'OPTIONS', TRequest, Opts>;
306
+
307
+ // configuration
308
+ setBaseUrl(url: BaseURL): this;
309
+ setHeaders(headers: HeadersInit): this;
310
+ setHeader(key: string, value: string): this;
311
+ setBearer(token: string): this;
312
+ setRetry(cfg: AspiRetryConfig<TRequest>): this;
313
+ setQueryParams(
314
+ params: Record<string, string> | string[][] | string | URLSearchParams,
315
+ ): this;
316
+ use<T extends TRequest, U extends TRequest>(
317
+ fn: RequestTransformer<T, U>,
318
+ ): Request<U>;
319
+
320
+ // schema validation
321
+ schema<TSchema extends StandardSchemaV1>(
322
+ schema: TSchema,
323
+ ): Request<
324
+ Method,
325
+ TRequest,
326
+ Merge<
327
+ Omit<Opts, 'schema'>,
328
+ {
329
+ schema: TSchema;
330
+ error: Merge<
331
+ Opts['error'],
332
+ {
333
+ parseError: CustomError<
334
+ 'parseError',
335
+ StandardSchemaV1.FailureResult['issues']
336
+ >;
337
+ }
338
+ >;
339
+ }
340
+ >
341
+ >;
342
+
343
+ // result / throwable toggles
344
+ withResult(): Request<
345
+ Method,
346
+ TRequest,
347
+ Merge<
348
+ Omit<Opts, 'withResult' | 'throwable'>,
349
+ {
350
+ withResult: true;
351
+ throwable: false;
352
+ }
353
+ >
354
+ >;
355
+ throwable(): Request<
356
+ Method,
357
+ TRequest,
358
+ Merge<
359
+ Omit<Opts, 'withResult' | 'throwable'>,
360
+ {
361
+ withResult: false;
362
+ throwable: true;
363
+ }
364
+ >
365
+ >;
366
+
367
+ // custom error handling
368
+ error<Tag extends string, A extends {}>(
369
+ tag: Tag,
370
+ status: HttpErrorStatus,
371
+ cb: CustomErrorCb<TRequest, A>,
372
+ ): Request<
373
+ Method,
374
+ TRequest,
375
+ Merge<
376
+ Omit<Opts, 'error'>,
377
+ {
378
+ error: {
379
+ [K in Tag | keyof Opts['error']]: K extends Tag
380
+ ? CustomError<Tag, A>
381
+ : Opts['error'][K];
382
+ };
383
+ }
384
+ >
385
+ >;
386
+ notFound<A>(cb: CustomErrorCb<TRequest, A>): this;
387
+ tooManyRequests<A>(cb: CustomErrorCb<TRequest, A>): this;
388
+ conflict<A>(cb: CustomErrorCb<TRequest, A>): this;
389
+ badRequest<A>(cb: CustomErrorCb<TRequest, A>): this;
390
+ unauthorised<A>(cb: CustomErrorCb<TRequest, A>): this;
391
+ forbidden<A>(cb: CustomErrorCb<TRequest, A>): this;
392
+ notImplemented<A>(cb: CustomErrorCb<TRequest, A>): this;
393
+ internalServerError<A>(cb: CustomErrorCb<TRequest, A>): this;
394
+
395
+ // helpers
396
+ url(): string;
397
+
398
+ // response parsers
399
+ json<T extends StandardSchemaV1.InferOutput<Opts['schema']>>(): Promise<
400
+ Opts['withResult'] extends true
401
+ ? Result.Result<
402
+ AspiResultOk<TRequest, T>,
403
+ | AspiError<TRequest>
404
+ | (Opts extends { error: any }
405
+ ? Opts['error'][keyof Opts['error']]
406
+ : never)
407
+ | JSONParseError
408
+ >
409
+ : Opts['throwable'] extends true
410
+ ? AspiPlainResponse<TRequest, T>
411
+ : [
412
+ AspiResultOk<TRequest, T> | null,
413
+ (
414
+ | (
415
+ | AspiError<TRequest>
416
+ | (Opts extends { error: any }
417
+ ? Opts['error'][keyof Opts['error']]
418
+ : never)
419
+ | JSONParseError
420
+ )
421
+ | null
422
+ ),
423
+ ]
424
+ >;
425
+ text(): Promise<
426
+ Opts['withResult'] extends true
427
+ ? Result.Result<
428
+ AspiResultOk<TRequest, string>,
429
+ | AspiError<TRequest>
430
+ | (Opts extends { error: any }
431
+ ? Opts['error'][keyof Opts['error']]
432
+ : never)
433
+ >
434
+ : Opts['throwable'] extends true
435
+ ? AspiPlainResponse<TRequest, string>
436
+ : [
437
+ AspiResultOk<TRequest, string> | null,
438
+ (
439
+ | (
440
+ | AspiError<TRequest>
441
+ | (Opts extends { error: any }
442
+ ? Opts['error'][keyof Opts['error']]
443
+ : never)
444
+ )
445
+ | null
446
+ ),
447
+ ]
448
+ >;
449
+ blob(): Promise<
450
+ Opts['withResult'] extends true
451
+ ? Result.Result<
452
+ AspiResultOk<TRequest, Blob>,
453
+ | AspiError<TRequest>
454
+ | (Opts extends { error: any }
455
+ ? Opts['error'][keyof Opts['error']]
456
+ : never)
457
+ >
458
+ : Opts['throwable'] extends true
459
+ ? AspiPlainResponse<TRequest, Blob>
460
+ : [
461
+ AspiResultOk<TRequest, Blob> | null,
462
+ (
463
+ | (
464
+ | AspiError<TRequest>
465
+ | (Opts extends { error: any }
466
+ ? Opts['error'][keyof Opts['error']]
467
+ : never)
468
+ )
469
+ | null
470
+ ),
471
+ ]
472
+ >;
473
+ }
242
474
  ```
243
475
 
244
- ##### Schema Validation
245
-
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.
247
-
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
- });
476
+ ---
259
477
 
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();
478
+ ## License
272
479
 
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
- ```
480
+ MIT © Aspi contributors