Skip to content

feat: make TanStack Query plugin respect responseStyle: 'fields'#3662

Open
JorrinKievit wants to merge 7 commits intohey-api:mainfrom
JorrinKievit:feat/tanstack-enriched-errors
Open

feat: make TanStack Query plugin respect responseStyle: 'fields'#3662
JorrinKievit wants to merge 7 commits intohey-api:mainfrom
JorrinKievit:feat/tanstack-enriched-errors

Conversation

@JorrinKievit
Copy link
Copy Markdown

@JorrinKievit JorrinKievit commented Mar 28, 2026

Problem

When using the TanStack Query plugin, there's no way to access the HTTP Response object — status codes, headers, or request metadata. The plugin always destructures { data } from the SDK response and discards everything else, even when responseStyle: 'fields' is configured.

This makes it impossible to:

  • Check HTTP status codes on errors (e.g., show a 404 page, redirect on 401)
  • Read response headers on success (e.g., pagination X-Total-Count, rate-limit headers)
  • Implement global error handling based on status codes in QueryCache.onError

Related issues: #3628, #3632, #2070, #1762

Solution

This PR makes the TanStack Query plugin actually respect responseStyle. A new responseStyle config option is added to the plugin. I decided to have "data" as the default, since that kinda is happening right now for the plugin.

Plugin-level config

plugins: [
  {
    name: '@tanstack/react-query',
    responseStyle: 'fields', // opt-in
  },
]

Per-query override

Each generated function accepts a generic TStyle parameter, so you can override per-query without changing the global default:

// Default — same as today
const { data } = useQuery(getUserOptions());
data?.name; // TData directly

// Opt-in per query
const { data, error, isError } = useQuery(getUserOptions({ responseStyle: 'fields' }));
data?.data.name;              // typed response data
data?.response.status;        // HTTP status
data?.response.headers;       // response headers

if (isError) {
  error.response.status;      // 404
  error.error.message;        // typed error body
}

Also works the other way — if the plugin default is 'fields', you can override specific queries back to 'data'.

Discussion point

For me the primary reason is being able to access the response type on Errors so I can handle them globally. But also included it for succesfull responses because I saw other issues.

What I don't like is that I kinda want to enable it globally, but only for Errors and not for the data itself. Because that would mean I am gonna have nested data objects everywhere 🤔

@bolt-new-by-stackblitz
Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 28, 2026

Someone is attempting to deploy a commit to the Hey API Team on Vercel.

A member of the Team first needs to authorize it.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 28, 2026

⚠️ No Changeset found

Latest commit: d83ee3c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 28, 2026

Codecov Report

❌ Patch coverage is 6.75676% with 138 lines in your changes missing coverage. Please review.
✅ Project coverage is 39.23%. Comparing base (8d3167c) to head (d83ee3c).

Files with missing lines Patch % Lines
...plugins/@tanstack/query-core/v5/mutationOptions.ts 5.00% 34 Missing and 4 partials ⚠️
...rc/plugins/@tanstack/query-core/v5/queryOptions.ts 5.12% 33 Missing and 4 partials ⚠️
...ns/@tanstack/query-core/v5/infiniteQueryOptions.ts 5.40% 31 Missing and 4 partials ⚠️
...i-ts/src/plugins/@tanstack/query-core/v5/plugin.ts 12.50% 20 Missing and 1 partial ⚠️
...c/plugins/@hey-api/client-angular/bundle/client.ts 0.00% 1 Missing and 1 partial ⚠️
...src/plugins/@hey-api/client-fetch/bundle/client.ts 0.00% 1 Missing and 1 partial ⚠️
...rc/plugins/@hey-api/client-ofetch/bundle/client.ts 0.00% 1 Missing and 1 partial ⚠️
...ts/src/plugins/@hey-api/client-ky/bundle/client.ts 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3662      +/-   ##
==========================================
- Coverage   39.39%   39.23%   -0.16%     
==========================================
  Files         520      520              
  Lines       19279    19383     +104     
  Branches     5714     5722       +8     
