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
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
}
}
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.
Edit
src/routes/api.demo-names.ts
This file already exists. No Changes aer neededEdit
src/routes/demo.start.server-funcs.tsx
This file already exists. No changes are needed-
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 theCapacitor
plugin
- Add import for
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.
-
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 theuseEffect
- Added
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.
-
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 intanstackStart
plugin configuration -
Path Aliases:
outputPath
required byCapacitor
andbuild.outDir
also required byCapacitor
- Tailwind CSS: Already integrated for styling
If your config looks different, make sure it includes the CORS configuration in the
server
section. -
CORS Configuration: Created to support API calls from the mobile device, without it you will get
Step 5: Adding Capacitor for Mobile Development
Now we'll add Capacitor to your existing TanStack Start project to enable mobile app development.
-
Install Capacitor Dependencies:
Add Capacitor to your existing project:
npm install @capacitor/cli @capacitor/core @capacitor/ios @capacitor/android @capacitor/status-bar
-
Initialize Capacitor:
This command creates thecapacitor.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)
-
Add Native Platforms:
This generates the actualios/
andandroid/
native project folders.
npx cap add android # Add Android platform npx cap add ios # Add iOS platform (requires macOS)
-
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 todist/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.
-
Current Scripts (Already Present)
Yourpackage.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 }
-
Add Mobile Development Scripts
Add these additional scripts to yourpackage.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:
-
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
- Starts the Vite development server on
-
Test in Browser:
Openhttp://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
- The main application at
-
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
orfile://
) 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.
You can see here where we are just making an API call all is well
Recommended approach for mobile:
- Use explicit API routes for mobile calls (like
src/routes/api.demo-names.ts
) and fetch them with a fully-qualified base URL. - Configure
VITE_SERVER_BASE_URL
to your deployed Start app, and in routes likesrc/routes/demo.start.api-request.tsx
, switch between relative and absolute URLs usingCapacitor.isNativePlatform()
. - Ensure your deployed backend allows CORS from
capacitor://localhost
,http://localhost
, and your device IPs used during development. - 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!
- 🎥 YouTube Channel
- 🌐 GitHub
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."
- 🎥 YouTube Channel
- 🌐 GitHub
Getting Started with the TasnstackStart Project
To run this application:
npm install
npm run start
Building For Production
To build this application for production:
npm run build
Testing
This project uses Vitest for testing. You can run the tests with:
npm run test
Styling
This project uses Tailwind CSS for styling.
Routing
This project uses TanStack Router. The initial setup is a…
Top comments (0)