Jun-14, 2025 ยท 5min
change languageDeeply explore how to integrate ts-rest with SWR to achieve end-to-end type-safe API calls, including custom hooks development, error handling, and middleware configuration.
Recently, I was writing an ai integration application, initially using tRPC and ai-sdk to implement it, but I found that tRPC cannot combine with ai-sdk well, its return type must go through its internal serialization, and cannot return the response type returned by ai-sdk alone.
Finally, I chose ts-rest, which can very flexibly define the return type, and can combine with ai-sdk very well. Perfectly implement my needs
ts-rest recommends using react-query, but I personally prefer swr which is simpler.
Here is my initial usage.
const values = useSWR(
data?.user.id ? [contract.getCredits.path, data?.user.id] : null,
async ([_, id]) => {
const res = await api.getCredits({
query: {
userId: id,
},
})
if (res.status !== 200) {
return null
}
return res.body
},
)
It can be found that it is very cumbersome, for example, this response in the case of non-200, almost every request has to do similar processing
Refer to the integration scheme of tRPC and React Query trpc-with-react-query The final effect is similar to this
const values = api.getCredits.useSWR(
{
query: {
userId: data?.user.id || '',
},
},
{
enabled: !!data?.user.id,
},
)
The data
type of the returned values
is only when 200
, the request can be sent by controlling enabled
, the overall implementation is also relatively simple, the type inference may be relatively complex, the following is the specific implementation, refer to the implementation of ts-rest initClient
import {
getRouteQuery,
initClient,
isAppRoute,
isAppRouteQuery,
} from '@ts-rest/core'
import useSWR from 'swr'
import useSWRMutation from 'swr/mutation'
import type {
AppRoute,
AppRouteDeleteNoBody,
AppRouteMutation,
AppRouteQuery,
AppRouter,
AreAllPropertiesOptional,
ClientArgs,
ClientInferRequest,
ClientInferResponseBody,
ClientInferResponses,
InitClientArgs,
PartialClientInferRequest,
Prettify,
} from '@ts-rest/core'
import type { SWRConfiguration, SWRResponse } from 'swr'
import type {
SWRMutationConfiguration,
SWRMutationResponse,
} from 'swr/mutation'
function getSWRRouteQuery(route: AppRouteQuery, clientArgs: InitClientArgs) {
const queryFn = getRouteQuery(route, clientArgs)
return {
useSWR: (
args: ClientInferRequest<AppRouteMutation, ClientArgs>,
options: {
enabled?: boolean
} & SWRConfiguration = {},
) => {
const { enabled = true, ...SWROptions } = options
const values = useSWR(
enabled ? [route.path, args] : null,
async () => {
const res = await queryFn(args)
if (res.status !== 200) {
throw new Error('error')
}
return res.body
},
SWROptions,
)
return values
},
}
}
function getSWRRouteMutation(
route: AppRouteMutation | AppRouteDeleteNoBody,
clientArgs: InitClientArgs,
) {
const mutationFn = getRouteQuery(route, clientArgs)
return {
useSWRMutation: (options: SWRMutationConfiguration<any, any, any> = {}) => {
const values = useSWRMutation(
[route.path],
async (_url: string, { arg }: { arg: any }) => {
const res = await mutationFn(arg)
if (res.status !== 200) {
throw new Error('error')
}
return res.body
},
options,
)
return values
},
}
}
type AppSWRRouteFunction<
TRoute extends AppRoute,
TClientArgs extends ClientArgs,
TArgs = PartialClientInferRequest<TRoute, TClientArgs>,
> =
AreAllPropertiesOptional<TArgs> extends true
? (
args?: Prettify<TArgs>,
) => Promise<Prettify<ClientInferResponses<TRoute>>>
: TRoute extends AppRouteQuery
? {
useSWR: <Data = Prettify<ClientInferResponseBody<TRoute, 200>>>(
args?: Prettify<TArgs>,
options?: {
enabled?: boolean
} & SWRConfiguration<Data>,
) => SWRResponse<Data>
}
: TRoute extends AppRouteMutation | AppRouteDeleteNoBody
? {
useSWRMutation: <
Data = Prettify<ClientInferResponseBody<TRoute, 200>>,
ExtraArg = Prettify<TArgs>,
>(
options?: SWRMutationConfiguration<Data, any, any, ExtraArg>,
) => SWRMutationResponse<Data, any, any, ExtraArg>
}
: (
args: Prettify<TArgs>,
) => Promise<Prettify<ClientInferResponses<TRoute>>>
type RecursiveProxyObj<T extends AppRouter, TClientArgs extends ClientArgs> = {
[TKey in keyof T]: T[TKey] extends AppRoute
? AppSWRRouteFunction<T[TKey], TClientArgs>
: T[TKey] extends AppRouter
? RecursiveProxyObj<T[TKey], TClientArgs>
: never
}
export type InitClientReturn<
T extends AppRouter,
TClientArgs extends ClientArgs,
> = RecursiveProxyObj<T, TClientArgs>
export function createApi<
T extends AppRouter,
TClientArgs extends InitClientArgs,
>(router: T, args: TClientArgs): InitClientReturn<T, TClientArgs> {
const api = Object.fromEntries(
Object.entries(router).map(([key, subRouter]) => {
if (isAppRoute(subRouter)) {
if (isAppRouteQuery(subRouter)) {
return [key, getSWRRouteQuery(subRouter, args)]
}
return [key, getSWRRouteMutation(subRouter, args)]
} else {
return [key, initClient(subRouter, args)]
}
}),
)
return api
}
export const api = createApi(contract, {
baseUrl: '.',
baseHeaders: {},
throwOnUnknownStatus: true,
})
The above implementation can freely add middleware in getSWRRouteQuery
and getSWRRouteMutation
, for example, add error handling and global prompt.