Skip to content

Server Function Benefits

Posted on:January 21, 2025

So initially I was going to write one big blog post on why server functions matter but instead of that I have split it into two parts. If you haven’t read the first one, check it out here. It goes in depth into the differences between server components and server functions.

This article however is more about the similarities between the two, and how you can get similar benefits with server functions as compared to server components.

So what exactly do server functions give you?

No manual server api endpoints

So talking with your backend has evolved a lot from a SPA standpoint. The first iteration was to make client side fetch requests to a completely separate backend, probably in a different language. It looked something like this:

export function HomePage() {
  const [user, setUser] = useState(null);
  useEffect(() => {
    async function fetchData() {
      const res = await fetch("http://some-server/api-endpoint");
      const data = await res.json();
      setUser(data);
    }
    fetchData();
  }, []);

  return <>{JSON.stringify(user)}</>;
}

This is even a simplified example because it doesn’t handle things like race conditions, errors, or caching at all.

Also if you ever want to change what that api endpoint returns you would have to reach out to the backend team (presuming its in Python for example).

So as frontend developers we want more control over this api endpoint ourselves. To do this we use a pattern called BFF (backend for frontend). This allows us to put a node server in between us and this endpoint. Then we can just return the fields needed for the UI. Let’s say we only need the name, we can change the BFF to make the same API call but return a slice of the data.

So how would we introduce this node server in modern frameworks? The first step is setup an api endpoint.

// routes/api/user.ts
import { createAPIFileRoute } from "@tanstack/start/api";

export const APIRoute = createAPIFileRoute("/user")({
  GET: async ({ request }) => {
    const res = await fetch("http://some-server/api-endpoint");
    const data = await res.json();

    return new Response(
      JSON.stringify({
        user: {
          name: data.name,
        },
      })
    );
  },
});

Now we can change our frontend logic to point at the new endpoint:

export function HomePage() {
  const [user, setUser] = useState(null);
  useEffect(() => {
    async function fetchData() {
      const res = await fetch("/user");
      const data = await res.json();
      setUser(data);
    }
    fetchData();
  }, []);

  return <>{JSON.stringify(user)}</>;
}

We haven’t actually shrunk the code and instead we’ve made it larger. We have two places to manage. The benefit however is we control how we consume the backend.

We could even go a step further and query our database directly from the API route instead of our separate python server if needed.

// routes/api/user.ts
import { createAPIFileRoute } from "@tanstack/start/api";

export const APIRoute = createAPIFileRoute("/user")({
  GET: async ({ request }) => {
    const user = await db.account.findUnique({ where: { id: 1 } });

    return new Response(
      JSON.stringify({
        user,
      })
    );
  },
});

Now that was maybe a bit of a slog just to get the the main point, but what if you could just call a function from within your component and all this boilerplate would just melt away. Like what is the simplest way we could express this. I’d argue something like this:

const getUser = createServerFn().handler(() => {
  const user = await db.account.findUnique({ where: { id: 1 } });
  return { user };
});

export const Route = createFileRoute("/")({
  loader: () => getUser(),
  component: HomePage,
});

export function HomePage() {
  const user = Route.useLoaderData();

  return <>{JSON.stringify(user)}</>;
}

It’s just a function call with no manual api endpoints! We are also down to like 10 lines of code. Now when this server function is called from the server it will just execute the function as normal, but if this is executed from the client it will wrap up a fetch request for you.

Now this probably isn’t quite as easy as writing await in async components in Next, but I don’t think this is too bad. In Next you basically just do the async call within the component body instead of extracting it out.

export async function HomePage() {
  const user = await db.account.findUnique({ where: { id: 1 } });

  return <>{JSON.stringify(user)}</>;
}

It’s almost less clear that this is running on the server, unless you know that by default all components are server only in Next.

Type-safety across the network

Another benefit of this is you get that type-safety across the network. Like can you guess what the type of user is in our component? It is the exact type we return from our createServerFn. No typeof loader or anything. This is a benefit that Next has with its server components but now we can also have it!

Many people ask about using TRPC but I think this eliminates some of the need for it, because of the out of the box type-safety from frontend to backend.

Keep large js on the server

Another benefit of server components is keeping your large libraries out of the client and instead rendering it on the server. You can also do this with server functions. A good example of this is a syntax highlighter. For example on the Solid Docs site we used one called Shiki. By default it was shipping almost 200kb to the client that we didn’t need to. We could shrink it down to zero and make an api call to a server function which just returns the final HTML that it calculates.

Conclusion

So my argument is you can get a lot of the same values of server components just using server functions, but what do you think? What do you think gives server components the edge?