T
TanStack9mo ago
foreign-sapphire

trpc style router

is there a reason why i shouldn't do something like this to create a trpc style router for tanstack start server functions?
import {
useQuery,
useMutation,
UseQueryResult,
UseMutationResult,
UseQueryOptions,
UseMutationOptions,
} from "@tanstack/react-query";
import { createServerFn } from "@tanstack/start";
import { z } from "zod";

type AsyncFunction = (...args: any[]) => Promise<any>;

type WrappedQuery<T extends AsyncFunction> = T & {
useQuery: (
args: Parameters<T>[0],
options?: Omit<
UseQueryOptions<
Awaited<ReturnType<T>>,
Error,
Awaited<ReturnType<T>>,
[string, Parameters<T>[0]]
>,
"queryKey" | "queryFn"
>
) => UseQueryResult<Awaited<ReturnType<T>>>;
useMutation: (
options?: Omit<
UseMutationOptions<Awaited<ReturnType<T>>, Error, Parameters<T>[0]>,
"mutationFn"
>
) => UseMutationResult<Awaited<ReturnType<T>>, Error, Parameters<T>[0]>;
};

type WrapperResult<T extends Record<string, AsyncFunction>> = {
[K in keyof T]: WrappedQuery<T[K]>;
};
import {
useQuery,
useMutation,
UseQueryResult,
UseMutationResult,
UseQueryOptions,
UseMutationOptions,
} from "@tanstack/react-query";
import { createServerFn } from "@tanstack/start";
import { z } from "zod";

type AsyncFunction = (...args: any[]) => Promise<any>;

type WrappedQuery<T extends AsyncFunction> = T & {
useQuery: (
args: Parameters<T>[0],
options?: Omit<
UseQueryOptions<
Awaited<ReturnType<T>>,
Error,
Awaited<ReturnType<T>>,
[string, Parameters<T>[0]]
>,
"queryKey" | "queryFn"
>
) => UseQueryResult<Awaited<ReturnType<T>>>;
useMutation: (
options?: Omit<
UseMutationOptions<Awaited<ReturnType<T>>, Error, Parameters<T>[0]>,
"mutationFn"
>
) => UseMutationResult<Awaited<ReturnType<T>>, Error, Parameters<T>[0]>;
};

type WrapperResult<T extends Record<string, AsyncFunction>> = {
[K in keyof T]: WrappedQuery<T[K]>;
};
27 Replies
foreign-sapphire
foreign-sapphireOP9mo ago
export function RouterWrapper<T extends Record<string, AsyncFunction>>(
functions: T
): WrapperResult<T> {
const wrappedFunctions: Partial<WrapperResult<T>> = {};

for (const key in functions) {
const func = functions[key];
if (typeof func !== "function") continue;

// Create the base function
const wrappedFunc = func as WrappedQuery<typeof func>;

// Attach properties to the function
wrappedFunc.useQuery = (
args: Parameters<typeof func>[0],
options?: Omit<
UseQueryOptions<
Awaited<ReturnType<typeof func>>,
Error,
Awaited<ReturnType<typeof func>>,
[string, Parameters<typeof func>[0]]
>,
"queryKey" | "queryFn"
>
) =>
useQuery({
queryKey: [key, args],
queryFn: () => func(args),
...options,
});

wrappedFunc.useMutation = (
options?: Omit<
UseMutationOptions<
Awaited<ReturnType<typeof func>>,
Error,
Parameters<typeof func>[0]
>,
"mutationFn"
>
) =>
useMutation({
mutationFn: (args: Parameters<typeof func>[0]) => func(args),
...options,
});

wrappedFunctions[key] = wrappedFunc;
}
return wrappedFunctions as WrapperResult<T>;
}

const api = RouterWrapper({
getBook: createServerFn({ method: "GET" })
.validator(z.object({ id: z.string() }))
.handler((ctx) => {
return ctx.data.id;
}),
});

