Skip to main content
BHPS, home

Writing

TanStack Start 1.0 is out: why we build on it and why I left Next.js

Jan Szotkowski

On this page

TanStack Start 1.0 is officially stable. Tanner Linsley announced it on October 13, 2026 on the TanStack blog, and I had been waiting for it for a long time. Not because I wasn't using Start yet. I was. I was waiting until I could tell clients, colleagues, and anyone who asks: it's done, it's stable, go ahead and build on it.

People ask me this a lot. "Which frontend technology would you pick for a new project today?" My answer has been the same for a while now: TanStack Start, plus the rest of TanStack. It's the first thing I reach for when I start a new project. And right now I don't have a single reason to change that.

In this article I'll sum up what 1.0 means and what Tanner himself says about it. Most of all, I'll show how we use Start in production on two projects: this website, bhpslabs.com, and the udalist.app application. No theory, just real code.

What "1.0" means

Plenty of companies have been running Start in production for a long time, from small projects to enterprise teams. As Tanner admits, many of them shipped it while it was still in beta, even when the team tried to talk them out of it. We were in the same boat.

So 1.0 isn't about Start suddenly starting to work. It's a formal commitment to the API. You can pick Start, learn it, and build a product on it without worrying that six months from now someone will rework the whole way your application works. For a company betting money and people on a technology, that's the most important property of all.

Tanner is also open about the road from release candidate to 1.0 taking longer than he expected. In the meantime, the framework got work on the compiler, streaming, hydration, and the boundaries between server and client. Manuel Schiller and Florian Pellet focused on SSR performance and got roughly 5.5× higher throughput in their benchmark. Christopher Horobin is the reason the router's extremely strong types don't grind your editor to a halt. None of this looks dramatic in a changelog, but you feel it in production.

What Tanner says (and why I agree)

I recommend reading the whole post in the original. For those short on time, here are the points that resonate with me most.

It all started with a router

Start didn't begin as a framework. It began as a router that Tanner wanted to know everything: which paths exist, which parameters they need, which search params are valid, what a loader returns, and what context is available where. And he wanted your editor to tell you, after every change, what else needs to change.

A few years ago, plenty of people didn't believe that was possible. Today I think it's the single biggest reason to use TanStack Router and Start.

A different path than "everything on the server"

Tanner describes how the React ecosystem narrowed down to a single idea of what an application should look like. Next.js bet on React Server Components both as a framework foundation and as an ideology. Start goes another way: an application stays an application, with state, client-side navigation, and data caching. You add server rendering, streaming, and server functions where they make sense, without rethinking how your whole app works.

Each route can choose to render on the server, to only load its data on the server, or to run purely on the client. Server functions are typed and can be called from a loader or an event handler. Middleware can run on both the client and the server, and the context types carry through.

RSC as an option, not an obligation

There's an interesting passage about React Server Components. The team spent a lot of time on them, and Start supports them, but without letting them take over the whole application. And the punchline: on tanstack.com they eventually turned them off, because it kept getting harder to reason about what was shipped where, for people and for AI models alike. Turning them off didn't mean rewriting the app. That's exactly the kind of freedom I want from a framework.

Host it anywhere

Start is built on Vite and Nitro (and now supports Rsbuild too), so deployment isn't tied to one provider. Netlify, Vercel, Cloudflare, Railway, Render, or your own Docker container running a Node server, which is what we do.

Types help AI agents too

I think this is the most underrated argument. Strict types and explicit APIs were built for people, but they help AI agents just as much. When an agent changes a route parameter or a server function's input, TypeScript immediately shows it every place that no longer agrees with that change. On top of that, TanStack has started shipping so-called skills inside its npm packages (via TanStack Intent), so an agent doesn't have to rely on outdated training data. It reads current best practices straight from the package.

Why I left Next.js

I used Next.js for a long time and I don't want to trash it. It's great at explaining and selling its ideas, and it taught a lot of people a lot. But more and more I felt like I was fighting the framework instead of building the product.

  • "use client" and "use server" directives decide where code runs, and the boundaries aren't always visible.
  • Caching changed fundamentally across several versions in a row, and it was hard to tell what was cached, where, and for how long.
  • The App Router mental model is hard for juniors (and honestly for seniors too): server components, client components, what's allowed to import what.
  • Type safety in routing is more of an add-on than a foundation. Search params are basically string | string[] | undefined.

Start is the opposite. Components are components, routes are routes, and when I need the server, I say so explicitly. I don't need a second mental model of my app just to get through the day.

