XavierX
tRPC12mo ago
14 replies
Xavier

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
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);
Was this page helpful?