Skip to content
Blog

Kubb clients 6.0

@kubb/plugin-fetch, @kubb/plugin-axios, and @kubb/plugin-client 6.0 run the fetch and axios clients on one shared core. Both clients now have the same interceptors, client factory, error class, and types. A transport only sends the request, so another HTTP library such as ky takes one line:

src/client.ts
import ky from 'ky'
import { client, toRequestInit } from './gen/.kubb/client'

client.setConfig({
  transport: (request) => ky(request.url, { ...toRequestInit(request), throwHttpErrors: false, retry: 0 }),
})

Code that calls the generated functions or hooks keeps working. Most projects only regenerate.

One core for fetch and axios

In v5 the two clients only looked alike. The core was already shared, but axios wrapped it in its own client, interceptors, and transport types, so an interceptor written for fetch did not run on axios. v6 drops that layer. The core builds the URL, serializes parameters, resolves auth, runs validation and interceptors, and splits success from error. Each package adds only a transport, the function that sends the resolved request.

Now one interceptor runs on both clients, because both hand it the core's ResolvedRequest, TransportResult, and ResponseError:

src/client.ts
client.interceptors.request.use((request) => {
  request.headers['X-Request-ID'] = crypto.randomUUID()
  return request
})

On axios, native handlers that need InternalAxiosRequestConfig or AxiosResponse go on the new axiosInstance export, the instance the default transport sends through.

Bring your own HTTP library

A transport can now return a plain web Response. The core parses it the same way the fetch client does, by responseType and Content-Type, and response interceptors still see the parsed result. toRequestInit(request) builds the RequestInit that fetch and ky take.

This works for any library that hands back an unread Response, including a polyfill such as node-fetch. Keep the library from throwing on an error status, as throwHttpErrors: false does for ky, so the client can return or throw its typed ResponseError.

Axios is the exception, since it never returns a web Response. Its transport still decodes responseType maps itself, so native axios interceptors see parsed data, as they did in v5. That is one more code path to keep in step, but it means handlers you move from v5 keep working.

Set a transport once with client.setConfig({ transport }), per client with createClient({ transport }), or per call with transport. See Use a custom transport.

An operation with an in: cookie parameter gets a <Name>Cookies type and a cookies option. The call, SDK method, query hooks, MCP tool, and Cypress request all take it, and Zod and faker get the same group:

src/session.ts
await getSession({ cookies: { session_id: 'abc' } })

cookies is always optional, because a browser sends its cookies itself. Query keys leave it out, as they do headers. A query key is written into SSR payloads, persisted caches, and devtools, and a session cookie has no business in any of them.

Fixes

  • Stream calls send Accept: text/event-stream. Before, axios asked for JSON, so a server that picks its format from Accept, such as Wikimedia EventStreams, answered with JSON and the stream never ended.
  • Axios honors credentials: include, same-origin, and omit.
  • On the Node http adapter, a responseType: 'blob' call returns a Blob instead of a corrupted string.
  • Both clients read bodies the same way. A body labeled JSON that is not JSON comes back as text, and a 204, 205, or 304 is undefined on both.
  • A failed blob or arraybuffer call parses a JSON or text error body, so error is the error object.
  • A failed call with an empty body reports { status, statusText } as its error, so if (error) and .unwrap() see the failure.
  • An aborted stream stops before the next event and rejects with the signal's reason, so lastEventId stays at the last event you read.
  • On fetch, the Next.js next option reaches fetch, so caching options take effect.

Breaking changes

Regenerate, run tsc --noEmit, and match each error to a row:

Your codeClientWhat to do
Calls generated functions, SDK methods, or hooksallRegenerate. Nothing else.
Imports from templates/fetch.ts, templates/axios.ts, or templates/core.tsfetch, axiosImport types from templates/client.ts, and the shared runtime from @kubb/plugin-client/templates/*.
Uses FetchOptions or AxiosOptionsfetch, axiosRename it to NativeOptions.
Names EventStreamResult<Pet>, StreamResult, or UnwrappedStreamResultfetch, axiosWrap the payload: EventStreamResult<ServerSentEvent<Pet>>.
Registers handlers on client.interceptorsaxiosMove them to axiosInstance.interceptors, or rewrite them for the core objects.
Passes an axios instance as transport, or sets defaultTransport.defaultsaxiosWrap it with createAxiosTransport(instance), and set defaults on axiosInstance.
Wrote your own client with stream helpersplugin-clientTake the full event type, export ServerSentEvent, and add contentType to RequestConfig.

Three axios changes compile, so check them by hand. client.interceptors.request handlers run in the order you register them. client.interceptors.error runs only for a ResponseError, not for a network error. A child from createClient() inherits the transport of client.

Upgrade

v6 needs Kubb 5.16.0 or later. Upgrade Kubb and your client plugin together, then regenerate:

Terminal
npm install --save-dev kubb@latest @kubb/plugin-fetch@6
npx kubb generate
npx tsc --noEmit

Use @kubb/plugin-axios@6 for the axios client. The query plugins (@kubb/plugin-react-query, @kubb/plugin-vue-query, @kubb/plugin-swr, and @kubb/plugin-mcp) work with both v5 and v6, so you can upgrade them separately.

Each client has a migration guide with a before and after for every change:

Feedback goes to GitHub or Discord.