@rexeus/typeweaver-clients 0.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 +411 -0
- package/dist/index.d.ts +1618 -0
- package/dist/index.js +985 -0
- package/dist/lib/ApiClient.ts +189 -0
- package/dist/lib/RequestCommand.ts +61 -0
- package/dist/lib/index.ts +2 -0
- package/dist/templates/Client.ejs +39 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# @rexeus/typeweaver-clients
|
|
2
|
+
|
|
3
|
+
HTTP client generators for TypeWeaver API specifications.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This plugin generates type-safe HTTP API clients from your TypeWeaver API definitions, providing
|
|
8
|
+
end-to-end type safety from API definition to client usage.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @rexeus/typeweaver-clients
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**Peer Dependencies:**
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @rexeus/typeweaver-core @rexeus/typeweaver-gen
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### CLI
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx typeweaver generate --input ./api/definitions --output ./api/generated --plugins clients
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Configuration File
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
// typeweaver.config.js
|
|
34
|
+
export default {
|
|
35
|
+
input: "./api/definitions",
|
|
36
|
+
output: "./api/generated",
|
|
37
|
+
plugins: ["clients"],
|
|
38
|
+
};
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Generated Output
|
|
42
|
+
|
|
43
|
+
This plugin generates HTTP API clients for each entity in your API definitions.
|
|
44
|
+
|
|
45
|
+
### Example Generated Client
|
|
46
|
+
|
|
47
|
+
For an API definition with a `users` entity, the plugin generates:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// UsersClient.ts
|
|
51
|
+
import { ApiClient } from "@rexeus/typeweaver-core";
|
|
52
|
+
import { GetUserRequestCommand } from "./GetUserRequest";
|
|
53
|
+
import { GetUserResponseValidator } from "./GetUserResponseValidator";
|
|
54
|
+
// ... other imports
|
|
55
|
+
|
|
56
|
+
export class UsersClient extends ApiClient {
|
|
57
|
+
public send(command: GetUserRequestCommand): Promise<GetUserResponse>;
|
|
58
|
+
public send(command: CreateUserRequestCommand): Promise<CreateUserResponse>;
|
|
59
|
+
// ... other overloads
|
|
60
|
+
|
|
61
|
+
public async send(command: unknown): Promise<unknown> {
|
|
62
|
+
if (command instanceof GetUserRequestCommand) {
|
|
63
|
+
return this.handleGetUser(command);
|
|
64
|
+
}
|
|
65
|
+
if (command instanceof CreateUserRequestCommand) {
|
|
66
|
+
return this.handleCreateUser(command);
|
|
67
|
+
}
|
|
68
|
+
// ... other handlers
|
|
69
|
+
|
|
70
|
+
throw new Error(`Unknown command type`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private async handleGetUser(command: GetUserRequestCommand): Promise<GetUserResponse> {
|
|
74
|
+
const response = await this.makeRequest(command);
|
|
75
|
+
const validator = new GetUserResponseValidator();
|
|
76
|
+
return validator.validate(response);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ... other private handlers
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Usage Examples
|
|
84
|
+
|
|
85
|
+
### Basic Usage
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { UsersClient } from "./api/generated/users/UsersClient";
|
|
89
|
+
import { GetUserRequestCommand } from "./api/generated/users/GetUserRequest";
|
|
90
|
+
|
|
91
|
+
const client = new UsersClient({
|
|
92
|
+
baseURL: "https://api.example.com",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Type-safe request
|
|
96
|
+
const command = new GetUserRequestCommand({
|
|
97
|
+
param: { userId: "123" },
|
|
98
|
+
header: { Authorization: "Bearer token" },
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// Fully typed response
|
|
103
|
+
const result = await client.send(command);
|
|
104
|
+
console.log(result.body.name); // Type-safe access
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// Typed error handling
|
|
107
|
+
if (error instanceof UserNotFoundErrorResponse) {
|
|
108
|
+
console.error("User not found:", error.body.message);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Advanced Configuration
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { UsersClient } from "./api/generated/users/UsersClient";
|
|
117
|
+
|
|
118
|
+
const client = new UsersClient({
|
|
119
|
+
baseURL: "https://api.example.com",
|
|
120
|
+
timeout: 5000,
|
|
121
|
+
headers: {
|
|
122
|
+
"User-Agent": "MyApp/1.0",
|
|
123
|
+
},
|
|
124
|
+
// Custom axios config
|
|
125
|
+
axios: {
|
|
126
|
+
validateStatus: status => status < 500,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Error Handling
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import {
|
|
135
|
+
UsersClient,
|
|
136
|
+
GetUserRequestCommand,
|
|
137
|
+
UserNotFoundErrorResponse,
|
|
138
|
+
ValidationErrorResponse,
|
|
139
|
+
} from "./api/generated";
|
|
140
|
+
|
|
141
|
+
const client = new UsersClient({ baseURL: "https://api.example.com" });
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const result = await client.send(
|
|
145
|
+
new GetUserRequestCommand({
|
|
146
|
+
param: { userId: "invalid-id" },
|
|
147
|
+
})
|
|
148
|
+
);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
// Type-safe error handling
|
|
151
|
+
if (error instanceof UserNotFoundErrorResponse) {
|
|
152
|
+
console.error(`User not found: ${error.body.message}`);
|
|
153
|
+
} else if (error instanceof ValidationErrorResponse) {
|
|
154
|
+
console.error("Validation failed:", error.body.issues);
|
|
155
|
+
} else {
|
|
156
|
+
console.error("Unexpected error:", error);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Client Features
|
|
162
|
+
|
|
163
|
+
### Type Safety
|
|
164
|
+
|
|
165
|
+
- **Request Commands** - Fully typed request objects
|
|
166
|
+
- **Response Types** - Typed response interfaces
|
|
167
|
+
- **Error Types** - Typed error response classes
|
|
168
|
+
- **Parameter Validation** - Runtime validation via Zod
|
|
169
|
+
|
|
170
|
+
### Method Overloading
|
|
171
|
+
|
|
172
|
+
Each client provides method overloads for perfect type inference:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// Each command type gets its own overload
|
|
176
|
+
client.send(getUserCommand); // Returns GetUserResponse
|
|
177
|
+
client.send(createUserCommand); // Returns CreateUserResponse
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Automatic Validation
|
|
181
|
+
|
|
182
|
+
- **Request validation** - Commands validate input data
|
|
183
|
+
- **Response validation** - Responses validated against schemas
|
|
184
|
+
- **Error parsing** - HTTP errors mapped to typed error classes
|
|
185
|
+
|
|
186
|
+
### Framework Integration
|
|
187
|
+
|
|
188
|
+
Works with any HTTP client framework (uses Axios by default):
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
// Custom HTTP adapter
|
|
192
|
+
const client = new UsersClient({
|
|
193
|
+
baseURL: "https://api.example.com",
|
|
194
|
+
httpAdapter: new FetchAdapter(), // Custom adapter
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Request Commands
|
|
199
|
+
|
|
200
|
+
### Command Structure
|
|
201
|
+
|
|
202
|
+
Generated request commands encapsulate all request data:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
import { GetUserRequestCommand } from "./GetUserRequest";
|
|
206
|
+
|
|
207
|
+
const command = new GetUserRequestCommand({
|
|
208
|
+
param: { userId: "123" }, // Path parameters
|
|
209
|
+
query: { include: ["posts"] }, // Query parameters
|
|
210
|
+
header: { Authorization: "Bearer token" }, // Headers
|
|
211
|
+
body: { name: "John Doe" }, // Request body
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Command Validation
|
|
216
|
+
|
|
217
|
+
Commands validate input data at construction time:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
try {
|
|
221
|
+
const command = new GetUserRequestCommand({
|
|
222
|
+
param: { userId: "invalid-uuid" }, // Will throw validation error
|
|
223
|
+
});
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.error("Invalid command:", error.issues);
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Response Handling
|
|
230
|
+
|
|
231
|
+
### Success Responses
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
const result = await client.send(command);
|
|
235
|
+
|
|
236
|
+
// Typed access to response data
|
|
237
|
+
console.log(result.statusCode); // HTTP status code
|
|
238
|
+
console.log(result.header); // Response headers
|
|
239
|
+
console.log(result.body.id); // Response body (fully typed)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Error Responses
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
try {
|
|
246
|
+
const result = await client.send(command);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (error instanceof UserNotFoundErrorResponse) {
|
|
249
|
+
// Specific error type
|
|
250
|
+
console.log(error.statusCode); // 404
|
|
251
|
+
console.log(error.body.message); // "User not found"
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Plugin Architecture
|
|
257
|
+
|
|
258
|
+
This plugin extends the TypeWeaver plugin system:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import { BasePlugin, type GeneratorContext } from "@rexeus/typeweaver-gen";
|
|
262
|
+
|
|
263
|
+
export default class ClientsPlugin extends BasePlugin {
|
|
264
|
+
public name = "clients";
|
|
265
|
+
|
|
266
|
+
public override generate(context: GeneratorContext): void {
|
|
267
|
+
// Generates API clients for each entity
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## Best Practices
|
|
273
|
+
|
|
274
|
+
### Client Configuration
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// Environment-specific configuration
|
|
278
|
+
const client = new UsersClient({
|
|
279
|
+
baseURL: process.env.API_BASE_URL,
|
|
280
|
+
timeout: parseInt(process.env.API_TIMEOUT || "5000"),
|
|
281
|
+
headers: {
|
|
282
|
+
"User-Agent": `${process.env.APP_NAME}/${process.env.APP_VERSION}`,
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Error Handling Strategy
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
// Centralized error handling
|
|
291
|
+
async function handleApiCall<T>(operation: () => Promise<T>): Promise<T> {
|
|
292
|
+
try {
|
|
293
|
+
return await operation();
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (error instanceof ValidationErrorResponse) {
|
|
296
|
+
// Log validation issues
|
|
297
|
+
logger.warn("Validation error:", error.body.issues);
|
|
298
|
+
} else if (error instanceof UnauthorizedErrorResponse) {
|
|
299
|
+
// Handle auth errors
|
|
300
|
+
redirectToLogin();
|
|
301
|
+
}
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Usage
|
|
307
|
+
const result = await handleApiCall(() =>
|
|
308
|
+
client.send(new GetUserRequestCommand({ param: { userId: "123" } }))
|
|
309
|
+
);
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Testing
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// Mock clients for testing
|
|
316
|
+
import { UsersClient } from "./api/generated";
|
|
317
|
+
|
|
318
|
+
// Mock the client
|
|
319
|
+
jest.mock("./api/generated/users/UsersClient");
|
|
320
|
+
|
|
321
|
+
const mockClient = new UsersClient() as jest.Mocked<UsersClient>;
|
|
322
|
+
mockClient.send.mockResolvedValue({
|
|
323
|
+
statusCode: 200,
|
|
324
|
+
body: { id: "123", name: "Test User" },
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Integration Examples
|
|
329
|
+
|
|
330
|
+
### React Hook
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
import { useState, useEffect } from "react";
|
|
334
|
+
import { UsersClient, GetUserRequestCommand } from "./api/generated";
|
|
335
|
+
|
|
336
|
+
const client = new UsersClient({ baseURL: "https://api.example.com" });
|
|
337
|
+
|
|
338
|
+
export function useUser(userId: string) {
|
|
339
|
+
const [user, setUser] = useState(null);
|
|
340
|
+
const [loading, setLoading] = useState(true);
|
|
341
|
+
const [error, setError] = useState(null);
|
|
342
|
+
|
|
343
|
+
useEffect(() => {
|
|
344
|
+
async function fetchUser() {
|
|
345
|
+
try {
|
|
346
|
+
setLoading(true);
|
|
347
|
+
const result = await client.send(
|
|
348
|
+
new GetUserRequestCommand({
|
|
349
|
+
param: { userId },
|
|
350
|
+
})
|
|
351
|
+
);
|
|
352
|
+
setUser(result.body);
|
|
353
|
+
} catch (err) {
|
|
354
|
+
setError(err);
|
|
355
|
+
} finally {
|
|
356
|
+
setLoading(false);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
fetchUser();
|
|
361
|
+
}, [userId]);
|
|
362
|
+
|
|
363
|
+
return { user, loading, error };
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Node.js Service
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
import { UsersClient, CreateUserRequestCommand } from "./api/generated";
|
|
371
|
+
|
|
372
|
+
export class UserService {
|
|
373
|
+
private client = new UsersClient({
|
|
374
|
+
baseURL: process.env.API_BASE_URL,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
async createUser(userData: CreateUserRequest["body"]) {
|
|
378
|
+
const command = new CreateUserRequestCommand({
|
|
379
|
+
body: userData,
|
|
380
|
+
header: {
|
|
381
|
+
Authorization: `Bearer ${process.env.API_TOKEN}`,
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
return await this.client.send(command);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## Troubleshooting
|
|
391
|
+
|
|
392
|
+
### Common Issues
|
|
393
|
+
|
|
394
|
+
**Import errors**: Ensure all TypeWeaver plugins are installed **Type errors**: Regenerate code
|
|
395
|
+
after API definition changes **Runtime errors**: Check that server API matches generated client
|
|
396
|
+
expectations
|
|
397
|
+
|
|
398
|
+
### Debug Mode
|
|
399
|
+
|
|
400
|
+
Enable verbose logging:
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
const client = new UsersClient({
|
|
404
|
+
baseURL: "https://api.example.com",
|
|
405
|
+
debug: true, // Enable debug logging
|
|
406
|
+
});
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## License
|
|
410
|
+
|
|
411
|
+
ISC © Dennis Wentzien 2025
|