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 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
+ ```