DEV Community

Aaron K Saunders
Aaron K Saunders

Posted on

TanStack Start to Mobile: Building Robust Apps with Capacitor

Discover how TanStack Start takes your full-stack web-to-mobile development to the next level with the power of the entire TanStack ecosystem – building amazing mobile apps, and a great developer experience.**

This is a companion document to this Video Tutorial

Full Source code available Here on Github

Introduction: Why TanStack Start for Your Next Mobile Adventure?

While other frameworks are fantastic, TanStack Start, built upon the mature and widely-adopted TanStack libraries (Router, Query), brings unique advantages:

  • Deep Type Safety: Imagine your API endpoints, data fetching, and UI components all benefiting from end-to-end type safety. TanStack Start pushes this further than many alternatives, reducing runtime errors and improving code maintainability.
  • Intuitive Server Functions (createServerFn): (Not Supported On Mobile) This is a game-changer. Instead of manually defining REST endpoints and separate API clients, you write plain TypeScript functions that feel local but execute securely on your remote backend. It's an RPC-like paradigm that drastically simplifies full-stack development.
  • Unified Ecosystem: With TanStack Router handling sophisticated client-side and server-side routing, and TanStack Query providing advanced data management, caching, and mutations, you're working with a cohesive and highly optimized toolkit.

In this guide, we'll walk through creating a simple TanStack Start application, specifically designed to run its backend API on a remote server, while packaging its client-side frontend into a native iOS and Android app using Ionic Capacitor. We'll cover the crucial configuration for development and production, demystifying how these decoupled pieces work together.

The Core Idea: One Start App + Mobile Client Using API Routes

For packaged mobile apps, the reliable approach is:

  • Single TanStack Start App (Web + API): Deploy your Start app as usual. Expose API endpoints using file-based API routes (e.g., src/routes/api.*).
  • Capacitor Mobile Client (SPA): Package the client-side build into the native app. From the mobile app, call your deployed API endpoints via a fully-qualified base URL.

Notes:

  • createServerFn works great when the client and Start server share an origin (web/SSR). In a packaged mobile app (capacitor://localhost/file://), there is no co-located server at /__server, so use explicit API routes instead. See why server functions don't work in the mobile app.
  • Configure CORS on your deployed app to allow capacitor://localhost and local dev origins.

What We're Building

Modifying the TanstackStart project to support building fullstack mobile applications with Ionic Capacitor

For details on why we avoid calling createServerFn directly from the mobile UI, see Troubleshooting: Why server functions don't work in the mobile app.


Step 1: Project Setup - Initialize TanStack Start

Let's begin by creating a new TanStack Start project.

# 1. Create a new TanStack Start project
npm create @tanstack/start@latest

# When prompted, choose:
# - framework: react
# - language: typescript
# - ssr mode: spa (Single Page Application mode for Capacitor)
# - css: tailwind (or your preference)

# 2. Navigate into your new project directory
cd your-app-name

# 3. Install project dependencies
npm install
Enter fullscreen mode Exit fullscreen mode

After running these commands, you'll have a fully functional TanStack Start application with the following structure:

  • src/routes/ - File-based routing directory
  • src/routes/__root.tsx - Root route component
  • src/routes/index.tsx - Main index page
  • src/router.tsx - Router configuration
  • vite.config.ts - Vite configuration
  • package.json - Dependencies and scripts

Initial package.json Notes

After running npm install, your package.json will contain essential scripts and dependencies. Here's what you'll find in the actual implementation:

// package.json (excerpt - actual dependencies)
{
  "name": "tanstack-mobile-1",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite dev --port 3000",        // Starts the Vite development server
    "start": "node .output/server/index.mjs", // Starts the production server
    "build": "vite build",                // Builds the client-side SPA for production
    "serve": "vite preview --host",       // Serves the production build locally
    "serve:backend": "vite dev --host",   // Serves the backend for development
    "test": "vitest run"                  // Runs tests
  },
  "dependencies": {
    "@capacitor/cli": "^7.4.3",           // Capacitor CLI for mobile development
    "@capacitor/core": "^7.4.3",          // Core Capacitor runtime
    "@capacitor/ios": "^7.4.3",           // iOS platform support
    "@capacitor/status-bar": "^7.0.3",    // Status bar plugin
    "@tanstack/react-router": "^1.132.0", // Type-safe, file-based routing
    "@tanstack/react-start": "^1.132.0",  // The core TanStack Start framework
    "@tanstack/router-plugin": "^1.132.0", // Router plugin for TanStack Start
    "react": "^19.0.0",                   // React framework
    "react-dom": "^19.0.0",               // React DOM for web
    "tailwindcss": "^4.0.6"               // CSS framework
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.3.4",     // Vite plugin for React
    "typescript": "^5.7.2",
    "vite": "^6.3.5",
    "vitest": "^3.0.5",
    // ... other dev dependencies
  }
}
Enter fullscreen mode Exit fullscreen mode

