@rexeus/typeweaver 0.8.0 → 0.9.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 CHANGED
@@ -68,20 +68,20 @@ More plugins are planned. If you want to build your own, check out the plugin sy
68
68
 
69
69
  ## ⌨️ CLI
70
70
 
71
- Generate TypeScript code from your API definitions:
71
+ Generate TypeScript code from a spec entrypoint file:
72
72
 
73
73
  ```bash
74
74
  # Node.js (npm)
75
- npx typeweaver generate --input ./api/definition --output ./api/generated --plugins clients
75
+ npx typeweaver generate --input ./api/spec/index.ts --output ./api/generated --plugins clients
76
76
 
77
77
  # Node.js (pnpm)
78
- pnpx typeweaver generate --input ./api/definition --output ./api/generated --plugins clients
78
+ pnpx typeweaver generate --input ./api/spec/index.ts --output ./api/generated --plugins clients
79
79
 
80
80
  # Deno
81
- deno run -A npm:@rexeus/typeweaver generate --input ./api/definition --output ./api/generated --plugins clients
81
+ deno run -A npm:@rexeus/typeweaver generate --input ./api/spec/index.ts --output ./api/generated --plugins clients
82
82
 
83
83
  # Bun
84
- bunx typeweaver generate --input ./api/definition --output ./api/generated --plugins clients
84
+ bunx typeweaver generate --input ./api/spec/index.ts --output ./api/generated --plugins clients
85
85
  ```
86
86
 
87
87
  > **Note**: Deno may require the `--sloppy-imports` flag or equivalent configuration in `deno.json`
@@ -89,10 +89,8 @@ bunx typeweaver generate --input ./api/definition --output ./api/generated --plu
89
89
 
90
90
  ### ⚙️ Options
91
91
 
92
- - `--input, -i <path>`: Input directory containing API definitions (required)
92
+ - `--input, -i <path>`: Spec entrypoint file (required)
93
93
  - `--output, -o <path>`: Output directory for generated code (required)
94
- - `-s, --shared <path>`: Shared directory for reusable schemas (optional, defaults to
95
- `<input-path>/shared`)
96
94
  - `--config, -c <path>`: Configuration file path (optional)
97
95
  - `--plugins, -p <plugins>`: Comma-separated list of plugins to use (e.g., "clients,hono" or "all"
98
96
  for all plugins)
@@ -105,7 +103,7 @@ Create a config file (e.g. `typeweaver.config.js`) for more complex configuratio
105
103
 