api.getBook.useQuery({ data: { id: "asdfasdf" } }, { gcTime: 500 });
export function RouterWrapper<T extends Record<string, AsyncFunction>>(
functions: T
): WrapperResult<T> {
const wrappedFunctions: Partial<WrapperResult<T>> = {};

for (const key in functions) {
const func = functions[key];
if (typeof func !== "function") continue;

// Create the base function
const wrappedFunc = func as WrappedQuery<typeof func>;

// Attach properties to the function
wrappedFunc.useQuery = (
args: Parameters<typeof func>[0],
options?: Omit<
UseQueryOptions<
Awaited<ReturnType<typeof func>>,
Error,
Awaited<ReturnType<typeof func>>,
[string, Parameters<typeof func>[0]]
>,
"queryKey" | "queryFn"
>
) =>
useQuery({
queryKey: [key, args],
queryFn: () => func(args),
...options,
});

wrappedFunc.useMutation = (
options?: Omit<
UseMutationOptions<
Awaited<ReturnType<typeof func>>,
Error,
Parameters<typeof func>[0]
>,
"mutationFn"
>
) =>
useMutation({
mutationFn: (args: Parameters<typeof func>[0]) => func(args),
...options,
});

wrappedFunctions[key] = wrappedFunc;
}
return wrappedFunctions as WrapperResult<T>;
}

const api = RouterWrapper({
getBook: createServerFn({ method: "GET" })
.validator(z.object({ id: z.string() }))
.handler((ctx) => {
return ctx.data.id;
}),
});

api.getBook.useQuery({ data: { id: "asdfasdf" } }, { gcTime: 500 });
advantage of this is that it wraps your queries in useQuery (essential for calling on the frontend anyway) and automatically generates keys for you but is there some reason why this may be a bad idea?
dependent-tan
dependent-tan9mo ago
I wish this was the API too.
foreign-sapphire
foreign-sapphireOP9mo ago
was pretty easy to write this wrapper, but i wonder if there's a better solution tanner could come up with, or if this relies on type inference that'll be too slow. seems like you can't have a wrapper like createServerFunctionHook that wraps createServerFn since it'll mess up the babel transform that extracts the server function into the backend
yappiest-sapphire
yappiest-sapphire8mo ago
I would say that instead of doing this, automate the generation of options that you pass to useQuery The API you're going for is:
import { serverFnToQueryOptions } from 'yourlib';
const getBook = createServerFn(/* same as you shared */)
const getBookQueryOptions = serverFnToQueryOptions(getBook)

// on a component
const { data } = useSuspenseQuery(getBookQueryOptions({ id: 4 }))
import { serverFnToQueryOptions } from 'yourlib';
const getBook = createServerFn(/* same as you shared */)
const getBookQueryOptions = serverFnToQueryOptions(getBook)

