Skip to content

feat: openapi typescript client#2885

Open
Marshevskyy wants to merge 42 commits into
mainfrom
feat/ts-client-gen
Open

feat: openapi typescript client#2885
Marshevskyy wants to merge 42 commits into
mainfrom
feat/ts-client-gen

Conversation

@Marshevskyy

@Marshevskyy Marshevskyy commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

What/Why/How?

Adds @redocly/client-generator and the redocly generate-client command: generate a typed TypeScript client from an OpenAPI description. The emitted client has zero runtime dependencies (web-standard fetch/AbortController/URLSearchParams) and is built via the TypeScript compiler AST, so output is correct by construction. typescript is the only peer dep (the CLI provides it).

Existing tools force a trade-off: types-only (you hand-write every fetch/auth/retry) or a full client that drags a runtime dependency into your bundle forever. This generates the full client into code you own — no runtime dep.

This PR includes approximately 54,000 lines of code. However, around 20,000 lines are examples, and another ~20,000 lines are tests and documentation, so the actual implementation is not that large.

  • Input: OpenAPI 3.0/3.1/3.2 + Swagger 2.0.
  • Output: single/split/tags/tags-split layouts; functions or service-class facade (per-instance config + credentials); flat/grouped args.
  • Types: inline types, discriminated-union is<Member>() guards, <Op>* aliases, Date typing, typed multipart (binary → Blob).
  • Runtime: auth from securitySchemes (+ async providers, per-instance), composable middleware, opt-in abort-aware retries, parseAs, query-serialization styles, result error mode, typed Server-Sent Events (auto-reconnect, OAS 3.2 itemSchema).
  • Generators (--generators): sdk (default), zod, tanstack-query (React/Vue/Svelte/Solid), swr, transformers, mock (MSW + baked/faker), plus a custom-generator plugin API (@redocly/client-generator/plugin).
  • Config: CLI flags, redocly.yaml x-client-generator, or a defineConfig file.
  • Hardened: safe-identifier coercion, comment escaping, bounded SSE reader.

Reference

Testing

  • npm test green (compile + typecheck + unit + e2e); 100% per-file coverage on the package.
  • e2e generates clients, type-checks them under strict tsc, and runs them against a mock server.
  • Runnable, drift-checked examples under packages/client-generator/examples/.

Screenshots (optional)

Check yourself

  • This PR follows the contributing guide
  • All new/updated code is covered by tests
  • Core code changed? - Tested with other Redocly products (internal contributions only)
  • New package installed? - Tested in different environments (browser/node)
  • Documentation update has been considered

Security

  • The security impact of the change has been considered
  • Code follows company security practices and guidelines

Note

Medium Risk
Large new surface area (codegen + emitted runtime affecting all HTTP calls) but experimental status and heavy test/docs mitigate; auth/retry/middleware paths warrant careful review on upgrade.

Overview
Introduces @redocly/client-generator and wires redocly generate-client into the CLI. From OpenAPI 3.x or Swagger 2.0 it emits a zero runtime dependency TypeScript client (AST-based codegen, fetch-only runtime) with configurable layout (single/split/tags), facade (functions vs service-class), args style, error mode, auth, retries, SSE, and optional add-ons via --generators (zod, TanStack Query, SWR, transformers, MSW mocks, custom plugins).

Middleware & publishing: RequestContext carries typed ctx.operation.{id,path,tags} and an OPERATIONS map for stable keys; --setup bakes defineClientSetup({ config, middleware }) into published SDKs. Request bodies are serialized after onRequest, so middleware can mutate ctx.body and have that sent.

Docs & tests: Large command docs, README section, monorepo test/lint ignores for generated e2e output, CLI build banner shims for bundled typescript, and dev deps (MSW, React Query, etc.) for e2e coverage.

Reviewed by Cursor Bugbot for commit 58573fd. Bugbot is set up for automated code reviews on this repo. Configure here.

…enAPI client

