trpc-server-functions brings co-located server functions to tRPC + Vite apps.
Define a server function anywhere in your frontend, and generate matching tRPC procedures for your backend router.
This package is designed for a split setup: a Vite client app and a separate server app.
your-app/
client/
src/
App.tsx
main.tsx
vite.config.ts
server/
src/
db.ts
trpc.ts
router.ts
generated/
trpc-server-functions.ts
The generated registry is written by the client-side Vite plugin or by the CLI.
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { db } from "../../server/src/db";
import { createServerFn } from "trpc-server-functions";
export const getCount = createServerFn().query(async () => {
return db.getCount();
});
export const incrementCount = createServerFn().mutation(async () => {
return db.increment();
});
export function Counter() {
const queryClient = useQueryClient();
const countQuery = useQuery(getCount.queryOptions());
const incrementMutation = useMutation({
...incrementCount.mutationOptions(),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: getCount.queryOptions().queryKey,
});
},
});
return (
<main>
<h1>Counter</h1>
<p>{countQuery.data ?? "..."}</p>
<button
type="button"
disabled={incrementMutation.isPending}
onClick={() => incrementMutation.mutate(undefined)}
>
{incrementMutation.isPending ? "Incrementing..." : "Increment"}
</button>
</main>
);
}At build time:
- the client keeps typed RPC proxies
- the real handlers are removed from the browser bundle
- generated server modules turn these exports into normal
tRPCprocedures
Install the package:
npm install trpc-server-functionsimport path from "node:path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { trpcServerFunctionsPlugin } from "trpc-server-functions/vite";
export default defineConfig({
plugins: [
react(),
trpcServerFunctionsPlugin({
procedure: {
importPath: path.resolve("../server/src/trpc.ts"),
exportName: "publicProcedure",
},
generatedModulePath: "../server/src/generated/trpc-server-functions.ts",
}),
],
});trpc-server-functions generate \
--root ./client \
--generated-module-path ../server/src/generated/trpc-server-functions.ts \
--procedure-import-path ../server/src/trpc.ts \
--procedure-export-name publicProcedureimport { trpcServerFunctions } from "./generated/trpc-server-functions";
import { router } from "./trpc";
export const appRouter = router({
...trpcServerFunctions(),
});import { createTRPCUntypedClient, httpBatchLink } from "@trpc/client";
import {
createTRPCClientTransport,
setServerFnTransport,
} from "trpc-server-functions";
const trpcClient = createTRPCUntypedClient({
links: [httpBatchLink({ url: "/api/trpc" })],
});
setServerFnTransport(createTRPCClientTransport(trpcClient));After that, queryOptions(), mutationOptions(), and call() use your existing tRPC endpoint.
The generator produces two kinds of output:
server/src/generated/trpc-server-functions.ts- the registry that imports extracted server functions and returns a
tRPCprocedure record
- the registry that imports extracted server functions and returns a
server/src/generated/__trpc_server_functions__/src/...- mirrored backend-safe extracted modules for the colocated server-function exports found in the client app
The mirrored lib and routes folders are expected. They preserve the source layout of your client app so the backend can execute colocated handlers without importing raw route/component modules directly.
The public tRPC paths generated by this package are stable hash-only route keys, for example:
/api/trpc/sf_a19175ccef5b6fcd5059e41b
They are derived from the source file path and export name, but the public API surface does not expose those source paths directly.
Internally, the generated manifest still keeps metadata such as relativePath and exportName so the build can map each hashed key back to the correct server-function export.
- Server functions must be exported.
- The plugin only discovers top-level exported
createServerFn(...)calls. - Keep colocated server functions in the files where they are used; the generated backend modules are build artifacts and should not be edited manually.