I've also met Tanner in person in Amsterdam, and that conversation confirmed what I'd sensed from the code for a long time: behind TanStack is someone who cares about tools that are good to use, not just tools that are easy to sell.

Developer experience and the learning curve

In my view, Start's DX is a class above, and a few concrete things show it best.

Typed routes without keeping them in your head. The router generates its own route tree (routeTree.gen.ts). A link to a page that doesn't exist or a missing parameter is a compile-time error, not a 404 in production. On udalist.app we have more than 80 <Link> components, and every one of them is type-checked.

Search params as typed state. validateSearch (used by seven routes on Udalist) turns search params into a validated, typed object. Filters, pagination, or form state in the URL stop being a source of bugs.

An explicit server. A server function is a plain function you call. You can see what goes in, what comes out, and that it runs on the server. No magic.

The learning curve. Anyone who knows React and a bit of TypeScript is productive in Start within a few days. There's no new paradigm to learn, just the router. And if you know TanStack Query, you'll feel at home even faster. With the Next.js App Router, I've watched experienced developers struggle for weeks.

SEO and GEO: where Start really shines

We spent a lot of time on SEO and GEO (Generative Engine Optimization, meaning optimization for AI assistants like ChatGPT, Claude, or Perplexity) on this website. Start didn't get in our way once. Quite the opposite.

Metadata on every route

Every route has a head() function that returns the title, description, canonical, hreflang, Open Graph, and JSON-LD. It's typed, it knows the route's parameters, and it runs during SSR, so crawlers get finished HTML. Here's a simplified version of the route for this very article:

export const Route = createFileRoute('/$locale/blog/$slug')({
    beforeLoad: ({ params }) => {
        if (!findPost(posts, params.locale, params.slug)) throw notFound();
    },
    head: ({ params }) => {
        const post = findPost(posts, params.locale, params.slug);
        return pageHead({
            title: post.title,
            description: post.description,
            ogType: 'article',
            alternates, // hreflang to the article's translation
            nodes: [blogPostingNode({ headline: post.title /* … */ })], // JSON-LD
        });
    },
    component: ArticlePage,
});

Notice the beforeLoad as well. An old URL format redirects with a permanent 301, and an article that doesn't exist returns a real 404, not an empty page with a 200 status code. That matters a lot for SEO.

Prerendering and the sitemap right in the config

We prerender static pages at build time, and the sitemap is generated automatically. On both projects it's a few lines in vite.config.ts:

tanstackStart({
    pages: getTanstackStartPages(),
    prerender: { enabled: true, crawlLinks: false, autoStaticPathsDiscovery: false },
    sitemap: { enabled: true, host: 'https://bhpslabs.com/' },
}),

On Udalist, the prerender also has a filter that skips the signed-in part of the app (the dashboard and authentication). Marketing pages are static and lightning fast, while the app behind the login runs as a classic SPA.

A website AI understands too

This is where it pays off that Start doesn't hide anything. The custom server entry (src/server.ts) is a plain fetch handler, and you can add whatever you need to it. Thanks to that, on bhpslabs.com we:

  • return the same page as Markdown when an AI agent asks for it with an Accept: text/markdown header. The agent doesn't have to parse HTML and gets clean text,
  • build a Content-Security-Policy for each response from the hashes of its inline scripts, without unsafe-inline,
  • serve files under /.well-known/ for agent discovery (a catalog, agent skills),
  • and on top of that, a robots.txt with rules for AI crawlers and an llms.txt that tells models what we do and when to recommend us.
export default {
    async fetch(request: Request) {
        const wellKnown = await wellKnownResponse(request);
        if (wellKnown) return wellKnown;

        const markdown = wantsMarkdown(request.headers.get('accept'));
        const response = await paraglideMiddleware(request, () => handler.fetch(request));

        if (markdown) return toMarkdownResponse(response);
        // … CSP from inline script hashes, Vary, gzip
        return response;
    },
};

Udalist has a very similar entry: Paraglide middleware, security headers, Markdown for the home page, and /.well-known/ files. Designed once, used twice.

Our projects in production

bhpslabs.com

This website runs on TanStack Start, TanStack Router, React 19, Vite, and Nitro, deployed as a Node server in Docker. It's bilingual (Czech and English) through Paraglide, and paths look like /$locale/....

A few things worth mentioning:

  • The blog (yes, including this article) is loaded from Markdown files through a custom Vite plugin that validates them at build time and turns them into typed data. Translations of an article are linked by their folder, not their URL.
  • The enquiry form sends data through a typed server function with validation and rate limiting:
export const sendAuditRequest = createServerFn({ method: 'POST' })
    .validator((value: unknown): SendInput => {
        if (!isInput(value)) throw new Error('Invalid input');
        return value;
    })
    .handler(async ({ data }): Promise<SendResult> => {
        if (!allow(getRequestIP({ xForwardedFor: true }) ?? 'unknown')) return { status: 'unavailable' };
        // … validation and sending the email
    });
  • Static HTML, caching, and compression are handled through Nitro routeRules: fonts and assets with a long cache, HTML with revalidation, and precompressed JS and CSS.

The result? Lighthouse gives bhpslabs.com 100 / 100 / 100 / 100 across all four categories (Performance, Accessibility, Best Practices, SEO), plus Agentic Browsing 3 / 3 in the new category that measures how well an AI agent can find its way around a site. That's not down to a single trick. It's the sum of everything above: prerendering, SSR without unnecessary JavaScript, metadata on every route, and a server you can extend with whatever you need. Start didn't get in the way of any of it.

udalist.app

Udalist collects photos and videos from events (weddings, parties, corporate events) into one shared gallery. Guests scan a QR code and upload straight from their phone's browser, with no app and no account. It's our flagship, and it's more of an application than a website, so it shows the other side of Start:

  • 33 route files with pathless layouts: _public for marketing, _guest for guests, and _secure for signed-in users. Each group has its own layout and its own rules, while the URL stays clean.
  • TanStack Query lives in the router context, so every loader and beforeLoad has access to it. It pulls data from a Kotlin backend and is used in around 25 files across the app.
  • TanStack Form powers nine forms, from contact to event security settings.
  • Seven languages through Paraglide, and Sentry through its official SDK for TanStack Start.
  • A home page A/B test runs through a server function that reads a cookie and sets response headers.
  • Nitro cache rules are typed against the list of routes the router generates. Rename or delete a route, and the cache config stops compiling:
import type { FileRouteTypes } from '../src/routeTree.gen';

type AppFullPath = FileRouteTypes['fullPaths'];

const MARKETING_PATHS_WITH_LONG_CACHE = [
    '/',
    '/pricing',
    '/faq',
    '/blog/',
    // …
] as const satisfies readonly AppFullPath[];

A typo in a path is no longer a silent bug in production, where a page suddenly ships without caching and without security headers. It's a red squiggle in the editor. That's exactly what I love about TanStack.

Both projects deliberately share the same foundation. We're a team of four, and we don't want to maintain two different stacks. Whatever we solve on one, we carry over to the other.

The ecosystem is the biggest advantage

Start is great on its own, but its real strength is that it isn't alone:

  • TanStack Query for server data and caching, the standard you already know,
  • TanStack Router with type safety you won't find anywhere else,
  • TanStack Form for typed forms without the ceremony,
  • TanStack Table and Virtual for tables and long lists,
  • and more: Store, Pacer, DB, AI, Devtools, Markdown…

All the libraries share the same philosophy: headless, type-safe, framework-agnostic (React, Solid, Vue, and more), and designed to work well together. Learn one, and the others feel familiar. And if you decide not to use one of them, nothing breaks.

The ecosystem is also backed by strong partners: Netlify, Vercel, Cloudflare, Railway, Render, Lovable, Clerk, WorkOS, Sentry, Prisma, and more. These aren't just logos on a website. They're SDKs, templates, and integrations you can use right away.

What isn't perfect

So this isn't just a hymn of praise: Nitro v3, which Start runs on in production, is still in beta. It works for us without problems, but it's good to know. And some advanced features (RSC, for example) are still evolving, so if you want to build your whole application on them, keep that in mind.

Neither was a reason for us to put off Start.

How to get started

If you've been waiting for 1.0, now's the time:

pnpm create @tanstack/start@latest

Then I recommend the Start documentation, and if you're migrating from Next.js, TanStack has a migration guide too. And as Tanner himself suggests: feel free to put an AI agent on it. With up-to-date skills from the packages, TypeScript, and tests, it gets surprisingly far.

Conclusion

For me, TanStack Start 1.0 confirms something I've known for a long time. A framework doesn't have to dictate what your whole application looks like. It can give you strong, typed, explicit tools and leave the decisions to you.

We run it in production on this website and on Udalist, and everything works as it should. Anyone who asks me what to pick for the frontend today gets a clear answer: TanStack Start. Plus the rest of TanStack.

Thank you, Tanner, and thanks to the whole team behind TanStack. And if you're wondering whether and how to move to Start, get in touch. We're happy to help.