Add `@redocly/openapi-typescript` and the `redocly generate-client` command:
generate a typed TypeScript client from an OpenAPI description. The emitted
client has zero runtime dependencies (web-standard fetch/AbortController/
URLSearchParams) and is produced via the TypeScript compiler AST, so output is
correct by construction; `typescript` is the only peer dependency.

Input: OpenAPI 3.0/3.1/3.2.0 + Swagger 2.0 (normalized to 3.x); file, URL, or a
redocly.yaml `apis:` alias; operationId synthesized when absent.

Output: single / split / tags / tags-split layouts; `functions` or
`service-class` facade (per-instance config + credentials); flat or grouped
argument styles.

Types: inline types; enums as unions or runtime const objects; discriminated-
union `is<Member>()` guards; `<Op>Result/Error/Params/Body/Headers/Variables`
aliases with collision suppression; JSDoc from validation keywords; optional
`Date` typing; typed multipart bodies (binary → Blob) auto-serialized to FormData.

Runtime: setBaseUrl + typed ClientConfig; composable middleware (onRequest/
onResponse/onError); opt-in abort-aware retries (backoff, jitter, Retry-After,
custom retryOn); per-call parseAs; OpenAPI query-serialization styles;
`--error-mode result` discriminated returns; minification-safe OPERATIONS map;
typed Server-Sent Events (async iterators, auto-reconnect, OAS 3.2 itemSchema).

Auth: Basic / Bearer / apiKey (header, query, cookie) from securitySchemes, async
token providers, and per-instance credentials via ClientConfig.auth.

Generators (--generators): sdk (default), zod, tanstack-query (react/vue/svelte/
solid), swr, transformers, mock (MSW handlers + baked or faker data, seedable),
plus an experimental custom-generator plugin API (@redocly/openapi-typescript/
plugin) with dual loading (inline + import specifier) and a validated
compatibility contract. Each generator declares requires/facades/errorModes/
dateTypes, validated up front.

Configuration via CLI flags, a redocly.yaml `x-openapi-typescript` block, or a
defineConfig file; plus `--watch`. Hardened: document-derived names coerced to
safe unique identifiers, comment text escaped, bounded SSE reader. Architecture,
ADRs (0001-0012), and runnable examples included.
@Marshevskyy Marshevskyy requested review from a team as code owners June 16, 2026 07:24
@changeset-bot

changeset-bot Bot commented Jun 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 58573fd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@redocly/client-generator Minor
@redocly/cli Minor
@redocly/openapi-core Minor
@redocly/respect-core Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment thread packages/cli/src/commands/generate-client.ts
Comment thread packages/cli/src/commands/generate-client.ts
Comment thread .changeset/openapi-typescript.md Outdated
Comment thread CONTRIBUTING.md Outdated
Comment thread CONTRIBUTING.md Outdated
Comment thread CONTRIBUTING.md Outdated
Comment thread CONTRIBUTING.md Outdated
Comment thread README.md Outdated
Comment thread CONTRIBUTING.md Outdated
Comment thread CONTRIBUTING.md Outdated
Comment thread CONTRIBUTING.md Outdated
Comment thread CONTRIBUTING.md Outdated
Comment thread packages/client-generator/src/index.ts
Comment thread packages/client-generator/src/emitters/runtime.ts
Co-authored-by: Jacek Łękawa <164185257+JLekawa@users.noreply.github.com>
Comment thread packages/client-generator/src/emitters/runtime.ts
Comment thread packages/client-generator/src/generators/index.ts
Comment thread packages/client-generator/src/index.ts
JLekawa and others added 3 commits June 18, 2026 12:17
…ing docs

Switch the runnable examples and the cafe e2e fixture/snapshot to the spec's
`servers[0].url` (api.cafe.redocly.com), regenerated with the current generator.
Demonstrate middleware in the fetch-functions example via `onResponse` so it
isn't blocked by the demo API's CORS preflight (it doesn't allow a custom
`X-Request-Id` request header). Add a "Testing the generated client" section to
the README (Node / browser-CORS / MSW mocks).
Comment thread packages/client-generator/src/generators/resolve.ts
Resolve conflicts in package.json, package-lock.json, packages/cli/package.json,
tsconfig.json, tsconfig.build.json, and vitest.config.ts.