Key Dependencies:

  • TanStack Start: Full-stack React framework with file-based routing
  • Capacitor: Mobile app development platform
  • React 19: Latest React version with improved features
  • Tailwind CSS: Utility-first CSS framework

Step 2: Editing Existing Route Files

The create-tanstack-app command already created several demo route files for us. We'll edit these existing files to add our API routes and server functions.

  1. Edit src/routes/api.demo-names.ts
    This file already exists. No Changes aer needed

  2. Edit src/routes/demo.start.server-funcs.tsx
    This file already exists. No changes are needed

  3. Edit src/routes/demo.start.api-request.tsx
    Replace its contents with the following code.

    // src/routes/demo.start.api-request.tsx
    import { createFileRoute } from "@tanstack/react-router";
    import { Capacitor } from "@capacitor/core";
    
    /**
     * Demo route that shows how to make API requests in a mobile context.
     * Uses Capacitor to detect if running in a native app and adjusts the URL accordingly.
     */
    export const Route = createFileRoute("/demo/start/api-request")({
      component: Home,
      loader: async () => {
        // We need to use the full URL here because in a mobile app context,
        // so we check for mobile with capacitor and use the full URL to access the server
        let url = "";
        if (Capacitor.isNativePlatform()) {
          url = `${import.meta.env.VITE_SERVER_BASE_URL}/api/demo-names`;
        } else {
          url = `/api/demo-names`;
        }
        return await fetch(url).then((res) => res.json());
      },
    });
    
    function Home() {
      const names = Route.useLoaderData();
      console.log("names", names);
    
      return (
        <div
          className="flex items-center justify-center min-h-screen p-4 text-white"
          style={{
            backgroundColor: "#000",
            backgroundImage:
              "radial-gradient(ellipse 60% 60% at 0% 100%, #444 0%, #222 60%, #000 100%)",
          }}
        >
          <div className="w-full max-w-2xl p-8 rounded-xl backdrop-blur-md bg-black/50 shadow-xl border-8 border-black/10">
            <h1 className="text-2xl mb-4">Start API Request Demo - Names List</h1>
            <ul className="mb-4 space-y-2">
              {names.map((name: any) => (
                <li
                  key={name}
                  className="bg-white/10 border border-white/20 rounded-lg p-3 backdrop-blur-sm shadow-md"
                >
                  <span className="text-lg text-white">{name}</span>
                </li>
              ))}
            </ul>
          </div>
        </div>
      );
    }
    

    Note: These route files already exist in your project created by create-tanstack-app. We're simply editing their contents to add our custom functionality.

    Key Changes:

    • Add import for Capacitor
    • Update the loader to set the root url based on evnironment using the Capacitor plugin

Step 3: Updating the Root Route for Mobile Support

