React 19 server functions: the good, the weird, the type-safe
Server functions blur the client/server line. We unpack the mental model, where TanStack Start nails it, and the foot-guns to avoid in real apps.
Server functions are the part of React 19 that take the longest to click mentally, because they look like normal function calls but quietly cross a network boundary. Once the model clicks, though, a huge amount of boilerplate - API route, fetch call, serialization, error handling - just disappears.
The mental model that actually helps
Stop thinking of a server function as "a function." Think of it as an RPC endpoint that React's compiler and runtime conspire to make look like a function call. Every argument gets serialized, sent over the wire, and the return value gets serialized back. That's it. Once you internalize "this is a network call wearing a function's clothing," the rules around what you can and can't pass into one stop feeling arbitrary.
'use server';
export async function createTicket(input: { title: string; body: string }) {
const session = await getSession();
if (!session) throw new Error('Unauthorized');
return db.insert(tickets).values({ ...input, userId: session.userId });
}Where TanStack Start gets the ergonomics right
The thing we appreciate most about TanStack Start's take on server functions is that they're explicit, not magic. There's a real `createServerFn` call, real input validation via your schema library of choice, and a clear line between "this runs on the server" and "this runs on the client" that you can see in the code, not just infer from a directive comment. That explicitness costs a little ceremony, but it pays for itself the first time you're debugging a production issue at 2am and need to know exactly where code executes.
The foot-guns
- Passing non-serializable values (functions, class instances, Dates in some setups) into a server function fails silently in dev and loudly in prod - validate your input schema and you sidestep this entirely.
- Calling a server function from inside a tight render loop re-triggers a network round trip every time - memoize or move the call to an effect/event handler.
- Treating server functions as a replacement for proper API design - if three different server functions need the same business logic, that logic belongs in a shared service layer, not copy-pasted across functions.
- Forgetting that server functions still need authorization checks inside the function body - the network boundary doesn't enforce access control for you.
Type safety end to end
The genuine win is that TypeScript infers the server function's return type all the way to the client call site, with no manual API client generation step. For a team that's been burned by API contracts drifting out of sync with frontend expectations, this alone is worth the migration.
Written by Appesto Engineering.