Select Interactive
Tech Stack & Tools · Engineering9 min read

TanStack Router vs Next.js: The Case for a Pure SPA in Enterprise Engagements

When the API lives in a separate application, a pure client-side SPA on TanStack Router beats Next.js on type safety, speed, team boundaries, and operational surface. Here is the architecture we use for enterprise engagements and why developers keep migrating in the same direction.

By Jeremy Burton

Partner, Select Interactive

Key takeaway

Want the short version? Skip down for a concise summary.

Jump to summary

There is an engagement shape we keep seeing in enterprise work: the client has its own backend team shipping a Python or Node.js/Express API, and they hire a custom web development agency to build the application UI. Somewhere in the kickoff conversations, someone asks the reasonable-sounding question: "why not just use Next.js?" We compared TanStack Start and Next.js as full-stack frameworks in a previous article. This one covers a different architecture entirely: no meta-framework at all, a pure client-side SPA.

Our position up front: when a separate team owns every API the application calls, TanStack Router as a pure SPA behind a thin Express shell is not merely defensible, it is the stronger choice. The reasons are operational, not aesthetic: type safety the compiler enforces, speed at every stage of the loop, clean team boundaries, and a server surface small enough to audit in an afternoon.

The Architecture on the Table

Before arguing about frameworks, describe the actual system. There are three pieces. First, the single page application: TanStack Router for routing, TanStack Query for data, built with Vite into a static bundle that can be served from any host or CDN. Second, a very small Node.js/Express server whose entire job is app authentication and handing the app its configuration and environment values at initial load. That is the complete server surface we ship. Third, the enterprise API, built and owned by the client's own team in Python or Node.js/Express, on its own repo, its own deploy cadence, and its own on-call rotation.

The request story is short. On first load, the browser hits the Express shell: authenticate the user, load configuration, serve the bundle. From that point forward, every API call goes directly from the browser to the client-owned API. The shell never proxies application data.

Hold that picture, because it frames the rest of the article. Given that system, the question is not "is Next.js good?" It is: what would Next.js actually be doing here?

Type Safety Out of the Box, Not Bolted On

TanStack Router was designed TypeScript-first. Route paths, path params, search params, loader data, and even <Link> props are checked by the compiler. Rename a route, change a param, or reshape a search schema, and TypeScript finds every broken link and every stale consumer in the codebase before the build runs. Nothing about this requires plugins, codegen steps, or an experimental flag. It is the default.

This matters more in an enterprise SPA, not less. The UI is param-heavy: table sort, filter, and pagination state, wizard steps, drill-down IDs, deep links teammates paste into tickets. In a pure client-side application, the URL is the only durable, shareable state contract, and TanStack Router types the entire contract, validating every search param at the route boundary with a schema.

Next.js on the client hands you useSearchParams and untyped strings. Its typed-routes support has improved but remains partial and bolted on, and its file conventions were designed for a server-rendered world. For an application whose whole life is client-side param manipulation, that difference is a steady stream of bugs that one framework catches at compile time and the other catches in QA, or production.

Speed, Speed, and More Speed

The development loop first. TanStack Router apps run on Vite, where hot module replacement is measured in milliseconds. Teams that migrated describe the difference bluntly: Inngest reported an 83% reduction in local development time after leaving Next.js, with initial local page loads that used to push 10 to 12 seconds becoming near-instant, after two Turbopack migration attempts that never delivered the improvement.

Runtime speed follows the same line. Migration write-ups report meaningful bundle reductions after moving off Next.js (one team measured a 34% smaller JavaScript bundle), and TanStack Router ships intent-based preloading out of the box: hover or focus a link and the route code and loader data are already in flight before the click. Combined with route loaders and the Query cache, navigations feel instant.

This is worth stating directly because stakeholders often assume SSR is what makes an app feel fast. For an authenticated application, it is not. What makes it feel fast is a small bundle, preloaded routes, and a client cache that already has the answer. A SPA does all three without a server render in the loop.

What Next.js Would Actually Be Doing Here

Be honest about what Next.js offers: server-side rendering and streaming, React Server Components, Server Actions, incremental static regeneration, image optimization, middleware. Every one of those features assumes the framework sits between the user and the data. That is the deal: Next.js delivers its value when it owns the data layer.

Now walk the list against this architecture. The application lives behind a login, so there is nothing for a crawler to index and SSR/ISR buy nothing. The API is owned by another team, so Server Actions and RSC data fetching are bypassed by design. Middleware is replaced by a two-endpoint Express shell. What remains is routing and a client React app, which is precisely the ground where TanStack Router is stronger.

