NativeScript Blog

TanStack Router - Using file routing in NativeScript

Nathan Walker March 12, 2026

In the first post, we focused on the navigation contract itself: why TanStack Router works so naturally with NativeScript once you accept that Frame and Page are the runtime truth.

That post focused on the runtime contract:

  • how TanStack Router drives a real native page stack

This post focuses on the authoring model:

  • how file-based routing fits into that same architecture without changing the native navigation story

TanStack Router's file routing gives NativeScript apps a route authoring model that is easier to scale, easier to read, easier to refactor, and still fully type-safe. More importantly, it does that without changing the native runtime contract at all.

The router still decides matches, params, search state, and loaders.

NativeScript still decides page creation, transitions, backstack behavior, and lifecycle.

File routing simply improves how the route tree is declared.


Why File Routing Matters More on Native Than You Might Expect

Native apps grow route trees quickly:

  • list screens
  • detail screens
  • modal screens
  • settings areas
  • nested flows
  • deep links
  • auth boundaries

With code-based route declarations, those relationships stay precise, but the route definition layer gets longer and denser over time.

File routing shifts that complexity into the filesystem, where teams already expect to reason about screen structure.

That gives you a practical scaling model:

  • route path shape becomes visible from the directory layout
  • params become visible from route file names
  • route-local loader logic stays near the screen
  • page-level refactors become easier because route ownership is more obvious

And because this is TanStack Router, you are not giving up type inference to get that convenience.

You still keep strongly typed:

  • Link destinations
  • route params
  • search state
  • loader data
  • route APIs

That combination is exactly why this pairing works so well.

The Core Idea: Generate, Then Run Natively

The working model is simple:

  1. TanStack Router reads route files.
  2. It generates routeTree.gen.ts.
  3. The app creates a router from that generated tree.
  4. NativeScriptRouterProvider turns router state into native navigation.

That means file routing lives entirely on the authoring and generation side.

Runtime code stays lean:

import {
  NativeScriptRouterProvider,
  createNativeScriptRouter,
} from '@nativescript/tanstack-router/solid'
import { routeTree } from './routeTree.gen'

const router = createNativeScriptRouter({
  routeTree,
  initialPath: '/',
})

declare module '@nativescript/tanstack-router/solid' {
  interface Register {
    router: typeof router
  }
}

export function App() {
  return (
    <gridlayout>
      <NativeScriptRouterProvider router={router} debug={true} />
    </gridlayout>
  )
}

That is important. The file-route system does not complicate the NativeScript side. It keeps the router creation story clean.

What Makes the Vite Integration Work

@nativescript/vite already has an extensibility model where installed packages can contribute additional Vite configurations through an external nativescript.vite.mjs file.

That means @nativescript/vite can discover configurations from installed dependencies without forcing every app to wire custom plugin code manually.

So the file-routing story works like this:

  1. The app installs @nativescript/tanstack-router.
  2. @nativescript/vite discovers @nativescript/tanstack-router/nativescript.vite.mjs.
  3. That config enables the TanStack Router Vite plugin when the app has file-route signals such as tsr.config.json or src/routes.
  4. TanStack generates routeTree.gen.ts automatically.

That is a particularly strong fit for NativeScript because it keeps the app-level Vite config extremely small:

import { defineConfig, UserConfig } from 'vite'
import { solidConfig } from '@nativescript/vite/solid'

export default defineConfig(({ mode }): UserConfig => {
  return solidConfig({ mode })
})

The app does not need to manually compose the TanStack plugin in its own Vite file.

The Minimum App Structure

From our app, the main pieces are:

src/
  app.tsx
  routeTree.gen.ts
  routes/
    __root.tsx
    index.tsx
    about.tsx
    posts.tsx
    posts.$postId.tsx
    users.$userId.tsx
    -virtual/
      layout.tsx
      index.tsx
      inspector.tsx
tsr.config.json

The generated file is exactly what you would expect:

  • it imports each route module
  • assigns parent relationships
  • builds the route tree
  • augments TanStack's route types

That generated augmentation is the reason typed links like to="/posts/$postId" and params={{ postId: '2' }} continue to work.

The File Route Config

For the physical route portion, the sample uses a standard tsr.config.json:

{
  "routesDirectory": "./src/routes",
  "generatedRouteTree": "./src/routeTree.gen.ts",
  "routeFileIgnorePrefix": "-",
  "quoteStyle": "double",
  "semicolons": true
}

That tells TanStack where route files live, where to generate the route tree, and that files prefixed with - should not be discovered as ordinary physical routes.

That last point becomes especially useful for virtual route patterns.

Physical File Routes in Practice

Root route:

import { createRootRoute } from '@tanstack/solid-router'

export const Route = createRootRoute()

Index route:

import Home from '../components/home'
import { getHomeLoaderData } from '../demo-data'

export const Route = createFileRoute({
  component: Home,
  loader: async () => getHomeLoaderData(),
})

Dynamic param route:

import PostDetail from '../components/post-detail'
import { getPostDetailLoaderData } from '../demo-data'

export const Route = createFileRoute({
  component: PostDetail,
  loader: async ({ params }) => getPostDetailLoaderData(params.postId),
})

A few things matter here:

  • loaders stay route-local
  • route params stay typed
  • route modules stay small and focused
  • the runtime does not care whether the route tree came from files or code

Why This Works So Well with NativeScript Pages

The fit is strong because NativeScript already thinks in screens.

A route file in this setup is not merely a URL segment definition. It maps naturally to a screen boundary, or at least to a page-level screen concern.

That leads to a very clean mental model:

  • route file defines route behavior
  • screen component defines page UI
  • loader defines data contract
  • generated route tree defines type-safe connectivity
  • NativeScript provider defines native navigation behavior

Those responsibilities remain separated, which is exactly why the system stays understandable as the app grows.

File Routing Still Preserves Native Modal Behavior

In the app, modal behavior is still expressed through TanStack Router state and search handling, while the NativeScript provider interprets that into a real modal presentation.

That means file routing and native navigation specialization remain complementary:

  • route files define the route
  • typed search state triggers modal intent
  • NativeScriptRouterProvider decides how that intent is rendered natively

Generated Types Are the Quiet Superpower

The generated routeTree.gen.ts file is not just busywork. It is what turns route files into a strongly typed control plane.

In the current sample, the generated tree contains entries such as:

  • /
  • /about
  • /posts
  • /posts/$postId
  • /users/$userId
  • /virtual
  • /virtual/inspector

It also augments the router with information like:

  • FileRoutesByFullPath
  • FileRoutesByTo
  • FileRoutesById

That is why links become self-checking:

<Link to="/posts/$postId" params={{ postId: '2' }}>
  <label text="Open" />
</Link>

If the route path changes, or params no longer match, TypeScript tells you.

That is exactly the kind of feedback loop that keeps navigation refactors safe.

Virtual Routes: The Other Half of the Story

Physical file routes are only half of what makes TanStack Router's file story interesting.

The other half is virtual routing.

Virtual routes matter when you want route structure to come from configuration rather than raw directory shape.

That is useful when:

  • you want to group screens differently than folders are laid out
  • you want a subtree declared centrally
  • you want to mix physical discovery with explicit mounted route files
  • you want to keep certain route files hidden from normal file discovery

We added a /virtual subtree to demonstrate exactly that in the app.

The Virtual Route

The app currently uses an inline virtualRouteConfig inside tsr.config.json:

{
  "routesDirectory": "./src/routes",
  "generatedRouteTree": "./src/routeTree.gen.ts",
  "virtualRouteConfig": {
    "type": "root",
    "file": "__root.tsx",
    "children": [
      {
        "type": "physical",
        "directory": ".",
        "pathPrefix": ""
      },
      {
        "type": "route",
        "path": "virtual",
        "file": "-virtual/layout.tsx",
        "children": [
          {
            "type": "index",
            "file": "-virtual/index.tsx"
          },
          {
            "type": "route",
            "path": "inspector",
            "file": "-virtual/inspector.tsx"
          }
        ]
      }
    ]
  },
  "routeFileIgnorePrefix": "-"
}

That config does two things at once:

  1. physical('.') behavior is represented by the physical subtree entry, preserving the ordinary file-routed app.
  2. A virtual /virtual subtree is mounted from files that are intentionally ignored by normal discovery.

That second part is important.

The files under src/routes/-virtual are not treated as ordinary physical route files. They exist specifically so the virtual config can mount them where it wants.

That is a powerful pattern for larger apps.

Why Virtual Routes Are a Natural Fit for Native Apps