// on a component
const { data } = useSuspenseQuery(getBookQueryOptions({ id: 4 }))
I think that API is just as good, right? serverFnToQueryOptions just looks like:
function serverFnToQueryOptions(serverFn) {
return (params) => queryOptions({
queryKey: [serverFn.name],
queryFn: (params) => serverFn(parmas)
})
}
function serverFnToQueryOptions(serverFn) {
return (params) => queryOptions({
queryKey: [serverFn.name],
queryFn: (params) => serverFn(parmas)
})
}
Just need to infer the typings from the serverFn
harsh-harlequin
harsh-harlequin8mo ago
May I know what is the goal of this tRPC style wrapper? Thanks. What I did was installing Tanstack Query and use server functions within.
foreign-sapphire
foreign-sapphireOP8mo ago
nice to have a type safe way to call the server functions from inside components with query keys encapsulated so any call to the server function will go into the query client cache
extended-salmon
extended-salmon8mo ago
I've used both tRPC and Server Functions and the way I ended up doing it was having a file of the query options for each query that included the use of the server function
foreign-sapphire
foreign-sapphireOP8mo ago
yeah but when working on a project with multiple people, one of the best things about trpc is that it automatically creates the query wrapper for every function. you could also have something like my wrapper generate the queryOptions (also includes the key) which would be good for calling stuff like ensureQueryData inside route loaders
yappiest-sapphire
yappiest-sapphire8mo ago
Yeah - I realized you can even make the query wrapper fully a hook to make it even smaller
function makeUseQuery(serverFn) {
// do all the stuff I said above and return `useQuery` with the generated query options
}
function makeUseQuery(serverFn) {
// do all the stuff I said above and return `useQuery` with the generated query options
}
Then it's just:
const useLoadPosts = makeUseQuery(loadPosts) // makeUseSuspenseQuery(loadPosts) also
function Component() {
const { data } = useLoadposts()
// or const { data } = makeUseQuery(loadPosts)
}
const useLoadPosts = makeUseQuery(loadPosts) // makeUseSuspenseQuery(loadPosts) also
function Component() {
const { data } = useLoadposts()
// or const { data } = makeUseQuery(loadPosts)
}
Couldn't be easier
foreign-sapphire
foreign-sapphireOP8mo ago
not that useful without typescript
yappiest-sapphire
yappiest-sapphire8mo ago
You can definitely get it fully typed - same API, I just left out the type inference
fair-rose
fair-rose8mo ago
Just wanted to jump into this thread and +1 the idea overall. I used the code from both of y'all and stored it in a Gist here for others to find - https://gist.github.com/tomkennedy22/7b54166deb2e4e44374507d0081fbdd4 It works pretty well, I create individual serverFns, stuff it into the useQuery builder that y'all provided, and it seems to be fully type-aware in my components. I think this is functionality that would be cool to be baked into Start by itself, but this is a good intermediate solution.
Gist
TanStack Start - Create tRPC-like API Router
TanStack Start - Create tRPC-like API Router. GitHub Gist: instantly share code, notes, and snippets.
foreign-sapphire
foreign-sapphire8mo ago
Would redirects work though since we can't use the useServerFn now
conscious-sapphire
conscious-sapphire8mo ago
How would you expose those server functions as API routes to use in a RN app for example? Would love to have something like:
export const getGreeting = createServerFn({
method: "GET"
})
.validator(z.object({ name: z.string() }))
.path("/greeting")
.handler(async ({ data }) => {
const { name } = data;
return `Hello, ${name}!`;
});
export const getGreeting = createServerFn({
method: "GET"
})
.validator(z.object({ name: z.string() }))
.path("/greeting")
.handler(async ({ data }) => {
const { name } = data;
return `Hello, ${name}!`;
});
So path would do a createAPIFileRoute under the hood.
fair-rose
fair-rose8mo ago
Yeah thats interesting - I admittedly havent used APIs at all in TsS yet. One challenge I've encountered with server functions is they HAVE to be created & assigned to a variable, and cant be created inside of a loop. So it limits a lot of fun functionality. I opened a ticket here about it - https://github.com/TanStack/router/issues/3200
GitHub
[Start] - Internal server error: createServerFn must be assigned to...
Which project does this relate to? Start Describe the bug Hi all, I'm running into an error with TanStack Start - Internal server error: createServerFn must be assigned to a variable! My basic ...
correct-apricot
correct-apricot7mo ago
queryKey: [serverFn.name], this will cause issues, you will want to include your params in the queryKey as well
passive-yellow
passive-yellow7mo ago
I don't think this is ever possible. this would require the loop to be evaluated statically I think you could achieve this with macros I'll try to create an example for you later
foreign-sapphire
foreign-sapphireOP7mo ago
i don't think loop is necessary but assigning them as part of keys on an object or static methods on a class would be useful would be good to export a function like defineRouter() that lets you put a bunch of related createServerFns on it as properties and could serve as a hint to the babel plugin
passive-yellow
passive-yellow7mo ago
ah no that won't work
foreign-sapphire
foreign-sapphireOP7mo ago
what about my version with the defineRouter higher order function @Manuel Schiller
passive-yellow
passive-yellow7mo ago
how would that look like? just some example code what you envision the API to look like
foreign-sapphire
foreign-sapphireOP7mo ago
export const authRouter =
defineRouter({
signIn: createServerFn().handler(...),
signOut: createServerFn().handler(...)
});


// in another function

const signInMutation = useServerFnMutation(authRouter.signIn);
export const authRouter =
defineRouter({
signIn: createServerFn().handler(...),
signOut: createServerFn().handler(...)
});


// in another function

const signInMutation = useServerFnMutation(authRouter.signIn);
passive-yellow
passive-yellow7mo ago
i am a bit out of the loop here, how does that help with https://github.com/TanStack/router/issues/3200 ?
foreign-sapphire
foreign-sapphireOP7mo ago
I think currently they have to be defined at the top level right? this would let you group them into routers with related functions.
passive-yellow
passive-yellow7mo ago
but you would still have to spell them all out?
foreign-sapphire
foreign-sapphireOP7mo ago
yeah it’s a way to define them, could maybe add middleware via the defineRouter, haven’t thought about what else would be in there
fair-rose
fair-rose7mo ago
Yep, correct. It doesn’t really stop you from being Trpc-like, just small nuisance

Did you find this page helpful?