Now we need to update the existing src/routes/__root.tsx file to add Capacitor support for mobile development.

  1. Update src/routes/__root.tsx
    Edit the existing root route file to add Capacitor status bar support:

    // src/routes/__root.tsx
    import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router";
    import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
    import { TanstackDevtools } from "@tanstack/react-devtools";
    
    import Header from "../components/Header";
    import appCss from "../styles.css?url";
    import { useEffect } from "react";
    import { StatusBar } from "@capacitor/status-bar"; // Add this import
    
    export const Route = createRootRoute({
      head: () => ({
        meta: [
          {
            charSet: "utf-8",
          },
          {
            name: "viewport",
            content: "width=device-width, initial-scale=1",
          },
          {
            title: "TanStack Start Starter",
          },
        ],
        links: [
          {
            rel: "stylesheet",
            href: appCss,
          },
        ],
      }),
    
      shellComponent: RootDocument,
    });
    
    function RootDocument({ children }: { children: React.ReactNode }) {
      useEffect(() => {
        // Add Capacitor status bar configuration for mobile apps
        StatusBar.setOverlaysWebView({ overlay: false });
      }, []);
    
      return (
        <html lang="en">
          <head>
            <HeadContent />
          </head>
          <body>
            <Header />
            {children}
            <TanstackDevtools
              config={{
                position: "bottom-left",
              }}
              plugins={[
                {
                  name: "Tanstack Router",
                  render: <TanStackRouterDevtoolsPanel />,
                },
              ]}
            />
            <Scripts />
          </body>
        </html>
      );
    }
    

    Key Changes:

    • Added import { StatusBar } from "@capacitor/status-bar"
    • Added StatusBar.setOverlaysWebView({ overlay: false }) in the useEffect

Step 4: Configure Vite for Mobile Development

