@afoures/http-client 0.0.0 → 0.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 +1 -1
- package/dist/index.d.mts +5 -1
- package/dist/index.mjs +5 -1
- package/dist/lib/endpoint.d.mts +40 -0
- package/dist/lib/endpoint.mjs +191 -0
- package/dist/lib/errors.d.mts +54 -0
- package/dist/lib/errors.mjs +58 -0
- package/dist/lib/http-client.d.mts +64 -0
- package/dist/lib/http-client.mjs +134 -0
- package/dist/lib/types.d.mts +218 -0
- package/dist/lib/types.mjs +4 -0
- package/dist/lib/utils.mjs +69 -0
- package/docs/endpoint-definition.md +128 -0
- package/docs/error-handling.md +148 -0
- package/docs/http-client.md +147 -0
- package/docs/response-parsing.md +243 -0
- package/docs/retry-policy.md +197 -0
- package/docs/schema-integration.md +221 -0
- package/docs/serialization.md +211 -0
- package/package.json +7 -6
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Schema Integration
|
|
2
|
+
|
|
3
|
+
`@afoures/http-client` uses the [Standard Schema spec](https://github.com/standard-schema/standard-schema) for schema validation. Any compliant library works.
|
|
4
|
+
|
|
5
|
+
## Zod
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
|
|
10
|
+
const endpoint = new Endpoint({
|
|
11
|
+
method: 'POST',
|
|
12
|
+
pathname: '/users',
|
|
13
|
+
body: {
|
|
14
|
+
schema: z.object({
|
|
15
|
+
name: z.string().min(1),
|
|
16
|
+
email: z.string().email(),
|
|
17
|
+
}),
|
|
18
|
+
},
|
|
19
|
+
data: {
|
|
20
|
+
schema: z.object({
|
|
21
|
+
id: z.string(),
|
|
22
|
+
name: z.string(),
|
|
23
|
+
createdAt: z.string().datetime(),
|
|
24
|
+
}),
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Transforms
|
|
30
|
+
|
|
31
|
+
Zod transforms work for both input and output:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
const endpoint = new Endpoint({
|
|
35
|
+
method: 'GET',
|
|
36
|
+
pathname: '/users',
|
|
37
|
+
query: {
|
|
38
|
+
schema: z.object({
|
|
39
|
+
page: z.number().transform(String), // number input, string output
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
data: {
|
|
43
|
+
schema: z.object({
|
|
44
|
+
createdAt: z.string().transform(s => new Date(s)), // parse ISO to Date
|
|
45
|
+
}),
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Input: { page: 1 }
|
|
50
|
+
// Query string: ?page=1
|
|
51
|
+
// Response: { createdAt: "2024-01-15T10:30:00Z" }
|
|
52
|
+
// Output: { createdAt: Date }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## ArkType
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { type } from 'arktype'
|
|
59
|
+
|
|
60
|
+
const endpoint = new Endpoint({
|
|
61
|
+
method: 'POST',
|
|
62
|
+
pathname: '/users',
|
|
63
|
+
body: {
|
|
64
|
+
schema: type({
|
|
65
|
+
name: 'string>0',
|
|
66
|
+
email: 'string',
|
|
67
|
+
age: 'number?',
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
70
|
+
data: {
|
|
71
|
+
schema: type({
|
|
72
|
+
id: 'string',
|
|
73
|
+
name: 'string',
|
|
74
|
+
'email?': 'string',
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Transforms
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { type } from 'arktype'
|
|
84
|
+
|
|
85
|
+
const endpoint = new Endpoint({
|
|
86
|
+
method: 'GET',
|
|
87
|
+
pathname: '/users',
|
|
88
|
+
data: {
|
|
89
|
+
schema: type({
|
|
90
|
+
id: 'string',
|
|
91
|
+
'createdAt': 'string.parse(v => new Date(v))',
|
|
92
|
+
}),
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Valibot
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import * as v from 'valibot'
|
|
101
|
+
|
|
102
|
+
const endpoint = new Endpoint({
|
|
103
|
+
method: 'POST',
|
|
104
|
+
pathname: '/users',
|
|
105
|
+
body: {
|
|
106
|
+
schema: v.object({
|
|
107
|
+
name: v.pipe(v.string(), v.minLength(1)),
|
|
108
|
+
email: v.pipe(v.string(), v.email()),
|
|
109
|
+
}),
|
|
110
|
+
},
|
|
111
|
+
data: {
|
|
112
|
+
schema: v.object({
|
|
113
|
+
id: v.string(),
|
|
114
|
+
name: v.string(),
|
|
115
|
+
}),
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Transforms
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import * as v from 'valibot'
|
|
124
|
+
|
|
125
|
+
const endpoint = new Endpoint({
|
|
126
|
+
method: 'GET',
|
|
127
|
+
pathname: '/users',
|
|
128
|
+
data: {
|
|
129
|
+
schema: v.object({
|
|
130
|
+
id: v.string(),
|
|
131
|
+
createdAt: v.pipe(v.string(), v.transform(s => new Date(s))),
|
|
132
|
+
}),
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Input vs Output Types
|
|
138
|
+
|
|
139
|
+
Schemas define both input validation and output parsing:
|
|
140
|
+
|
|
141
|
+
- **Input** (`Schema.infer_input`): What you pass to the endpoint
|
|
142
|
+
- **Output** (`Schema.infer_output`): What you get back after validation/transforms
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
const schema = z.object({
|
|
146
|
+
id: z.string().transform(s => parseInt(s)),
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// Input: string
|
|
150
|
+
// Output: number
|
|
151
|
+
|
|
152
|
+
const endpoint = new Endpoint({
|
|
153
|
+
method: 'GET',
|
|
154
|
+
pathname: '/items',
|
|
155
|
+
query: {
|
|
156
|
+
schema: z.object({
|
|
157
|
+
id: z.string().transform(parseInt),
|
|
158
|
+
}),
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// You pass: { query: { id: '123' } } (string)
|
|
163
|
+
// URL becomes: /items?id=123
|
|
164
|
+
// After validation, id is: 123 (number)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Reusable Schemas
|
|
168
|
+
|
|
169
|
+
Share schemas across endpoints:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import { z } from 'zod'
|
|
173
|
+
|
|
174
|
+
const UserSchema = z.object({
|
|
175
|
+
id: z.string(),
|
|
176
|
+
name: z.string(),
|
|
177
|
+
email: z.string().email(),
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const CreateUserSchema = UserSchema.omit({ id: true })
|
|
181
|
+
|
|
182
|
+
const api = http_client({
|
|
183
|
+
origin: 'https://api.example.com',
|
|
184
|
+
endpoints: {
|
|
185
|
+
users: {
|
|
186
|
+
list: new Endpoint({
|
|
187
|
+
method: 'GET',
|
|
188
|
+
pathname: '/users',
|
|
189
|
+
data: { schema: z.array(UserSchema) },
|
|
190
|
+
}),
|
|
191
|
+
get: new Endpoint({
|
|
192
|
+
method: 'GET',
|
|
193
|
+
pathname: '/users/(:id)',
|
|
194
|
+
data: { schema: UserSchema },
|
|
195
|
+
}),
|
|
196
|
+
create: new Endpoint({
|
|
197
|
+
method: 'POST',
|
|
198
|
+
pathname: '/users',
|
|
199
|
+
body: { schema: CreateUserSchema },
|
|
200
|
+
data: { schema: UserSchema },
|
|
201
|
+
}),
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
})
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Custom Schema Libraries
|
|
208
|
+
|
|
209
|
+
Any library implementing the Standard Schema spec works:
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
213
|
+
readonly '~standard': {
|
|
214
|
+
readonly version: 1
|
|
215
|
+
readonly vendor: string
|
|
216
|
+
readonly validate: (value: Input) => StandardResult<Output>
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The HTTP client uses `schema['~standard'].validate()` for both input serialization and output parsing.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Serialization
|
|
2
|
+
|
|
3
|
+
Endpoints serialize path params, query strings, and request bodies using schemas. All serialization validates input and can transform data.
|
|
4
|
+
|
|
5
|
+
## Params
|
|
6
|
+
|
|
7
|
+
Path parameters are serialized from the `params` input into the URL pathname.
|
|
8
|
+
|
|
9
|
+
### Without Schema
|
|
10
|
+
|
|
11
|
+
If no schema is provided, params are inferred from the pathname pattern:
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
const endpoint = new Endpoint({
|
|
15
|
+
method: 'GET',
|
|
16
|
+
pathname: '/users/(:id)',
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const url = await endpoint.generate_url({
|
|
20
|
+
origin: 'https://api.example.com',
|
|
21
|
+
params: { id: '123' },
|
|
22
|
+
})
|
|
23
|
+
// https://api.example.com/users/123
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### With Schema
|
|
27
|
+
|
|
28
|
+
Use a schema to validate and transform params:
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
const endpoint = new Endpoint({
|
|
32
|
+
method: 'GET',
|
|
33
|
+
pathname: '/users/(:id)',
|
|
34
|
+
params: {
|
|
35
|
+
schema: z.object({
|
|
36
|
+
id: z.string().uuid(),
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Custom Serialization
|
|
43
|
+
|
|
44
|
+
Provide a `serialization` function to transform validated params:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
const endpoint = new Endpoint({
|
|
48
|
+
method: 'GET',
|
|
49
|
+
pathname: '/users/(:id)',
|
|
50
|
+
params: {
|
|
51
|
+
schema: z.object({ id: z.number() }),
|
|
52
|
+
serialization: (data) => ({ id: `user-${data.id}` }),
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const url = await endpoint.generate_url({
|
|
57
|
+
origin: 'https://api.example.com',
|
|
58
|
+
params: { id: 123 },
|
|
59
|
+
})
|
|
60
|
+
// https://api.example.com/users/user-123
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Query
|
|
64
|
+
|
|
65
|
+
Query parameters are serialized into the URL search string.
|
|
66
|
+
|
|
67
|
+
### Object Schema
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
const endpoint = new Endpoint({
|
|
71
|
+
method: 'GET',
|
|
72
|
+
pathname: '/users',
|
|
73
|
+
query: {
|
|
74
|
+
schema: z.object({
|
|
75
|
+
page: z.number().transform(String),
|
|
76
|
+
search: z.string().optional(),
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const url = await endpoint.generate_url({
|
|
82
|
+
origin: 'https://api.example.com',
|
|
83
|
+
query: { page: 1, search: 'john' },
|
|
84
|
+
})
|
|
85
|
+
// https://api.example.com/users?page=1&search=john
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Custom Serialization
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
const endpoint = new Endpoint({
|
|
92
|
+
method: 'GET',
|
|
93
|
+
pathname: '/users',
|
|
94
|
+
query: {
|
|
95
|
+
schema: z.object({
|
|
96
|
+
tags: z.array(z.string()),
|
|
97
|
+
}),
|
|
98
|
+
serialization: (data) => {
|
|
99
|
+
const params = new URLSearchParams()
|
|
100
|
+
params.set('tags', data.tags.join(','))
|
|
101
|
+
return params
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const url = await endpoint.generate_url({
|
|
107
|
+
origin: 'https://api.example.com',
|
|
108
|
+
query: { tags: ['admin', 'active'] },
|
|
109
|
+
})
|
|
110
|
+
// https://api.example.com/users?tags=admin,active
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Body
|
|
114
|
+
|
|
115
|
+
Request bodies are serialized for POST, PUT, PATCH, and DELETE methods.
|
|
116
|
+
|
|
117
|
+
### JSON (Default)
|
|
118
|
+
|
|
119
|
+
For JSON-compatible schemas, JSON serialization is automatic:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
const endpoint = new Endpoint({
|
|
123
|
+
method: 'POST',
|
|
124
|
+
pathname: '/users',
|
|
125
|
+
body: {
|
|
126
|
+
schema: z.object({
|
|
127
|
+
name: z.string(),
|
|
128
|
+
email: z.string().email(),
|
|
129
|
+
}),
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
const { body, content_type } = await endpoint.serialize_body({
|
|
134
|
+
body: { name: 'John', email: 'john@example.com' },
|
|
135
|
+
})
|
|
136
|
+
// body: '{"name":"John","email":"john@example.com"}'
|
|
137
|
+
// content_type: 'application/json'
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Custom Serialization
|
|
141
|
+
|
|
142
|
+
For non-JSON bodies (FormData, text, etc.):
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
const endpoint = new Endpoint({
|
|
146
|
+
method: 'POST',
|
|
147
|
+
pathname: '/upload',
|
|
148
|
+
body: {
|
|
149
|
+
schema: z.object({
|
|
150
|
+
file: z.instanceof(File),
|
|
151
|
+
name: z.string(),
|
|
152
|
+
}),
|
|
153
|
+
serialization: (data) => {
|
|
154
|
+
const formData = new FormData()
|
|
155
|
+
formData.append('file', data.file)
|
|
156
|
+
formData.append('name', data.name)
|
|
157
|
+
return { body: formData, content_type: 'multipart/form-data' }
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### URL-Encoded
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
const endpoint = new Endpoint({
|
|
167
|
+
method: 'POST',
|
|
168
|
+
pathname: '/login',
|
|
169
|
+
body: {
|
|
170
|
+
schema: z.object({
|
|
171
|
+
username: z.string(),
|
|
172
|
+
password: z.string(),
|
|
173
|
+
}),
|
|
174
|
+
serialization: (data) => {
|
|
175
|
+
const params = new URLSearchParams()
|
|
176
|
+
params.set('username', data.username)
|
|
177
|
+
params.set('password', data.password)
|
|
178
|
+
return { body: params, content_type: 'application/x-www-form-urlencoded' }
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
})
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Plain Text
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
const endpoint = new Endpoint({
|
|
188
|
+
method: 'POST',
|
|
189
|
+
pathname: '/echo',
|
|
190
|
+
body: {
|
|
191
|
+
schema: z.string(),
|
|
192
|
+
serialization: (text) => ({
|
|
193
|
+
body: text,
|
|
194
|
+
content_type: 'text/plain',
|
|
195
|
+
}),
|
|
196
|
+
},
|
|
197
|
+
})
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Validation Errors
|
|
201
|
+
|
|
202
|
+
If input fails schema validation, a `SerializationError` is returned:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
const result = await endpoint.serialize_body({ body: { name: '' } })
|
|
206
|
+
|
|
207
|
+
if (result instanceof SerializationError) {
|
|
208
|
+
console.log(result.message) // "Body serialization failed"
|
|
209
|
+
console.log(result.cause) // Schema validation issues
|
|
210
|
+
}
|
|
211
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@afoures/http-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "A typesafe and robust HTTP client",
|
|
5
5
|
"homepage": "https://github.com/afoures/http-client#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
|
+
"docs",
|
|
17
18
|
"package.json",
|
|
18
19
|
"README.md"
|
|
19
20
|
],
|
|
@@ -34,13 +35,13 @@
|
|
|
34
35
|
"@standard-schema/spec": "^1.1.0"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
|
-
"@afoures/auto-release": "^0.
|
|
38
|
+
"@afoures/auto-release": "^0.4.1",
|
|
38
39
|
"@arktype/attest": "^0.56.0",
|
|
39
|
-
"@types/node": "^24.
|
|
40
|
+
"@types/node": "^24.12.0",
|
|
40
41
|
"msw": "^2.12.10",
|
|
41
|
-
"oxfmt": "^0.
|
|
42
|
-
"oxlint": "^1.
|
|
43
|
-
"tsdown": "^0.
|
|
42
|
+
"oxfmt": "^0.36.0",
|
|
43
|
+
"oxlint": "^1.51.0",
|
|
44
|
+
"tsdown": "^0.21.0",
|
|
44
45
|
"typescript": "^5.9.3",
|
|
45
46
|
"zod": "^4.3.6"
|
|
46
47
|
},
|