TanStack Start: Full-Stack React Framework
HOW TO GUIDES Jan. 13, 2026, 5:30 a.m.

TanStack Start: Full-Stack React Framework

TanStack Start is the newest addition to the TanStack family, designed to streamline full‑stack React development. It bundles routing, data fetching, server‑side rendering, and type‑safe API contracts into a single, opinionated framework. In this post we’ll explore its core concepts, walk through a real‑world example, and share pro tips to help you get the most out of TanStack Start.

What is TanStack Start?

At its heart, TanStack Start is a zero‑configuration full‑stack framework built on top of React, TanStack Router, and TanStack Query. It abstracts away the boilerplate of setting up a Node server, API routes, and client‑side data fetching, letting you focus on business logic.

The framework follows a “file‑system routing” paradigm similar to Next.js, but with tighter integration to TanStack’s data layer. Every route can export a loader function that runs on the server, pre‑fetches data, and hydrates the client automatically.

Core Concepts

  • File‑system routing: Create a file under src/routes and it becomes a route.
  • Typed loaders: Loaders return data that is type‑checked on both server and client.
  • Integrated Query: TanStack Query handles caching, background refetching, and optimistic updates out of the box.
  • SSR + Streaming: Server‑side rendering streams HTML as data resolves, improving Time‑to‑First‑Byte.

Getting Started

First, install the starter kit. The CLI scaffolds a project with all required dependencies and a basic folder layout.

npx create-tanstack-start@latest my-app
cd my-app
npm run dev

The npm run dev command launches a development server that supports hot‑module replacement for both client and server code. Open http://localhost:3000 to see the default landing page.

Project Structure

  • src/routes/ – Route files (e.g., index.tsx, dashboard.tsx).
  • src/server/ – Server‑only utilities, like database clients.
  • src/lib/ – Shared hooks, types, and helper functions.
  • src/components/ – UI components, often built with Tailwind or your favorite CSS library.

All files inside src/routes automatically become endpoints. Adding a loader export will run server‑side logic before the component renders.

Building a Full‑Stack Feature

Let’s build a simple “Projects Dashboard” that lists projects from a mock database, allows creating a new project, and demonstrates optimistic UI updates.

1. Defining the Data Model

In src/server/db.ts we’ll create an in‑memory store. In a real app you’d replace this with Prisma, Drizzle, or any ORM.

type Project = {
  id: string;
  name: string;
  createdAt: Date;
};

const projects: Project[] = [];

export const db = {
  list: async (): Promise<Project[]> => projects,
  create: async (name: string): Promise<Project> => {
    const newProject = { id: crypto.randomUUID(), name, createdAt: new Date() };
    projects.push(newProject);
    return newProject;
  },
};

2. Creating the Route with a Loader

Now add src/routes/dashboard.tsx. The loader fetches projects on the server and returns them to the component.

import { db } from "../server/db";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router";

export const loader = async () => {
  const data = await db.list();
  return { projects: data };
};