Dead weight is not free. Choosing Next.js here means hosting, patching, and upgrading a full server runtime whose server features are turned off on day one, and absorbing App Router era churn (caching semantics revisions, React canary coupling, breaking upgrade paths) for capabilities the architecture will never use.

Team Boundaries Are an Architecture Feature

This engagement has two teams with different repos, different deploy cadences, and different pagers. The SPA architecture makes the OpenAPI contract the entire interface between them: the backend team publishes it, the frontend consumes it, and nobody owns ambiguous middle-tier code.

Next.js erodes that boundary by design. Server Actions and route handlers make it easy, even idiomatic, to put server logic in the frontend repo. In a one-team product that is a convenience. In a two-team engagement it is a governance problem: server code the backend team never reviews, never monitors, and cannot own accumulates in a repo they do not deploy.

The thin Express shell is the one deliberate exception, and it stays small precisely because there is nowhere else for server code to live. Auth handshake, configuration endpoint, static assets. A few hundred lines that hand over cleanly with the codebase at the end of the engagement.

The Data Layer Is TanStack Query, Not the Framework

In this architecture the interesting data problems are all client-side: caching, background refetch, invalidation after mutations, optimistic updates, request deduplication, retry policy, and auth token injection. TanStack Query handles every one of them, against whatever the client's team ships. Python or Express makes no difference to the frontend: one typed API client module wraps fetch, injects the token, and exposes typed hooks to the rest of the app.

Contrast the Next.js story. Its recommended data patterns (RSC fetch, cache(), Server Actions) assume framework-mediated fetching. Point them at a third-party API you do not control and you are either fighting the defaults or ignoring the framework data layer entirely, at which point you are writing the SPA anyway, just inside a server framework.

The operational payoff shows up when the backend team versions an endpoint. The change surfaces in one API client module and its types, and TypeScript walks you to every affected screen. It does not scatter across server fetching layers, client fetching layers, and a cache configuration in between.

Developers Are Migrating in One Direction (and When They Shouldn't)

We are not the only ones drawing this conclusion. Public migration write-ups keep landing in the same place: Inngest's engineering team described "constantly fighting the framework rather than shipping features" before moving to TanStack, Appwrite catalogued the broader exodus, and detailed migration guides credit the type system with preventing production bugs outright. Cosden Solutions has walked through the same migration on video, step by step, for developers who want to see the before and after rather than read about it. Scroll engineering X for a week and you will find the same arc told over and over: less magic, faster feedback loops, type errors instead of incidents.

It is also the architecture behind our own client work, not just a thesis we argue from the sidelines. Performance Course, a multi-state parent registration and staff operations platform with 60,000-plus registered users, runs on TanStack Router, TanStack Query, and TanStack Form. So do Alcon's internal revenue forecasting platform and MJEC's internal app and client tools. The pattern in this article is not new for us. It is how we build and operate enterprise applications across multiple clients.

Now the counterweight as not every app fits this approach. If the application is public, content-heavy, and genuinely needs SSR for SEO or streaming first paint, we still would not reach for Next.js. We would reach for TanStack Start, the full-stack sibling of TanStack Router and our default for that shape of work. We are not just recommending it: this site runs on TanStack Start, and we have written up the full stack behind it. The real fork in this heuristic was never Next.js versus TanStack Router, it is meta-framework versus pure SPA: pick a full-stack framework, TanStack Start in our shop, when the frontend needs to own a data source or serve public, crawlable content; pick the pure SPA covered in this article when a separate application already owns the API. And name the SPA costs plainly: first-load bundle size requires discipline, a cold load shows a loading state instead of server-rendered content, and token handling in the browser must be done carefully. Code splitting, Router preloading, and the thin-shell auth pattern contain each of them.

If your framework will never own a data source, do not deploy a framework built around owning the data source.

Match the Framework to Who Owns the Backend

Our previous comparison weighed two full-stack frameworks for teams that own their whole stack. This engagement shape is different. Most of Next.js would be turned off on day one, and the part that remains, routing and a client React application, is exactly where TanStack Router is stronger: typed end to end, faster in development and at runtime, and paired with TanStack Query for the real data layer. Router plus Query plus a thin Express shell is not a lesser stack. It is the whole required stack, and every piece of it earns its place.

If you are weighing a pure SPA against a meta-framework for an application where another team owns the API, get in touch. We will walk your specific constraints through the same heuristic and show you what the architecture looks like in practice, including the parts of it we would hand your team at the end.

Work With Us

Have a project in mind?

We build the web's most demanding applications. Let's talk about yours.

Get in Touch