- Adopt main's esbuild-bundled CLI build; add @redocly/openapi-typescript to
  packages/cli devDependencies so it bundles alongside openapi-core/respect-core.
- Bump openapi-typescript's @redocly/openapi-core dependency 2.31.4 -> 2.34.0 to
  match the workspace, so npm symlinks the workspace package instead of a nested
  copy (the mismatch produced divergent Config type identities).
- Extend the CLI bundle banner to shim __filename/__dirname (via var, to coexist
  with deps that self-declare them) so the bundled typescript compiler used by
  generate-client runs in ESM scope.
- Keep the per-glob 100% coverage threshold for openapi-typescript alongside
  main's repo-wide branches:73.
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Performance Benchmark (Lower is Faster)

CLI Version Bundle Lint Check Config
cli-latest ▓ 1.00x ± 0.01 ▓ 1.00x (Fastest) ▓ 1.00x ± 0.01
cli-next ▓ 1.00x (Fastest) ▓ 1.00x ± 0 ▓ 1.00x (Fastest)

Comment thread packages/client-generator/src/emitters/runtime.ts
Comment thread packages/client-generator/src/generators/resolve.ts
…MD009

Three prose lines had a stray single trailing space (lines 644, 651, 939),
which markdownlint MD009 rejects (expects 0 or 2). Verified clean with
markdownlint-cli2 v0.22.0 against the repo's .markdownlint.yaml.
Comment thread packages/cli/src/commands/generate-client.ts Outdated
The vale job used reviewdog with filter_mode: file, which fetches the PR
diff to scope findings to changed files. GitHub's diff API caps at 20000
lines, so large PRs (this one adds ~54k lines) return 406 and reviewdog
fails on the post step — not on any actual vale finding.

filter_mode: nofilter skips the diff fetch and lints the full files
directly. Verified the committed docs are clean (0 errors/warnings/
suggestions across 320 files with vale 3.15.1), so nofilter adds no noise
and the error-level gate still holds.
The consumer harnesses (base/cafe/sse) have tracked index*.ts that import a
generated `./api.js`. The repo-wide `tsc --noEmit` includes tests/**/*.ts, so
it typechecks those imports — but `api.ts` was gitignored and only created when
the e2e suite ran, so a fresh checkout (CI) failed with TS2307.