export default function Dashboard() {
  const { data } = useLoaderData<{ projects: Project[] }>();
  const queryClient = useQueryClient();

  const createMutation = useMutation({
    mutationFn: async (name: string) => db.create(name),
    onMutate: async (name) => {
      await queryClient.cancelQueries({ queryKey: ["projects"] });
      const previous = queryClient.getQueryData<Project[]>(["projects"]);
      queryClient.setQueryData(["projects"], (old) => [
        ...(old ?? []),
        { id: "temp-" + Date.now(), name, createdAt: new Date() },
      ]);
      return { previous };
    },
    onError: (err, _variables, context) => {
      if (context?.previous) {
        queryClient.setQueryData(["projects"], context.previous);
      }
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ["projects"] });
    },
  });

  const handleAdd = () => {
    const name = prompt("Project name:");
    if (name) createMutation.mutate(name);
  };

  return (
    <div className="p-4">
      <h1 className="text-2xl font-bold mb-4">Projects</h1>
      <button onClick={handleAdd} className="mb-4 btn-primary">
        Add Project
      </button>
      <ul>
        {data?.projects.map((p) => (
          <li key={p.id} className="mb-2">
            <Link to="/project/$id" params={{ id: p.id }}>
              {p.name}
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

The useLoaderData hook (provided by TanStack Router) injects the data returned from the server‑side loader. The mutation demonstrates optimistic UI: a temporary project appears instantly while the server processes the request.

3. Adding a Detail Page

For a complete experience, create src/routes/project/$id.tsx. The $id segment captures the project ID from the URL.

import { db } from "../../server/db";
import { useLoaderData } from "@tanstack/react-router";

export const loader = async ({ params }) => {
  const project = (await db.list()).find((p) => p.id === params.id);
  if (!project) throw new Response("Not found", { status: 404 });
  return { project };
};

export default function ProjectDetail() {
  const { project } = useLoaderData<{ project: Project }>();
  return (
    <div className="p-4">
      <h2 className="text-xl font-semibold">{project.name}</h2>
      <p>Created at: {project.createdAt.toLocaleString()}</p>
    </div>
  );
}

This route showcases how TanStack Start re‑uses the same loader logic on both the server (for SSR) and the client (for client‑side navigation), keeping data fresh without extra fetch calls.

Real‑World Use Cases

Dashboard‑style admin panels – The loader‑first approach eliminates the “loading spinner” flash because data is already present when the component mounts.

E‑commerce product pages – Combine server‑side SEO rendering with client‑side cart mutations, all powered by TanStack Query’s cache.

Collaborative apps – Use TanStack Query’s subscription features to push real‑time updates to all connected clients while keeping the API contracts type‑safe.

Pro Tips

Tip 1: Keep your loaders pure. Avoid side effects like logging or analytics inside loaders; instead, use middleware on the server layer.

Tip 2: Leverage defer in TanStack Router to stream parts of a page while other data continues loading. This improves perceived performance for large data sets.

Tip 3: When using a real database, wrap each loader in a try/catch block and throw a Response with the appropriate HTTP status. TanStack Router will render the nearest ErrorBoundary automatically.

Testing Your TanStack Start App

Testing follows the same patterns as a standard React app, but you also get the benefit of testing loaders directly. Use vitest or jest to call the loader function with mock params.

import { loader as projectLoader } from "./$id";
import { db } from "../../server/db";

test("loader returns correct project", async () => {
  const mockProject = await db.create("Test Project");
  const result = await projectLoader({ params: { id: mockProject.id } });
  expect(result.project.name).toBe("Test Project");
});

This approach validates both data fetching logic and type safety without spinning up a full server.

Performance Considerations

TanStack Start streams HTML as soon as the first loader resolves. To maximize this, keep each loader lightweight and split heavy queries into separate routes or nested loaders. Also, enable HTTP/2 server push for static assets if your deployment platform supports it.

On the client side, TanStack Query’s staleTime defaults to 0, meaning data refetches on every navigation. For dashboards that rarely change, set a longer staleTime to reduce unnecessary network traffic.

Deploying to Production

TanStack Start produces a single Node.js bundle that can be deployed to Vercel, Netlify Functions, or any container platform. The CLI provides a build command that outputs a dist folder with both server and client assets.

npm run build
# Deploy the contents of ./dist
docker build -t my-app .

Remember to set NODE_ENV=production and configure your database connection string as an environment variable. The framework automatically enables compression and caching headers in production mode.

Conclusion

TanStack Start packs routing, data fetching, and SSR into a cohesive developer experience, letting you build full‑stack React applications with minimal configuration. By embracing loader‑first data, type‑safe contracts, and TanStack Query’s powerful cache, you can ship performant, SEO‑friendly apps faster than ever. Dive in, experiment with the starter kit, and let TanStack Start handle the plumbing while you focus on delivering value.

Share this article