106
104
  ```javascript
107
105
  export default {
108
- input: "./api/definition",
106
+ input: "./api/spec/index.ts",
109
107
  output: "./api/generated",
110
108
  plugins: ["clients", "hono", "aws-cdk"],
111
109
  format: true,
@@ -126,222 +124,103 @@ npx typeweaver generate --config ./typeweaver.config.js
126
124
 
127
125
  ### 📁 Project Structure
128
126
 
129
- Your API definition must follow this structure:
130
-
131
- - Each resource needs its own directory under the specified input directory (e.g. input dir:
132
- `api/definition` contains `user/`, `post/` subdirectories)
133
- - The directory name defines the resource name (e.g. `user`, `post`)
134
- - The structure inside a resource directory can be nested to provide better organization (e.g.
135
- `user/errors/...`, `user/mutations/...`)
136
- - Inside a resource directory, each operation or response definition gets its own file (e.g.
137
- `CreateUserDefinition.ts`, `UserNotFoundErrorDefinition.ts`)
138
- - An operation definition file must include one default export of a `HttpOperationDefinition`
139
- instance (e.g. `export default new HttpOperationDefinition({...})`)
140
- - It is recommended to specify separate schemas for requests and responses, but this is not strictly
141
- required.
142
- - If separating schemas, Zod utilities can be used to apply general schemas case-specifically
143
- (useful Zod utilities: pick, omit, merge...)
144
- - A response definition file must include one default export of a `HttpResponseDefinition` instance
145
- (e.g. `export default new HttpResponseDefinition({...})`)
146
- - Responses shared across operations are possible, but need to be placed in the `shared` directory.
147
- - The shared directory can be specified using the `--shared` option, but must be located within
148
- the input directory
149
- - Default shared directory is `<input-path>/shared`
150
- - The shared directory is suitable not only as a place for responses but also for shared schemas
151
-
152
- As you can see, the structure of the input directory is essential. However, you are completely free
153
- to choose the structure and nesting within resource directories.
154
-
155
- **Important**: All definition files and their dependencies (like separate schemas etc.) must be
156
- self-contained within the input directory. Generated code creates an immutable snapshot of your
157
- definitions, so any external imports (relative imports outside the input directory) will not work.
158
- NPM package imports continue to work normally.
159
-
160
- ```
161
- api/definition/
162
- ├── user/ # Resource directory
163
- │ ├── errors/ # Resource-specific error definitions
164
- │ │ │ # -> Because they are inside a resource directory,
165
- │ │ │ # they can only be used within this resource
166
- │ │ └── UserNotFoundErrorDefinition.ts
167
- │ │ └── UserStatusTransitionInvalidErrorDefinition.ts
168
- │ ├── CreateUserDefinition.ts # Operation definitions
169
- │ ├── GetUserDefinition.ts
170
- │ ├── ListUserDefinition.ts
171
- │ ├── UpdateUserDefinition.ts
172
- │ └── userSchema.ts # Schema for the resource, can be reused across operations
173
- ├── post/
174
- │ ├── errors/
175
- │ ├── CreatePostDefinition.ts
176
- │ ├── GetPostDefinition.ts
177
- │ ├── ...
178
- ├── ...
179
- └── shared/ # Shared responses and schemas
180
- │ # -> While it doesn't matter where schemas are defined
181
- │ # inside the input directory, responses can only be
182
- │ # shared across resources if they are located in the
183
- │ # shared directory
184
- ├── ConflictErrorDefinition.ts
185
- ├── ForbiddenErrorDefinition.ts
186
- ├── InternalServerErrorDefinition.ts
187
- ├── NotFoundErrorDefinition.ts # Like BaseApiErrors, can be extended to be resource-specific
188
- ├── TooManyRequestsErrorDefinition.ts
189
- ├── UnauthorizedErrorDefinition.ts
190
- ├── ValidationErrorDefinition.ts
191
- └── sharedResponses.ts # Collection of responses relevant for every operation
127
+ Typeweaver reads a single spec entrypoint. Organize files however you want, then assemble the
128
+ resource map in `defineSpec(...)`. Here is an example layout:
129
+
130
+ ```text
131
+ api/spec/
132
+ ├── index.ts # Spec entrypoint exports defineSpec(...)
133
+ ├── user/
134
+ │ ├── index.ts # Barrel exports for the user resource
135
+ │ ├── userSchema.ts # Zod schemas for the user entity
136
+ │ ├── GetUserDefinition.ts # defineOperation(...) for GET /users/:userId
137
+ │ └── errors/
138
+ │ └── UserNotFoundErrorDefinition.ts
139
+ └── shared/
140
+ ├── sharedResponses.ts # Array of common error responses
141
+ └── ValidationErrorDefinition.ts
192
142
  ```
193
143
 
194
- ### 💻 Sample Definitions
144
+ This is just one way to organize your spec. The directory layout is up to you — typeweaver only
145
+ cares about the `defineSpec(...)` entrypoint, not about folder names or file conventions.
195
146
 
196
- ```typescript
197
- // api/definition/user/userSchema.ts
198
- import { z } from "zod";
147
+ - Resource names come from `defineSpec({ resources: ... })`, not from directory names.
148
+ - Shared responses and schemas can live anywhere that your spec entrypoint imports from.
149
+ - The CLI bundles the entrypoint, so local spec imports should stay within your project.
199
150
 
200
- // General schema for user status
201
- export const userStatusSchema = z.enum(["ACTIVE", "INACTIVE", "SUSPENDED"]);
202
-
203
- // General user schema, can be reused across operations
204
- export const userSchema = z.object({
205
- id: z.uuid(),
206
- name: z.string(),
207
- email: z.email(),
208
- status: userStatusSchema,
209
- createdAt: z.iso.date(),
210
- updatedAt: z.iso.date(),
211
- });
212
- ```
151
+ ### 💻 Sample Spec
213
152
 