==========================================
+ Hits         7595     7605      +10     
- Misses       9445     9529      +84     
- Partials     2239     2249      +10     
Flag Coverage Δ
unittests 39.23% <6.75%> (-0.16%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 28, 2026

Open in StackBlitz

@hey-api/codegen-core

npm i https://pkg.pr.new/@hey-api/codegen-core@3662

@hey-api/json-schema-ref-parser

npm i https://pkg.pr.new/@hey-api/json-schema-ref-parser@3662

@hey-api/nuxt

npm i https://pkg.pr.new/@hey-api/nuxt@3662

@hey-api/openapi-ts

npm i https://pkg.pr.new/@hey-api/openapi-ts@3662

@hey-api/shared

npm i https://pkg.pr.new/@hey-api/shared@3662

@hey-api/spec-types

npm i https://pkg.pr.new/@hey-api/spec-types@3662

@hey-api/types

npm i https://pkg.pr.new/@hey-api/types@3662

@hey-api/vite-plugin

npm i https://pkg.pr.new/@hey-api/vite-plugin@3662

commit: d83ee3c

@JorrinKievit JorrinKievit marked this pull request as ready for review March 28, 2026 01:36
@pullfrog
Copy link
Copy Markdown

pullfrog bot commented Mar 28, 2026

Reviewed PR #3662. Found a critical runtime/type mismatch bug where the default TStyle = 'fields' doesn't align with the runtime check options?.responseStyle === 'fields' (fails when undefined), a regression where .export(plugin.config.mutationOptions.exported) was dropped to .export(), and significant code duplication across three files. Review posted.

Task list (5/5 completed)
  • Read the diff TOC and identify high-risk areas of change
  • Review core plugin logic changes (non-snapshot files)
  • Review generated code / snapshot changes for correctness
  • Check for impact on existing APIs (breaking changes)
  • Submit review with findings
Pullfrog  | [View workflow run](https://github.com/hey-api/openapi-ts/actions/runs/23674544498/job/68974902316) | Triggered by [Pullfrog](https://pullfrog.com) | [𝕏](https://x.com/pullfrogai)

@dosubot dosubot bot added size:L This PR changes 100-499 lines, ignoring generated files. feature 🚀 Feature request. labels Mar 28, 2026
Copy link
Copy Markdown

@pullfrog pullfrog bot left a comment

Choose a reason for hiding this comment

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

Thanks for working on this — the feature is genuinely useful and the overall design (plugin-level config + per-query TStyle override) is solid. There is one critical runtime bug and a few other issues that need addressing before this can merge.

Critical: The runtime conditional options?.responseStyle === 'fields' does not match when responseStyle is undefined (the common case when the user omits it), yet the type-level default for TStyle is 'fields'. This causes a type-safety violation where TypeScript thinks the return is { data, request, response } but the actual runtime value is just the unwrapped data.

Other issues: a regression where .export(plugin.config.mutationOptions.exported) was changed to .export(), duplicate imports, and heavy code duplication across three files that could be reduced significantly.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run𝕏

@JorrinKievit JorrinKievit force-pushed the feat/tanstack-enriched-errors branch from 8889ad1 to 9086190 Compare April 1, 2026 15:46
@pullfrog
Copy link
Copy Markdown

pullfrog bot commented Apr 4, 2026

TL;DR — Adds a responseStyle: 'fields' option to every TanStack Query plugin so generated query/mutation helpers can return the full { data, request, response } object (or throw { error, request, response }) instead of only the parsed data. This gives consumers access to HTTP status codes, headers, and other response metadata without abandoning the default behavior.

Key changes

  • Add responseStyle config to all TanStack Query plugins — Each variant (React, Vue, Svelte, Solid, Preact, Angular) gains a responseStyle?: 'data' | 'fields' user config option (default 'data') with JSDoc documentation.
  • Generate ResponseResult / ResponseError conditional types — When responseStyle is 'fields', the plugin emits two utility type aliases that use a TStyle generic to conditionally wrap or unwrap response/error payloads.
  • Branch code generation for queryOptions, mutationOptions, and infiniteQueryOptions — Each generator now forks into a 'fields' path (with TStyle generic, responseStyle: 'fields' passed to the SDK, and runtime ternary to return full or data-only result) and a 'data' path that preserves the original output unchanged.
  • Update client bundles to throw enriched errors — All four client bundles (Angular, Fetch, Ky, oFetch) now check opts.responseStyle === 'fields' inside the throwOnError branch and throw { error, ...result } instead of just the error.
  • Add test case and snapshot coverage — A new responseStyle test config for @tanstack/react-query generates full snapshot suites across OpenAPI 2.0.x, 3.0.x, and 3.1.x specs.

Summary | 286 files | 3 commits | base: mainfeat/tanstack-enriched-errors


responseStyle plugin configuration

Before: TanStack Query plugins had no awareness of responseStyle; generated queryFn / mutationFn always returned only parsed data.
After: A new responseStyle: 'data' | 'fields' option (default 'data') is available on every TanStack Query plugin. When set to 'fields', generated functions accept an optional responseStyle param and return ResponseResult<TData, TStyle> / throw ResponseError<TError, TStyle>.

The option is added to both UserConfig (what users pass) and Config (the resolved internal type) for all six TanStack Query variants. The default config sets responseStyle: 'data' to preserve backward compatibility.

react-query/types.ts · react-query/config.ts


Conditional ResponseResult and ResponseError types

Before: Query generics used raw TData / TError types directly.
After: When responseStyle is 'fields', two conditional type aliases are emitted into the generated output: ResponseResult<TData, TStyle> resolves to { data: TData; request: Request; response: Response } when TStyle is 'fields', and plain TData otherwise. ResponseError<TError, TStyle> follows the same pattern with an error field.

These types are built using a RawTypeTsDsl escape hatch that wraps raw ts.ConditionalTypeNode AST nodes — necessary because the TS DSL doesn't natively support conditional types.

Why a RawTypeTsDsl class? The existing code generation DSL has no primitive for conditional types (TStyle extends 'fields' ? ... : ...). Rather than extending the DSL, this PR introduces a minimal TsDsl subclass that wraps an already-built ts.TypeNode and returns it verbatim from toAst(). This keeps the surface area small while enabling the conditional type pattern.

query-core/v5/plugin.ts


Forked code generation in query/mutation/infinite options

Before: queryOptions, mutationOptions, and infiniteQueryOptions generators produced a single code path that read responseStyle from the SDK plugin's config.
After: Each generator checks plugin.config.responseStyle at codegen time. The 'fields' branch adds a TStyle generic parameter, passes responseStyle: 'fields' to the SDK call, captures the full result, and uses a runtime ternary (options?.responseStyle === 'fields' ? result : result.data) to return the appropriate shape. The 'data' branch preserves the original behavior identically.

Code path SDK call includes responseStyle Return value Generic types
'data' (default) No data or destructured { data } TData, TError
'fields' responseStyle: 'fields' Conditional on options.responseStyle ResponseResult<TData, TStyle>, ResponseError<TError, TStyle>

queryOptions.ts · mutationOptions.ts · infiniteQueryOptions.ts


Enriched error throwing in client bundles

Before: When throwOnError was true, all client bundles threw finalError directly.
After: If opts.responseStyle === 'fields', the client throws { error: finalError, ...result } instead, giving the catch handler access to the request, response, and other result fields alongside the error.

This change is applied identically to client-angular, client-fetch, client-ky, and client-ofetch bundles.

client-fetch/bundle/client.ts


Test coverage

Before: No test scenario for TanStack Query with responseStyle: 'fields'.
After: A new test config in plugins.test.ts generates @tanstack/react-query output with responseStyle: 'fields', producing snapshot suites for all three OpenAPI spec versions (2.0.x, 3.0.x, 3.1.x).

plugins.test.ts

Pullfrog  | View workflow run | Triggered by Pullfrog𝕏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature 🚀 Feature request. size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant