For years, choosing a full-stack React framework mostly meant choosing Next.js. TanStack Start changes that. It reached 1.0 in October 2026, after a release candidate period that began in September 2025, and it takes a different approach: explicit server functions, fully type-safe routing, and a client-first mental model.

This guide explains the pieces you need to build a real app: file routes, loaders, server functions, validation and deployment. It is written for developers who know React and want to know whether Start fits their next project.

What TanStack Start is

TanStack Start is a full-stack framework built on two libraries many teams already use: TanStack Router for type-safe routing and data loading, and Vite for development and builds. On top of these it adds server-side rendering, streaming, server routes (API endpoints) and server functions.

The key idea is that the router is the centre of the app. Every route's params, search params and loader data are typed end to end, so a link to a route that does not exist, or a missing search parameter, is a compile error rather than a runtime bug.

How it differs from Next.js

Next.js App Router defaults to React Server Components and lets you mark functions with 'use server' to create Server Actions. TanStack Start deliberately does not support 'use server' actions. The TanStack team's reasons are that implicit actions have been an attack vector and that they hide where the network boundary is.

Instead, you write server functions with createServerFn(). They always run on the server, can be called from anywhere in your client code, and behave like type-safe RPC calls. You can see exactly what crosses the network because you created it on purpose.

React Server Components are available in Start only as an experimental feature, so treat Start as a client-first framework with excellent SSR rather than an RSC-first one.

The execution model in one paragraph

This is the part that trips people up. In TanStack Start, route loaders are isomorphic: they run on the server during the initial server render, and in the browser when the user navigates between pages. That means you must not read secrets or query a database directly inside a loader. Instead, put server-only work inside a server function and call that function from the loader. The official execution model guide recommends exactly this pattern.

Create a project

Use the scaffolder on the official TanStack Start quick start page. It sets up the Vite config, the router and a root route. The files you will touch most are:

  • src/routes/__root.tsx, the HTML shell and layout.
  • src/routes/index.tsx and other files in src/routes/, one per route.
  • src/routeTree.gen.ts, generated for you; never edit it by hand.

Write your first server function

Server functions are imported from @tanstack/react-start. Here is a GET function that reads data on the server:

typescript
// src/server/notes.ts
import { createServerFn } from '@tanstack/react-start'
import { db } from './db' // your server-only database client

export const getNotes = createServerFn({ method: 'GET' }).handler(async () => {
  return db.note.findMany({ orderBy: { createdAt: 'desc' }, take: 20 })
})

Because getNotes is a server function, the database client never ships to the browser. When client code calls it, Start makes an HTTP request behind the scenes and returns the typed result.

Validate input before it reaches the handler

Anything sent from the browser is untrusted. Server functions take a .validator() step that runs before the handler. You can pass a plain function or a schema library such as Zod:

typescript
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'
import { db } from './db'

const NewNote = z.object({
  title: z.string().min(1).max(120),
  body: z.string().max(5000),
})

export const createNote = createServerFn({ method: 'POST' })
  .validator((input: unknown) => NewNote.parse(input))
  .handler(async ({ data }) => {
    // data is typed as { title: string; body: string }
    return db.note.create({ data })
  })

If validation throws, the handler never runs. The type of data inside the handler comes from the validator, so the client and server agree on the shape without a separate API contract.

Note: release candidates used .inputValidator(). If you are upgrading an early project, rename it to .validator().

Load data in a route

Call the server function from the route's loader. The loader runs before the route renders, both on the server and during client navigation, and its return value is typed:

tsx
// src/routes/notes.tsx
import { createFileRoute } from '@tanstack/react-router'
import { getNotes } from '../server/notes'

export const Route = createFileRoute('/notes')({
  loader: () => getNotes(),
  component: NotesPage,
})

function NotesPage() {
  const notes = Route.useLoaderData()
  return (
    <ul>
      {notes.map((note) => (
        <li key={note.id}>{note.title}</li>
      ))}
    </ul>
  )
}

Route.useLoaderData() is fully typed from getNotes, with no manual interface.

Call a server function from an event handler

For mutations triggered by the user, wrap the function with useServerFn and call it with a data object. Then invalidate the router so loaders re-run:

tsx
import { useRouter } from '@tanstack/react-router'
import { useServerFn } from '@tanstack/react-start'
import { createNote } from '../server/notes'

function NewNoteForm() {
  const router = useRouter()
  const create = useServerFn(createNote)

  async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    const form = new FormData(event.currentTarget)
    await create({
      data: { title: String(form.get('title')), body: String(form.get('body')) },
    })
    await router.invalidate()
  }

  return (
    <form onSubmit={onSubmit}>
      <input name="title" required />
      <textarea name="body" />
      <button type="submit">Save</button>
    </form>
  )
}

useServerFn returns a plain callable function. It does not give you loading or error state; for that, pair server functions with TanStack Query's useMutation.

Secure your server functions

Server functions are public HTTP endpoints, even if no page links to them. Treat each one like an API route:

  • Validate every input with .validator().
  • Check authentication and authorisation inside the handler or in middleware, not only in the UI.
  • Return only the fields the client needs; never return whole user records.
  • Keep secrets in server-only modules and environment variables.

Deploying

Start builds with Vite and runs on most hosts. Netlify announced support for 1.0 on release day, running SSR, server routes, server functions and middleware on Netlify Functions, and Cloudflare and Vercel also host Start apps. Check your host's adapter docs for the build preset.

When to choose TanStack Start

  • Good fit: dashboards and apps with lots of client interactivity, teams already using TanStack Router or Query, and anyone who wants explicit, type-safe client–server calls.
  • Think twice: content-heavy sites that benefit most from React Server Components, or teams deeply invested in the Next.js ecosystem.

Key takeaways

  • TanStack Start 1.0 is stable and built on TanStack Router and Vite.
  • Loaders run on both server and client; put secrets and database calls in server functions.
  • createServerFn() gives you explicit, type-safe RPC; validate input with .validator().
  • There is no 'use server'; the network boundary is always visible in your code.
  • Every server function is a public endpoint, so authenticate and validate inside it.

Sources: TanStack Start 1.0 announcement, Server functions guide, Execution model, Netlify changelog.