TanStack Ecosystem Complete Guide: Router, Query, and Start in 2026

A comprehensive guide to the TanStack ecosystem in 2026 — Router, Query, and Start. How they fit together, when to use each, and what production reality looks like for solo SaaS developers.

Huifer
Huifer
August 14, 202611 min read
Also available in:Deutsch

Written by Huifer, solo developer and maintainer of TanStack Ship. I shipped the first TanStack-based production app on Cloudflare Workers in late 2024, and since then I have shipped 12 more on the same stack. The TanStack ecosystem — Router, Query, and Start — is the only React stack I trust to put a single dev's work in front of paying customers without a runtime surprise. This guide is the field manual I wish I had when I started: when each library earns its keep, when to skip it, and the production patterns that actually work.

Verified sources: TanStack Router Documentation · TanStack Query Documentation · TanStack Start Documentation · TanStack GitHub Organization · Cloudflare Workers Documentation · TanStack Ship Features · TanStack Ship Blog Index · TanStack Router vs React Router Last updated: 2026-06-20 · Changelog


TL;DR: The TanStack ecosystem in 2026 is three libraries that solve three different problems and snap together cleanly. TanStack Router is a fully type-safe client-side router with first-class search-params, loaders, and code-splitting. TanStack Query is a server-state cache that handles fetching, caching, and synchronization so you do not build it yourself. TanStack Start is a full-stack framework built on top of Router that adds server functions, SSR, and deployment adapters. For a solo SaaS developer in 2026, this is the only React stack that gets you type-safety end-to-end, deploys to the edge, and does not require a separate API service.


Why the TanStack ecosystem exists

There are a lot of React stacks in 2026. Next.js, Remix, Astro, Redwood — each one makes a different bet about the boundary between server and client. The TanStack ecosystem makes a different bet: keep the client router, the client server-state cache, and the server runtime as three independent, composable pieces, and let the developer wire them together.

According to the TanStack Router documentation, the router was built to be fully type-safe — the route tree is a type the compiler reasons about, so useParams(), useSearch(), and Link are all checked at compile time. According to the TanStack Query documentation, the query library was built because "data fetching is hard and most apps get it wrong" — it is the most-downloaded data-fetching library for React. According to the TanStack Start documentation, Start composes Router and Query with server functions and a deployment adapter.

The reason this matters in 2026 is that the alternatives force you to choose between type safety, edge deployment, and a clean mental model. Next.js gives you edge deployment and SSR but the router is not fully type-safe. Remix has been folded into React Router. Astro is great for content sites but not stateful SaaS apps. The TanStack stack is the only one I have shipped in 2026 that gives me all three. For a wider view of where this fits, see the TanStack Ship features page and the TanStack Ship comparison hub.

What "the ecosystem" actually means

TanStack is a family of libraries that share an author, a type-strictness philosophy, and a deployment target, but each library can be used independently. You can adopt Query without Router. You can adopt Router without Start. You can use Start without ever touching Query. This composability is the feature, not a side effect.

In production, I use Router for the route tree and code-splitting, Query for every server-state read on the client, and Start for the server functions, the SSR entry point, and the Cloudflare Workers adapter. I do not use the Router loader API for data fetching — I use Query for that, because the cache invalidation story is what I want. I do use the Router loader API for route-level guards. The boundary is simple: the server is for things only the server can do (write to the database, call Stripe, sign a JWT), and the client is for everything else.

TanStack Router — the type-safe route tree

TanStack Router is the most opinionated piece of the ecosystem. It replaces React Router, Next.js App Router, and the various file-based routers with a code-first route tree that the compiler understands. The type of that tree is the type of the entire app, and every Link, useNavigate, and useParams is checked against it. If a param does not exist, the compiler fails. If a search-param is missing, the type tells you.

The biggest production win is search-params validation. In most React stacks, useSearchParams() returns URLSearchParams and you have to parse, validate, and trust the data yourself. In TanStack Router, search-params are a Zod schema:

typescript
// app/routes/products.tsx
import { z } from 'zod'
import { createRoute, useSearch } from '@tanstack/react-router'

const productSearchSchema = z.object({
  category: z.enum(['all', 'electronics', 'apparel']).default('all'),
  page: z.number().int().positive().default(1),
  sort: z.enum(['price-asc', 'price-desc', 'newest']).default('newest'),
})

export const Route = createRoute({
  getParentRoute: () => rootRoute,
  path: '/products',
  validateSearch: productSearchSchema,
  component: ProductsPage,
})

function ProductsPage() {
  const { category, page, sort } = useSearch({ from: Route.id })
  // category, page, sort are typed — no casts, no `as`
}

This is the kind of thing that sounds small until you ship it. I had a class of bug in 2024 that no longer exists: a search-param that did not match what the consumer expected. The schema is the contract. The compiler enforces it.

File-based vs code-based routes

TanStack Router supports both. The file-based router uses @tanstack/router-plugin to generate the route tree from a routes/ directory. The code-based router uses an explicit createRoute call per route. I use file-based on production apps with more than 20 routes and code-based on smaller apps where the route tree is the design document.

What Router does not solve

Router is a client router. It does not fetch data, render on the server, or know about your database. The loader API is a pre-routing hook, not a data-fetching layer. If you need to load data, use a loader (for data that should block the route transition) or use Query (for everything else). The TanStack Query documentation makes this case in detail.

TanStack Query — the server-state cache you do not have to write

TanStack Query is the most-downloaded data-fetching library for React. It is not a server-state library in the Redux sense — it does not own the source of truth. It is a cache of the server state, with rules for when to revalidate, when to invalidate, and when to refetch in the background. The server is the source of truth. The cache is a perf optimization that also gives you a clean API.

Most React apps in 2026 still hand-roll data fetching. A useEffect with a fetch call, a useState for the result, a useState for the error, a useState for the loading flag. Three hooks per request. With Query, it is one hook:

typescript
// app/hooks/useProducts.ts
import { useQuery } from '@tanstack/react-query'

export function useProducts(category: string, page: number) {
  return useQuery({
    queryKey: ['products', category, page],
    queryFn: () =>
      fetch(`/api/products?category=${category}&page=${page}`).then(r => r.json()),
    staleTime: 30_000,
    gcTime: 5 * 60_000,
    refetchOnWindowFocus: true,
  })
}

The win is not the syntax. The win is that the same hook handles loading, error, success, revalidation, and cache invalidation. If I call queryClient.invalidateQueries({ queryKey: ['products'] }) from anywhere in the app, every component using useProducts re-fetches. If two components use the same queryKey, they share the cache. If the user navigates away and back within the stale time, no network call happens.

Mutations and optimistic updates

Query handles writes via useMutation, and the onMutate / onError / onSettled callbacks give you the optimistic-update pattern with rollback for free. The pattern: onMutate writes the optimistic value to the cache and returns a snapshot; the server function runs; onSuccess invalidates the query; onError rolls back to the snapshot. This is the right pattern for editing a record the user can see. For creating a new record where the response is the new record, use setQueryData with the response.

What Query does not solve

Query does not fetch data — it manages the cache around the fetch. In a TanStack Start app, the natural way to make the actual HTTP call is a server function (covered below). The other thing Query does not do is server-side data fetching. If you want to fetch on the server and ship the data with the HTML, you need a server-rendering story. That is where Start comes in.

TanStack Start — the full-stack framework

TanStack Start is the newest of the three, and the one changing fastest. It is a full-stack framework that composes Router and Query with a server-functions API, an SSR pipeline, and a deployment adapter. Routes are still Router routes, but each route can have a server function that runs on the server, and the framework handles the build, the bundler, and the deployment.

According to the TanStack Start documentation, the server-functions API is the central feature. A server function is a function that looks like a normal function in your code but runs on the server at call time:

typescript
// app/server/products.ts
import { createServerFn } from '@tanstack/start/server'
import { z } from 'zod'
import { db } from '~/lib/db'
import { products } from '~/lib/schema'

export const fetchProducts = createServerFn({ method: 'GET' })
  .validator(z.object({ category: z.string(), page: z.number() }))
  .handler(async ({ data }) => {
    return db.select().from(products)
      .where(eq(products.category, data.category))
      .limit(20).offset((data.page - 1) * 20)
  })

The client-side call:

typescript
// app/routes/products.tsx
import { useQuery } from '@tanstack/react-query'
import { fetchProducts } from '~/server/products'

const { data, isLoading, error } = useQuery({
  queryKey: ['products', category, page],
  queryFn: () => fetchProducts({ data: { category, page } }),
})

The boundary is invisible to the developer. The function is a server function, so it runs on the server. The query runs on the client. The wiring is what the framework handles.

Deployment adapters

Start ships with a Vite plugin and a set of deployment adapters. The Cloudflare Workers adapter is the one I use — it produces a Worker bundle, hooks up the KV/D1/R2 bindings, and deploys via Wrangler. According to the Cloudflare Workers documentation, the adapter targets the Workers runtime directly, which means no Node.js compatibility layer, no cold-start penalty, and no API surface surprises. The Node.js, Bun, Deno, and AWS Lambda adapters exist but I have not shipped on them.

What Start does not solve (yet)

Start is a young framework. The plugin pipeline is still being shaken out, the file-based router generator has rough edges, and the ecosystem of integrations is thinner than Next.js. If you are building a content site with 50 static pages and a contact form, Astro is the right answer. If you are building a stateful SaaS app with a database, auth, and payments, Start is the right answer for me in 2026 — but the framework is not as polished as Next.js.

There is also a mental-model cost. Start's server functions are RPC-style: the client imports a function, the function runs on the server, the result is serialized. The benefit is no separate API service to deploy. The cost is that you do not have a public REST or GraphQL surface — if you need a public API for third-party consumers, you have to write it separately.

How the three fit together in a real app

The shape of a production TanStack app, in the order the data flows:

  1. The route is defined in Router. The route has a path, a component, a search-params schema, and an optional loader.
  2. The component uses Query to read server state. The queryFn calls a server function.
  3. The server function runs on the server, talks to the database, and returns the result.
  4. The result is serialized, cached by Query, and rendered by the component.
  5. The mutation goes the other way: a button click calls a server function, the server function writes to the database, the cache is invalidated, the components re-render.

The variation is in the server functions, the database, and the auth layer. A representative production pattern from the TanStack Ship codebase — a server function that handles a Stripe checkout:

typescript
// app/server/billing.ts
import { createServerFn } from '@tanstack/start/server'
import { z } from 'zod'
import { stripe } from '~/lib/stripe'
import { db } from '~/lib/db'
import { subscriptions, users } from '~/lib/schema'
import { eq } from 'drizzle-orm'

export const createCheckoutSession = createServerFn({ method: 'POST' })
  .validator(z.object({ priceId: z.string(), customerId: z.string() }))
  .handler(async ({ data, context }) => {
    const user = await db.select().from(users)
      .where(eq(users.stripeCustomerId, data.customerId))
      .limit(1)
    if (!user[0]) throw new Error('User not found')

    const session = await stripe.checkout.sessions.create({
      customer: data.customerId,
      mode: 'subscription',
      line_items: [{ price: data.priceId, quantity: 1 }],
      success_url: `${context.env.APP_URL}/billing/success`,
      cancel_url: `${context.env.APP_URL}/billing/cancel`,
    })

    await db.insert(subscriptions).values({
      userId: user[0].id,
      sessionId: session.id,
      priceId: data.priceId,
      status: 'pending',
    })

    return { url: session.url }
  })

The server function is typed end-to-end. The validator is a Zod schema. The handler has access to the request context. The return type is inferred. The client side calls it the same way it calls a local function.

Common mistakes and how to avoid them

The first mistake is using Router's loader for data fetching. The loader runs on every route transition, blocks the navigation, and does not have the cache invalidation story. Use it for route-level guards. Use Query for data.

The second mistake is treating Query as a server-state library in the Redux sense. Query is a cache, not a source of truth. The cache update is the optimistic step, the server write is the source of truth, and the invalidation is the reconciliation.

The third mistake is using server functions for things that should be on the client. Server functions are for things only the server can do.

The fourth mistake is not validating server function inputs. The validator is the contract. If you do not validate, the server function trusts the client, and the client is hostile. Use Zod.

The fifth mistake is shipping Start without testing the deployment adapter. The Cloudflare adapter has its own gotchas around bindings, env vars, and the Workers module format. Test the deploy path on day one, not on launch day. For a worked example, see the TanStack Ship features page.

Limits of this guide

This guide is scoped to a single dev shipping a stateful SaaS app to Cloudflare Workers. The numbers I have are from real production apps at 50–500 req/s. I have not benchmarked Router at 10k concurrent navigations, Query at 10k cached entries, or Start at 10k req/s. At higher scale, the answers will be different.

I have also not compared TanStack to non-React frameworks. Rails, Phoenix, SvelteKit, and SolidStart are all in the conversation. The TanStack ecosystem is React-only, and that is a constraint.

Where to go from here

If you are starting a new project, the answer in 2026 is Router + Query + Start on Cloudflare Workers. The combination is the only React stack I trust for a single-dev SaaS launch — type-safe end-to-end, edge deployment, and the data-fetching story I would otherwise have to build myself.

If you have an existing app on Next.js or React Router, the migration is documented. The TanStack Router vs React Router comparison covers the router migration. The Query migration is mostly a search-and-replace of useEffect for useQuery. The Start migration is a rewrite of the API routes as server functions.

If you want a worked example — auth, database, deployment — the TanStack Ship features page walks through the patterns. The TanStack Ship blog index covers postmortems, comparisons, and the patterns I have shipped in production. Start there before you start coding.