@rexeus/typeweaver-server 0.7.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
@@ -46,7 +46,7 @@ npm install @rexeus/typeweaver-core
46
46
  ## 💡 How to use
47
47
 
48
48
  ```bash
49
- npx typeweaver generate --input ./api/definition --output ./api/generated --plugins server
49
+ npx typeweaver generate --input ./api/spec/index.ts --output ./api/generated --plugins server
50
50
  ```
51
51
 
52
52
  More on the CLI in
@@ -113,8 +113,8 @@ export const userHandlers: ServerUserApiHandler = {
113
113
  };
114
114
  ```
115
115
 
116
- > Generated response classes (e.g. `GetUserSuccessResponse`) are also available for when you need
117
- > runtime type checks or `instanceof` discrimination in error handling.
116
+ > Generated response factory functions (e.g. `createGetUserSuccessResponse`) are also available for
117
+ > constructing typed responses with pre-set `type` and `statusCode` discriminators.
118
118
 
119
119
  ### Create the app
120
120
 
@@ -310,48 +310,67 @@ const app = new TypeweaverApp({
310
310
 
311
311
  Each router accepts `TypeweaverRouterOptions`:
312
312
 
313
- | Option | Type | Default | Description |
314
- | -------------------------- | ---------------------------- | ---------- | ---------------------------------- |
315
- | `requestHandlers` | `Server<Resource>ApiHandler` | _required_ | Handler methods for each operation |
316
- | `validateRequests` | `boolean` | `true` | Enable/disable request validation |
317
- | `handleValidationErrors` | `boolean \| function` | `true` | Handle validation errors |
318
- | `handleHttpResponseErrors` | `boolean \| function` | `true` | Handle thrown `HttpResponse` |
319
- | `handleUnknownErrors` | `boolean \| function` | `true` | Handle unexpected errors |
313
+ | Option | Type | Default | Description |
314
+ | -------------------------------- | ---------------------------- | ---------- | ---------------------------------- |
315
+ | `requestHandlers` | `Server<Resource>ApiHandler` | _required_ | Handler methods for each operation |
316
+ | `validateRequests` | `boolean` | `true` | Enable/disable request validation |
317
+ | `validateResponses` | `boolean` | `true` | Enable/disable response validation |
318
+ | `handleRequestValidationErrors` | `boolean \| function` | `true` | Handle request validation errors |
319
+ | `handleResponseValidationErrors` | `boolean \| function` | `true` | Handle response validation errors |
320
+ | `handleHttpResponseErrors` | `boolean \| function` | `true` | Handle thrown typed HTTP responses |
321
+ | `handleUnknownErrors` | `boolean \| function` | `true` | Handle unexpected errors |
320
322
 
321
323
  When set to `true`, error handlers use sensible defaults (400/500 responses). When set to `false`,
322
- errors fall through to the next handler in the chain. When set to a function, it receives the error
323
- and `ServerContext` and must return an `IHttpResponse`.
324
+ errors fall through to the next handler in the chain (except `handleResponseValidationErrors`, where
325
+ `false` means the invalid response is returned as-is — validation still runs for field stripping,
326
+ but invalid responses pass through unchanged). When set to a function, it receives the error and
327
+ `ServerContext` and must return an `IHttpResponse`. If a custom error handler throws, the framework
328
+ catches the exception and falls through gracefully to the next handler.
324
329
 
325
330
  ### 🚨 Error Handling
326
331
 
327
332
  #### Throwing errors in handlers
328
333
 
329
- All generated error response classes (e.g. `NotFoundErrorResponse`, `ValidationErrorResponse`)
330
- extend `HttpResponse`. Throw them in your handlers — the framework catches them automatically:
334
+ Throw any object matching `ITypedHttpResponse` (i.e. `{ type: string, statusCode: number, ... }`)
335
+ from your handlers — the framework catches it automatically and returns it as the response:
331
336
 
332
337
  ```ts
333
338
  import { HttpStatusCode } from "@rexeus/typeweaver-core";
334
- import { GetUserSuccessResponse, NotFoundErrorResponse } from "./generated";
335
339
 
336
340
  async handleGetUserRequest(request) {
337
341
  const user = await db.findUser(request.param.userId);
338
342
  if (!user) {
339
- throw new NotFoundErrorResponse({
343
+ // Plain objects work — anything with `type` and `statusCode` is recognized
344
+ throw {
345
+ type: "NotFoundError",
340
346
  statusCode: HttpStatusCode.NOT_FOUND,
341
347
  header: { "Content-Type": "application/json" },
342
348
  body: { message: "Resource not found", code: "NOT_FOUND_ERROR" },
343
- });
349
+ };
344
350
  }
345
- return new GetUserSuccessResponse({
351
+ return {
352
+ type: "GetUserSuccess",
346
353
  statusCode: HttpStatusCode.OK,
347
354
  header: { "Content-Type": "application/json" },
348
355
  body: user,
349
- });
356
+ };
350
357
  }
351
358
  ```
352
359
 
353
- When `handleHttpResponseErrors` is `true` (the default), thrown `HttpResponse` instances are
354
- returned as-is. No extra configuration needed.
360
+ Generated factory functions (e.g. `createNotFoundErrorResponse`) are a convenient shorthand they
361
+ set `type` and `statusCode` for you so you only pass `header` and `body`:
362
+
363
+ ```ts
364
+ import { createNotFoundErrorResponse } from "./generated";
365
+
366
+ throw createNotFoundErrorResponse({
367
+ header: { "Content-Type": "application/json" },
368
+ body: { message: "Resource not found", code: "NOT_FOUND_ERROR" },
369
+ });
370
+ ```
371
+
372
+ When `handleHttpResponseErrors` is `true` (the default), thrown typed HTTP responses
373
+ (`ITypedHttpResponse`) are returned as-is. No extra configuration needed.
355
374
 
356
375
  #### Custom error mapping
357
376
 
@@ -362,21 +381,21 @@ Use custom handler functions to transform errors into your own response shape.
362
381
  ```ts
363
382
  new UserRouter({
364
383
  requestHandlers: userHandlers,
365
- handleValidationErrors: (error, ctx) =>
366
- new ValidationErrorResponse({
367
- statusCode: HttpStatusCode.BAD_REQUEST,
368
- header: { "Content-Type": "application/json" },
369
- body: {
370
- code: "VALIDATION_ERROR",
371
- message: "Request is invalid",
372
- issues: {
373
- body: error.bodyIssues,
374
- query: error.queryIssues,
375
- param: error.pathParamIssues,
376
- header: error.headerIssues,
377
- },
384
+ handleRequestValidationErrors: (error, ctx) => ({
385
+ type: "ValidationError",
386
+ statusCode: HttpStatusCode.BAD_REQUEST,
387
+ header: { "Content-Type": "application/json" },
388
+ body: {
389
+ code: "VALIDATION_ERROR",
390
+ message: "Request is invalid",
391
+ issues: {
392
+ body: error.bodyIssues,
393
+ query: error.queryIssues,
394
+ param: error.pathParamIssues,
395
+ header: error.headerIssues,
378
396
  },
379
- }),
397
+ },
398
+ }),
380
399
  });
381
400
  ```
382
401
 
@@ -402,25 +421,27 @@ new UserRouter({
402
421
  requestHandlers: userHandlers,
403
422
  handleUnknownErrors: (error, ctx) => {
404
423
  logger.error("Unhandled error", { error, path: ctx.request.path });
405
- return new InternalServerErrorResponse({
424
+ return {
425
+ type: "InternalServerError",
406
426
  statusCode: HttpStatusCode.INTERNAL_SERVER_ERROR,
407
427
  header: { "Content-Type": "application/json" },
408
428
  body: { code: "INTERNAL_SERVER_ERROR", message: "Something went wrong" },
409
- });
429
+ };
410
430
  },
411
431
  });
412
432
  ```
413
433
 
414
434
  ### 📋 Error Responses
415
435
 
416
- | Status | Code | When |
417
- | ------ | ----------------------- | ------------------------------------------------------------- |
418
- | `400` | `BAD_REQUEST` | Malformed request body |
419
- | `400` | Validation issues | `handleValidationErrors: true` and request fails validation |
420
- | `404` | `NOT_FOUND` | No matching route |
421
- | `405` | `METHOD_NOT_ALLOWED` | Route exists but method not allowed (includes `Allow` header) |
422
- | `413` | `PAYLOAD_TOO_LARGE` | Request body exceeds `maxBodySize` |
423
- | `500` | `INTERNAL_SERVER_ERROR` | Unhandled error in handler |
436
+ | Status | Code | When |
437
+ | ------ | ----------------------- | -------------------------------------------------------------------- |
438
+ | `400` | `BAD_REQUEST` | Malformed request body |
439
+ | `400` | Validation issues | `handleRequestValidationErrors: true` and request fails validation |
440
+ | `404` | `NOT_FOUND` | No matching route |
441
+ | `405` | `METHOD_NOT_ALLOWED` | Route exists but method not allowed (includes `Allow` header) |
442
+ | `413` | `PAYLOAD_TOO_LARGE` | Request body exceeds `maxBodySize` |
443
+ | `500` | `INTERNAL_SERVER_ERROR` | `handleResponseValidationErrors: true` and response fails validation |
444
+ | `500` | `INTERNAL_SERVER_ERROR` | Unhandled error in handler |
424
445
 
425
446
  All error responses follow the shape: `{ code: string, message: string }`.
426
447
 
package/dist/index.cjs CHANGED
@@ -6,16 +6,12 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
8
  var __copyProps = (to, from, except, desc) => {
9
- if (from && typeof from === "object" || typeof from === "function") {
10
- for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
- key = keys[i];
12
- if (!__hasOwnProp.call(to, key) && key !== except) {
13
- __defProp(to, key, {
14
- get: ((k) => from[k]).bind(null, key),
15
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
- });
17
- }
18
- }
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
19
15
  }
20
16
  return to;
21
17
  };
@@ -23,7 +19,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
19
  value: mod,
24
20
  enumerable: true
25
21
  }) : target, mod));
26
-
27
22
  //#endregion
28
23
  let node_path = require("node:path");
29
24
  node_path = __toESM(node_path);
@@ -32,77 +27,48 @@ let _rexeus_typeweaver_gen = require("@rexeus/typeweaver-gen");
32
27
  let _rexeus_typeweaver_core = require("@rexeus/typeweaver-core");
33
28
  let case$1 = require("case");
34
29
  case$1 = __toESM(case$1);
35
-
36
- //#region src/RouterGenerator.ts
30
+ //#region src/routerGenerator.ts
37
31
  /**
38
32
  * Generates TypeweaverRouter subclasses from API definitions.
39
33
  *
40
34
  * For each resource (e.g., `Todo`, `Account`), produces a `<ResourceName>Router.ts`
41
35
  * file that extends `TypeweaverRouter` and registers all operations as routes.
42
36
  */
43
- var RouterGenerator = class {
44
- /**
45
- * Generates router files for all resources in the given context.
46
- *
47
- * @param context - The generator context containing resources, templates, and output configuration
48
- */
49
- static generate(context) {
50
- const moduleDir = node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
51
- const templateFile = node_path.default.join(moduleDir, "templates", "Router.ejs");
52
- for (const [entityName, entityResource] of Object.entries(context.resources.entityResources)) this.writeRouter(entityName, templateFile, entityResource.operations, context);
53
- }
54
- static writeRouter(entityName, templateFile, operationResources, context) {
55
- const pascalCaseEntityName = case$1.default.pascal(entityName);
56
- const outputDir = node_path.default.join(context.outputDir, entityName);
57
- const outputPath = node_path.default.join(outputDir, `${pascalCaseEntityName}Router.ts`);
58
- const operations = operationResources.filter((resource) => resource.definition.method !== _rexeus_typeweaver_core.HttpMethod.HEAD).map((resource) => this.createOperationData(resource)).sort((a, b) => this.compareRoutes(a, b));
59
- const content = context.renderTemplate(templateFile, {
60
- coreDir: _rexeus_typeweaver_gen.Path.relative(outputDir, context.outputDir),
61
- entityName,
62
- pascalCaseEntityName,
63
- operations
64
- });
65
- const relativePath = node_path.default.relative(context.outputDir, outputPath);
66
- context.writeFile(relativePath, content);
67
- }
68
- static createOperationData(resource) {
69
- const operationId = resource.definition.operationId;
70
- const className = case$1.default.pascal(operationId);
71
- return {
72
- operationId,
73
- className,
74
- handlerName: `handle${className}Request`,
75
- method: resource.definition.method,
76
- path: resource.definition.path
77
- };
78
- }
79
- static compareRoutes(a, b) {
80
- const aSegments = a.path.split("/").filter((s) => s);
81
- const bSegments = b.path.split("/").filter((s) => s);
82
- if (aSegments.length !== bSegments.length) return aSegments.length - bSegments.length;
83
- for (let i = 0; i < aSegments.length; i++) {
84
- const aSegment = aSegments[i];
85
- const bSegment = bSegments[i];
86
- const aIsParam = aSegment.startsWith(":");
87
- if (aIsParam !== bSegment.startsWith(":")) return aIsParam ? 1 : -1;
88
- if (aSegment !== bSegment) return aSegment.localeCompare(bSegment);
89
- }
90
- return this.getMethodPriority(a.method) - this.getMethodPriority(b.method);
91
- }
92
- static METHOD_PRIORITY = {
93
- GET: 1,
94
- POST: 2,
95
- PUT: 3,
96
- PATCH: 4,
97
- DELETE: 5,
98
- OPTIONS: 6,
99
- HEAD: 7
37
+ /**
38
+ * Generates router files for all resources in the given context.
39
+ *
40
+ * @param context - The generator context containing resources, templates, and output configuration
41
+ */
42
+ function generate(context) {
43
+ const moduleDir = node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
44
+ const templateFile = node_path.default.join(moduleDir, "templates", "Router.ejs");
45
+ for (const resource of context.normalizedSpec.resources) writeRouter(resource, templateFile, context);
46
+ }
47
+ function writeRouter(resource, templateFile, context) {
48
+ const pascalCaseEntityName = case$1.default.pascal(resource.name);
49
+ const outputDir = context.getResourceOutputDir(resource.name);
50
+ const outputPath = node_path.default.join(outputDir, `${pascalCaseEntityName}Router.ts`);
51
+ const operations = resource.operations.filter((operation) => operation.method !== _rexeus_typeweaver_core.HttpMethod.HEAD).map((operation) => createOperationData(operation)).sort((a, b) => (0, _rexeus_typeweaver_gen.compareRoutes)(a, b));
52
+ const content = context.renderTemplate(templateFile, {
53
+ coreDir: (0, _rexeus_typeweaver_gen.relative)(outputDir, context.outputDir),
54
+ entityName: resource.name,
55
+ pascalCaseEntityName,
56
+ operations
57
+ });
58
+ const relativePath = node_path.default.relative(context.outputDir, outputPath);
59
+ context.writeFile(relativePath, content);
60
+ }
61
+ function createOperationData(operation) {
62
+ const operationId = operation.operationId;
63
+ const className = case$1.default.pascal(operationId);
64
+ return {
65
+ operationId,
66
+ className,
67
+ handlerName: `handle${className}Request`,
68
+ method: operation.method,
69
+ path: operation.path
100
70
  };
101
- static getMethodPriority(method) {
102
- return this.METHOD_PRIORITY[method] ?? 999;
103
- }
104
- };
105
-
71
+ }
106
72
  //#endregion
107
73
  //#region src/index.ts
108
74
  const moduleDir = node_path.default.dirname((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
@@ -123,9 +89,8 @@ var ServerPlugin = class extends _rexeus_typeweaver_gen.BasePlugin {
123
89
  generate(context) {
124
90
  const libSourceDir = node_path.default.join(moduleDir, "lib");
125
91
  this.copyLibFiles(context, libSourceDir, this.name);
126
- RouterGenerator.generate(context);
92
+ generate(context);
127
93
  }
128
94
  };
129
-
130
95
  //#endregion
131
- module.exports = ServerPlugin;
96
+ module.exports = ServerPlugin;
package/dist/index.mjs CHANGED
@@ -1,79 +1,50 @@
1
1
  import path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
- import { BasePlugin, Path } from "@rexeus/typeweaver-gen";
3
+ import { BasePlugin, compareRoutes, relative } from "@rexeus/typeweaver-gen";
4
4
  import { HttpMethod } from "@rexeus/typeweaver-core";
5
5
  import Case from "case";
6
-
7
- //#region src/RouterGenerator.ts
6
+ //#region src/routerGenerator.ts
8
7
  /**
9
8
  * Generates TypeweaverRouter subclasses from API definitions.
10
9
  *
11
10
  * For each resource (e.g., `Todo`, `Account`), produces a `<ResourceName>Router.ts`
12
11
  * file that extends `TypeweaverRouter` and registers all operations as routes.
13
12
  */
14
- var RouterGenerator = class {
15
- /**
16
- * Generates router files for all resources in the given context.
17
- *
18
- * @param context - The generator context containing resources, templates, and output configuration
19
- */
20
- static generate(context) {
21
- const moduleDir = path.dirname(fileURLToPath(import.meta.url));
22
- const templateFile = path.join(moduleDir, "templates", "Router.ejs");
23
- for (const [entityName, entityResource] of Object.entries(context.resources.entityResources)) this.writeRouter(entityName, templateFile, entityResource.operations, context);
24
- }
25
- static writeRouter(entityName, templateFile, operationResources, context) {
26
- const pascalCaseEntityName = Case.pascal(entityName);
27
- const outputDir = path.join(context.outputDir, entityName);
28
- const outputPath = path.join(outputDir, `${pascalCaseEntityName}Router.ts`);
29
- const operations = operationResources.filter((resource) => resource.definition.method !== HttpMethod.HEAD).map((resource) => this.createOperationData(resource)).sort((a, b) => this.compareRoutes(a, b));
30
- const content = context.renderTemplate(templateFile, {
31
- coreDir: Path.relative(outputDir, context.outputDir),
32
- entityName,
33
- pascalCaseEntityName,
34
- operations
35
- });
36
- const relativePath = path.relative(context.outputDir, outputPath);
37
- context.writeFile(relativePath, content);
38
- }
39
- static createOperationData(resource) {
40
- const operationId = resource.definition.operationId;
41
- const className = Case.pascal(operationId);
42
- return {
43
- operationId,
44
- className,
45
- handlerName: `handle${className}Request`,
46
- method: resource.definition.method,
47
- path: resource.definition.path
48
- };
49
- }
50
- static compareRoutes(a, b) {
51
- const aSegments = a.path.split("/").filter((s) => s);
52
- const bSegments = b.path.split("/").filter((s) => s);
53
- if (aSegments.length !== bSegments.length) return aSegments.length - bSegments.length;
54
- for (let i = 0; i < aSegments.length; i++) {
55
- const aSegment = aSegments[i];
56
- const bSegment = bSegments[i];
57
- const aIsParam = aSegment.startsWith(":");
58
- if (aIsParam !== bSegment.startsWith(":")) return aIsParam ? 1 : -1;
59
- if (aSegment !== bSegment) return aSegment.localeCompare(bSegment);
60
- }
61
- return this.getMethodPriority(a.method) - this.getMethodPriority(b.method);
62
- }
63
- static METHOD_PRIORITY = {
64
- GET: 1,
65
- POST: 2,
66
- PUT: 3,
67
- PATCH: 4,
68
- DELETE: 5,
69
- OPTIONS: 6,
70
- HEAD: 7
13
+ /**
14
+ * Generates router files for all resources in the given context.
15
+ *
16
+ * @param context - The generator context containing resources, templates, and output configuration
17
+ */
18
+ function generate(context) {
19
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
20
+ const templateFile = path.join(moduleDir, "templates", "Router.ejs");
21
+ for (const resource of context.normalizedSpec.resources) writeRouter(resource, templateFile, context);
22
+ }
23
+ function writeRouter(resource, templateFile, context) {
24
+ const pascalCaseEntityName = Case.pascal(resource.name);
25
+ const outputDir = context.getResourceOutputDir(resource.name);
26
+ const outputPath = path.join(outputDir, `${pascalCaseEntityName}Router.ts`);
27
+ const operations = resource.operations.filter((operation) => operation.method !== HttpMethod.HEAD).map((operation) => createOperationData(operation)).sort((a, b) => compareRoutes(a, b));
28
+ const content = context.renderTemplate(templateFile, {
29
+ coreDir: relative(outputDir, context.outputDir),
30
+ entityName: resource.name,
31
+ pascalCaseEntityName,
32
+ operations
33
+ });
34
+ const relativePath = path.relative(context.outputDir, outputPath);
35
+ context.writeFile(relativePath, content);
36
+ }
37
+ function createOperationData(operation) {
38
+ const operationId = operation.operationId;
39
+ const className = Case.pascal(operationId);
40
+ return {
41
+ operationId,
42
+ className,
43
+ handlerName: `handle${className}Request`,
44
+ method: operation.method,
45
+ path: operation.path
71
46
  };
72
- static getMethodPriority(method) {
73
- return this.METHOD_PRIORITY[method] ?? 999;
74
- }
75
- };
76
-
47
+ }
77
48
  //#endregion
78
49
  //#region src/index.ts
79
50
  const moduleDir = path.dirname(fileURLToPath(import.meta.url));
@@ -94,10 +65,10 @@ var ServerPlugin = class extends BasePlugin {
94
65
  generate(context) {
95
66
  const libSourceDir = path.join(moduleDir, "lib");
96
67
  this.copyLibFiles(context, libSourceDir, this.name);
97
- RouterGenerator.generate(context);
68
+ generate(context);
98
69
  }
99
70
  };
100
-
101
71
  //#endregion
102
72
  export { ServerPlugin as default };
73
+
103
74
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/RouterGenerator.ts","../src/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { HttpMethod } from \"@rexeus/typeweaver-core\";\nimport { Path } from \"@rexeus/typeweaver-gen\";\nimport type {\n GeneratorContext,\n OperationResource,\n} from \"@rexeus/typeweaver-gen\";\nimport Case from \"case\";\n\ntype OperationData = {\n readonly operationId: string;\n readonly className: string;\n readonly handlerName: string;\n readonly method: string;\n readonly path: string;\n};\n\n/**\n * Generates TypeweaverRouter subclasses from API definitions.\n *\n * For each resource (e.g., `Todo`, `Account`), produces a `<ResourceName>Router.ts`\n * file that extends `TypeweaverRouter` and registers all operations as routes.\n */\nexport class RouterGenerator {\n /**\n * Generates router files for all resources in the given context.\n *\n * @param context - The generator context containing resources, templates, and output configuration\n */\n public static generate(context: GeneratorContext): void {\n const moduleDir = path.dirname(fileURLToPath(import.meta.url));\n const templateFile = path.join(moduleDir, \"templates\", \"Router.ejs\");\n\n for (const [entityName, entityResource] of Object.entries(\n context.resources.entityResources\n )) {\n this.writeRouter(\n entityName,\n templateFile,\n entityResource.operations,\n context\n );\n }\n }\n\n private static writeRouter(\n entityName: string,\n templateFile: string,\n operationResources: OperationResource[],\n context: GeneratorContext\n ): void {\n const pascalCaseEntityName = Case.pascal(entityName);\n const outputDir = path.join(context.outputDir, entityName);\n const outputPath = path.join(outputDir, `${pascalCaseEntityName}Router.ts`);\n\n const operations = operationResources\n .filter(resource => resource.definition.method !== HttpMethod.HEAD)\n .map(resource => this.createOperationData(resource))\n .sort((a, b) => this.compareRoutes(a, b));\n\n const content = context.renderTemplate(templateFile, {\n coreDir: Path.relative(outputDir, context.outputDir),\n entityName,\n pascalCaseEntityName,\n operations,\n });\n\n const relativePath = path.relative(context.outputDir, outputPath);\n context.writeFile(relativePath, content);\n }\n\n private static createOperationData(\n resource: OperationResource\n ): OperationData {\n const operationId = resource.definition.operationId;\n const className = Case.pascal(operationId);\n\n return {\n operationId,\n className,\n handlerName: `handle${className}Request`,\n method: resource.definition.method,\n path: resource.definition.path,\n };\n }\n\n private static compareRoutes(a: OperationData, b: OperationData): number {\n const aSegments = a.path.split(\"/\").filter(s => s);\n const bSegments = b.path.split(\"/\").filter(s => s);\n\n // 1. Compare by depth first (shallow to deep)\n if (aSegments.length !== bSegments.length) {\n return aSegments.length - bSegments.length;\n }\n\n // 2. Compare segment by segment\n for (let i = 0; i < aSegments.length; i++) {\n const aSegment = aSegments[i]!;\n const bSegment = bSegments[i]!;\n\n const aIsParam = aSegment.startsWith(\":\");\n const bIsParam = bSegment.startsWith(\":\");\n\n // Static segments before parameters\n if (aIsParam !== bIsParam) {\n return aIsParam ? 1 : -1;\n }\n\n // Within same type, alphabetical order\n if (aSegment !== bSegment) {\n return aSegment.localeCompare(bSegment);\n }\n }\n\n // 3. Same path = sort by HTTP method priority\n return this.getMethodPriority(a.method) - this.getMethodPriority(b.method);\n }\n\n private static readonly METHOD_PRIORITY: Record<string, number> = {\n GET: 1,\n POST: 2,\n PUT: 3,\n PATCH: 4,\n DELETE: 5,\n OPTIONS: 6,\n HEAD: 7,\n };\n\n private static getMethodPriority(method: string): number {\n return this.METHOD_PRIORITY[method] ?? 999;\n }\n}\n","import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { BasePlugin } from \"@rexeus/typeweaver-gen\";\nimport type { GeneratorContext } from \"@rexeus/typeweaver-gen\";\nimport { RouterGenerator } from \"./RouterGenerator\";\n\nconst moduleDir = path.dirname(fileURLToPath(import.meta.url));\n\n/**\n * Typeweaver plugin that generates a lightweight, dependency-free server\n * with built-in routing and middleware support.\n *\n * Copies the runtime library files (`TypeweaverApp`, `TypeweaverRouter`, `Router`,\n * `Middleware`, etc.) and generates typed router classes for each resource.\n */\nexport default class ServerPlugin extends BasePlugin {\n public name = \"server\";\n\n /**\n * Generates the server runtime and typed routers for all resources.\n *\n * @param context - The generator context\n */\n public override generate(context: GeneratorContext): void {\n const libSourceDir = path.join(moduleDir, \"lib\");\n this.copyLibFiles(context, libSourceDir, this.name);\n\n RouterGenerator.generate(context);\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAwBA,IAAa,kBAAb,MAA6B;;;;;;CAM3B,OAAc,SAAS,SAAiC;EACtD,MAAM,YAAY,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;EAC9D,MAAM,eAAe,KAAK,KAAK,WAAW,aAAa,aAAa;AAEpE,OAAK,MAAM,CAAC,YAAY,mBAAmB,OAAO,QAChD,QAAQ,UAAU,gBACnB,CACC,MAAK,YACH,YACA,cACA,eAAe,YACf,QACD;;CAIL,OAAe,YACb,YACA,cACA,oBACA,SACM;EACN,MAAM,uBAAuB,KAAK,OAAO,WAAW;EACpD,MAAM,YAAY,KAAK,KAAK,QAAQ,WAAW,WAAW;EAC1D,MAAM,aAAa,KAAK,KAAK,WAAW,GAAG,qBAAqB,WAAW;EAE3E,MAAM,aAAa,mBAChB,QAAO,aAAY,SAAS,WAAW,WAAW,WAAW,KAAK,CAClE,KAAI,aAAY,KAAK,oBAAoB,SAAS,CAAC,CACnD,MAAM,GAAG,MAAM,KAAK,cAAc,GAAG,EAAE,CAAC;EAE3C,MAAM,UAAU,QAAQ,eAAe,cAAc;GACnD,SAAS,KAAK,SAAS,WAAW,QAAQ,UAAU;GACpD;GACA;GACA;GACD,CAAC;EAEF,MAAM,eAAe,KAAK,SAAS,QAAQ,WAAW,WAAW;AACjE,UAAQ,UAAU,cAAc,QAAQ;;CAG1C,OAAe,oBACb,UACe;EACf,MAAM,cAAc,SAAS,WAAW;EACxC,MAAM,YAAY,KAAK,OAAO,YAAY;AAE1C,SAAO;GACL;GACA;GACA,aAAa,SAAS,UAAU;GAChC,QAAQ,SAAS,WAAW;GAC5B,MAAM,SAAS,WAAW;GAC3B;;CAGH,OAAe,cAAc,GAAkB,GAA0B;EACvE,MAAM,YAAY,EAAE,KAAK,MAAM,IAAI,CAAC,QAAO,MAAK,EAAE;EAClD,MAAM,YAAY,EAAE,KAAK,MAAM,IAAI,CAAC,QAAO,MAAK,EAAE;AAGlD,MAAI,UAAU,WAAW,UAAU,OACjC,QAAO,UAAU,SAAS,UAAU;AAItC,OAAK,IAAI,IAAI,GAAG,IAAI,UAAU,QAAQ,KAAK;GACzC,MAAM,WAAW,UAAU;GAC3B,MAAM,WAAW,UAAU;GAE3B,MAAM,WAAW,SAAS,WAAW,IAAI;AAIzC,OAAI,aAHa,SAAS,WAAW,IAAI,CAIvC,QAAO,WAAW,IAAI;AAIxB,OAAI,aAAa,SACf,QAAO,SAAS,cAAc,SAAS;;AAK3C,SAAO,KAAK,kBAAkB,EAAE,OAAO,GAAG,KAAK,kBAAkB,EAAE,OAAO;;CAG5E,OAAwB,kBAA0C;EAChE,KAAK;EACL,MAAM;EACN,KAAK;EACL,OAAO;EACP,QAAQ;EACR,SAAS;EACT,MAAM;EACP;CAED,OAAe,kBAAkB,QAAwB;AACvD,SAAO,KAAK,gBAAgB,WAAW;;;;;;AC5H3C,MAAM,YAAY,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;;;;;;;;AAS9D,IAAqB,eAArB,cAA0C,WAAW;CACnD,AAAO,OAAO;;;;;;CAOd,AAAgB,SAAS,SAAiC;EACxD,MAAM,eAAe,KAAK,KAAK,WAAW,MAAM;AAChD,OAAK,aAAa,SAAS,cAAc,KAAK,KAAK;AAEnD,kBAAgB,SAAS,QAAQ"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/routerGenerator.ts","../src/index.ts"],"sourcesContent":["import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { HttpMethod } from \"@rexeus/typeweaver-core\";\nimport { compareRoutes, relative } from \"@rexeus/typeweaver-gen\";\nimport type {\n GeneratorContext,\n NormalizedOperation,\n NormalizedResource,\n} from \"@rexeus/typeweaver-gen\";\nimport Case from \"case\";\n\ntype OperationData = {\n readonly operationId: string;\n readonly className: string;\n readonly handlerName: string;\n readonly method: string;\n readonly path: string;\n};\n\n/**\n * Generates TypeweaverRouter subclasses from API definitions.\n *\n * For each resource (e.g., `Todo`, `Account`), produces a `<ResourceName>Router.ts`\n * file that extends `TypeweaverRouter` and registers all operations as routes.\n */\n\n/**\n * Generates router files for all resources in the given context.\n *\n * @param context - The generator context containing resources, templates, and output configuration\n */\nexport function generate(context: GeneratorContext): void {\n const moduleDir = path.dirname(fileURLToPath(import.meta.url));\n const templateFile = path.join(moduleDir, \"templates\", \"Router.ejs\");\n\n for (const resource of context.normalizedSpec.resources) {\n writeRouter(resource, templateFile, context);\n }\n}\n\nfunction writeRouter(\n resource: NormalizedResource,\n templateFile: string,\n context: GeneratorContext\n): void {\n const pascalCaseEntityName = Case.pascal(resource.name);\n const outputDir = context.getResourceOutputDir(resource.name);\n const outputPath = path.join(outputDir, `${pascalCaseEntityName}Router.ts`);\n\n const operations = resource.operations\n .filter(operation => operation.method !== HttpMethod.HEAD)\n .map(operation => createOperationData(operation))\n .sort((a, b) => compareRoutes(a, b));\n\n const content = context.renderTemplate(templateFile, {\n coreDir: relative(outputDir, context.outputDir),\n entityName: resource.name,\n pascalCaseEntityName,\n operations,\n });\n\n const relativePath = path.relative(context.outputDir, outputPath);\n context.writeFile(relativePath, content);\n}\n\nfunction createOperationData(operation: NormalizedOperation): OperationData {\n const operationId = operation.operationId;\n const className = Case.pascal(operationId);\n\n return {\n operationId,\n className,\n handlerName: `handle${className}Request`,\n method: operation.method,\n path: operation.path,\n };\n}\n","import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { BasePlugin } from \"@rexeus/typeweaver-gen\";\nimport type { GeneratorContext } from \"@rexeus/typeweaver-gen\";\nimport { generate as generateRouters } from \"./routerGenerator\";\n\nconst moduleDir = path.dirname(fileURLToPath(import.meta.url));\n\n/**\n * Typeweaver plugin that generates a lightweight, dependency-free server\n * with built-in routing and middleware support.\n *\n * Copies the runtime library files (`TypeweaverApp`, `TypeweaverRouter`, `Router`,\n * `Middleware`, etc.) and generates typed router classes for each resource.\n */\nexport default class ServerPlugin extends BasePlugin {\n public name = \"server\";\n\n /**\n * Generates the server runtime and typed routers for all resources.\n *\n * @param context - The generator context\n */\n public override generate(context: GeneratorContext): void {\n const libSourceDir = path.join(moduleDir, \"lib\");\n this.copyLibFiles(context, libSourceDir, this.name);\n\n generateRouters(context);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA+BA,SAAgB,SAAS,SAAiC;CACxD,MAAM,YAAY,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;CAC9D,MAAM,eAAe,KAAK,KAAK,WAAW,aAAa,aAAa;AAEpE,MAAK,MAAM,YAAY,QAAQ,eAAe,UAC5C,aAAY,UAAU,cAAc,QAAQ;;AAIhD,SAAS,YACP,UACA,cACA,SACM;CACN,MAAM,uBAAuB,KAAK,OAAO,SAAS,KAAK;CACvD,MAAM,YAAY,QAAQ,qBAAqB,SAAS,KAAK;CAC7D,MAAM,aAAa,KAAK,KAAK,WAAW,GAAG,qBAAqB,WAAW;CAE3E,MAAM,aAAa,SAAS,WACzB,QAAO,cAAa,UAAU,WAAW,WAAW,KAAK,CACzD,KAAI,cAAa,oBAAoB,UAAU,CAAC,CAChD,MAAM,GAAG,MAAM,cAAc,GAAG,EAAE,CAAC;CAEtC,MAAM,UAAU,QAAQ,eAAe,cAAc;EACnD,SAAS,SAAS,WAAW,QAAQ,UAAU;EAC/C,YAAY,SAAS;EACrB;EACA;EACD,CAAC;CAEF,MAAM,eAAe,KAAK,SAAS,QAAQ,WAAW,WAAW;AACjE,SAAQ,UAAU,cAAc,QAAQ;;AAG1C,SAAS,oBAAoB,WAA+C;CAC1E,MAAM,cAAc,UAAU;CAC9B,MAAM,YAAY,KAAK,OAAO,YAAY;AAE1C,QAAO;EACL;EACA;EACA,aAAa,SAAS,UAAU;EAChC,QAAQ,UAAU;EAClB,MAAM,UAAU;EACjB;;;;ACrEH,MAAM,YAAY,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;;;;;;;;AAS9D,IAAqB,eAArB,cAA0C,WAAW;CACnD,OAAc;;;;;;CAOd,SAAyB,SAAiC;EACxD,MAAM,eAAe,KAAK,KAAK,WAAW,MAAM;AAChD,OAAK,aAAa,SAAS,cAAc,KAAK,KAAK;AAEnD,WAAgB,QAAQ"}
@@ -5,6 +5,11 @@
5
5
  * @generated by @rexeus/typeweaver
6
6
  */
7
7
 
8
+ import {
9
+ createDefaultErrorBody,
10
+ internalServerErrorDefaultError,
11
+ payloadTooLargeDefaultError,
12
+ } from "@rexeus/typeweaver-core";
8
13
  import { PayloadTooLargeError } from "./Errors";
9
14
  import type { TypeweaverApp } from "./TypeweaverApp";
10
15
  import type { IncomingMessage, ServerResponse } from "node:http";
@@ -73,26 +78,24 @@ async function handleRequest(
73
78
  } catch (error) {
74
79
  if (error instanceof PayloadTooLargeError) {
75
80
  if (!res.headersSent) {
76
- res.writeHead(413, { "content-type": "application/json" });
81
+ res.writeHead(payloadTooLargeDefaultError.statusCode, {
82
+ "content-type": "application/json",
83
+ });
77
84
  }
78
85
  res.end(
79
- JSON.stringify({
80
- code: "PAYLOAD_TOO_LARGE",
81
- message: "Request body exceeds the size limit",
82
- })
86
+ JSON.stringify(createDefaultErrorBody(payloadTooLargeDefaultError))
83
87
  );
84
88
  return;
85
89
  }
86
90
 
87
91
  console.error(error);
88
92
  if (!res.headersSent) {
89
- res.writeHead(500, { "content-type": "application/json" });
93
+ res.writeHead(internalServerErrorDefaultError.statusCode, {
94
+ "content-type": "application/json",
95
+ });
90
96
  }
91
97
  res.end(
92
- JSON.stringify({
93
- code: "INTERNAL_SERVER_ERROR",
94
- message: "An unexpected error occurred",
95
- })
98
+ JSON.stringify(createDefaultErrorBody(internalServerErrorDefaultError))
96
99
  );
97
100
  }
98
101
  }
@@ -9,7 +9,10 @@ import type {
9
9
  HttpMethod,
10
10
  IHttpResponse,
11
11
  IRequestValidator,
12
+ IResponseValidator,
13
+ ITypedHttpResponse,
12
14
  RequestValidationError,
15
+ ResponseValidationError,
13
16
  } from "@rexeus/typeweaver-core";
14
17
  import type { RequestHandler } from "./RequestHandler";
15
18
  import type { ServerContext } from "./ServerContext";
@@ -30,7 +33,8 @@ export type RouteDefinition = {
30
33
  readonly operationId: string;
31
34
  readonly method: HttpMethod;
32
35
  readonly path: string;
33
- readonly validator: IRequestValidator;
36
+ readonly requestValidator: IRequestValidator;
37
+ readonly responseValidator: IResponseValidator;
34
38
  readonly handler: RequestHandler<any, any, any>;
35
39
  /** Reference to the router config for error handling. */
36
40
  readonly routerConfig: RouterErrorConfig;
@@ -41,28 +45,44 @@ export type RouteDefinition = {
41
45
  */
42
46
  export type RouterErrorConfig = {
43
47
  readonly validateRequests: boolean;
48
+ readonly validateResponses: boolean;
44
49
  readonly handleHttpResponseErrors: HttpResponseErrorHandler | boolean;
45
- readonly handleValidationErrors: ValidationErrorHandler | boolean;
50
+ readonly handleRequestValidationErrors:
51
+ | RequestValidationErrorHandler
52
+ | boolean;
53
+ readonly handleResponseValidationErrors:
54
+ | ResponseValidationErrorHandler
55
+ | boolean;
46
56
  readonly handleUnknownErrors: UnknownErrorHandler | boolean;
47
57
  };
48
58
 
49
59
  /**
50
60
  * Handles HTTP response errors thrown by request handlers.
51
- * The error parameter is an `HttpResponse` instance (thrown via `throw new HttpResponse(...)`).
61
+ * The error parameter is a typed HTTP response object (thrown via `throw { type, statusCode, ... }`).
52
62
  */
53
63
  export type HttpResponseErrorHandler = (
54
- error: IHttpResponse,
64
+ error: ITypedHttpResponse,
55
65
  ctx: ServerContext
56
66
  ) => Promise<IHttpResponse> | IHttpResponse;
57
67
 
58
68
  /**
59
69
  * Handles request validation errors.
60
70
  */
61
- export type ValidationErrorHandler = (
71
+ export type RequestValidationErrorHandler = (
62
72
  error: RequestValidationError,
63
73
  ctx: ServerContext
64
74
  ) => Promise<IHttpResponse> | IHttpResponse;
65
75
 
76
+ /**
77
+ * Handles response validation errors.
78
+ * Called when a handler returns a response that does not match the expected schema.
79
+ */
80
+ export type ResponseValidationErrorHandler = (
81
+ error: ResponseValidationError,
82
+ response: IHttpResponse,
83
+ ctx: ServerContext
84
+ ) => Promise<IHttpResponse> | IHttpResponse;
85
+
66
86
  /**
67
87
  * Handles any unknown errors not caught by other handlers.
68
88
  */
@@ -5,7 +5,18 @@
5
5
  * @generated by @rexeus/typeweaver
6
6
  */
7
7
 
8
- import { HttpResponse, RequestValidationError } from "@rexeus/typeweaver-core";
8
+ import {
9
+ badRequestDefaultError,
10
+ createDefaultErrorBody,
11
+ createDefaultErrorResponse,
12
+ internalServerErrorDefaultError,
13
+ isTypedHttpResponse,
14
+ methodNotAllowedDefaultError,
15
+ notFoundDefaultError,
16
+ payloadTooLargeDefaultError,
17
+ RequestValidationError,
18
+ validationDefaultError,
19
+ } from "@rexeus/typeweaver-core";
9
20
  import type { IHttpResponse } from "@rexeus/typeweaver-core";
10
21
  import { BodyParseError, PayloadTooLargeError } from "./Errors";
11
22
  import { FetchApiAdapter } from "./FetchApiAdapter";
@@ -16,10 +27,11 @@ import type { Middleware } from "./Middleware";
16
27
  import type { RequestHandler } from "./RequestHandler";
17
28
  import type {
18
29
  HttpResponseErrorHandler,
30
+ ResponseValidationErrorHandler,
19
31
  RouteDefinition,
20
32
  RouteMatch,
21
33
  UnknownErrorHandler,
22
- ValidationErrorHandler,
34
+ RequestValidationErrorHandler,
23
35
  } from "./Router";
24
36
  import type { ServerContext } from "./ServerContext";
25
37
  import type { StateRequirementError, TypedMiddleware } from "./TypedMiddleware";
@@ -57,10 +69,9 @@ export type TypeweaverAppOptions = {
57
69
  };
58
70
 
59
71
  export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
60
- private static readonly INTERNAL_SERVER_ERROR_BODY = {
61
- code: "INTERNAL_SERVER_ERROR",
62
- message: "An unexpected error occurred",
63
- } as const;
72
+ private static readonly INTERNAL_SERVER_ERROR_BODY = createDefaultErrorBody(
73
+ internalServerErrorDefaultError
74
+ );
64
75
 
65
76
  private readonly router = new Router();
66
77
  private readonly middlewares: Middleware[] = [];
@@ -164,19 +175,14 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
164
175
  } catch (error) {
165
176
  if (error instanceof PayloadTooLargeError) {
166
177
  this.safeOnError(error);
167
- return this.adapter.toResponse({
168
- statusCode: 413,
169
- body: {
170
- code: "PAYLOAD_TOO_LARGE",
171
- message: "Request body exceeds the size limit",
172
- },
173
- });
178
+ return this.adapter.toResponse(
179
+ createDefaultErrorResponse(payloadTooLargeDefaultError)
180
+ );
174
181
  }
175
182
  if (error instanceof BodyParseError) {
176
- return this.adapter.toResponse({
177
- statusCode: 400,
178
- body: { code: "BAD_REQUEST", message: "Malformed request body" },
179
- });
183
+ return this.adapter.toResponse(
184
+ createDefaultErrorResponse(badRequestDefaultError)
185
+ );
180
186
  }
181
187
  this.safeOnError(error);
182
188
  return TypeweaverApp.createErrorResponse();
@@ -224,28 +230,27 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
224
230
  if (match) {
225
231
  const routeCtx = this.withPathParams(ctx, match.params);
226
232
  try {
227
- return await this.executeHandler(routeCtx, match.route);
233
+ const response = await this.executeHandler(routeCtx, match.route);
234
+ return await this.validateResponse(match.route, response, routeCtx);
228
235
  } catch (error) {
236
+ if (
237
+ isTypedHttpResponse(error) &&
238
+ match.route.routerConfig.validateResponses
239
+ ) {
240
+ return await this.validateResponse(match.route, error, routeCtx);
241
+ }
229
242
  return this.handleError(error, routeCtx, match.route);
230
243
  }
231
244
  }
232
245
 
233
246
  const pathMatch = this.router.matchPath(pathname);
234
247
  if (pathMatch) {
235
- return {
236
- statusCode: 405,
248
+ return createDefaultErrorResponse(methodNotAllowedDefaultError, {
237
249
  header: { Allow: pathMatch.allowedMethods.join(", ") },
238
- body: {
239
- code: "METHOD_NOT_ALLOWED",
240
- message: "Method not supported for this resource",
241
- },
242
- };
250
+ });
243
251
  }
244
252
 
245
- return {
246
- statusCode: 404,
247
- body: { code: "NOT_FOUND", message: "No matching resource found" },
248
- };
253
+ return createDefaultErrorResponse(notFoundDefaultError);
249
254
  }
250
255
 
251
256
  private withPathParams(
@@ -261,44 +266,123 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
261
266
  route: RouteDefinition
262
267
  ): Promise<IHttpResponse> {
263
268
  const validatedRequest = route.routerConfig.validateRequests
264
- ? route.validator.validate(ctx.request)
269
+ ? route.requestValidator.validate(ctx.request)
265
270
  : ctx.request;
266
271
 
267
272
  return route.handler(validatedRequest, ctx);
268
273
  }
269
274
 
275
+ /**
276
+ * Validates a response against the operation's response validator.
277
+ *
278
+ * Behavior depends on configuration:
279
+ * - `validateResponses: false` → returns the original response unchanged.
280
+ * - `validateResponses: true` (default) → runs validation:
281
+ * - Valid response → returns the stripped response (extra fields removed).
282
+ * - Invalid response + handler configured → calls the handler safely.
283
+ * If the handler throws, falls back to the original response.
284
+ * - Invalid response + `handleResponseValidationErrors: false` → returns
285
+ * the original (invalid) response as-is.
286
+ *
287
+ * @param route - The route definition containing the response validator and config
288
+ * @param response - The response to validate
289
+ * @param ctx - The server context for the current request
290
+ * @returns The validated (and stripped) response, the handler's response, or the original
291
+ */
292
+ private async validateResponse(
293
+ route: RouteDefinition,
294
+ response: IHttpResponse,
295
+ ctx: ServerContext
296
+ ): Promise<IHttpResponse> {
297
+ if (!route.routerConfig.validateResponses) return response;
298
+
299
+ const result = route.responseValidator.safeValidate(response);
300
+
301
+ if (result.isValid) return result.data;
302
+
303
+ const handler = this.resolveErrorHandler<ResponseValidationErrorHandler>(
304
+ route.routerConfig.handleResponseValidationErrors,
305
+ TypeweaverApp.defaultResponseValidationHandler
306
+ );
307
+
308
+ if (handler) {
309
+ const handlerResponse = await this.safelyExecuteErrorHandler(() =>
310
+ handler(result.error, response, ctx)
311
+ );
312
+ if (handlerResponse) return handlerResponse;
313
+ }
314
+
315
+ return response;
316
+ }
317
+
318
+ /**
319
+ * Safely executes an error handler and returns null if it fails.
320
+ * This allows for graceful fallback to the next handler in the chain
321
+ * without crashing the request pipeline.
322
+ *
323
+ * If the handler throws, the error is reported via `safeOnError`
324
+ * and null is returned so the caller can fall through to the next handler.
325
+ *
326
+ * @param handlerFn - Function that executes the error handler
327
+ * @returns The handler's response if successful, null if the handler throws
328
+ */
329
+ private async safelyExecuteErrorHandler(
330
+ handlerFn: () => Promise<IHttpResponse> | IHttpResponse
331
+ ): Promise<IHttpResponse | null> {
332
+ try {
333
+ return await handlerFn();
334
+ } catch (error) {
335
+ this.safeOnError(error);
336
+ return null;
337
+ }
338
+ }
339
+
270
340
  /**
271
341
  * Handle errors using the route's configured error handlers.
272
- * Handler errors bubble up to the safety net in `fetch()`.
273
342
  */
274
- private handleError(
343
+ private async handleError(
275
344
  error: unknown,
276
345
  ctx: ServerContext,
277
346
  route: RouteDefinition
278
- ): IHttpResponse | Promise<IHttpResponse> {
347
+ ): Promise<IHttpResponse> {
279
348
  const config = route.routerConfig;
280
349
 
281
350
  if (error instanceof RequestValidationError) {
282
- const handler = this.resolveErrorHandler<ValidationErrorHandler>(
283
- config.handleValidationErrors,
284
- TypeweaverApp.defaultValidationHandler
351
+ const handler = this.resolveErrorHandler<RequestValidationErrorHandler>(
352
+ config.handleRequestValidationErrors,
353
+ TypeweaverApp.defaultRequestValidationHandler
285
354
  );
286
- if (handler) return handler(error, ctx);
355
+ if (handler) {
356
+ const response = await this.safelyExecuteErrorHandler(() =>
357
+ handler(error, ctx)
358
+ );
359
+ if (response) return response;
360
+ }
287
361
  }
288
362
 
289
- if (error instanceof HttpResponse) {
363
+ if (isTypedHttpResponse(error)) {
290
364
  const handler = this.resolveErrorHandler<HttpResponseErrorHandler>(
291
365
  config.handleHttpResponseErrors,
292
366
  TypeweaverApp.defaultHttpResponseHandler
293
367
  );
294
- if (handler) return handler(error, ctx);
368
+ if (handler) {
369
+ const response = await this.safelyExecuteErrorHandler(() =>
370
+ handler(error, ctx)
371
+ );
372
+ if (response) return response;
373
+ }
295
374
  }
296
375
 
297
376
  const handler = this.resolveErrorHandler<UnknownErrorHandler>(
298
377
  config.handleUnknownErrors,
299
378
  this.defaultUnknownHandler
300
379
  );
301
- if (handler) return handler(error, ctx);
380
+ if (handler) {
381
+ const response = await this.safelyExecuteErrorHandler(() =>
382
+ handler(error, ctx)
383
+ );
384
+ if (response) return response;
385
+ }
302
386
 
303
387
  throw error;
304
388
  }
@@ -339,30 +423,32 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
339
423
  return issues.map(({ message, path }) => ({ message, path }));
340
424
  }
341
425
 
342
- private static defaultValidationHandler: ValidationErrorHandler = (
343
- err
344
- ): IHttpResponse => {
345
- const issues: Record<string, unknown> = Object.create(null);
426
+ private static defaultRequestValidationHandler: RequestValidationErrorHandler =
427
+ (err): IHttpResponse => {
428
+ const issues: Record<string, unknown> = Object.create(null);
346
429
 
347
- const header = TypeweaverApp.sanitizeIssues(err.headerIssues);
348
- const body = TypeweaverApp.sanitizeIssues(err.bodyIssues);
349
- const query = TypeweaverApp.sanitizeIssues(err.queryIssues);
350
- const param = TypeweaverApp.sanitizeIssues(err.pathParamIssues);
430
+ const header = TypeweaverApp.sanitizeIssues(err.headerIssues);
431
+ const body = TypeweaverApp.sanitizeIssues(err.bodyIssues);
432
+ const query = TypeweaverApp.sanitizeIssues(err.queryIssues);
433
+ const param = TypeweaverApp.sanitizeIssues(err.pathParamIssues);
351
434
 
352
- if (header) issues.header = header;
353
- if (body) issues.body = body;
354
- if (query) issues.query = query;
355
- if (param) issues.param = param;
435
+ if (header) issues.header = header;
436
+ if (body) issues.body = body;
437
+ if (query) issues.query = query;
438
+ if (param) issues.param = param;
356
439
 
357
- return {
358
- statusCode: 400,
359
- body: {
360
- code: "VALIDATION_ERROR",
361
- message: err.message,
362
- issues,
363
- },
440
+ return {
441
+ statusCode: validationDefaultError.statusCode,
442
+ body: {
443
+ ...createDefaultErrorBody(validationDefaultError),
444
+ issues,
445
+ },
446
+ };
364
447
  };
365
- };
448
+
449
+ private static defaultResponseValidationHandler: ResponseValidationErrorHandler =
450
+ (): IHttpResponse =>
451
+ createDefaultErrorResponse(internalServerErrorDefaultError);
366
452
 
367
453
  private static defaultHttpResponseHandler: HttpResponseErrorHandler = (
368
454
  err
@@ -372,14 +458,17 @@ export class TypeweaverApp<TState extends Record<string, unknown> = {}> {
372
458
  error
373
459
  ): IHttpResponse => {
374
460
  this.safeOnError(error);
375
- return { statusCode: 500, body: TypeweaverApp.INTERNAL_SERVER_ERROR_BODY };
461
+ return {
462
+ statusCode: internalServerErrorDefaultError.statusCode,
463
+ body: TypeweaverApp.INTERNAL_SERVER_ERROR_BODY,
464
+ };
376
465
  };
377
466
 
378
467
  private static createErrorResponse(): Response {
379
468
  return new Response(
380
469
  JSON.stringify(TypeweaverApp.INTERNAL_SERVER_ERROR_BODY),
381
470
  {
382
- status: 500,
471
+ status: internalServerErrorDefaultError.statusCode,
383
472
  headers: { "content-type": "application/json" },
384
473
  }
385
474
  );
@@ -7,14 +7,19 @@
7
7
  * @generated by @rexeus/typeweaver
8
8
  */
9
9
 
10
- import type { HttpMethod, IRequestValidator } from "@rexeus/typeweaver-core";
10
+ import type {
11
+ HttpMethod,
12
+ IRequestValidator,
13
+ IResponseValidator,
14
+ } from "@rexeus/typeweaver-core";
11
15
  import type { RequestHandler } from "./RequestHandler";
12
16
  import type {
13
17
  HttpResponseErrorHandler,
18
+ ResponseValidationErrorHandler,
14
19
  RouteDefinition,
15
20
  RouterErrorConfig,
16
21
  UnknownErrorHandler,
17
- ValidationErrorHandler,
22
+ RequestValidationErrorHandler,
18
23
  } from "./Router";
19
24
 
20
25
  /**
@@ -39,22 +44,42 @@ export type TypeweaverRouterOptions<
39
44
  readonly validateRequests?: boolean;
40
45
 
41
46
  /**
42
- * Configure handling of HttpResponse errors thrown by handlers.
43
- * - `true`: Use default handler (returns the error as response)
44
- * - `false`: Disable this handler (errors fall through to the unknown error handler)
45
- * - `function`: Use custom error handler
47
+ * Enable response validation using generated validators.
48
+ * When true, responses are validated and stripped of extra fields before sending.
46
49
  * @default true
47
50
  */
48
- readonly handleHttpResponseErrors?: HttpResponseErrorHandler | boolean;
51
+ readonly validateResponses?: boolean;
49
52
 
50
53
  /**
51
54
  * Configure handling of request validation errors.
52
55
  * - `true`: Use default handler (400 with error details)
53
56
  * - `false`: Disable this handler (errors fall through to the unknown error handler)
57
+ * - `function`: Use custom request validation error handler
58
+ * @default true
59
+ */
60
+ readonly handleRequestValidationErrors?:
61
+ | RequestValidationErrorHandler
62
+ | boolean;
63
+
64
+ /**
65
+ * Configure handling of response validation errors.
66
+ * - `true`: Use default handler (500 Internal Server Error)
67
+ * - `false`: Disable response validation error handling (return response as-is)
68
+ * - `function`: Use custom response validation error handler
69
+ * @default true
70
+ */
71
+ readonly handleResponseValidationErrors?:
72
+ | ResponseValidationErrorHandler
73
+ | boolean;
74
+
75
+ /**
76
+ * Configure handling of HttpResponse errors thrown by handlers.
77
+ * - `true`: Use default handler (returns the error as response)
78
+ * - `false`: Disable this handler (errors fall through to the unknown error handler)
54
79
  * - `function`: Use custom error handler
55
80
  * @default true
56
81
  */
57
- readonly handleValidationErrors?: ValidationErrorHandler | boolean;
82
+ readonly handleHttpResponseErrors?: HttpResponseErrorHandler | boolean;
58
83
 
59
84
  /**
60
85
  * Configure handling of unknown errors.
@@ -91,8 +116,10 @@ export abstract class TypeweaverRouter<
91
116
  const {
92
117
  requestHandlers,
93
118
  validateRequests = true,
119
+ validateResponses = true,
94
120
  handleHttpResponseErrors = true,
95
- handleValidationErrors = true,
121
+ handleRequestValidationErrors = true,
122
+ handleResponseValidationErrors = true,
96
123
  handleUnknownErrors = true,
97
124
  } = options;
98
125
 
@@ -100,8 +127,10 @@ export abstract class TypeweaverRouter<
100
127
 
101
128
  this.errorConfig = {
102
129
  validateRequests,
130
+ validateResponses,
103
131
  handleHttpResponseErrors,
104
- handleValidationErrors,
132
+ handleRequestValidationErrors,
133
+ handleResponseValidationErrors,
105
134
  handleUnknownErrors,
106
135
  };
107
136
  }
@@ -113,20 +142,23 @@ export abstract class TypeweaverRouter<
113
142
  * @param method - HTTP method (GET, POST, PUT, DELETE, etc.)
114
143
  * @param path - Path pattern with `:param` placeholders
115
144
  * @param validator - Request validator for this operation
145
+ * @param responseValidator - Response validator for this operation
116
146
  * @param handler - Type-safe request handler
117
147
  */
118
148
  protected route(
119
149
  operationId: string,
120
150
  method: HttpMethod,
121
151
  path: string,
122
- validator: IRequestValidator,
152
+ requestValidator: IRequestValidator,
153
+ responseValidator: IResponseValidator,
123
154
  handler: RequestHandler<any, any, any>
124
155
  ): void {
125
156
  this.routes.push({
126
157
  operationId,
127
158
  method,
128
159
  path,
129
- validator,
160
+ requestValidator,
161
+ responseValidator,
130
162
  handler,
131
163
  routerConfig: this.errorConfig,
132
164
  });
package/dist/lib/index.ts CHANGED
@@ -15,9 +15,10 @@ export {
15
15
  export { HttpMethod } from "@rexeus/typeweaver-core";
16
16
  export type {
17
17
  HttpResponseErrorHandler,
18
+ RequestValidationErrorHandler,
19
+ ResponseValidationErrorHandler,
18
20
  RouteMetadata,
19
21
  UnknownErrorHandler,
20
- ValidationErrorHandler,
21
22
  } from "./Router";
22
23
  export type { ServerContext } from "./ServerContext";
23
24
  export type { RequestHandler } from "./RequestHandler";
@@ -1,3 +1,7 @@
1
+ import {
2
+ createDefaultErrorBody,
3
+ unauthorizedDefaultError,
4
+ } from "@rexeus/typeweaver-core";
1
5
  import type { IHttpResponse } from "@rexeus/typeweaver-core";
2
6
  import { defineMiddleware } from "../TypedMiddleware";
3
7
  import type { ServerContext } from "../ServerContext";
@@ -9,7 +13,6 @@ export type BasicAuthOptions = {
9
13
  ctx: ServerContext
10
14
  ) => boolean | Promise<boolean>;
11
15
  readonly realm?: string;
12
- readonly unauthorizedMessage?: string;
13
16
  readonly onUnauthorized?: (ctx: ServerContext) => IHttpResponse;
14
17
  };
15
18
 
@@ -17,12 +20,11 @@ const BASIC_PREFIX = "Basic ";
17
20
 
18
21
  export function basicAuth(options: BasicAuthOptions) {
19
22
  const realm = options.realm ?? "Secure Area";
20
- const message = options.unauthorizedMessage ?? "Unauthorized";
21
23
 
22
24
  const defaultResponse: IHttpResponse = {
23
- statusCode: 401,
25
+ statusCode: unauthorizedDefaultError.statusCode,
24
26
  header: { "www-authenticate": `Basic realm="${realm}"` },
25
- body: { code: "UNAUTHORIZED", message },
27
+ body: createDefaultErrorBody(unauthorizedDefaultError),
26
28
  };
27
29
 
28
30
  const deny = (ctx: ServerContext): IHttpResponse =>
@@ -1,3 +1,7 @@
1
+ import {
2
+ createDefaultErrorBody,
3
+ unauthorizedDefaultError,
4
+ } from "@rexeus/typeweaver-core";
1
5
  import type { IHttpResponse } from "@rexeus/typeweaver-core";
2
6
  import { defineMiddleware } from "../TypedMiddleware";
3
7
  import type { ServerContext } from "../ServerContext";
@@ -8,7 +12,6 @@ export type BearerAuthOptions = {
8
12
  ctx: ServerContext
9
13
  ) => boolean | Promise<boolean>;
10
14
  readonly realm?: string;
11
- readonly unauthorizedMessage?: string;
12
15
  readonly onUnauthorized?: (ctx: ServerContext) => IHttpResponse;
13
16
  };
14
17
 
@@ -16,12 +19,11 @@ const BEARER_PREFIX = "Bearer ";
16
19
 
17
20
  export function bearerAuth(options: BearerAuthOptions) {
18
21
  const realm = options.realm ?? "Secure Area";
19
- const message = options.unauthorizedMessage ?? "Unauthorized";
20
22
 
21
23
  const defaultResponse: IHttpResponse = {
22
- statusCode: 401,
24
+ statusCode: unauthorizedDefaultError.statusCode,
23
25
  header: { "www-authenticate": `Bearer realm="${realm}"` },
24
- body: { code: "UNAUTHORIZED", message },
26
+ body: createDefaultErrorBody(unauthorizedDefaultError),
25
27
  };
26
28
 
27
29
  const deny = (ctx: ServerContext): IHttpResponse =>
@@ -11,6 +11,7 @@ import { HttpMethod, TypeweaverRouter, type RequestHandler, type TypeweaverRoute
11
11
  import type { I<%- operation.className %>Request } from "./<%- operation.className %>Request";
12
12
  import { <%- operation.className %>RequestValidator } from "./<%- operation.className %>RequestValidator";
13
13
  import type { <%- operation.className %>Response } from "./<%- operation.className %>Response";
14
+ import { <%- operation.className %>ResponseValidator } from "./<%- operation.className %>ResponseValidator";
14
15
  <% } %>
15
16
 
16
17
  export type Server<%- pascalCaseEntityName %>ApiHandler<
@@ -36,6 +37,7 @@ export class <%- pascalCaseEntityName %>Router<
36
37
  HttpMethod.<%- operation.method %>,
37
38
  '<%- operation.path %>',
38
39
  new <%- operation.className %>RequestValidator(),
40
+ new <%- operation.className %>ResponseValidator(),
39
41
  this.requestHandlers.<%- operation.handlerName %>.bind(this.requestHandlers)
40
42
  );
41
43
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rexeus/typeweaver-server",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Generates a lightweight, dependency-free server with built-in routing and middleware from your API definitions. Powered by Typeweaver.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -47,21 +47,21 @@
47
47
  },
48
48
  "homepage": "https://github.com/rexeus/typeweaver#readme",
49
49
  "peerDependencies": {
50
- "@rexeus/typeweaver-core": "^0.7.0",
51
- "@rexeus/typeweaver-gen": "^0.7.0"
50
+ "@rexeus/typeweaver-core": "^0.9.0",
51
+ "@rexeus/typeweaver-gen": "^0.9.0"
52
52
  },
53
53
  "devDependencies": {
54
- "get-port": "^7.1.0",
54
+ "get-port": "^7.2.0",
55
55
  "test-utils": "file:../test-utils",
56
56
  "tsx": "^4.21.0",
57
- "@rexeus/typeweaver-core": "^0.7.0",
58
- "@rexeus/typeweaver-gen": "^0.7.0"
57
+ "@rexeus/typeweaver-core": "^0.9.0",
58
+ "@rexeus/typeweaver-gen": "^0.9.0"
59
59
  },
60
60
  "dependencies": {
61
61
  "case": "^1.6.3"
62
62
  },
63
63
  "scripts": {
64
- "typecheck": "tsc --noEmit",
64
+ "typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
65
65
  "format": "oxfmt",
66
66
  "build": "tsdown && mkdir -p ./dist/templates ./dist/lib && cp -r ./src/templates/* ./dist/templates/ && cp -r ./src/lib/* ./dist/lib/ && cp ../../LICENSE ../../NOTICE ./dist/",
67
67
  "test": "vitest --run",