Native apps often have flows that are conceptually grouped, but not necessarily best expressed by raw directory structure alone.

For example:

  • onboarding flows
  • settings sections
  • account areas
  • feature-flagged route clusters
  • multi-screen transactional flows

Virtual routing lets you express those relationships intentionally.

That is especially useful in NativeScript because the route tree is not just about URLs. It also becomes a very readable definition of screen hierarchy and page intent.

The Virtual Screen Files Are Still Ordinary Route Modules

Even though /virtual is mounted from config, the screen files themselves still look like standard TanStack file route modules.

Example layout route:

import { Outlet } from '@tanstack/solid-router'
import { Link } from '@nativescript/tanstack-router/solid'

export const Route = createFileRoute({
  component: VirtualRoutesLayout,
})

function VirtualRoutesLayout() {
  return (
    <>
      <actionbar title="Virtual Routes" />
      <scrollview>
        <stacklayout>
          <Link to="/virtual">
            <label text="Overview" />
          </Link>
          <Link to="/virtual/inspector">
            <label text="Inspector" />
          </Link>
          <Outlet />
        </stacklayout>
      </scrollview>
    </>
  )
}

That is another reason this pairing works well.

Virtual routing changes placement and mounting, not the component authoring model.

Why the Pairing Feels So Cohesive

There are several reasons why this may feel better than many native/web hybrid routing stories.

1. The runtime responsibilities are not confused

TanStack Router handles:

  • route matching
  • parsing
  • params
  • loaders
  • typed navigation intent

NativeScript handles:

  • page stack mutations
  • modal presentation
  • transitions
  • back button behavior
  • lifecycle boundaries

Those are clean boundaries.

2. File routing improves authoring without changing execution

That is exactly what you want from a routing authoring feature.

3. Type generation keeps the developer experience honest

This is not a loose convention system. The generated route tree becomes a compile-time contract.

4. Screen-oriented apps map naturally to route files

NativeScript already encourages screen-level thinking. File routes reinforce that instead of fighting it.

What About Code-Based Routes?

They still work.

File routing is additive, not replacement-only.

Teams that prefer explicit code-based route trees can keep them.

Teams that want filesystem-driven authoring can use file routes.

Teams that want a hybrid story can use physical file routes plus virtual route configuration for selected areas.

A Practical Setup Path

If you want to try file routing in a NativeScript Solid app today, the shortest path looks like this.

1. Create the app

ns create myapp --solid
cd myapp

2. Install the router packages

npm install @nativescript/tanstack-router @tanstack/solid-router @tanstack/history

# devDependency only
npm install -D @nativescript/vite

Init Vite:

npx nativescript-vite init

If you want to author virtual route config directly, also install:

npm install -D @tanstack/virtual-file-routes

3. Add route files

Create:

  • src/routes/__root.tsx
  • src/routes/index.tsx
  • src/routes/about.tsx

4. Add tsr.config.json

{
  "routesDirectory": "./src/routes",
  "generatedRouteTree": "./src/routeTree.gen.ts",
  "routeFileIgnorePrefix": "-",
  "quoteStyle": "double",
  "semicolons": true
}

5. Create the NativeScript router from the generated tree

import {
  NativeScriptRouterProvider,
  createNativeScriptRouter,
} from '@nativescript/tanstack-router/solid'
import { routeTree } from './routeTree.gen'

6. Run the app

ns run ios
# or
ns run android

At that point, the route tree is generated and your navigation remains fully native.

An Important Implementation Note

In the app, the virtual routing example is shown using the inline virtualRouteConfig object inside tsr.config.json.

That detail is intentional.

It reflects the version of the generator currently being exercised in the NativeScript sample and keeps the example aligned with what is working cleanly end-to-end right now.

As this ecosystem evolves, additional config-authoring styles such as external virtual route config files and subtree config helpers can be expanded further. But the key integration idea is already proven:

  • physical file routes work
  • mixed virtual plus physical routing works
  • NativeScript runtime behavior remains native

Sample App

The working sample app used for this post can be found here:

References

Help Accelerate the Work

This is still early work, and there is plenty of room to improve the ergonomics, examples, tests, and cross-framework reach.

If this direction is useful to your team, support helps turn these ideas into durable tooling.

Join the conversation

Share your feedback or ask follow-up questions below.