@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.
@@ -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.0.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.3.0",
38
+ "@afoures/auto-release": "^0.4.1",
38
39
  "@arktype/attest": "^0.56.0",
39
- "@types/node": "^24.10.13",
40
+ "@types/node": "^24.12.0",
40
41
  "msw": "^2.12.10",
41
- "oxfmt": "^0.25.0",
42
- "oxlint": "^1.48.0",
43
- "tsdown": "^0.20.3",
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
  },