apps/api and no oRPC client. Your backend logic lives in server functions: typed async functions you write once and call from the client, while the body only ever runs on the server.
API as a function. No routes to register, no client to generate — createServerFn(...).handler(...), then call it.
This is TanStack Start’s createServerFn. Ship wires it up for you; this page is the one obvious way to use it.
SPA mode still has a server
Ship runsapps/web in SPA mode — see vite.config.ts:
vite.config.ts
Import from @tanstack/react-start
createServerFn comes from @tanstack/react-start — the framework package — not from @tanstack/react-router.
The shipped example
The web-only template ships one server function atapps/web/src/server/greeting.ts so you have a working pattern to copy:
apps/web/src/server/greeting.ts
createServerFn({ method })—'GET'for reads,'POST'for writes. The method is how the client transports the call to the server..handler(async (...) => ...)— the body. It runs only on the server, so this is where you reach for secrets, a database, or any server-only dependency. Nothing here ships to the browser.- The return value is sent back to the caller, fully typed end-to-end. The client sees the handler’s return type with no codegen.
Consume it from a route loader
A server function is just an async function, so you can call it anywhere. The idiomatic place is a routeloader: TanStack Router runs it before the route renders, and the component reads the result with Route.useLoaderData().
apps/web/src/routes/index.tsx
Route.useLoaderData() reads it back, typed.
Inputs and validation
Pass data with.inputValidator(). It parses the input on the server before the handler runs — give it a Zod schema and you get a validated, typed data argument:
apps/web/src/server/notes.ts
method: 'GET' and read data in the handler.
When to use what
Server functions are the web-only data layer. In a full-stack project you have a different, richer option.Server functions — web-only
No
apps/api. Backend logic runs on the Start server as createServerFn handlers, called from loaders and components. Reach for server-only deps directly in the handler.oRPC client — full-stack
There’s an
apps/api. The web app talks to it through a fully typed oRPC client and the useApiQuery / useApiMutation / useApiForm hooks the Auth plugin adds.| You scaffolded | Data layer | Import from |
|---|---|---|
| TanStack Start web-only | Server functions | @tanstack/react-start |
| PostgreSQL + TanStack Start | oRPC typed client | @/services/api-client.service (Auth plugin) |
Both shapes start from the same
apps/web base — a landing page plus the getGreeting example. The difference is where data comes from. If you later add an API, you move data fetching from server functions to the oRPC client; the routing, components and TanStack Query setup are unchanged.Next
- Overview — the
apps/webstack and the two data-fetching shapes. - Routing — loaders, params and guards in depth.
- How Ship works — the resource-owns-everything model and end-to-end type flow.