214
153
  ```typescript
215
- // api/definition/user/GetUserDefinition.ts
216
- import { HttpOperationDefinition, HttpMethod, HttpStatusCode } from "@rexeus/typeweaver-core";
154
+ // api/spec/user/GetUserDefinition.ts
155
+ import {
156
+ defineOperation,
157
+ defineResponse,
158
+ HttpMethod,
159
+ HttpStatusCode,
160
+ } from "@rexeus/typeweaver-core";
217
161
  import { z } from "zod";
218
162
  import { sharedResponses } from "../shared/sharedResponses";
219
163
  import { userSchema } from "./userSchema";
220
164
  import UserNotFoundErrorDefinition from "./errors/UserNotFoundErrorDefinition";
221
165
 
222
- export default new HttpOperationDefinition({
166
+ export default defineOperation({
223
167
  operationId: "GetUser",
224
168
  method: HttpMethod.GET,
225
169
  path: "/users/:userId",
170
+ summary: "Get a user by id",
226
171
  request: {
227
172
  param: z.object({
228
173
  userId: z.uuid(),
229
174
  }),
230
175
  },
231
176
  responses: [
232
- // - the only success response in this operation is defined inline
233
- // - the response could also be defined in a separate file and be imported here
234
- // - generally also multiple success responses could be defined
235
- // - in this case the "general" user schema is imported and used
236
- {
177
+ defineResponse({
178
+ name: "GetUserSuccess",
237
179
  statusCode: HttpStatusCode.OK,
238
180
  description: "User successfully retrieved",
239
181
  header: z.object({
240
182
  "Content-Type": z.literal("application/json"),
241
183
  }),
242
184
  body: userSchema,
243
- },
244
- UserNotFoundErrorDefinition, // Resource specific response
245
- ...sharedResponses, // Commonly used responses across all operations, e.g. 401, 403, 500...
185
+ }),
186
+ UserNotFoundErrorDefinition,
187
+ ...sharedResponses,
246
188
  ],
247
189
  });
248
190
  ```
249
191
 
250
192
  ```typescript
251
- // api/definition/user/UpdateUserDefinition.ts
252
- import { HttpOperationDefinition, HttpMethod, HttpStatusCode } from "@rexeus/typeweaver-core";
253
- import { z } from "zod";
254
- import { sharedResponses } from "../shared/sharedResponses";
255
- import { userSchema } from "./userSchema";
256
- import UserNotFoundErrorDefinition from "./errors/UserNotFoundErrorDefinition";
257
- import UserStatusTransitionInvalidErrorDefinition from "./errors/UserStatusTransitionInvalidErrorDefinition";
258
-
259
- export default new HttpOperationDefinition({
260
- operationId: "UpdateUser",
261
- method: HttpMethod.PATCH,
262
- path: "/users/:userId",
263
- request: {
264
- param: z.object({
265
- userId: z.uuid(),
266
- }),
267
- // general user schema is processed via zod's pick and partial methods
268
- // to match the update operation's requirements
269
- body: userSchema
270
- .pick({
271
- name: true,
272
- email: true,
273
- status: true,
274
- })
275
- .partial(),
276
- },
277
- responses: [
278
- {
279
- statusCode: HttpStatusCode.OK,
280
- description: "User successfully updated",
281
- header: z.object({
282
- "Content-Type": z.literal("application/json"),
283
- }),
284
- body: userSchema,
193
+ // api/spec/index.ts
194
+ import { defineSpec } from "@rexeus/typeweaver-core";
195
+ import GetUserDefinition from "./user/GetUserDefinition";
196
+
197
+ export default defineSpec({
198
+ resources: {
199
+ user: {
200
+ operations: [GetUserDefinition],
285
201
  },
286
- UserNotFoundErrorDefinition, // Resource specific response
287
- UserStatusTransitionInvalidErrorDefinition, // Resource specific response
288
- ...sharedResponses, // Commonly used responses across all operations, e.g. 401, 403, 500...
289
- ],
202
+ },
290
203
  });
291
204
  ```
292
205
 
