Dosu LogoDosu Logo
Ask
Join our Discord
Kubb's SpacePublic
Kubb
DocumentsKubb's Space
transport
transport
Type
External
Status
Published
Created
Sep 2, 2026
Updated
Sep 5, 2026
Updated by
Dosu Bot
Source
snippets/how-to/transport.md

Use a custom transport#

The client Kubb generates splits into two layers: a shared core that builds the URL, serializes the query and body, resolves auth, and runs the interceptors, and a transport that takes the finished request and sends it last. Swap the transport and you change how a request leaves your app without touching anything the core already handled.

You set the transport at runtime on the client, not in kubb.config.ts. Plugin options control what gets generated, while the transport controls how those generated functions reach the network.

  • @kubb/plugin-fetch takes a transport function.
  • @kubb/plugin-axios takes an axios instance.

When to reach for it#

Most apps never need a custom transport. The defaults send through globalThis.fetch and axios.create(), and the auth, baseURL, and headers options cover the common cases. Replace the transport when the send itself needs to change:

  • Add retries, timeouts, or circuit breaking around every request.
  • Route through a runtime-specific HTTP client, such as undici on Node or a service-worker proxy in the browser.
  • Capture metrics or structured logs for each call.
  • Return canned responses in tests without hitting the network.

Tip

For per-request concerns like adding a header or reading a response, an interceptor or the auth resolver is the lighter tool. Reach for a custom transport when you need to own the send.

Fetch: a transport function#

@kubb/plugin-fetch types the transport as a function that receives a fully resolved request and returns a result:

type Transport = (request: ResolvedRequest) => Promise<TransportResult>

type ResolvedRequest = {
  url: string
  method: string
  headers: Record<string, string>
  body?: RequestBody
  signal?: AbortSignal
  credentials?: RequestCredentials
  options?: FetchOptions
  responseType?: ResponseType
}

type TransportResult<TData = unknown> = {
  data: TData
  status: number
  statusText: string
  headers: Headers
  contentType?: string
  request: Request
  response: Response
}

The core hands you a ResolvedRequest with the URL built, the query and body serialized, and auth headers set. Return the parsed data along with the native request and response, so status, headers, and the raw body stay reachable on the result.

Wrap the default send#

A custom transport can delegate to fetch and add behavior around it. This one retries a failed GET with exponential backoff:

import { client, type Transport } from './gen/.kubb/client'

const withRetry: Transport = async (request) => {
  const maxAttempts = 3

  for (let attempt = 1; ; attempt++) {
    const response = await globalThis.fetch(request.url, {
      method: request.method,
      headers: request.headers,
      body: request.body,
      signal: request.signal,
      credentials: request.credentials,
    })

    if (response.ok || request.method !== 'GET' || attempt === maxAttempts) {
      return {
        data: response.status === 204 ? undefined : await response.clone().json().catch(() => undefined),
        status: response.status,
        statusText: response.statusText,
        headers: response.headers,
        request: new Request(request.url),
        response,
      }
    }

    await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 100))
  }
}

client.setConfig({ transport: withRetry })

setConfig updates the shared client every generated function imports, so every call now retries. The core still parses data off the TransportResult you return and turns a non-2xx status into a thrown ResponseError when throwOnError is on, exactly as the default transport does.

Mock the network in tests#

Because the transport is the only piece that touches the network, a test can replace it with a function that returns a fixed result:

import { createClient } from './gen/.kubb/client'
import { getPetById } from './gen/clients/getPetById'

const testClient = createClient({
  transport: async (request) => ({
    data: { id: 1, name: 'Fluffy' },
    status: 200,
    statusText: 'OK',
    headers: new Headers(),
    request: new Request(request.url),
    response: new Response(),
  }),
})

const { data } = await getPetById({ path: { petId: 1 }, client: testClient })
// ^ { id: 1, name: 'Fluffy' }

createClient returns an isolated instance bound to your transport, so the test never mutates the shared client. Pass it per call with the client option, or hand it to a query plugin.

Axios: a custom instance#

@kubb/plugin-axios types the transport as an AxiosInstance. The default is axios.create(), and you replace it with your own pre-configured instance:

type ClientConfig = {
  // ...
  transport?: AxiosInstance
}

This keeps you on axios's own API for the send, so an instance you already configure elsewhere drops straight in. Kubb still owns the URL, query, body, and auth, then forwards them to the instance as an AxiosRequestConfig.

Pass a pre-configured instance#

Give the client an instance with a timeout, default headers, and a logging interceptor:

import axios from 'axios'
import { client } from './gen/.kubb/client'

const instance = axios.create({
  timeout: 10_000,
  headers: { 'X-Client': 'kubb' },
})

instance.interceptors.response.use((response) => {
  console.info(`${response.config.method?.toUpperCase()} ${response.config.url} -> ${response.status}`)
  return response
})

client.setConfig({ transport: instance })

Every generated function now sends through your instance, so its timeout, headers, and interceptors apply to each call.

Note

Kubb sets transformRequest, paramsSerializer, and validateStatus on each request so its own serialization and throwOnError handling stay in charge. Configure cross-cutting concerns like timeouts, retries, and interceptors on the instance instead of overriding those fields. For a native axios field on a single call, such as timeout or onUploadProgress, pass options instead of building a new instance.

