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 +402 -209
- package/dist/index.cjs +486 -140
- package/dist/index.d.cts +580 -136
- package/dist/index.d.ts +580 -136
- package/dist/index.js +486 -140
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,109 +1,165 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Aspi
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
16
|
+
## Installation
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
npm
|
|
19
|
+
`bash
|
|
20
|
+
npm install aspi
|
|
21
|
+
`
|
|
22
22
|
|
|
23
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
.
|
|
34
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
64
|
+
getTodo(1);
|
|
56
65
|
```
|
|
57
66
|
|
|
58
|
-
|
|
67
|
+
---
|
|
59
68
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
import { aspi, Result } from 'aspi';
|
|
95
|
-
import { z, ZodError } from 'zod';
|
|
150
|
+
## Schema validation (Zod example)
|
|
96
151
|
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
106
|
-
const response = await
|
|
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
|
|
125
|
-
console.error(
|
|
178
|
+
const parseErr = err.data as z.ZodError;
|
|
179
|
+
console.error('Validation failed:', parseErr.errors);
|
|
126
180
|
} else {
|
|
127
|
-
|
|
181
|
+
console.error('Other error', err);
|
|
128
182
|
}
|
|
129
183
|
},
|
|
130
184
|
});
|
|
131
|
-
}
|
|
185
|
+
}
|
|
132
186
|
```
|
|
133
187
|
|
|
134
|
-
|
|
188
|
+
---
|
|
135
189
|
|
|
136
|
-
|
|
137
|
-
import { aspi, Result } from 'aspi';
|
|
190
|
+
## Retry & back‑off
|
|
138
191
|
|
|
139
|
-
|
|
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
|
|
148
|
-
retryOn: [404],
|
|
198
|
+
retryDelay: 1000, // simple fixed delay
|
|
199
|
+
retryOn: [404, 500], // retry on specific status codes
|
|
149
200
|
});
|
|
150
201
|
|
|
151
|
-
//
|
|
152
|
-
|
|
202
|
+
// Override retry options for a single request
|
|
203
|
+
api
|
|
153
204
|
.get('/todos/1')
|
|
154
|
-
.setHeader('
|
|
155
|
-
// Updating retry options for this request
|
|
205
|
+
.setHeader('Accept', 'application/json')
|
|
156
206
|
.setRetry({
|
|
157
|
-
//
|
|
158
|
-
retryDelay: (
|
|
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((
|
|
163
|
-
Result.match(
|
|
164
|
-
onOk: (data) =>
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
268
|
+
These helpers allow you to write:
|
|
216
269
|
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
.get('
|
|
270
|
+
```ts
|
|
271
|
+
api
|
|
272
|
+
.get('/secret')
|
|
273
|
+
.unauthorised(() => ({ message: 'You need a token' }))
|
|
220
274
|
.withResult()
|
|
221
|
-
.json
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
),
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|