293
206
  ```typescript
294
- // api/definition/user/errors/UserNotFoundErrorDefinition.ts
207
+ // api/spec/user/userSchema.ts
295
208
  import { z } from "zod";
296
- import { NotFoundErrorDefinition } from "../../shared";
297
-
298
- // - uses the shared NotFoundErrorDefinition as "base" and extends it
299
- // - adds a specific message and code for the user resource
300
- export default NotFoundErrorDefinition.extend({
301
- name: "UserNotFoundError",
302
- description: "User not found",
303
- body: z.object({
304
- message: z.literal("User not found"),
305
- code: z.literal("USER_NOT_FOUND_ERROR"),
306
- actualValues: z.object({
307
- userId: z.uuid(),
308
- }),
309
- }),
310
- });
311
- ```
312
209
 
313
- ```typescript
314
- // api/definition/user/errors/UserStatusTransitionInvalidErrorDefinition.ts
315
- import { HttpResponseDefinition, HttpStatusCode } from "@rexeus/typeweaver-core";
316
- import { z } from "zod";
317
- import { userStatusSchema } from "../userSchema";
318
-
319
- // could also extend the shared ConflictErrorDefinition:
320
- // export default ConflictErrorDefinition.extend({...});
321
-
322
- // or in this case does not extend a BaseApiError and defines everything itself
323
- export default new HttpResponseDefinition({
324
- name: "UserStatusTransitionInvalidError",
325
- description: "User status transition is conflicting with current status",
326
- body: z.object({
327
- message: z.literal("User status transition is conflicting with current status"),
328
- code: z.literal("USER_STATUS_TRANSITION_INVALID_ERROR"),
329
- context: z.object({
330
- userId: z.uuid(),
331
- currentStatus: userStatusSchema,
332
- }),
333
- actualValues: z.object({
334
- requestedStatus: userStatusSchema,
335
- }),
336
- expectedValues: z.object({
337
- allowedStatuses: z.array(userStatusSchema),
338
- }),
339
- }),
210
+ export const userStatusSchema = z.enum(["ACTIVE", "INACTIVE", "SUSPENDED"]);
211
+
212
+ export const userSchema = z.object({
213
+ id: z.uuid(),
214
+ name: z.string(),
215
+ email: z.email(),
216
+ status: userStatusSchema,
217
+ createdAt: z.iso.date(),
218
+ updatedAt: z.iso.date(),
340
219
  });
341
220
  ```
342
221
 
343
222
  ```typescript
344
- // api/definition/shared/sharedResponses.ts
223
+ // api/spec/shared/sharedResponses.ts
345
224
  import ForbiddenErrorDefinition from "./ForbiddenErrorDefinition";
346
225
  import InternalServerErrorDefinition from "./InternalServerErrorDefinition";
347
226
  import TooManyRequestsErrorDefinition from "./TooManyRequestsErrorDefinition";
@@ -349,8 +228,6 @@ import UnauthorizedErrorDefinition from "./UnauthorizedErrorDefinition";
349
228
  import UnsupportedMediaTypeErrorDefinition from "./UnsupportedMediaTypeErrorDefinition";
350
229
  import ValidationErrorDefinition from "./ValidationErrorDefinition";
351
230
 
352
- // various error responses which are relevant for every operation
353
- // can be spread in the responses array of an HttpOperationDefinition
354
231
  export const sharedResponses = [
355
232
  ForbiddenErrorDefinition,
356
233
  InternalServerErrorDefinition,
@@ -367,31 +244,29 @@ export const sharedResponses = [
367
244
  # Generate with plugins:
368
245
  # - Hono: to easily provide a web server
369
246
  # - Clients: to get fitting API clients
370
- npx typeweaver generate --input ./api/definition --output ./api/generated --plugins clients,hono
247
+ npx typeweaver generate --input ./api/spec/index.ts --output ./api/generated --plugins clients,hono
371
248
  ```
372
249
 
250
+ > The CLI accepts a default export, a named `spec` export, or the module namespace itself as the
251
+ > `SpecDefinition` entrypoint.
252
+
373
253
  ### 🌐 Create Hono web server
374
254
 