Add retries with a plugin#

Because the transport is a real axios instance, axios plugins work on it. Wire up axios-retry on the instance you pass as the transport:

import axios from 'axios'
import axiosRetry from 'axios-retry'
import { createClient } from './gen/.kubb/client'

const instance = axios.create({ baseURL: 'https://petstore.swagger.io/v2' })
axiosRetry(instance, { retries: 3, retryDelay: axiosRetry.exponentialDelay })

export const apiClient = createClient({ transport: instance })

Where to set it#

A transport rides the same ClientConfig as baseURL and auth, so you set it the same three ways.

Call client.setConfig({ transport }) to cover the whole app at once, since every generated function imports the shared client. Call createClient({ transport }) for an isolated client you pass on the client option or hand to a query plugin, which suits tests and talking to more than one backend. Pass the transport option on a single request to override both for that one call.

See also#

  • @kubb/plugin-fetch
  • @kubb/plugin-axios
  • Interceptors
  • Authentication guide
  • Set your own baseURL
Documents
Axios Plugin
a-working-mcp-server-from-a-spec
base-url
basic-usage
build-a-url-without-sending
calling-operations
choose-the-client-when-two-are-registered
class-based-sdk
claude-mcp-plugin
comparison
ecosystem
generate
generators
index
index
installation
introduction
kubb-invalid-plugin-options
kubb-plugin-failed
migration
nuxt
options
options
parsers
plugin-client
plugin-cypress
plugin-mcp
plugins
point-at-an-env-driven-host
point-at-an-env-driven-host
query-errors-transport
README
recipes
register-handlers-with-a-server
rspack
serialization
serialization
stream-server-sent-events
transport
typed-request-helpers-against-staging
v5
validate-every-api-response
validate-requests-and-responses
validate-requests-and-responses
OpenAPI Schema Conversion
a-working-mcp-server-from-a-spec
adapter-oas
adapters
ast
calling-operations
calling-operations
changelog
class-based-sdk
claude
claude-code-plugin
claude-mcp-plugin
coerce-query-and-form-input
comparison
contributing
downgrade-int64-to-a-plain-number
ecosystem
encode-a-custom-type-on-requests
faq
format-date-fields-with-dayjs
generate
generators
How can you extend Kubb's generated code to access OpenAPI security schemes inside fetch functions, so each generated client knows which security scheme it requires?
index
index
index
index
index
index
index
index
introduction
kubb-adapter-required
kubb-deprecated
kubb-invalid-document
kubb-invalid-server-variable
kubb-plugin-failed
kubb-ref-not-found
kubb-unsupported-format
map-spec-types-to-native-ts
migration
nuxt
options
parsers
plugin-mcp
plugin-ts
plugin-zod
plugins
prefix-every-schema-type-name
printers
README
recipes
rspack
serialization
serialization
serialization
tree-shakeable-enums
v3
v5
Package Version Management
init
kubb-plugin-failed
kubb-update-available
migration
nuxt
parsers
rspack
AGENTS
CLAUDE
CONTRIBUTING
GEMINI
SKILL
SKILL
What new feature was proposed and implemented for the `kubb-cli` tool regarding OpenAPI/Swagger file validation?
a-barrel-in-every-folder
adapters
architecture
ast
astro
authentication
authentication
authentication
auto-generated-mock-data
barrel
barrel-files
base-url
base-url
build-a-url-without-sending
calling-operations
calling-operations
calling-operations
configuration
copilot-instructions
creating-plugins
custom-query-keys
custom-query-keys
deterministic-data-with-a-seed
diagnostics
diagnostics
engine
error-handling
error-handling
error-handling
esbuild
exclude
farm
generators
grouping
handlers-you-fill-from-tests
hooks
immutable-requests
include
index
index
index
index
index
index
index
index
infinite-scroll-query
infinite-scroll-query
interceptors
interceptors
interceptors
jsx
kit
kit
kubb-clean-root
kubb-format-failed
kubb-input-not-found
kubb-input-request-failed
kubb-input-required
kubb-input-unreachable
kubb-legacy-input
kubb-lint-failed
kubb-path-traversal
kubb-performance
kubb-plugin-info
kubb-plugin-not-found
kubb-plugin-warning
kubb-post-generate-failed
kubb-unknown
llmstxt
localized-mock-data
macros
macros
macros-option
markdown
mcp
mcp
named-re-exports-for-tree-shaking
one-wildcard-barrel
options
options
options
options
options
options
options
options
options
options
options
options
options
output-banner
output-footer
override
parsers
plain-language
plugin-faker
plugin-msw
plugin-react-query
plugin-swr
plugin-vue-query
plugins
prefix-every-generated-type-name
printers
pull_request_template
reactive-params-that-refetch
renderers
renderers
resolvers
resolvers
resolvers
resolvers
rolldown
rollup
security
server-sent-events
server-sent-events
skip-a-request-until-ready
standalone-api-docs-page
storage
storage
strip-descriptions-with-a-macro
strip-descriptions-with-a-macro
suspense-hooks
telemetry
testing
transport
transport
tree-shakeable-schemas-with-zod-mini
turn-barrels-on
usa-english
v4
validate
vite
webpack
wrap-hooks-with-shared-options
zod-as-the-single-source-of-truth