aspi 1.1.0-beta.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/LICENSE +21 -0
- package/README.md +292 -0
- package/dist/index.cjs +1237 -0
- package/dist/index.d.cts +1285 -0
- package/dist/index.d.ts +1285 -0
- package/dist/index.js +1210 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Harsh Pareek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# aspi
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Why Aspi?
|
|
6
|
+
|
|
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.
|
|
17
|
+
|
|
18
|
+
## Example
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { aspi, Result } from 'aspi';
|
|
22
|
+
|
|
23
|
+
const apiClient = new Aspi({
|
|
24
|
+
baseUrl: 'https://api.example.com',
|
|
25
|
+
headers: {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const getTodos = async (id: number) => {
|
|
31
|
+
const [value, error] = await apiClient
|
|
32
|
+
.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
|
+
}
|
|
45
|
+
|
|
46
|
+
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
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
getTodos(1);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## With Result type
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
const getTodos = async (id: number) => {
|
|
62
|
+
const [value, error] = await apiClient
|
|
63
|
+
.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
|
+
}>();
|
|
73
|
+
|
|
74
|
+
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
|
+
}
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
getTodos(1);
|
|
88
|
+
};
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Example with Schema Validation (with Zod)
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { aspi, Result } from 'aspi';
|
|
95
|
+
import { z, ZodError } from 'zod';
|
|
96
|
+
|
|
97
|
+
// JSON Placeholder API Client
|
|
98
|
+
const apiClient = new Aspi({
|
|
99
|
+
baseUrl: 'https://jsonplaceholder.typicode.com',
|
|
100
|
+
headers: {
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const getTodo = async (id: number) => {
|
|
106
|
+
const response = await apiClient
|
|
107
|
+
.get(`/todos/${id}`)
|
|
108
|
+
.withResult()
|
|
109
|
+
.schema(
|
|
110
|
+
z.object({
|
|
111
|
+
id: z.number(),
|
|
112
|
+
title: z.string(),
|
|
113
|
+
completed: z.boolean(),
|
|
114
|
+
}),
|
|
115
|
+
)
|
|
116
|
+
.json();
|
|
117
|
+
|
|
118
|
+
Result.match(response, {
|
|
119
|
+
onOk: (data) => {
|
|
120
|
+
console.log(data);
|
|
121
|
+
},
|
|
122
|
+
onErr: (err) => {
|
|
123
|
+
if (err.tag === 'parseError') {
|
|
124
|
+
const error = err.data as ZodError;
|
|
125
|
+
console.error(error.errors);
|
|
126
|
+
} else {
|
|
127
|
+
// do something else
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
};
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Example with retry
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import { aspi, Result } from 'aspi';
|
|
138
|
+
|
|
139
|
+
const apiClient = new Aspi({
|
|
140
|
+
baseUrl: 'https://example.com',
|
|
141
|
+
headers: {
|
|
142
|
+
'Content-Type': 'application/json',
|
|
143
|
+
},
|
|
144
|
+
}).setRetry({
|
|
145
|
+
retries: 3,
|
|
146
|
+
retryDelay: 1000,
|
|
147
|
+
// retry on 404 error
|
|
148
|
+
retryOn: [404],
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// the given GET endpoint does not exist
|
|
152
|
+
apiClient
|
|
153
|
+
.get('/todos/1')
|
|
154
|
+
.setHeader('Content-Type', 'application/json')
|
|
155
|
+
// Updating retry options for this request
|
|
156
|
+
.setRetry({
|
|
157
|
+
// Exponential backoff
|
|
158
|
+
retryDelay: (attempts) => Math.pow(2, attempts) * 1000,
|
|
159
|
+
})
|
|
160
|
+
.withResult()
|
|
161
|
+
.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
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Installation
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
npm install aspi
|
|
182
|
+
```
|
|
183
|
+
|
|
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]
|
|
213
|
+
```
|
|
214
|
+
|
|
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.
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
const response = await new Aspi({ baseUrl: '...' })
|
|
219
|
+
.get('...')
|
|
220
|
+
.withResult()
|
|
221
|
+
.json<{ data: any }>();
|
|
222
|
+
|
|
223
|
+
// Result<AspiResultOk<AspiRequestInit, { data: any; }>, JSONParseError | AspiError<AspiRequestInit>>
|
|
224
|
+
```
|
|
225
|
+
|
|
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
|
+
)
|
|
234
|
+
.pipe(
|
|
235
|
+
Result.catchError('aspiError', () => {
|
|
236
|
+
console.log('aspi error');
|
|
237
|
+
}),
|
|
238
|
+
)
|
|
239
|
+
.pipe(
|
|
240
|
+
Result.catchError('jsonParseError', () =>
|
|
241
|
+
console.log('failed to parse json error'),
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
.execute();
|
|
245
|
+
|
|
246
|
+
// Result<AspiResultOk<AspiRequestInit, { data: any; }>, never>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
##### Schema Validation
|
|
250
|
+
|
|
251
|
+
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.
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
import { aspi, Result } from 'aspi';
|
|
255
|
+
import { z, ZodError } from 'zod';
|
|
256
|
+
|
|
257
|
+
// JSON Placeholder API Client
|
|
258
|
+
const apiClient = new Aspi({
|
|
259
|
+
baseUrl: 'https://jsonplaceholder.typicode.com',
|
|
260
|
+
headers: {
|
|
261
|
+
'Content-Type': 'application/json',
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const getTodo = async (id: number) => {
|
|
266
|
+
const response = await apiClient
|
|
267
|
+
.get(`/todos/${id}`)
|
|
268
|
+
.withResult()
|
|
269
|
+
.schema(
|
|
270
|
+
z.object({
|
|
271
|
+
id: z.number(),
|
|
272
|
+
title: z.string(),
|
|
273
|
+
completed: z.boolean(),
|
|
274
|
+
}),
|
|
275
|
+
)
|
|
276
|
+
.json();
|
|
277
|
+
|
|
278
|
+
Result.match(response, {
|
|
279
|
+
onOk: (data) => {
|
|
280
|
+
console.log(data);
|
|
281
|
+
},
|
|
282
|
+
onErr: (err) => {
|
|
283
|
+
if (err.tag === 'parseError') {
|
|
284
|
+
const error = err.data as ZodError;
|
|
285
|
+
console.error(error.errors);
|
|
286
|
+
} else {
|
|
287
|
+
// do something else
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
};
|
|
292
|
+
```
|