375
255
  ```typescript
376
256
  // api/user-handlers.ts
377
- import { HttpResponse, HttpStatusCode } from "@rexeus/typeweaver-core";
378
- import {
379
- type HonoUserApiHandler,
380
- type IGetUserRequest,
381
- GetUserResponse,
382
- GetUserSuccessResponse,
383
- type ICreateUserRequest,
384
- CreateUserResponse,
385
- type IUpdateUserRequest,
386
- UpdateUserResponse,
387
- type IListUserRequest,
388
- ListUserResponse,
389
- } from "./generated";
257
+ import type { Context } from "hono";
258
+ import type { HonoUserApiHandler, IGetUserRequest, GetUserResponse } from "./generated";
259
+ import { createGetUserSuccessResponse } from "./generated";
390
260
 
261
+ // Implement HonoUserApiHandler — the generated interface enforces
262
+ // that every operation in the "user" resource has a handler.
391
263
  export class UserHandlers implements HonoUserApiHandler {
392
264
  public constructor() {}
393
265
 
394
- public async handleGetUserRequest(request: IGetUserRequest): Promise<GetUserResponse> {
266
+ public async handleGetUserRequest(
267
+ request: IGetUserRequest,
268
+ context: Context
269
+ ): Promise<GetUserResponse> {
395
270
  // Simulate fetching user data
396
271
  const fetchedUser = {
397
272
  id: request.param.userId,
@@ -402,8 +277,7 @@ export class UserHandlers implements HonoUserApiHandler {
402
277
  updatedAt: new Date("2023-01-01").toISOString(),
403
278
  };
404
279
 
405
- return new GetUserSuccessResponse({
406
- statusCode: HttpStatusCode.OK,
280
+ return createGetUserSuccessResponse({
407
281
  header: {
408
282
  "Content-Type": "application/json",
409
283
  },
@@ -411,17 +285,9 @@ export class UserHandlers implements HonoUserApiHandler {
411
285
  });
412
286
  }
413
287
 
414
- public handleCreateUserRequest(request: ICreateUserRequest): Promise<CreateUserResponse> {
415
- throw new Error("Not implemented");
416
- }
417
-
418
- public handleUpdateUserRequest(request: IUpdateUserRequest): Promise<UpdateUserResponse> {
419
- throw new Error("Not implemented");
420
- }
421
-
422
- public handleListUserRequest(request: IListUserRequest): Promise<ListUserResponse> {
423
- throw new Error("Not implemented");
424
- }
288
+ // Implement further handlers for each operation in the resource.
289
+ // TypeScript enforces the contract — every handler declared in
290
+ // HonoUserApiHandler must be implemented before the code compiles.
425
291
  }
426
292
  ```
427
293
 
@@ -472,23 +338,21 @@ tsx api/server.ts
472
338
 
473
339
  ```typescript
474
340
  // api/client-test.ts
475
- import { UserClient, GetUserRequestCommand, UserNotFoundErrorResponse } from "./generated";
341
+ import { UserClient, GetUserRequestCommand } from "./generated";
476
342
 
477
343
  const client = new UserClient({ baseUrl: "http://localhost:3000" });
478
344
 
479
- try {
480
- const getUserRequestCommand = new GetUserRequestCommand({
481
- param: { userId: "123" },
482
- });
483
- const result = await client.send(getUserRequestCommand);
484
-
485
- console.log("Successfully fetched user:", result.body);
486
- } catch (error) {
487
- if (error instanceof UserNotFoundErrorResponse) {
488
- console.error("User not found:", error.body);
489
- } else {
490
- console.error("Other error occurred:", error);
491
- }
345
+ const getUserRequestCommand = new GetUserRequestCommand({
346
+ param: { userId: "123" },
347
+ });
348
+ const response = await client.send(getUserRequestCommand);
349
+
350
+ if (response.type === "GetUserSuccess") {
351
+ console.log("Successfully fetched user:", response.body);
352
+ } else if (response.type === "UserNotFoundError") {
353
+ console.error("User not found:", response.body);
354
+ } else {
355
+ console.error("Other error occurred:", response.type);
492
356
  }
493
357
  ```
494
358