Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Start] - Internal server error: createServerFn must be assigned to a variable #3200

Open
tomkennedy22 opened this issue Jan 20, 2025 · 2 comments

Comments

@tomkennedy22
Copy link

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 use case is trying to convert from a tRPC project, in which I really enjoy to DX of defining all of my APIs and then it's as simple as calling trpc..useQuery(param) in a component and you can rock from there

I understand how to create serverFns in Start and use them in components, and have also plugged in TanStack Router to handle some of this in components. My issue is that it can become a little verbose in using these serverFns in client components, like this: (just for example, specifics don't matter too much here)

const { data: count } = useQuery({
    queryKey: ["getCount"],
    queryFn: () => getCount(),
  });

  const { data: serverTime } = useQuery({
    queryKey: ["getServerTime"],
    queryFn: () => getServerTime(),
  });

  const { data: greeting } = useQuery({
    queryKey: ["greet", { name: "Tommy" }],
    queryFn: () => greet({ data: { name: "Tommy" } }),
  });

  const { data: user } = useQuery({
    queryKey: ["getUserById", { id: 1 }],
    queryFn: () => getUserById({ data: { id: 1 } }),
  });

  const updateCountMutation = useMutation({
    mutationFn: async () => await updateCount({ data: 1 }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["getCount"] });
    },
  });

I'm trying to re-create tRPC-like functionality in TanStack Start, and have some code below to start it - but then I run in to the error in the title: "Internal server error: createServerFn must be assigned to a variable!"

const serverFns = {
  getCount: createServerFn({
    method: "GET",
  }).handler(async () => {
    count = parseInt(
      await fs.promises.readFile(filePath, "utf-8").catch(() => "0")
    );
    return count;
  }),
  updateCount: createServerFn({
    method: "POST",
  }).handler(async () => {
    count += 1;
    await fs.promises.writeFile(filePath, count.toString());
  }),
  getGreeting: createServerFn({
    method: "GET",
  })
    .validator(z.object({ name: z.string() }))
    .handler(async ({ data }) => {
      const { name } = data;
      return `Hello, ${name}!`;
    }),
  getUserById: createServerFn({
    method: "GET",
  })
    .validator(z.object({ id: z.number() }))
    .handler(async ({ data }) => {
      const { id } = data;
      return { id, name: "Tommy" };
    }),
};

type ServerFnBase = {
  _type: "server-fn";
  method: "GET" | "POST";
  validator?: z.ZodType<any>;
  handler: (args: { data: any }) => Promise<any>;
};

type InferValidator<T> = T extends { validator: z.ZodType<infer U> } ? U : void;
type InferHandler<T> = T extends { handler: (args: any) => Promise<infer U> }
  ? U
  : never;

type ServerHooks<T extends Record<string, ServerFnBase>> = {
  [K in keyof T]: T[K]["method"] extends "GET"
    ? (
        params: InferValidator<T[K]>,
        options?: Omit<
          UseQueryOptions<InferHandler<T[K]>, Error>,
          "queryKey" | "queryFn"
        >
      ) => UseQueryResult<InferHandler<T[K]>, Error>
    : (
        params: InferValidator<T[K]>,
        options?: Omit<
          UseMutationOptions<InferHandler<T[K]>, Error, InferValidator<T[K]>>,
          "mutationKey" | "mutationFn"
        >
      ) => UseMutationResult<InferHandler<T[K]>, Error, InferValidator<T[K]>>;
};

const generateHooks = <T extends Record<string, ServerFnBase>>(
  serverFns: T
): ServerHooks<T> => {
  const hooks: Record<string, any> = {};

  for (const [fnName, fn] of Object.entries(serverFns)) {
    if (fn.method === "GET") {
      hooks[fnName] = (params: any, options?: any) =>
        useQuery({
          queryKey: [fnName, params],
          queryFn: () => fn.handler({ data: params }),
          ...options,
        });
    } else {
      hooks[fnName] = (params: any, options?: any) =>
        useMutation({
          mutationKey: [fnName],
          mutationFn: (data) => fn.handler({ data }),
          ...options,
        });
    }
  }

  return hooks as ServerHooks<T>;
};

export const serverHooks = generateHooks(serverFns);

Is there any way to get full type-awareness, plus TanStack Query functionality, with reducing verbosity?

There was a similar convo on the Discord, but didn't help me get fully there - https://discord.com/channels/719702312431386674/1322986702553223288

Your Example Website or App

none

Steps to Reproduce the Bug or Issue

Detailed in description

Expected behavior

Detailed in description

Screenshots or Videos

No response

Platform

Detailed in description

Additional context

No response

@tomkennedy22 tomkennedy22 changed the title [Start] - Internal server error: createServerFn must be assigned to a variable! [Start] - Internal server error: createServerFn must be assigned to a variable Jan 20, 2025
@tomkennedy22
Copy link
Author

Also tried a workaround where I define the functions in a loop then assign to a object, but ran into similar error in Server Functions cannot be nested in other blocks or functions

@tomkennedy22
Copy link
Author

I suppose this problem arises from this line:

if (!rootCallExpression.parentPath.isVariableDeclarator()) {

And from what I can tell, it requires the function to be set to a variable so the variable name can later be used for internal routing to the right function. Which makes sense, but ultimately blocks the creation of these functions in a dictionary-like router (from what I can tell)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant