TanStack Start
Hello 👋
React
with the server
without compromise:
TanStack Start
Sreetam Das

Senior Frontend Engineer II
Web Experience at Remote
Chennai 👋
Remote
Help companies hire, manage and pay people
Full time employment, contractors, payroll
No offices!
Yes, I work remotely for Remote
Yes, we have received questions about TV remote issues
Engineering at Remote
Since 2020: Growth Engineering for 4 years, now WebXP
~300 across engineering, ~50 teams across 40+ countries
Elixir backend: 5 million+ lines of code
Next.js SPA, TanStack Query, TanStack Table, and friends
Why?
sreetamdas.com
Over the years
plain HTML + CSS
React SPA
Next.js (pages router)
Next.js App Router + RSC
Cloudflare / OpenNext
TanStack Start
the benchmark: Next.js
RSC, first teased December 2020 — I was hyped
Layouts: a genuinely great approach to share data and rendering
No more API routes just to talk to the server
But the model flipped
server-first by default.
"use client"serializable props
Composition footgun
Caching was suddenly a thing you had to think about
The dev server seemed super slow
Looking around for options
SvelteKit
SvelteKit made Vite click
data loading in nested routes

Remix
Vite-pilled!
In summary
What?
TanStack Start
full-stack on top of TanStack Router
client-first by default
Vite, TypeScript, deploy anywhere
Client-first, server-available
SSR
streaming
server routes
server functions
middleware
route-level caching
TypeScript fatigue
obvious benefits: catches bugs, helps maintain code as it grows
types become a whole dimension you wrangle continuously
you know exactly what I mean if you've ever reached for
type-fest
Type safety, the old way vs. Start
type Query = { postId: string };
export const getStaticProps: GetStaticProps<Props, Query> = async ({ params }) => ({
props: { postId: params!.postId },
});The Best TypeScript
is the one you don't need to write yourself
URL state belongs to the route
the route owns the schema
bad input becomes a safe default
hooks get typed state
navigation stays valid
URL state, in code
validateSearchparses onceloaderDepsmakes data dependencies explicitRoute.useSearch()returns the validated shapeeverything typed, everywhere
function parseStatsSearch( search: Record<string, unknown>,): StatsSearch { return { period: parseDateRange(search.period) };}<Link to="/stats" search={{ period: "91d" }}> Last 90 days</Link>;
Loader data is cached
route owns the data contract
loaderDepsshapes the cache keystaleTimesays how fresh it is
createFileRoute("/stats")({ validateSearch: parseStatsSearch, loaderDeps: ({ search }) => ({ period: search.period }), loader: ({ deps }) => ({ live: getLiveStats({ data: deps }), }), staleTime: 1000 * 60 * 5,});
Server functions / server routes
routes: public HTTP endpoints
functions: app-to-server RPC
validated input, typed output
GETwhen the result should be cacheable
const getLiveStats = createServerFn({ method: "GET" }) .validator((data): StatsSearch => parseStatsSearch(data)) .handler(async ({ data }) => fetchPlausibleStats(data.period));
Middleware across the boundary
.client()context before the call leaves the browser.server()context before the handler runsone boundary across the network
const statsMiddleware = createMiddleware({ type: "function" }) .client(async ({ next }) => next({ context: { timezone: getUserTimeZone() } }), ) .server(async ({ next }) => next({ context: { requestId: crypto.randomUUID() } }), );const getLiveStats = createServerFn({ method: "GET" }).middleware([ statsMiddleware,]);
Rendering is a dial
full SSR
data-only SSR
client-only
per route
createFileRoute("/stats")({ ssr: true,});
Streaming SSR
shell now
slow data later
return an un-awaited promise
loader: async ({ deps }) => ({ historic: await getHistoricStats(), live: getLiveStats({ data: deps }), // promise, not awaited});
Build-time server functions
same primitive
build once, serve static data
const getHistoricStats = createServerFn({ method: "GET" }) .middleware([staticFunctionMiddleware]) .handler(async () => { const { importedGoogleAnalytics } = await import("./-ga-import"); return importedGoogleAnalytics; });
One route, many choices
The route is the unit of decision
typed URL state
server function
caching policy
rendering mode
createFileRoute("/stats")({ validateSearch: parseStatsSearch, loaderDeps: ({ search }) => ({ period: search.period }), loader: async ({ deps }) => ({ historic: await getHistoricStats(), live: getLiveStats({ data: deps }), }), staleTime: 1000 * 60 * 5, ssr: true,});
RSC as an opt-in data primitive
const Renderable = <MDXContent source={post.raw} />;client-driven, not RSC-driven
In Start, ordinary React components are interactive by default.
Use RSC when it buys you something
Pass it through loader data
Keep the client in charge of composition
Agnostic
Deployment target: node, bun, Cloudflare workers
React, Solid today
🔜 Vue, Angular
uncompromised
So, why does this feel good?
Type Safety across the stack
No lock-in, devs are the focus, not a parent company - open-source!
No mental model update needed
How
How to start
@tanstack/clifor all of TanStack's libraries
TanStack CLI connects setup, docs, add-ons, and generated changes
Agent ready!
More to explore
composite components
import protection
createServerOnlyfncreateClientOnlyfncreateIsomorphicfnDeferred hydration
What next?
Still in RC
many in production already
Say hi!