The harnesses already expected api.ts present for typecheck (see the note in
sse.runtime.test.ts); gitignoring it was the gap. generate-client output is
byte-deterministic for these fixtures (fixed ports, fixed specs — verified by
regenerating and diffing), so commit the files instead of touching tsconfig.
The e2e suite still regenerates them each run, producing identical content
(verified: no drift after running base.test.ts).
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 84.48% (🎯 81%) 9461 / 11199
🔵 Statements 84.28% (🎯 80%) 10102 / 11985
🔵 Functions 87.8% (🎯 84%) 1988 / 2264
🔵 Branches 77.84% (🎯 73%) 6639 / 8528
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/cli/src/commands/generate-client.ts 0% 0% 0% 0% 17-145
packages/client-generator/src/config-file.ts 100% 100% 100% 100%
packages/client-generator/src/errors.ts 100% 100% 100% 100%
packages/client-generator/src/index.ts 93.1% 83.33% 100% 93.1% 104-105
packages/client-generator/src/loader.ts 100% 100% 100% 100%
packages/client-generator/src/plugin.ts 100% 100% 100% 100%
packages/client-generator/src/runtime-contract.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/auth.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/client.ts 99.23% 98.07% 100% 100% 428
packages/client-generator/src/emitters/faker.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/identifier.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/jsdoc.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/mock.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/operation-aliases.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/operation-signature.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/operation-types.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/operations.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/runtime.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/sample.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/setup-bake.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/sse.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/support.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/swr.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/tanstack-query.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/transformers.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/ts.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/type-guards.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/types.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/wrapper-support.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/zod.ts 100% 100% 100% 100%
packages/client-generator/src/generators/index.ts 100% 100% 100% 100%
packages/client-generator/src/generators/mock.ts 100% 100% 100% 100%
packages/client-generator/src/generators/resolve.ts 100% 100% 100% 100%
packages/client-generator/src/generators/sdk.ts 100% 100% 100% 100%
packages/client-generator/src/generators/swr.ts 100% 100% 100% 100%
packages/client-generator/src/generators/tanstack-query.ts 100% 100% 100% 100%
packages/client-generator/src/generators/transformers.ts 100% 100% 100% 100%
packages/client-generator/src/generators/zod.ts 100% 100% 100% 100%
packages/client-generator/src/ir/build.ts 100% 100% 100% 100%
packages/client-generator/src/ir/normalize-swagger2.ts 100% 100% 100% 100%
packages/client-generator/src/ir/refs.ts 100% 100% 100% 100%
packages/client-generator/src/ir/sanitize-identifiers.ts 100% 100% 100% 100%
packages/client-generator/src/writers/group-by-tag.ts 100% 100% 100% 100%
packages/client-generator/src/writers/index.ts 100% 100% 100% 100%
packages/client-generator/src/writers/single-file-writer.ts 100% 100% 100% 100%
packages/client-generator/src/writers/split-writer.ts 100% 100% 100% 100%
packages/client-generator/src/writers/tagged.ts 100% 100% 100% 100%
packages/client-generator/src/writers/tags-split-writer.ts 100% 100% 100% 100%
packages/client-generator/src/writers/tags-writer.ts 100% 100% 100% 100%
packages/client-generator/src/writers/util.ts 100% 100% 100% 100%
Generated in workflow #10521 for commit 18705f9 by the Vitest Coverage Report Action

Comment thread packages/cli/src/commands/generate-client.ts Outdated
…apshots

The branch added `react`/`react-dom` `^18.2.0` to the root devDependencies
(main has neither — it resolves react 19.2.7 via packages/cli's
`^17 || ^18.2.0 || ^19.2.7` range). That root `^18.2.0` cap forced npm to
hoist react 18.3.1 for the whole workspace, so build-docs rendered React 18's
`useId` format (`tab:R9pq:0`) instead of the React 19 format (`tab_R_9pq_0`)
the committed redoc-static snapshots were generated with — failing
build-docs.test.ts on CI.

Bump root react/react-dom to `^19.2.0` so npm resolves 19.2.7, matching main.
Verified: build-docs.test.ts (7/7) and the react-19 consumers
(tanstack-query.runtime, swr) all pass.
filter_mode: nofilter alone wasn't enough — the github-pr-annotations reporter
still fetches the PR diff to position comments, and GitHub caps that diff at
20000 lines, so this 54k-line PR gets a 406 and reviewdog exits 1 (on the diff
fetch, not on any vale finding).

Switch reporter to `local`: reviewdog prints findings to the job log and exits
non-zero only on vale errors, with no GitHub API call and therefore no diff to
fetch. The gate still holds (committed docs verified: 0 vale errors across 320
files). Trade-off: findings show in the Actions log rather than as inline PR
annotations — which a 20k-line-capped diff can't render on a PR this size anyway.

@tatomyr tatomyr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked a couple of root files. Haven't checked any actual implementation yet.

Comment thread .github/workflows/docs-tests.yaml Outdated
with:
files: '["README.md", "docs", ".changeset"]'
filter_mode: file
# `nofilter` instead of `file`: file-level filtering makes reviewdog

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this change for?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vale failed due to too GH api limitation, I'll try to change it back and see if GA action can work...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reverted, but I expect GH action to fail now

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's expected for large docs changes. You can run vale locally to ensure the docs are correct.