The existing vite.config.ts already has most of what we need, but we should verify it has the correct CORS configuration for mobile development.

  1. Check vite.config.ts
    Your existing Vite config should look like this (it's already configured correctly):

    // vite.config.ts
    import { defineConfig } from "vite";
    import { tanstackStart } from "@tanstack/react-start/plugin/vite";
    import viteReact from "@vitejs/plugin-react";
    import viteTsConfigPaths from "vite-tsconfig-paths";
    import tailwindcss from "@tailwindcss/vite";
    
    const config = defineConfig({
      server: {
        cors: {
          origin: "*",
          credentials: true,
          methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
          allowedHeaders: ["Content-Type"],
        },
      },
      plugins: [
        // This is the plugin that enables path aliases
        viteTsConfigPaths({
          projects: ["./tsconfig.json"],
        }),
        tailwindcss(),
        tanstackStart({
          spa: {
            enabled: true,
            prerender: {
              crawlLinks: true,
              outputPath: "index.html",
            },
          },
        }),
        viteReact(),
      ],
      build: {
        outDir: "dist",
      },
    });
    
    export default config;
    

    Key Configuration Notes:

    • CORS Configuration: Created to support API calls from the mobile device, without it you will get CORS errors
    • SPA Mode: Add the whole spa object in tanstackStart plugin configuration
    • Path Aliases: outputPath required by Capacitor and build.outDir also required by Capacitor
    • Tailwind CSS: Already integrated for styling

    If your config looks different, make sure it includes the CORS configuration in the server section.


Step 5: Adding Capacitor for Mobile Development

Now we'll add Capacitor to your existing TanStack Start project to enable mobile app development.

  1. Install Capacitor Dependencies:
    Add Capacitor to your existing project:

    npm install @capacitor/cli @capacitor/core @capacitor/ios @capacitor/android @capacitor/status-bar
    
  2. Initialize Capacitor:
    This command creates the capacitor.config.ts file and prompts you for basic app info.

    npx cap init "tanstack-mobile" "com.example.tanstack.mobile"
    # Follow the prompts:
    # - App name: tanstack-mobile
    # - App ID: com.example.tanstack.mobile (use a unique reverse-domain identifier)
    
  3. Add Native Platforms:
    This generates the actual ios/ and android/ native project folders.

    npx cap add android  # Add Android platform
    npx cap add ios      # Add iOS platform (requires macOS)
    
  4. Verify capacitor.config.ts
    The generated config file should look like this after the edits are made

    // capacitor.config.ts
    import type { CapacitorConfig } from "@capacitor/cli";
    
    const config: CapacitorConfig = {
      appId: "com.example.tanstack.mobile",
      appName: "tanstack-mobile",
      webDir: "dist/client",
    };
    
    export default config;
    

    Configuration Notes:

    • webDir: Points to dist/client where TanStackStart builds the SPA
    • appId: Use a reverse-domain identifier (e.g., com.yourcompany.appname)
    • appName: Display name for your app in app stores
    • No server config: For production builds, Capacitor uses the bundled files

Step 6: Adding Mobile Development Scripts

Your existing package.json already has the essential scripts. Let's add some convenient scripts for mobile development.

  1. Current Scripts (Already Present)
    Your package.json already includes these essential scripts:

    "scripts": {
      "dev": "vite dev --port 3000",        // Starts the Vite dev server
      "start": "node .output/server/index.mjs", // Starts the production server
      "build": "vite build",                // Builds the client-side SPA
      "serve": "vite preview --host",       // Serves the built client locally
      "serve:backend": "vite dev --host",   // Serves the backend for development
      "test": "vitest run"                  // Runs tests
    }
    
  2. Add Mobile Development Scripts
    Add these additional scripts to your package.json for easier mobile development:

    // Add these scripts to your existing package.json
    "scripts": {
      // ... your existing scripts ...
    
      // Capacitor-specific commands
      "cap:sync": "npx cap sync",               // Copy web assets to native projects
      "cap:open:ios": "npx cap open ios",       // Open iOS project in Xcode
      "cap:open:android": "npx cap open android",// Open Android project in Android Studio
    
      // Development with live reload
      "cap:dev:ios": "npm run dev & npx cap run ios --external",
      "cap:dev:android": "npm run dev & npx cap run android --external",
    
      // Production build for mobile
      "cap:build": "npm run build && npm run cap:sync"
    }
    

    Key Script Notes:

    • dev: Already configured to run on port 3000
    • build: Already creates both client and server builds
    • cap:sync: Copies your built web assets to native projects
    • cap:open:*: Opens native IDEs for platform-specific development
    • cap:dev:*: Runs development server with live reload on mobile

Development Workflow: Testing Your Mobile App

Now that you've set up your TanStack Start project with Capacitor, here's how to test it:

  1. Start the Development Server:
    Open your terminal and run:

    npm run dev
    

    This single command:

    • Starts the Vite development server on http://localhost:3000
    • Enables both frontend and backend functionality
    • Handles API routes and server functions automatically
    • Provides hot module replacement for fast development
  2. Test in Browser:
    Open http://localhost:3000 in your browser to test:

    • The main application at /
    • API request demo at /demo/start/api-request
    • Server functions demo at /demo/start/server-funcs
  3. Test on Mobile Device/Emulator:
    For mobile testing, you have two options:

    Option A: Build and test with native IDEs

    # Build and sync to native projects
    npm run build
    npm run cap:sync
    
    # Open in native IDE
    npm run cap:open:ios     # For iOS (macOS only)
    npm run cap:open:android # For Android
    

    Option B: Live reload development (recommended)

    # Start dev server in one terminal
    npm run dev
    
    # In another terminal, run with live reload
    npm run cap:dev:ios     # For iOS
    npm run cap:dev:android # For Android
    

Key Advantages:

  • Single Command: No need to manage multiple processes
  • Automatic CORS: Built-in CORS handling for mobile development
  • Hot Reload: Changes reflect immediately in both browser and mobile
  • Full-Stack: Both API routes and server functions work out of the box

Key Benefits:

  • Single Deployment: One TanStack Start app serves both web and mobile
  • Automatic Detection: Mobile context is handled automatically
  • Type Safety: Full end-to-end type safety maintained
  • Easy Updates: Update the web app and mobile users get the latest backend functionality

Troubleshooting: Why server functions don't work in the mobile app

When the app runs inside a Capacitor WebView, calling createServerFn directly from the packaged SPA fails. Here's why and how to fix it:

  • Root cause 1: createServerFn targets the current origin (e.g., capacitor://localhost or file://) and expects a co-located Start server at /__server. In a packaged mobile app, there is no server at that origin, so requests are unreachable.

What you’ll see in the mobile network panel (Web Inspector):

This is when the mobile app is trying to connect to the server, using a serverFunction, but the url is off.
Mobile network panel unresolved host

You can see here where we are just making an API call all is well
Requests hitting wrong origin

Recommended approach for mobile:

  1. Use explicit API routes for mobile calls (like src/routes/api.demo-names.ts) and fetch them with a fully-qualified base URL.
  2. Configure VITE_SERVER_BASE_URL to your deployed Start app, and in routes like src/routes/demo.start.api-request.tsx, switch between relative and absolute URLs using Capacitor.isNativePlatform().
  3. Ensure your deployed backend allows CORS from capacitor://localhost, http://localhost, and your device IPs used during development.
  4. Reserve createServerFn for environments where the Start server is co-located with the client (web/SSR). For packaged mobile apps, prefer REST-like API routes or RPC endpoints hosted remotely.

This is why in this guide we demonstrate mobile-friendly API calls in demo.start.api-request.tsx instead of invoking server functions directly from the mobile UI.

Key Takeaways

  • Unified Development: TanStack Start provides a single codebase that works for both web and mobile, with automatic mobile detection.
  • File-Based Routing: API routes and server functions are defined using TanStack Start's file-based routing system.
  • Automatic Mobile Detection: Use Capacitor.isNativePlatform() to detect mobile context and adjust URLs accordingly.
  • Environment Variables: Use VITE_SERVER_BASE_URL to configure the backend URL for mobile builds.
  • Simplified Workflow: Single npm run dev command handles both frontend and backend development.
  • Built-in CORS: TanStack Start handles CORS configuration automatically for mobile development.
  • Type Safety: Full end-to-end type safety maintained from API routes to React components.

Conclusion

By leveraging TanStack Start's powerful full-stack capabilities, especially its createServerFn, and combining it with Capacitor, you gain an incredibly efficient and type-safe workflow to build native-feeling mobile applications from your web codebase. You can now confidently extend your web development skills to the mobile realm, building robust, modern applications with a truly unified and delightful developer experience.

Happy coding, and go build some amazing mobile apps!

GitHub logo aaronksaunders / tanstack-capacitor-mobile-1

tanstackStart with Ionic Capacitor for Fullstack Mobile Development, basic template

Understanding the TanStack Start + Capacitor Mobile App Project

  • There is a video tutorial available here - https://youtu.be/H84he9ijMfc
  • There is a full blog post explaining step by step how to recreate the project available here -

TANSTACK + Capacitor FINALLY Working Together, Fullstack Mobile Apps

Wanting to get TanStack Start running on mobile with Capacitor? This quick guide shows you the few quick config changes that makes it work—plus a live demo of what fails and what succeeds."

Getting Started with the TasnstackStart Project

To run this application:

npm install
npm run start
Enter fullscreen mode Exit fullscreen mode

Building For Production

To build this application for production:

npm run build
Enter fullscreen mode Exit fullscreen mode

Testing

This project uses Vitest for testing. You can run the tests with:

npm run test
Enter fullscreen mode Exit fullscreen mode

Styling

This project uses Tailwind CSS for styling.

Routing

This project uses TanStack Router. The initial setup is a…




Top comments (0)