Migrating our Website and Dashboard to TanStack Start

    S

    Sean

    Published on May 1, 2025

    Migrating our Website and Dashboard to TanStack Start

    Context

    We recently wanted to migrate our web dashboard to a modern framework which could smoothly support server-side rendering static marketing pages side-by-side with our existing JS-rich single page application. The primary motivation was to support the redesigned website (you are now viewing) which is integrated directly with our web dashboard applications.

    We evaluated a few options and eventually settled on TanStack Start + TanStack Router for this migration. This post will walk through our decision making process, the challenges we faced, the benefits, and our reflections. Overall, we are very satisfied with the performance, flexibility and developer experience TanStack Start has provided.

    We were starting with a fairly standard Vite based single page React application, which was using React Router for client side routing and TRPC + TanStack Query for API communication. We served a big JavaScript bundle to the client on every page load request. We want to move to a flexible solution that would improve performance and allow us to server-side render specific pages for SEO purposes.

    Defining Some Terms

    TanStack is a new player in the React framework space, so it might be helpful to start by defining the following:

    • TanStack Start: This is the full-stack framework (still in beta) which handles serving a TanStack Router based application, and provides features like SSR, streaming, server functions, etc.
    • TanStack Router: This is the (fully type-safe) routing library, which handles application routing.

    Both, in addition to rest of the TanStack repertoire, are maintained by the prolific and generous Tanner Linsley.

    Why Not Next.js?

    Let's address the elephant in the room first. Next.js has become the de facto React framework in recent years, and continues to push the entire ecosystem forward. It's the first option that may come to mind when considering a migration like this. Its extensive feature set and excellent developer experience make it an attractive option. However, we were also considering the following points:

    1. Flexibility: While Next.js offers a comprehensive solution, it's also very opinionated and less flexible. We wanted more granular control over our application architecture. TanStack Start provided this flexibility without the overhead of a full-featured framework.
    2. Type Safety: TanStack Router's first-class TypeScript support and type-safe routing capabilities were a major draw. The ability to catch routing errors at compile time rather than runtime aligned perfectly with our development philosophy. We are aware type-safe routing seems to be coming to other prominent routing libraries, but it's ready for production use today in TanStack Router.
    3. Vendor Independence: Next.js is tightly coupled to the Vercel ecosystem, and represents some risk of vendor lock-in that we'd like to avoid. We appreciate the flexibility of being able to manage our frontend deployments independently.

    The Migration Journey

    The main tasks and challenges we faced in this migration were the following:

    1. Establishing the basic setup that would allow us to support our existing single page application using TanStack Start and TanStack Router.
    2. Establishing the setup for server-side rendering static marketing pages.
    3. Migrating our entire routing structure from React Router to TanStack Router's recommended file-based routing approach.
    4. Getting the deployment to work (naturally).

    Getting the Initial Setup Right

    One of the biggest challenges we faced was getting the correct initial configuration which would support our existing application and our new SSR enabled marketing pages. This was challenging because TanStack Start is still a new framework (in beta, at the time of this writing), and there are not that many examples in the wild yet for how to use it (although to be clear there are many examples on the TanStack website).

    The biggest challenge was getting a setup working which supported our TRPC and React Query usage within our main application while also supporting SSR for other pages. Accomplishing this involved mix and matching code from a lot of the existing TanStack Start and Router example repos until we arrived at something which was working.

    The important setup code is the following:

    import { QueryClient } from "@tanstack/react-query";
    import {
      createRouter as createTanStackRouter,
      Navigate,
    } from "@tanstack/react-router";
    import { routerWithQueryClient } from "@tanstack/react-router-with-query";
    
    // It may look straightforward...
    // but it took a lot of trial and error to get right!
    export function createRouter() {
      const queryClient = new QueryClient();
      const router = routerWithQueryClient(
        createTanStackRouter({
          Wrap: function WrapComponent({ children }) {
            return (
              <TrpcProvider queryClient={queryClient}>
                {children}
              </TrpcProvider>
            );
          },
          context: { queryClient },
          defaultErrorComponent: ErrorPage,
          defaultNotFoundComponent: () => <Navigate to="/" />,
          defaultPreload: "intent",
          routeTree,
          transformer: routerTransformer,
        }),
        queryClient,
      );
      return router;
    }

    Adopting File-Based Routing

    The most significant amount of code change involved migrating from React Router's code-based routing to TanStack Router's file based routing.

    TanStack Router does support code-based routing, however we decided to adopt their recommended file-based routing. See their list of benefits of this approach here if you are interested in the rationale.

    A general version of what this migration looked like in code is the following:

    // Before: Route definition in a central file
    const routes = [
      {
        path: "/dashboard",
        component: DashboardPage,
      },
      // ... more routes
    ];
    
    // After: File-based routing
    // File: routes/dashboard.tsx
    export const Route = createFileRoute('/dashboard')({
      component: DashboardPage,
    });

    In total, our frontend dashboard and the new marketing website we developed in parallel with this migration have almost 90 routes and sub-path routes, so the migration to file-based routing involved substantial code changes to replace the previous React Router based approach.

    Adopting file-based routing also involved some overhead of learning new routing conventions, such as:

    • You can create "pathless route layouts" with an underscore which can be applied to any other routes by prefixing the route name with the pathless route (e.g. "_layout.tsx" is a pathless layout and "_layout.home.tsx" would apply that layout to the "home" route). Naming a file "route.tsx" will apply it as the default route layout for all routes at that directory level.
    • You can create route groups by wrapping a folder name in parentheses, e.g. `(marketing)` or `(dashboard)`. This is useful for route organizing similar routes together (e.g. routes that share a common layout).

    Deployment Challenges

    Predictably, when we first went to deploy this code (which had been working fine on our local computers) the deployment builds would fail due to some TailwindCSS compilation issue.

    We should mention that we like to adopt new technology and we are using Bun across the stack at Inference Cloud, from running our services, to compiling our client binaries, to serving our frontend applications. The Bun hosting documentation for TanStack Start is minimal and only mentions that you must upgrade to React 19.

    Getting the deployment online took a bit of work to debug various versioning issues in our libraries after upgrading to React 19. However, we still faced this inscrutable Tailwind build error:

    #12 2.601  ERROR  [vite:css] [postcss] /app/packages/console/src/styles/app.css?transform-only:1:1: The border-border class does not exist. If border-border is a custom class, make sure it is defined within a @layer directive.
    #12 2.601 file: /app/packages/console/src/styles/app.css?transform-only:1:0
    #12 2.601
    #12 2.601     file: /app/packages/console/src/styles/app.css?transform-only:1:0
    #12 2.601     at error (/app/node_modules/postcss/lib/input.js:109:20)
    #12 2.601     at processApply (node_modules/tailwindcss/lib/lib/expandApplyAtRules.js:380:29)
    #12 2.601     at <anonymous> (node_modules/tailwindcss/lib/lib/expandApplyAtRules.js:551:9)
    #12 2.601     at <anonymous> (node_modules/tailwindcss/lib/processTailwindFeatures.js:55:49)
    #12 2.601     at processTicksAndRejections (native:7:39)

    The builds, predictably, faced no issue when run locally.

    In the end to avoid this we reverted to compiling TailwindCSS manually using the Tailwind CLI. This works well enough.

    Main Benefits

    The migration has yielded several significant improvements, primarily around performance, developer experience and SEO optimization.

    Performance Gains

    Pre-rendering, SSR and automatic code-splitting have dramatically improved the initial load time for our entire website.

    Route preloading is one of the coolest features. In addition to splitting your code automatically for you, TanStack can actually preload the next route bundle for you when you hover navigation links. Check out it - watch the JS bundles get fetched as specific navigation links are hovered here:

    TanStack ✨ automatically pre-fetching JS route bundles on user navigation link interactions

    Developer Experience

    TanStack Router's type-safe routing is incredible. When using the file-based routing approach, TanStack will generate a route tree file for you, which is used to provide all of the routing type-safety. This applies to route search params, path parameters, etc. It's very through and works very well. Consider this example:

    import { Link } from "@tanstack/react-router";
    
    <Link
      // Not only is the route path typed
      to="/dashboard/workers/$workerId/details"
      // But these parameters are typed as well
      params={{ workerId: worker._id }}
    
      // Any mistake above and you will enjoy a compiler
      // error instead of a runtime error!
    >
      { /* ... snip ... */ }
    </Link>

    SEO Optimization

    TanStack has allowed us to server-side render the majority of the content on our marketing pages, which is now optimized for performance and SEO purposes.

    In fact, we've even enabled the pre-rendering feature which will pre-render selected routes if possible.

    Initializing prerenderer
    Building Nitro Server (preset: nitro-prerender, compatibility date: 2024-11-25)
    Nitro Server built
    You can preview this build using npx serve .output/public
    Prerendering 1 initial routes with crawler
      ├─ /leaderboard (486ms)
      ├─ /compute (128ms)
      ├─ /map (53ms)
      ├─ /pricing (15ms)
      ├─ /marketing (37ms)
      ├─ /marketing/models (32ms)
      ├─ /marketing/pricing (19ms)
      ├─ /marketing/explore/real-time-chat (24ms)
      ├─ /marketing/explore/data-extraction (17ms)
      ├─ /marketing/explore/batch-inference (17ms)
      ... etc.
    Prerendered 34 routes in 9.503 seconds

    Looking Forward

    Our migration to TanStack Start has positioned us well for future growth. The modularity and flexibility of our new architecture allow us to:

    • Easily integrate new features without framework constraints
    • Optimize performance on a per-route basis
    • Ship code quickly and confidently as a result of type-safe routing
    • Scale our marketing presence through improved SEO

    Conclusion

    While Next.js remains an excellent choice for many applications, our migration to TanStack Start has proven to be the right decision for our specific needs. The combination of flexibility, type safety, and performance has created a foundation that will serve us well as we continue to grow.

    The journey wasn't without its challenges, but the end result is a more maintainable, performant, and developer-friendly application. For teams facing similar decisions, we hope our experience provides valuable insights into the possibilities beyond the conventional choices in modern web development.

    Thanks for reading! Learn more about TanStack here.

    START BUILDING TODAY

    15 minutes could save you 50% or more on compute.