Comment thread docs/@v2/commands/generate-client.md Outdated
Comment thread CONTRIBUTING.md Outdated
Having `redocly.yaml` in the root of the project affects the unit tests, and console logs affect the e2e tests, so make sure to get rid of both before running tests.
Run `npm test` to start both unit and e2e tests (and additionally typecheck the code).

### Monorepo test conventions

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These additions are mostly duplications or explanations of a common testing approaches. Noone ever asked us to clarify that. I would refrain from adding this info to the contribution guide as it makes it harder to read and find actually useful info.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

Comment thread vitest.config.ts Outdated
@@ -24,6 +25,14 @@ const configExtension: { [key: string]: ViteUserConfig } = {
functions: 84,
statements: 80,
branches: 73,
// Strict per-file 100% coverage for the new client generator. Per-glob thresholds run

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't add separate thresholds per packages. We used to do it that way but eventually moved away because of the maintenance complications.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

Comment thread tsconfig.json Outdated
Comment on lines +26 to +27
"exclude": ["node_modules"],
"include": [

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated a bit, but we have to exclude examples since they include react code, which is not covered in this tsconfig...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's discuss offline whether we need React examples.

js: "import { createRequire as __createRequire } from 'node:module';\nconst require = __createRequire(import.meta.url);",
js: [
"import { createRequire as __createRequire } from 'node:module';",
"import { fileURLToPath as __fileURLToPath } from 'node:url';",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the packages that require these? Cannot we use ESM ones instead to have as few fallbacks as possible?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the new one...
I'll try to find a better approach

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question, this change tells the CLI bundler how to handle the typescript compiler that generate-client newly pulls in — without it, the bundled CLI crashes with __filename is not defined.
As alternative we can revert this changes, but add typescript as dep instead of devDep in packages/cli, what would you prefer?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's discuss that offline.

Comment thread packages/openapi-typescript/README.md Outdated
@@ -0,0 +1,387 @@
# @redocly/openapi-typescript

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just @redocly/sdk or something like that? What if we want to support flavours other than OpenAPI, say AsyncAPI? Or other languages?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about naming, but in the future, we can add support for other languages: openapi-go, openapi-python...
we can name it like sdk-ts, sdk-python, what do you think?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I just wouldn't restrict it to openapi since we could support other spes in the same package.

# Conflicts:
#	package-lock.json
#	packages/cli/package.json
The generated client's `__send` built the request payload from `body` *before*
running the `onRequest` middleware chain, so a middleware that mutated `ctx.body`
(enveloping, signing, camel/snake conversion) had its change silently dropped.

Move serialization to after `onRequest`, reading `context.body`/`context.headers`
so mutations take effect. `RequestContext` is documented as `body`-mutable, and a
new middleware e2e asserts a mutated body is what gets sent. Regenerated all
committed clients (the change is in the inlined runtime).
…er setup

Two related additions to the generated client:

- **Operation context.** `RequestContext` now carries `operation: { id, path, tags }`
  — the operationId, path *template* (stable across interpolation), and tags — so
  middleware/`onRequest` can target requests semantically (tracing span names,
  per-operation auth, metrics labels) instead of reverse-mapping an interpolated URL.
  `ctx.operation.{id,path,tags}` and the `OPERATIONS` map are typed as literal unions
  (`OperationId`/`OperationPath`/`OperationTag`, all exported) for autocomplete and
  compile-time typo-checking; `OPERATIONS` entries gained `tags`.

- **Baked publisher setup.** A `--setup` flag bakes a publisher-authored
  `defineClientSetup({ config, middleware })` module into the generated client (all
  output modes, both facades), so a published SDK ships its request/response defaults
  built in. The package exports the runtime-contract types + `defineClientSetup`.

New `customization` and `baked-setup` examples, ADRs 0014/0015, and e2e coverage
(operation-types, setup). Regenerated all committed clients.
Comment thread packages/client-generator/src/emitters/operation-signature.ts Fixed
Comment thread tests/e2e/generate-client/middleware.test.ts Fixed
Comment thread tests/e2e/generate-client/setup.test.ts Fixed
Comment thread tests/e2e/generate-client/setup.test.ts Fixed
Comment thread tests/e2e/generate-client/setup.test.ts Fixed
Comment thread tests/e2e/generate-client/setup.test.ts Fixed
Comment thread tests/e2e/generate-client/setup.test.ts Fixed
Comment thread tests/e2e/generate-client/base-consumer/server.ts Fixed
Comment thread tests/e2e/generate-client/cafe-consumer/server.ts Fixed
Comment thread tests/e2e/generate-client/path-param-idents.test.ts Fixed
Comment thread packages/client-generator/src/emitters/operations.ts
… cat)

- Bound the path-template regex inner class to [^{}] so it can't backtrack
  quadratically on adversarial spec paths (operation-signature, mock, operations).
  Output is unchanged — param names can't contain braces.
- Assert request URLs by parsed origin instead of startsWith() in the
  setup/middleware e2e tests (incomplete URL substring sanitization).
- Stop echoing error.message into the mock-server 500 responses.
- Read the generated client with readFileSync instead of spawning cat.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4b7b44a. Configure here.

Comment thread packages/client-generator/src/index.ts
* is absent. (A URL/registry `input` is left untouched.)
*/
function readRedoclyExtension(config: Config): Record<string, unknown> {
const raw = (config.resolvedConfig as Record<string, unknown>)['x-client-generator'];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of typecasting, let's add the property to our config in (firtsly in redocly-yaml).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, it could be just clientGenerator, no need in x- prefixes as this is not a specification, just our config which we can extend as desired.

function readRedoclyExtension(config: Config): Record<string, unknown> {
const raw = (config.resolvedConfig as Record<string, unknown>)['x-client-generator'];
if (!isPlainObject(raw)) return {};
const ext: Record<string, unknown> = { ...raw };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for?

const baseDir = config.configPath ? dirname(config.configPath) : undefined;
if (baseDir) {
if (
typeof ext.input === 'string' &&

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if there's a real need in this. It could be normalized during the config instantiation or directly where needed based on the configPath as the base dir.

}

export type GenerateClientCommandArgv = {
input?: string;

@tatomyr tatomyr Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's input? Is it the openapi root file? I would just use the same approach as in other commands like bundle or lint instead of declaring it in the config.
A think we use api/apis elsewhere for this.

input?: string;
output?: string;
config?: string;
'base-url'?: string;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this? Could we use the config location as the base?

Comment on lines +67 to +71
// Config sources, lowest → highest precedence: the `redocly.yaml` `x-client-generator`
// block (located via the standard `--config` flag, else discovered in cwd) → CLI flags.
const redoclyExtension = readRedoclyExtension(config);
const merged = mergeConfig(redoclyExtension as Partial<OpenApiTsConfig>, {
input: argv.input,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer something simple yet explicit, like the following:

Suggested change
// Config sources, lowest → highest precedence: the `redocly.yaml` `x-client-generator`
// block (located via the standard `--config` flag, else discovered in cwd) → CLI flags.
const redoclyExtension = readRedoclyExtension(config);
const merged = mergeConfig(redoclyExtension as Partial<OpenApiTsConfig>, {
input: argv.input,
const merged = {
input: argv.input ?? config?.clientGenerator?.output,

BTW, are we going to support per-api configuration? Depending on the answer, we have to read the configuration a little bit differently.


// Relative-path generator specifiers (and inline plugins) resolve against the
// `redocly.yaml` directory (the config's location), else the working directory.
const configDir = config.configPath ? dirname(config.configPath) : process.cwd();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd expect config.configPath to be resolved already.

…from cwd

- --base-url now accepts relative values (e.g. /v1) that OpenAPI allows for
  servers[].url and that the runtime concatenates with paths. Validation uses
  new URL(value, 'http://localhost') so absolute URLs still parse and truly
  malformed values are still rejected.
- A relative --setup is resolved against the cwd in the command handler (like
  --output) before reaching generateClient, instead of generateClient resolving
  it against the redocly.yaml directory — so sibling --output/--setup paths
  resolve consistently when --config points outside the cwd.
…hain

A typed multipart/form-data body was serialized to FormData at the call site,
before middleware ran, so onRequest saw FormData (not the plain object) and a
middleware that mutated or replaced ctx.body had its change dropped — contrary to
the documented body-mutation contract and to the JSON path, which already
serializes after onRequest.

The call site now passes the plain object plus a multipart flag, threaded through
__request/__requestResult into __send, which builds the FormData after the
onRequest chain. Everything is gated on needsMultipart, so non-multipart clients
are byte-identical.
…ke output

generateClient resolved a relative setup path against configDir while output
resolved against cwd, so with --config pointing outside the cwd a relative
--setup and --output resolved against different bases. Resolve setup against cwd
to match output; the CLI already pre-resolves --setup (cwd, like --output) and a
config-file setup is pre-resolved against the config dir, so both arrive absolute.
'@redocly/cli': patch
---

`generate-client`: the generated client now serializes the request body **after** the `onRequest` middleware chain runs, so a middleware that mutates `ctx.body` (enveloping, signing, case conversion) has its change sent. Previously the body was serialized before `onRequest`, silently dropping such mutations.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this changeset. It's sufficient to gather all changes from this PR into one.

configDir,
});
const summary =
result.files.length === 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it better to utilize the pluralize function exported from openapi-core instead?

Comment thread packages/cli/src/index.ts
'Generators to run, comma-separated (default: sdk). Built-in names (sdk, zod, tanstack-query, swr, transformers, mock) or a custom-generator path/package specifier. Example: --generators sdk,zod or --generators sdk,./my-generator.ts',
type: 'string',
coerce: (value: string | undefined): string[] | undefined => {
// Parse only — built-in names, inline custom names, and plugin specifiers are all

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need the comments. If something feels unclear, please use more descriptive names.

{ output: 'b.ts', outputMode: undefined, facade: 'service-class' }
);
expect(merged).toEqual({
input: 'spec.yaml',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we were to set the input/output files in the config, I'd suggest following the current approach (using the apis section).

Comment thread .oxfmtrc.json
"packages/respect-core/src/modules/runtime-expressions/abnf-parser.js",
"packages/core/src/rules/common/__tests__/fixtures/invalid-yaml.yaml",
"tests/performance/api-definitions/",
"tests/e2e/**/*.yaml",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are specific test files that you don't want to be formatted (e.g. with wrong content), please enlist them explicitly, don't suppress formatting for all test files.

Comment thread .oxlintrc.json Outdated
Comment on lines +14 to +19
"packages/client-generator/examples/*/src/api/**",
"packages/client-generator/examples/*/generate.ts",
"*.js",
"*.cjs",
"tests/e2e/generate-client/*.snapshot.ts",
"tests/e2e/generate-client/*-consumer/api.ts"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"packages/client-generator/examples/*/src/api/**",
"packages/client-generator/examples/*/generate.ts",
"*.js",
"*.cjs",
"tests/e2e/generate-client/*.snapshot.ts",
"tests/e2e/generate-client/*-consumer/api.ts"
"*.js",
"*.cjs",
"packages/client-generator/examples/*/src/api/**",
"packages/client-generator/examples/*/generate.ts",
"tests/e2e/generate-client/*.snapshot.ts",
"tests/e2e/generate-client/*-consumer/api.ts"

Comment thread CONTRIBUTING.md
To update snapshots, run `npm run unit -- -u`.
To skip coverage, run it with `--coverage=false`.

Run `npm run unit` with coverage reporting always enabled (the `coverage` block in the root config sets `enabled: true`); the HTML report is written to `coverage/`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Run `npm run unit` with coverage reporting always enabled (the `coverage` block in the root config sets `enabled: true`); the HTML report is written to `coverage/`.

It's just a common configuration. And the coverage is discussed in the previous paragraph.

Comment thread package.json
"license": "MIT",
"devDependencies": {
"@changesets/cli": "^2.26.2",
"@faker-js/faker": "^9.9.0",

@tatomyr tatomyr Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already use faker in respect-core. Does it make sense to reuse it from there?

Nevermind, I mixed this one with the package's package.json.

const SERVER_PORT = 3102;
const SERVER_BASE = `http://127.0.0.1:${SERVER_PORT}`;

async function waitForServerReady(timeoutMs: number): Promise<void> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In E2E tests, I'd expect to compare the command output against snapshots. What do we need a live server for?

join(dir, 'tsconfig.json'),
JSON.stringify({
compilerOptions: {
module: 'node16',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just in case, the minimal supported node version is v20.


{% admonition type="warning" name="Experimental" %}
`generate-client` is **experimental**.
Its CLI flags, generated output, configuration schema, and the custom-generator plugin API may change in any minor release until the feature is declared stable.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Its CLI flags, generated output, configuration schema, and the custom-generator plugin API may change in any minor release until the feature is declared stable.
Its interface, generated output, and configuration schema may change in any release until the feature is declared stable.

{% admonition type="warning" name="Experimental" %}
`generate-client` is **experimental**.
Its CLI flags, generated output, configuration schema, and the custom-generator plugin API may change in any minor release until the feature is declared stable.
Pin your `@redocly/cli` version if you depend on the generated output, and plan to regenerate when you upgrade.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be obvious from the above sentence.

We'd love your feedback while we stabilize it.
{% /admonition %}

Generate a typed TypeScript client from an OpenAPI description.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you follow the same structure as in other commands' docs? And it would be nice if we can shorten the description or restrutcure / split it into several pages as it's easy to get lost here.

```sh
npx @redocly/cli@latest generate-client <input> --output <file.ts>

# <input> is an OpenAPI description file path or an `apis:` alias from redocly.yaml

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

## Usage

```sh
npx @redocly/cli@latest generate-client <input> --output <file.ts>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to use one notation (either npx @redocly/cli ... or simply redocly ...) but not both in the same example as it looks confusing. (I prefer the second one for consistency with other docs.)


## Options

{% table %}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you use regular table here? We use Markdoc and Markdown tables here and there, but I'd like to make that uniform. And I don't see much benefits from the Marcdoc one over regular tables, so we'll probably convert them anyway to be uniform.

redocly generate-client --config ./config/redocly.yaml
```

**Precedence** (lowest to highest): the `redocly.yaml` `x-client-generator` block → individual CLI flags.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the same precedence order as for other commands (CLI flags -> config), and this is a common approach. So there's no need to state that separately, I believe.

"type": "module",
"types": "lib/index.d.ts",
"sideEffects": false,
"exports": {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to define separate exports. Let's just export everything from the root index if possible.

const input = join(workDir, 'spec.yaml');
await writeFile(
input,
`openapi: 3.0.3

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use outdent for readability.

import { builtinGenerators, validateGenerators } from './generators/index.js';
import { resolveGenerators } from './generators/resolve.js';
import type { GeneratorDescriptor } from './generators/types.js';
import { buildApiModel } from './ir/build.js';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is ir? Could you use meaningful file/folder names?


Generate a typed TypeScript client from an OpenAPI description.

Accepts **OpenAPI 3.x**, plus **Swagger 2.0** (normalized to the 3.x shape before generation).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not even sure it worth supporting Swagger at this point. Most likely all its audience already use something. I'd target new users (hence the recent standards) mostly.

…ient

The examples are effectively e2e fixtures (the drift test already lives in
tests/e2e/generate-client), so co-locate them there. The root tsconfig exclude is
repointed to the new path rather than removed: the root `npm run typecheck` globs
the whole repo and the examples compile under their own browser/JSX/bundler config,
so they must stay out of the Node-targeted pass wherever they live. Updated the
regen/typecheck scripts, oxlint/oxfmt ignores, the drift test path, and the doc
links.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants