@codewithagents/openapi-server 1.2.0 → 1.3.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 CHANGED
@@ -1,16 +1,20 @@
1
1
  # @codewithagents/openapi-server
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@codewithagents/openapi-server.svg)](https://npmjs.com/package/@codewithagents/openapi-server)
4
- [![codecov](https://codecov.io/gh/codewithagents/glue/graph/badge.svg?flag=openapi-server)](https://codecov.io/gh/codewithagents/glue)
4
+ [![CI](https://github.com/codewithagents/openapi-zod-ts/actions/workflows/ci.yml/badge.svg)](https://github.com/codewithagents/openapi-zod-ts/actions/workflows/ci.yml)
5
+ [![codecov](https://codecov.io/gh/codewithagents/openapi-zod-ts/graph/badge.svg?flag=openapi-server)](https://codecov.io/gh/codewithagents/openapi-zod-ts)
6
+ [![CodeQL](https://github.com/codewithagents/openapi-zod-ts/actions/workflows/codeql.yml/badge.svg)](https://github.com/codewithagents/openapi-zod-ts/actions/workflows/codeql.yml)
5
7
 
6
- Generate a typed service interface from your OpenAPI 3.x spec. Framework-agnostic by design — wire it to Hono, Express, Fastify, or any router you already use.
8
+ 📖 **[Full documentation](https://openapi.codewithagents.de/openapi-server)**
7
9
 
8
- - **Framework-agnostic service interface** `service.ts` is a plain TypeScript interface with no framework imports. Implement it however you want: Hono, Express, Fastify, Koa, plain `http`, Bun, Deno — anything.
9
- - **Optional router scaffolding** — set `"framework": "hono"` and get a ready-to-mount Hono router as a starting point. Set `"framework": "none"` and wire the interface yourself. The generated code only ever imports what you already have.
10
- - **Type-safe contract** the compiler tells you if your implementation drifts from the spec. Add an endpoint in the spec and forget to implement it: TypeScript fails the build.
11
- - **Prettier-clean output** every generated file passes `prettier --check` out of the box.
12
- - **OpenAPI 3.x** 3.1.x primary target, 3.0.x best-effort. Full support for `$ref`, `allOf`, `anyOf`, `oneOf`, `nullable`.
13
- - **TypeScript strict mode** all output passes `strict: true`.
10
+ Generate a typed service interface from your OpenAPI 3.x spec. Framework-agnostic by design: wire it to Hono, Express, Fastify, or any router you already use.
11
+
12
+ - **Framework-agnostic service interface**: `service.ts` is a plain TypeScript interface with no framework imports. Implement it however you want: Hono, Express, Fastify, Koa, plain `http`, Bun, Deno, or anything else.
13
+ - **Optional router scaffolding**: set `"framework": "hono"` and get a ready-to-mount Hono router as a starting point. Set `"framework": "none"` and wire the interface yourself. The generated code only ever imports what you already have.
14
+ - **Type-safe contract**: the compiler tells you if your implementation drifts from the spec. Add an endpoint in the spec and forget to implement it. TypeScript fails the build.
15
+ - **Prettier-clean output**: every generated file passes `prettier --check` out of the box.
16
+ - **OpenAPI 3.x**: 3.1.x primary target, 3.0.x best-effort. Full support for `$ref`, `allOf`, `anyOf`, `oneOf`, `nullable`.
17
+ - **TypeScript strict mode**: all output passes `strict: true`.
14
18
 
15
19
  ---
16
20
 
@@ -22,7 +26,7 @@ pnpm add -D @codewithagents/openapi-server
22
26
  npm install -D @codewithagents/openapi-server
23
27
  ```
24
28
 
25
- Requires [`@codewithagents/openapi-gen`](../openapi-gen) run both generators together.
29
+ Requires [`@codewithagents/openapi-gen`](../openapi-gen). Run both generators together.
26
30
 
27
31
  ---
28
32
 
@@ -48,8 +52,8 @@ npx openapi-server
48
52
 
49
53
  | File | What it contains |
50
54
  |---|---|
51
- | `service.ts` | TypeScript interface one method per API operation |
52
- | `router.ts` | `createRouter(service)` factory mounts every route on a Hono app |
55
+ | `service.ts` | TypeScript interface, one method per API operation |
56
+ | `router.ts` | `createRouter(service)` factory, mounts every route on a Hono app |
53
57
 
54
58
  Run `openapi-gen` first (or together) so `models.ts` exists before `service.ts` imports from it:
55
59
 
@@ -133,7 +137,7 @@ The router handles:
133
137
  - Path params: `{id}` → `:id` (Hono style), extracted via `c.req.param()`
134
138
  - Query params: extracted and typed (`string`, `number`, `boolean`)
135
139
  - Request bodies: parsed via `c.req.json<T>()` with the correct model type
136
- - Response status: `200` for GET, `201` for POST, `204` for DELETE derived from your spec
140
+ - Response status: `200` for GET, `201` for POST, `204` for DELETE, derived from your spec
137
141
 
138
142
  ---
139
143
 
@@ -203,23 +207,25 @@ serve({ fetch: app.fetch, port: 3001 })
203
207
 
204
208
  ## Config reference
205
209
 
210
+ See the [full configuration reference](https://openapi.codewithagents.de/openapi-server#configuration) in the docs for a detailed options table and the `--config` CLI flag.
211
+
206
212
  `openapi-server.config.json`:
207
213
 
208
214
  ```json
209
215
  {
210
- "input_openapi": "./spec/api.json", // required path to OpenAPI 3.x spec (JSON or YAML)
211
- "output": "./generated", // required directory to write generated files
212
- "framework": "hono", // optional router target (default: "hono")
213
- "input_schema": "./generated/schemas.ts" // optional Zod schema file for request validation
216
+ "input_openapi": "./spec/api.json", // required: path to OpenAPI 3.x spec (JSON or YAML)
217
+ "output": "./generated", // required: directory to write generated files
218
+ "framework": "hono", // optional: router target (default: "hono")
219
+ "input_schema": "./generated/schemas.ts" // optional: Zod schema file for request validation
214
220
  }
215
221
  ```
216
222
 
217
223
  | Field | Required | Default | Description |
218
224
  |---|---|---|---|
219
- | `input_openapi` | Yes | | Path to OpenAPI 3.x spec |
220
- | `output` | Yes | | Directory to write `service.ts` and `router.ts` |
225
+ | `input_openapi` | Yes | n/a | Path to OpenAPI 3.x spec |
226
+ | `output` | Yes | n/a | Directory to write `service.ts` and `router.ts` |
221
227
  | `framework` | No | `"hono"` | Router framework to generate. Use `"none"` to generate only `service.ts` |
222
- | `input_schema` | No | | Path to user-owned Zod schema file. Enables server-side request validation (see below) |
228
+ | `input_schema` | No | none | Path to user-owned Zod schema file. Enables server-side request validation (see below) |
223
229
 
224
230
  Use `--config <path>` to point at a config file in a different location:
225
231
 
@@ -233,6 +239,8 @@ Relative paths in the config resolve from the config file's directory.
233
239
 
234
240
  ## Zod request validation (`input_schema`)
235
241
 
242
+ See the [Zod validation](https://openapi.codewithagents.de/openapi-server#zod-validation-input_schema) section in the docs for the two-pass generation flow and schema naming convention.
243
+
236
244
  Point `input_schema` at the same `schemas.ts` you use with `@codewithagents/openapi-gen`. The server generator adds runtime validation to every route that receives a request body:
237
245
 
238
246
  **Config:**
@@ -270,9 +278,9 @@ Invalid requests get a structured `422` response instead of reaching your servic
270
278
  }
271
279
  ```
272
280
 
273
- **Same schemas, both sides of the wire** `openapi-gen` validates outgoing requests in the browser; `openapi-server` validates incoming requests on the server. One `schemas.ts`, one source of truth.
281
+ **Same schemas, both sides of the wire**: `openapi-gen` validates outgoing requests in the browser; `openapi-server` validates incoming requests on the server. One `schemas.ts`, one source of truth.
274
282
 
275
- **Drift detection** if schemas diverge from the spec (extra schema, missing schema), the generator warns to stderr. Builds still succeed the warning is advisory.
283
+ **Drift detection**: if schemas diverge from the spec (extra schema, missing schema), the generator warns to stderr. Builds still succeed; the warning is advisory.
276
284
 
277
285
  ---
278
286
 
@@ -280,11 +288,33 @@ Invalid requests get a structured `422` response instead of reaching your servic
280
288
 
281
289
  `service.ts` has no framework imports at all. It is always generated, always framework-agnostic, and works with anything.
282
290
 
283
- `router.ts` is optional and currently supports:
291
+ `router.ts` is optional and supports:
284
292
 
285
293
  | Value | What you get |
286
294
  |---|---|
287
- | `"none"` | Only `service.ts`. Wire the interface to Express, Fastify, Koa, plain Node `http`, Bun, Deno, or anything else yourself. |
288
- | `"hono"` | `service.ts` + a ready-to-mount `router.ts` using [Hono](https://hono.dev). Hono must be in your own `dependencies` — this package adds nothing. |
295
+ | `"none"` | Only `service.ts`. Wire the interface yourself. |
296
+ | `"hono"` | `service.ts` + a ready-to-mount `router.ts` using [Hono](https://hono.dev). Includes optional Zod request validation via `input_schema`. |
297
+ | `"express"` | `service.ts` + a ready-to-mount `router.ts` using [Express](https://expressjs.com) `Router`. Apply `express.json()` middleware before mounting. |
298
+ | `"fastify"` | `service.ts` + a route-registering `router.ts` using [Fastify](https://fastify.dev). Routes are registered onto a `FastifyInstance`; see mount pattern below. |
299
+
300
+ The framework package must be in your own `dependencies`. This package adds nothing at runtime.
301
+
302
+ **Mounting patterns:**
303
+
304
+ ```ts
305
+ // Hono
306
+ app.route('/api', createRouter(service))
307
+
308
+ // Express
309
+ app.use(express.json())
310
+ app.use('/api', createRouter(service))
311
+
312
+ // Fastify: createRouter registers routes onto the instance rather than returning one
313
+ fastify.register(async (instance) => { createRouter(instance, service) }, { prefix: '/api' })
314
+ ```
315
+
316
+ The `"none"` path is always available and keeps the zero-footprint promise: the generated code has no runtime dependencies that you did not already choose.
317
+
318
+ ## Error handling and troubleshooting
289
319
 
290
- More router targets (Express, Fastify) are planned. The `"none"` path is always available and keeps the zero-footprint promise: the generated code has no runtime dependencies that you did not already choose.
320
+ The generated router does not wrap service calls in `try/catch`. Errors propagate to the framework's own error handler. See [Error handling](https://openapi.codewithagents.de/openapi-server#error-handling) in the docs for per-framework error handler examples and [Troubleshooting](https://openapi.codewithagents.de/openapi-server#troubleshooting) for common issues such as missing Zod validation or `Cannot find module './models.js'`.
package/dist/cli.cjs CHANGED
@@ -18478,8 +18478,8 @@ async function loadConfig(cwd2, configPath) {
18478
18478
  if (typeof config["output"] !== "string" || !config["output"]) {
18479
18479
  throw new Error('Config missing required field: "output" (output directory)');
18480
18480
  }
18481
- if (config["framework"] !== void 0 && config["framework"] !== "hono" && config["framework"] !== "none") {
18482
- throw new Error('"framework" must be either "hono" or "none"');
18481
+ if (config["framework"] !== void 0 && config["framework"] !== "hono" && config["framework"] !== "express" && config["framework"] !== "fastify" && config["framework"] !== "none") {
18482
+ throw new Error('"framework" must be one of: "hono", "express", "fastify", or "none"');
18483
18483
  }
18484
18484
  if (config["input_schema"] !== void 0 && (typeof config["input_schema"] !== "string" || !config["input_schema"])) {
18485
18485
  throw new Error('"input_schema" must be a non-empty string path to your Zod schema file');
@@ -18505,8 +18505,14 @@ async function parseSpec(inputPath) {
18505
18505
 
18506
18506
  // ../openapi-gen/dist/utils/naming.js
18507
18507
  function toTypeName(name) {
18508
- const result = name.replace(/[^a-zA-Z0-9]+(.)/g, (_, char) => char.toUpperCase()).replace(/[^a-zA-Z0-9]+$/, "").replace(/^[^a-zA-Z_$]/, "_").replace(/^(.)/, (_, char) => char.toUpperCase());
18509
- return result.length > 0 ? result : "_";
18508
+ const parts = name.split(/[^a-zA-Z0-9]+/).filter(Boolean);
18509
+ if (parts.length === 0)
18510
+ return "_";
18511
+ const joined = parts.map((part, i) => i === 0 ? part : part[0].toUpperCase() + part.slice(1)).join("");
18512
+ const prefixed = /^[^a-zA-Z_$]/.test(joined) ? `_${joined}` : joined;
18513
+ const titled = prefixed.replace(/^(.)/, (_, char) => char.toUpperCase());
18514
+ const safe = titled.replace(/[^a-zA-Z0-9_$]/g, "");
18515
+ return safe.length > 0 ? safe : "_";
18510
18516
  }
18511
18517
 
18512
18518
  // src/plugins/service.ts
@@ -18582,7 +18588,11 @@ function deriveOperationName(method, path) {
18582
18588
  return prefix + parts.join("");
18583
18589
  }
18584
18590
  function normalizeParamName(name) {
18585
- return name.replace(/\[\]$/, "").replace(/'/g, "").replace(/[^a-zA-Z0-9]+([a-zA-Z])/g, (_, char) => char.toUpperCase()).replace(/[^a-zA-Z0-9]+$/, "").replace(/^[^a-zA-Z_$]/, "_");
18591
+ const stripped = name.replace(/\[\]$/, "").replace(/'/g, "");
18592
+ const parts = stripped.split(/[^a-zA-Z0-9]+/).filter(Boolean);
18593
+ if (parts.length === 0) return "_";
18594
+ const camel = parts.map((part, i) => i === 0 ? part : part[0].toUpperCase() + part.slice(1)).join("");
18595
+ return /^[^a-zA-Z_$]/.test(camel) ? `_${camel}` : camel;
18586
18596
  }
18587
18597
  function getQueryParams(operation, spec) {
18588
18598
  const parameters = operation.parameters;
@@ -18635,13 +18645,21 @@ function getReturnInfo(operation) {
18635
18645
  if (jsonContent === void 0 || jsonContent.schema === void 0) continue;
18636
18646
  const schema = jsonContent.schema;
18637
18647
  if (isRef(schema)) {
18638
- return { typeName: refToName(schema.$ref), isArray: false, isVoid: false };
18648
+ return {
18649
+ typeName: refToName(schema.$ref),
18650
+ isArray: false,
18651
+ isVoid: false
18652
+ };
18639
18653
  }
18640
18654
  const s = schema;
18641
18655
  if (s.type === "array") {
18642
18656
  const items = s.items;
18643
18657
  if (items !== void 0 && isRef(items)) {
18644
- return { typeName: refToName(items.$ref), isArray: true, isVoid: false };
18658
+ return {
18659
+ typeName: refToName(items.$ref),
18660
+ isArray: true,
18661
+ isVoid: false
18662
+ };
18645
18663
  }
18646
18664
  return { typeName: void 0, isArray: true, isVoid: false };
18647
18665
  }
@@ -18813,7 +18831,18 @@ function deriveOperationName2(method, path) {
18813
18831
  return prefix + parts.join("");
18814
18832
  }
18815
18833
  function normalizeParamName2(name) {
18816
- return name.replace(/\[\]$/, "").replace(/'/g, "").replace(/[^a-zA-Z0-9]+([a-zA-Z])/g, (_, char) => char.toUpperCase()).replace(/[^a-zA-Z0-9]+$/, "").replace(/^[^a-zA-Z_$]/, "_");
18834
+ const stripped = name.replace(/\[\]$/, "").replace(/'/g, "");
18835
+ const parts = stripped.split(/[^a-zA-Z0-9]+/).filter(Boolean);
18836
+ if (parts.length === 0) return "_";
18837
+ const camel = parts.map((part, i) => i === 0 ? part : part[0].toUpperCase() + part.slice(1)).join("");
18838
+ return /^[^a-zA-Z_$]/.test(camel) ? `_${camel}` : camel;
18839
+ }
18840
+ function schemaToTsType(schema) {
18841
+ if (schema === void 0 || isRef2(schema)) return "string";
18842
+ const s = schema;
18843
+ if (s.type === "number" || s.type === "integer") return "number";
18844
+ if (s.type === "boolean") return "boolean";
18845
+ return "string";
18817
18846
  }
18818
18847
  function getQueryParams2(operation, spec) {
18819
18848
  const parameters = operation.parameters;
@@ -18823,15 +18852,9 @@ function getQueryParams2(operation, spec) {
18823
18852
  const resolved = resolveParam2(p, spec);
18824
18853
  if (resolved === void 0 || resolved.in !== "query") continue;
18825
18854
  const schema = resolved.schema;
18826
- let tsType = "string";
18827
- if (schema !== void 0 && !isRef2(schema)) {
18828
- const s = schema;
18829
- if (s.type === "number" || s.type === "integer") tsType = "number";
18830
- else if (s.type === "boolean") tsType = "boolean";
18831
- }
18832
18855
  result.push({
18833
18856
  name: normalizeParamName2(resolved.name),
18834
- tsType,
18857
+ tsType: schemaToTsType(schema),
18835
18858
  required: resolved.required === true
18836
18859
  });
18837
18860
  }
@@ -18852,25 +18875,24 @@ function getBodyInfo2(operation) {
18852
18875
  }
18853
18876
  return { typeName: void 0 };
18854
18877
  }
18878
+ function response200IsVoid(resp) {
18879
+ if (isRef2(resp)) return false;
18880
+ const r = resp;
18881
+ const content = r.content;
18882
+ return content === void 0 || Object.keys(content).length === 0;
18883
+ }
18855
18884
  function getResponseStatus(operation, httpMethod) {
18856
18885
  const responses = operation.responses;
18857
- if (responses !== void 0) {
18858
- if (responses["201"] !== void 0) return { status: 201, isVoid: false };
18859
- if (responses["204"] !== void 0) return { status: 204, isVoid: true };
18860
- if (responses["200"] !== void 0) {
18861
- const resp = responses["200"];
18862
- if (!isRef2(resp)) {
18863
- const r = resp;
18864
- const content = r.content;
18865
- if (content === void 0 || Object.keys(content).length === 0) {
18866
- return { status: 204, isVoid: true };
18867
- }
18868
- }
18869
- return { status: 200, isVoid: false };
18870
- }
18886
+ if (responses === void 0) {
18887
+ return httpMethod === "delete" ? { status: 204, isVoid: true } : { status: 200, isVoid: false };
18871
18888
  }
18872
- if (httpMethod === "delete") return { status: 204, isVoid: true };
18873
- return { status: 200, isVoid: false };
18889
+ if (responses["201"] !== void 0) return { status: 201, isVoid: false };
18890
+ if (responses["204"] !== void 0) return { status: 204, isVoid: true };
18891
+ if (responses["200"] !== void 0) {
18892
+ if (response200IsVoid(responses["200"])) return { status: 204, isVoid: true };
18893
+ return { status: 200, isVoid: false };
18894
+ }
18895
+ return httpMethod === "delete" ? { status: 204, isVoid: true } : { status: 200, isVoid: false };
18874
18896
  }
18875
18897
  function collectOperations2(spec) {
18876
18898
  const paths = spec.paths;
@@ -18919,9 +18941,7 @@ function buildRouteHandler(op, indent, schemaNames) {
18919
18941
  lines.push(`${indent} const body = await c.req.json${typeAnnotation}()`);
18920
18942
  const schemaName = op.bodyInfo.typeName !== void 0 ? `${op.bodyInfo.typeName}Schema` : void 0;
18921
18943
  if (schemaName !== void 0 && schemaNames !== void 0 && schemaNames.has(schemaName)) {
18922
- lines.push(
18923
- `${indent} // Validate request body \u2014 returns 422 with Zod issues on failure`
18924
- );
18944
+ lines.push(`${indent} // Validate request body: returns 422 with Zod issues on failure`);
18925
18945
  lines.push(`${indent} const parseResult = ${schemaName}.safeParse(body)`);
18926
18946
  lines.push(`${indent} if (!parseResult.success) {`);
18927
18947
  lines.push(
@@ -18954,6 +18974,235 @@ function buildRouteHandler(op, indent, schemaNames) {
18954
18974
  lines.push(`${indent}})`);
18955
18975
  return lines.join("\n");
18956
18976
  }
18977
+ function buildExpressRouteHandler(op, indent, schemaNames) {
18978
+ const lines = [];
18979
+ lines.push(
18980
+ `${indent}router.${op.httpMethod}('${op.honoPath}', async (req: Request, res: Response) => {`
18981
+ );
18982
+ if (op.queryParams.length > 0) {
18983
+ const fields = op.queryParams.map((q) => {
18984
+ if (q.tsType === "number") {
18985
+ return ` ${q.name}: Number(req.query['${q.name}'] as string)`;
18986
+ }
18987
+ if (q.tsType === "boolean") {
18988
+ return ` ${q.name}: req.query['${q.name}'] === 'true'`;
18989
+ }
18990
+ return ` ${q.name}: req.query['${q.name}'] as string | undefined`;
18991
+ }).join(",\n");
18992
+ lines.push(`${indent} const params = {`);
18993
+ lines.push(fields);
18994
+ lines.push(`${indent} }`);
18995
+ }
18996
+ let bodyVarName = "body";
18997
+ if (op.bodyInfo !== void 0) {
18998
+ const schemaName = op.bodyInfo.typeName !== void 0 ? `${op.bodyInfo.typeName}Schema` : void 0;
18999
+ const useZod = schemaName !== void 0 && schemaNames !== void 0 && schemaNames.has(schemaName);
19000
+ if (useZod) {
19001
+ lines.push(`${indent} // Validate request body: returns 422 with Zod issues on failure`);
19002
+ lines.push(`${indent} const parseResult = ${schemaName}.safeParse(req.body)`);
19003
+ lines.push(`${indent} if (!parseResult.success) {`);
19004
+ lines.push(
19005
+ `${indent} return void res.status(422).json({ error: 'Invalid request body', issues: parseResult.error.issues })`
19006
+ );
19007
+ lines.push(`${indent} }`);
19008
+ lines.push(`${indent} const validatedBody = parseResult.data`);
19009
+ bodyVarName = "validatedBody";
19010
+ } else {
19011
+ const typeAnnotation = op.bodyInfo.typeName !== void 0 ? ` as ${op.bodyInfo.typeName}` : "";
19012
+ lines.push(`${indent} const body = req.body${typeAnnotation}`);
19013
+ }
19014
+ }
19015
+ const serviceArgs = [];
19016
+ for (const p of op.pathParams) {
19017
+ serviceArgs.push(`req.params['${p}']!`);
19018
+ }
19019
+ if (op.bodyInfo !== void 0) {
19020
+ serviceArgs.push(bodyVarName);
19021
+ }
19022
+ if (op.queryParams.length > 0) {
19023
+ serviceArgs.push("params");
19024
+ }
19025
+ const serviceCall = `service.${op.methodName}(${serviceArgs.join(", ")})`;
19026
+ if (op.responseStatus.isVoid) {
19027
+ lines.push(`${indent} await ${serviceCall}`);
19028
+ lines.push(`${indent} res.status(${op.responseStatus.status}).end()`);
19029
+ } else if (op.responseStatus.status === 201) {
19030
+ lines.push(`${indent} res.status(201).json(await ${serviceCall})`);
19031
+ } else {
19032
+ lines.push(`${indent} res.json(await ${serviceCall})`);
19033
+ }
19034
+ lines.push(`${indent}})`);
19035
+ return lines.join("\n");
19036
+ }
19037
+ function generateExpressRouter(spec, options) {
19038
+ const serviceName = deriveServiceName2(spec);
19039
+ const operations = collectOperations2(spec);
19040
+ const bodyTypes = /* @__PURE__ */ new Set();
19041
+ for (const op of operations) {
19042
+ if (op.bodyInfo?.typeName !== void 0) {
19043
+ bodyTypes.add(op.bodyInfo.typeName);
19044
+ }
19045
+ }
19046
+ const sortedBodyTypes = Array.from(bodyTypes).sort();
19047
+ const usedSchemaNames = /* @__PURE__ */ new Set();
19048
+ if (options?.schemaNames !== void 0) {
19049
+ for (const op of operations) {
19050
+ const typeName = op.bodyInfo?.typeName;
19051
+ if (typeName !== void 0) {
19052
+ const schemaName = `${typeName}Schema`;
19053
+ if (options.schemaNames.has(schemaName)) {
19054
+ usedSchemaNames.add(schemaName);
19055
+ }
19056
+ }
19057
+ }
19058
+ }
19059
+ const lines = [];
19060
+ lines.push("// This file is auto-generated. Do not edit manually.");
19061
+ lines.push(
19062
+ "// Express: apply express.json() middleware before mounting this router so req.body is populated."
19063
+ );
19064
+ lines.push("");
19065
+ lines.push("import { Router } from 'express'");
19066
+ lines.push("import type { Request, Response } from 'express'");
19067
+ if (sortedBodyTypes.length > 0) {
19068
+ lines.push(`import type { ${sortedBodyTypes.join(", ")} } from './models.js'`);
19069
+ }
19070
+ lines.push(`import type { ${serviceName} } from './service.js'`);
19071
+ if (usedSchemaNames.size > 0 && options?.schemaImportPath !== void 0) {
19072
+ lines.push(`import { z } from 'zod'`);
19073
+ const sortedUsedSchemas = Array.from(usedSchemaNames).sort();
19074
+ lines.push(`import { ${sortedUsedSchemas.join(", ")} } from '${options.schemaImportPath}'`);
19075
+ }
19076
+ lines.push("");
19077
+ lines.push(`export function createRouter(service: ${serviceName}): Router {`);
19078
+ lines.push(" const router = Router()");
19079
+ lines.push("");
19080
+ for (const op of operations) {
19081
+ lines.push(buildExpressRouteHandler(op, " ", options?.schemaNames));
19082
+ lines.push("");
19083
+ }
19084
+ lines.push(" return router");
19085
+ lines.push("}");
19086
+ lines.push("");
19087
+ return {
19088
+ filename: "router.ts",
19089
+ content: lines.join("\n")
19090
+ };
19091
+ }
19092
+ function buildFastifyRouteHandler(op, indent, schemaNames) {
19093
+ const lines = [];
19094
+ const genericParts = [];
19095
+ if (op.queryParams.length > 0) {
19096
+ const queryFields = op.queryParams.map((q) => {
19097
+ if (q.tsType === "number") return `${q.name}?: number`;
19098
+ if (q.tsType === "boolean") return `${q.name}?: boolean`;
19099
+ return `${q.name}?: string`;
19100
+ }).join("; ");
19101
+ genericParts.push(`Querystring: { ${queryFields} }`);
19102
+ }
19103
+ if (op.bodyInfo !== void 0 && op.bodyInfo.typeName !== void 0) {
19104
+ genericParts.push(`Body: ${op.bodyInfo.typeName}`);
19105
+ } else if (op.bodyInfo !== void 0) {
19106
+ genericParts.push("Body: unknown");
19107
+ }
19108
+ if (op.pathParams.length > 0) {
19109
+ const paramFields = op.pathParams.map((p) => `${p}: string`).join("; ");
19110
+ genericParts.push(`Params: { ${paramFields} }`);
19111
+ }
19112
+ const generic = genericParts.length > 0 ? `<{ ${genericParts.join("; ")} }>` : "";
19113
+ lines.push(`${indent}app.${op.httpMethod}${generic}('${op.honoPath}', async (req, reply) => {`);
19114
+ if (op.queryParams.length > 0) {
19115
+ const fields = op.queryParams.map((q) => ` ${q.name}: req.query.${q.name}`).join(",\n");
19116
+ lines.push(`${indent} const params = {`);
19117
+ lines.push(fields);
19118
+ lines.push(`${indent} }`);
19119
+ }
19120
+ let bodyVarName = "req.body";
19121
+ if (op.bodyInfo !== void 0) {
19122
+ const schemaName = op.bodyInfo.typeName !== void 0 ? `${op.bodyInfo.typeName}Schema` : void 0;
19123
+ const useZod = schemaName !== void 0 && schemaNames !== void 0 && schemaNames.has(schemaName);
19124
+ if (useZod) {
19125
+ lines.push(`${indent} // Validate request body: returns 422 with Zod issues on failure`);
19126
+ lines.push(`${indent} const parseResult = ${schemaName}.safeParse(req.body)`);
19127
+ lines.push(`${indent} if (!parseResult.success) {`);
19128
+ lines.push(
19129
+ `${indent} return reply.status(422).send({ error: 'Invalid request body', issues: parseResult.error.issues })`
19130
+ );
19131
+ lines.push(`${indent} }`);
19132
+ bodyVarName = "parseResult.data";
19133
+ }
19134
+ }
19135
+ const serviceArgs = [];
19136
+ for (const p of op.pathParams) {
19137
+ serviceArgs.push(`req.params.${p}`);
19138
+ }
19139
+ if (op.bodyInfo !== void 0) {
19140
+ serviceArgs.push(bodyVarName);
19141
+ }
19142
+ if (op.queryParams.length > 0) {
19143
+ serviceArgs.push("params");
19144
+ }
19145
+ const serviceCall = `service.${op.methodName}(${serviceArgs.join(", ")})`;
19146
+ if (op.responseStatus.isVoid) {
19147
+ lines.push(`${indent} await ${serviceCall}`);
19148
+ lines.push(`${indent} reply.status(${op.responseStatus.status}).send()`);
19149
+ } else if (op.responseStatus.status === 201) {
19150
+ lines.push(`${indent} reply.status(201)`);
19151
+ lines.push(`${indent} return ${serviceCall}`);
19152
+ } else {
19153
+ lines.push(`${indent} return ${serviceCall}`);
19154
+ }
19155
+ lines.push(`${indent}})`);
19156
+ return lines.join("\n");
19157
+ }
19158
+ function generateFastifyRouter(spec, options) {
19159
+ const serviceName = deriveServiceName2(spec);
19160
+ const operations = collectOperations2(spec);
19161
+ const bodyTypes = /* @__PURE__ */ new Set();
19162
+ for (const op of operations) {
19163
+ if (op.bodyInfo?.typeName !== void 0) {
19164
+ bodyTypes.add(op.bodyInfo.typeName);
19165
+ }
19166
+ }
19167
+ const sortedBodyTypes = Array.from(bodyTypes).sort();
19168
+ const usedSchemaNames = /* @__PURE__ */ new Set();
19169
+ if (options?.schemaNames !== void 0) {
19170
+ for (const op of operations) {
19171
+ const typeName = op.bodyInfo?.typeName;
19172
+ if (typeName !== void 0) {
19173
+ const schemaName = `${typeName}Schema`;
19174
+ if (options.schemaNames.has(schemaName)) {
19175
+ usedSchemaNames.add(schemaName);
19176
+ }
19177
+ }
19178
+ }
19179
+ }
19180
+ const lines = [];
19181
+ lines.push("// This file is auto-generated. Do not edit manually.");
19182
+ lines.push("");
19183
+ lines.push("import type { FastifyInstance } from 'fastify'");
19184
+ if (sortedBodyTypes.length > 0) {
19185
+ lines.push(`import type { ${sortedBodyTypes.join(", ")} } from './models.js'`);
19186
+ }
19187
+ lines.push(`import type { ${serviceName} } from './service.js'`);
19188
+ if (usedSchemaNames.size > 0 && options?.schemaImportPath !== void 0) {
19189
+ lines.push(`import { z } from 'zod'`);
19190
+ const sortedUsedSchemas = Array.from(usedSchemaNames).sort();
19191
+ lines.push(`import { ${sortedUsedSchemas.join(", ")} } from '${options.schemaImportPath}'`);
19192
+ }
19193
+ lines.push("");
19194
+ lines.push(`export function createRouter(app: FastifyInstance, service: ${serviceName}): void {`);
19195
+ for (const op of operations) {
19196
+ lines.push("");
19197
+ lines.push(buildFastifyRouteHandler(op, " ", options?.schemaNames));
19198
+ }
19199
+ lines.push("}");
19200
+ lines.push("");
19201
+ return {
19202
+ filename: "router.ts",
19203
+ content: lines.join("\n")
19204
+ };
19205
+ }
18957
19206
  function generateRouter(spec, options) {
18958
19207
  const serviceName = deriveServiceName2(spec);
18959
19208
  const operations = collectOperations2(spec);
@@ -18987,9 +19236,7 @@ function generateRouter(spec, options) {
18987
19236
  if (usedSchemaNames.size > 0 && options?.schemaImportPath !== void 0) {
18988
19237
  lines.push(`import { z } from 'zod'`);
18989
19238
  const sortedUsedSchemas = Array.from(usedSchemaNames).sort();
18990
- lines.push(
18991
- `import { ${sortedUsedSchemas.join(", ")} } from '${options.schemaImportPath}'`
18992
- );
19239
+ lines.push(`import { ${sortedUsedSchemas.join(", ")} } from '${options.schemaImportPath}'`);
18993
19240
  }
18994
19241
  lines.push("");
18995
19242
  lines.push(`export function createRouter(service: ${serviceName}): Hono {`);
@@ -19026,6 +19273,10 @@ async function generate2(cwd2, configPath) {
19026
19273
  generatedFiles.push(generateService(spec));
19027
19274
  if (framework === "hono") {
19028
19275
  generatedFiles.push(generateRouter(spec));
19276
+ } else if (framework === "express") {
19277
+ generatedFiles.push(generateExpressRouter(spec));
19278
+ } else if (framework === "fastify") {
19279
+ generatedFiles.push(generateFastifyRouter(spec));
19029
19280
  }
19030
19281
  console.log(`Writing output to: ${outputDir}`);
19031
19282
  await (0, import_promises2.mkdir)(outputDir, { recursive: true });
@@ -19034,7 +19285,7 @@ async function generate2(cwd2, configPath) {
19034
19285
  await (0, import_promises2.writeFile)(filePath, await formatTs(file.content, filePath), "utf-8");
19035
19286
  console.log(` \u2713 ${file.filename}`);
19036
19287
  }
19037
- if (framework === "hono" && config.input_schema !== void 0) {
19288
+ if ((framework === "hono" || framework === "express" || framework === "fastify") && config.input_schema !== void 0) {
19038
19289
  const schemaPath = (0, import_node_path2.resolve)(cwd2, config.input_schema);
19039
19290
  let schemaContent;
19040
19291
  try {
@@ -19052,10 +19303,15 @@ async function generate2(cwd2, configPath) {
19052
19303
  const relPath = (0, import_node_path2.relative)(outputDir, schemaPath).replace(/\\/g, "/");
19053
19304
  const schemaImportPath = relPath.startsWith(".") ? relPath : `./${relPath}`;
19054
19305
  const schemaImportPathJs = schemaImportPath.replace(/\.ts$/, ".js");
19055
- const routerFile = generateRouter(spec, {
19056
- schemaNames: exportedSchemas,
19057
- schemaImportPath: schemaImportPathJs
19058
- });
19306
+ const routerOptions = { schemaNames: exportedSchemas, schemaImportPath: schemaImportPathJs };
19307
+ let routerFile;
19308
+ if (framework === "hono") {
19309
+ routerFile = generateRouter(spec, routerOptions);
19310
+ } else if (framework === "express") {
19311
+ routerFile = generateExpressRouter(spec, routerOptions);
19312
+ } else {
19313
+ routerFile = generateFastifyRouter(spec, routerOptions);
19314
+ }
19059
19315
  const routerPath = (0, import_node_path2.join)(outputDir, routerFile.filename);
19060
19316
  await (0, import_promises2.writeFile)(routerPath, await formatTs(routerFile.content, routerPath), "utf-8");
19061
19317
  console.log(` \u2713 router.ts (with Zod validation for ${exportedSchemas.size} schema(s))`);
package/dist/config.d.ts CHANGED
@@ -4,7 +4,7 @@ export interface ServerConfig {
4
4
  /** Directory to write generated files */
5
5
  output: string;
6
6
  /** Framework to generate a router for. Default: 'none' */
7
- framework?: 'hono' | 'none';
7
+ framework?: 'hono' | 'express' | 'fastify' | 'none';
8
8
  /** Path to user-owned Zod schema file (same file as openapi-gen's input_schema). Optional. */
9
9
  input_schema?: string;
10
10
  }
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,aAAa,EAAE,MAAM,CAAA;IACrB,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC3B,8FAA8F;IAC9F,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAcD,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAI3D;AAED,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAW/D;AAED,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAW7D;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA0DxF"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,aAAa,EAAE,MAAM,CAAA;IACrB,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAA;IACd,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,CAAA;IACnD,8FAA8F;IAC9F,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAgCD,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAI3D;AAED,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAW/D;AAED,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAW7D;AAGD,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CA4DxF"}