KUBB Integration
KUBB generates type-safe React Query hooks from your OpenAPI specification. Query Cache Flow wraps these generated hooks with proper cache key management.
What is KUBB?
KUBB is a code generator that reads your OpenAPI/Swagger specification and produces:
- TypeScript types for all API schemas
- React Query hooks for all API endpoints
- Fully typed request/response handling
The Integration Flow
OpenAPI Spec → KUBB → Generated Hooks → Query Cache Flow Wrappers → Your Components
- OpenAPI Spec: Your backend's API definition (
openapi.json) - KUBB: Generates hooks like
useGetAccounts,useCreateAccount - Query Cache Flow: Wraps with proper cache keys and invalidation
- Components: Use the wrapped hooks with zero cache management
KUBB Configuration
Install Dependencies
npm install @kubb/core @kubb/plugin-oas @kubb/plugin-ts @kubb/plugin-react-query
kubb.config.ts
import { defineConfig } from '@kubb/core';
import { pluginOas } from '@kubb/plugin-oas';
import { pluginReactQuery } from '@kubb/plugin-react-query';
import { pluginTs } from '@kubb/plugin-ts';
export default defineConfig({
input: {
path: './openapi.json',
},
output: {
path: './src/generated',
clean: true,
barrelType: 'all',
},
plugins: [
// Parse OpenAPI spec
pluginOas(),
// Generate TypeScript types
pluginTs(),
// Generate React Query hooks
pluginReactQuery({
client: {
// Point to your axios client
importPath: '../../services/axios.ts',
dataReturnType: 'data',
},
output: {
path: './hooks',
barrelType: 'all',
},
mutation: {
// POST, PUT, DELETE, PATCH become mutations
methods: ['post', 'put', 'delete', 'patch'],
},
query: {
// GET becomes queries
methods: ['get'],
importPath: '@tanstack/react-query',
},
suspense: false,
}),
],
});
Generate Code
npx kubb generate
# Or add to package.json scripts:
# "generate": "kubb generate"
Generated Output Structure
After running KUBB, your project has:
src/
generated/
index.ts # Barrel export
types/ # TypeScript interfaces
Account.ts
Transaction.ts
...
hooks/
index.ts
useGetAccounts.ts
useGetAccountById.ts
useCreateAccount.ts
useUpdateAccount.ts
useDeleteAccount.ts
...
Wrapping Generated Hooks
Query Wrapper Pattern
// src/features/accounts/queries/useAccounts.ts
import { useGetAccounts as generatedUseAccounts } from 'src/generated';
import { accountsQueryGroup } from './index';
export const useAccounts = () =>
generatedUseAccounts({
query: {
queryKey: [accountsQueryGroup.list.queryKey],
},
});
Detail Query with ID
// src/features/accounts/queries/useAccount.ts
import { useGetAccountById as generatedUseAccount } from 'src/generated';
import { accountsQueryGroup } from './index';
export const useAccount = (accountId: string) =>
generatedUseAccount(accountId, {
query: {
queryKey: [accountsQueryGroup.detail.queryKey(accountId)],
},
});
Mutation Wrapper Pattern
// src/features/accounts/queries/useAccountCreate.ts
import { useCreateAccount as generatedAccountCreate } from 'src/generated';
import { accountsQueryGroup } from './index';
import { invalidateQueriesForKeys } from 'src/queries';
interface UseAccountCreateProps {
onSuccess?: (data: Account, variables: CreateAccountDto, context: unknown) => void;
onError?: (error: Error, variables: CreateAccountDto, context: unknown) => void;
}
export const useAccountCreate = ({ onSuccess, onError, ...rest }: UseAccountCreateProps = {}) =>
generatedAccountCreate({
mutation: {
mutationKey: [accountsQueryGroup.create.queryKey],
onSuccess: (data, variables, context) => {
// Apply optimistic update
accountsQueryGroup.create.normalize?.(data);
// Invalidate list queries
invalidateQueriesForKeys([
accountsQueryGroup.create.invalidates!,
]);
// Call user's callback
onSuccess?.(data, variables, context);
},
onError,
...rest,
},
});
Update Mutation with ID
// src/features/accounts/queries/useAccountUpdate.ts
import { useUpdateAccount as generatedAccountUpdate } from 'src/generated';
import { accountsQueryGroup } from './index';
import { invalidateQueriesForKeys } from 'src/queries';
interface UseAccountUpdateProps {
accountId: string;
onSuccess?: (data: Account, variables: UpdateAccountDto, context: unknown) => void;
}
export const useAccountUpdate = ({ accountId, onSuccess, ...rest }: UseAccountUpdateProps) =>
generatedAccountUpdate(accountId, {
mutation: {
mutationKey: [accountsQueryGroup.update.queryKey(accountId)],
onSuccess: (data, variables, context) => {
accountsQueryGroup.update.normalize?.(data);
invalidateQueriesForKeys(
accountsQueryGroup.update.invalidates(accountId)
);
onSuccess?.(data, variables, context);
},
...rest,
},
});
Project Structure
Organize your features with Query Cache Flow:
src/
features/
accounts/
queries/
index.ts # Query group definition
useAccounts.ts # List query wrapper
useAccount.ts # Detail query wrapper
useAccountCreate.ts
useAccountUpdate.ts
useAccountDelete.ts
components/
AccountList.tsx
AccountForm.tsx
pages/
AccountsPage.tsx
Query Group Index
// src/features/accounts/queries/index.ts
import { Account } from 'src/generated';
import { createQueryGroupCRUD, inyectKeysToQueries } from 'src/queries';
// Create base CRUD group
let accountsQueryGroupCRUD = createQueryGroupCRUD<Account['id']>('accounts');
// Add auth key to all queries
accountsQueryGroupCRUD = inyectKeysToQueries(accountsQueryGroupCRUD, {
auth: true,
});
// Export with any customizations
export const accountsQueryGroup = {
...accountsQueryGroupCRUD,
// Add custom queries if needed
};
Axios Client for KUBB
KUBB needs a custom axios client:
// src/services/axios.ts
import axios from 'axios';
import type { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
export type RequestConfig<TData = unknown> = {
baseURL?: string;
url?: string;
method: 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS';
params?: unknown;
data?: TData | FormData;
responseType?: 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream';
signal?: AbortSignal;
headers?: AxiosRequestConfig['headers'];
};
export type ResponseConfig<TData = unknown> = {
data: TData;
status: number;
statusText: string;
headers?: AxiosResponse['headers'];
};
export type ResponseErrorConfig<TError = unknown> = AxiosError<TError>;
const axiosInstance = axios.create({
baseURL: process.env.REACT_APP_API_URL,
});
// Add auth interceptor
axiosInstance.interceptors.request.use((config) => {
const token = getAuthToken(); // Your auth logic
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// The client function KUBB will use
export const axiosClient = async <TData, TError = unknown, TVariables = unknown>(
config: RequestConfig<TVariables>
): Promise<ResponseConfig<TData>> => {
return axiosInstance.request<TData, ResponseConfig<TData>>(config);
};
export default axiosClient;
Regenerating After API Changes
When your OpenAPI spec changes:
# 1. Update openapi.json from backend
cp ../backend/openapi.json ./openapi.json
# 2. Regenerate
npm run generate
# 3. Update any affected wrapper hooks
# (Usually no changes needed if only adding endpoints)
TypeScript Integration
KUBB generates types you can use:
import {
Account,
CreateAccountDto,
UpdateAccountDto,
AccountsQueryParams,
} from 'src/generated';
// Use in your query group
const accountsQueryGroup = createQueryGroupCRUD<Account['id']>('accounts');
// Use in wrapper hooks
export const useAccountCreate = (props: {
onSuccess?: (data: Account) => void;
}) => { ... };
Common Patterns
Conditional Query Options
export const useAccount = (accountId: string | undefined) =>
generatedUseAccount(accountId!, {
query: {
queryKey: [accountsQueryGroup.detail.queryKey(accountId!)],
enabled: !!accountId, // Only run when ID exists
},
});
With Query Parameters
export const useTransactions = (params?: TransactionsQueryParams) =>
generatedUseTransactions({
query: {
queryKey: [transactionsQueryGroup.list.queryKey(params)],
},
...params, // Pass filters to API
});
Custom Query Options
export const useAccounts = (options?: Partial<UseQueryOptions>) =>
generatedUseAccounts({
query: {
queryKey: [accountsQueryGroup.list.queryKey],
staleTime: 5 * 60 * 1000, // 5 minutes
...options,
},
});
Best Practices
1. Never Use Generated Hooks Directly
// Bad - no cache management
import { useGetAccounts } from 'src/generated';
const { data } = useGetAccounts();
// Good - proper cache keys
import { useAccounts } from 'src/features/accounts/queries';
const { data } = useAccounts();
2. Keep Wrappers Thin
Wrappers should only add cache keys and invalidation. Business logic belongs in components.
3. Use Generated Types
import { Account, CreateAccountDto } from 'src/generated';
// Don't manually define API types
4. Regenerate Frequently
Run code generation as part of your build process or CI pipeline.
Troubleshooting
"Module not found: src/generated"
Run npm run generate to create the generated files.
Type Mismatches After API Changes
Regenerate and check for breaking changes:
npm run generate
npm run typecheck
Cache Not Invalidating
Ensure wrapper hooks call invalidateQueriesForKeys in onSuccess.
Summary
KUBB + Query Cache Flow provides:
- Type safety from OpenAPI to components
- Zero manual hook writing for API calls
- Automatic cache management via wrappers
- Consistent patterns across all features
The two-layer approach (KUBB generates, Query Cache Flow wraps) keeps your code DRY while maintaining full cache control.