How to handle consistency for client-side only APIs?
Hi, we are using tRPC as a BFF (Backend for Frontend) which act as a proxy between ou UI and our dozen of backend services.
What's the recommended approach to handle direct client-side API calls when they cannot be implemented with tRPC due to legacy limitations?
Should we use React Query directly, or is there a way to extend tRPC's router in the client-side to maintain consistent API handling?
PS: For better understanding, we'd like to add client-side procedures because we need to calls APIs that can only be called in a browser
What's the recommended approach to handle direct client-side API calls when they cannot be implemented with tRPC due to legacy limitations?
Should we use React Query directly, or is there a way to extend tRPC's router in the client-side to maintain consistent API handling?
PS: For better understanding, we'd like to add client-side procedures because we need to calls APIs that can only be called in a browser
Solution
export function buildTrpcClientFromRouter<AppRouter extends AnyRouter>(router: AppRouter) {
function mockLink<TRouter extends AnyRouter>(): TRPCLink<TRouter> {
return () => {
return ({ op }) => {
return observable((observer) => {
const handler = async () => {
const result = await callTRPCProcedure({
procedures: appRouter._def.procedures,
ctx: {},
path: op.path,
input: op.input,
getRawInput: async () => op.input,
type: op.type,
signal: op.signal,
});
const isIterableResult = isAsyncIterable(result) || isObservable(result);
if (op.type !== 'subscription') {
if (isIterableResult) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Cannot return an async iterable in a non-subscription call',
});
}
observer.next({
result: {
type: 'data',
data: result,
},
context: op.context,
});
observer.complete();
} else {
if (!isIterableResult) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Cannot return a non-async iterable in a subscription call',
});
}
const iterable = isObservable(result) ? observableToAsyncIterable(result, op.signal) : result;
for await (const item of iterable) {
if (op.signal?.aborted) {
break;
}
observer.next({
result: {
type: 'data',
data: item,
},
context: op.context,
});
}
observer.complete();
}
};
void handler();
return () => {
observer.complete();
};
});
};
};
}
const trpcClient = createTRPCClient<AppRouter>({
links: [mockLink()],
});
return trpcClient;
}
const t = initTRPC.create({ isServer: true });
const appRouter = t.router({
hello: t.procedure.query(() => 'hi'),
});
const trpcClient = buildTrpcClientFromRouter(appRouter);export function buildTrpcClientFromRouter<AppRouter extends AnyRouter>(router: AppRouter) {
function mockLink<TRouter extends AnyRouter>(): TRPCLink<TRouter> {
return () => {
return ({ op }) => {
return observable((observer) => {
const handler = async () => {
const result = await callTRPCProcedure({
procedures: appRouter._def.procedures,
ctx: {},
path: op.path,
input: op.input,
getRawInput: async () => op.input,
type: op.type,
signal: op.signal,
});
const isIterableResult = isAsyncIterable(result) || isObservable(result);
if (op.type !== 'subscription') {
if (isIterableResult) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Cannot return an async iterable in a non-subscription call',
});
}
observer.next({
result: {
type: 'data',
data: result,
},
context: op.context,
});
observer.complete();
} else {
if (!isIterableResult) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Cannot return a non-async iterable in a subscription call',
});
}
const iterable = isObservable(result) ? observableToAsyncIterable(result, op.signal) : result;
for await (const item of iterable) {
if (op.signal?.aborted) {
break;
}
observer.next({
result: {
type: 'data',
data: item,
},
context: op.context,
});
}
observer.complete();
}
};
void handler();
return () => {
observer.complete();
};
});
};
};
}
const trpcClient = createTRPCClient<AppRouter>({
links: [mockLink()],
});
return trpcClient;
}
const t = initTRPC.create({ isServer: true });
const appRouter = t.router({
hello: t.procedure.query(() => 'hi'),
});
const trpcClient = buildTrpcClientFromRouter(appRouter);