.cursor/skills/tanstack-start-migration/SKILL.md
Migrate Next.js apps to TanStack Start. Covers setup (Vinxi/Vite), data handling with route loaders, converting Server Actions to Server Functions, API routes, and optional Server Component patterns. Use when migrating from Next.js to TanStack Start, setting up TanStack Start, or refactoring server actions, getServerSideProps, getStaticProps, or API routes.
npx skillsauth add FixMyBerlin/tilda-geo tanstack-start-migrationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Focus: data handling, server-side behavior, and routing. TanStack Start = TanStack Router + Vinxi (Vite-based server). No App Router RSC model; data flows via route loaders and server functions.
getServerSideProps / getStaticProps / getStaticPaths with loaders| Next.js | TanStack Start |
|---------|----------------|
| getServerSideProps | Route loader (runs on server per request) |
| getStaticProps | Route loader + build-time SSG or loaderDeps caching |
| getStaticPaths | Route tree + optional loader that returns 404 |
| Server Actions | Server Functions (createServerFn) |
| pages/api/* | Server functions or Vinxi createAPIFile/server handlers |
| App Router RSC + fetch | Loader fetches; component uses useLoaderData() |
| next/config rewrites/headers | Vinxi/server config |
See references/nextjs-to-start-mapping.md for edge cases and file layout.
Scaffold:
npm create vinxi@latest my-app
# Choose: TanStack Start (React)
Key structure:
app/ — app entry, routes, componentsapp/routes/ — file-based or manual route treeapp/entry-client.tsx — client hydrateapp/entry-server.tsx — server rendervinxi.config.ts or app.config.ts — Vinxi config (entry points, SSR on/off)SSR: Enabled by default in Start. For SPA-only, configure Vinxi server plugin accordingly.
Env: Use import.meta.env (Vite). Replace process.env with import.meta.env (e.g. import.meta.env.VITE_API_URL).
Route-level async loaders run on the server (or client in SPA mode). Component reads data with useLoaderData().
Define loader on route:
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/users/$userId')({
loader: async ({ params }) => {
const user = await fetchUser(params.userId);
return { user };
},
component: UserPage,
});
function UserPage() {
const { user } = Route.useLoaderData();
return <h1>{user.name}</h1>;
}
Loader params: params, search (from validateSearch), context, deps (see loaderDeps). Return value is serialized to client.
Critical: Loaders run when the route is loaded. For refetch on search/param change, use loaderDeps so the loader re-runs when deps change. Avoid side effects that must run only once in a different lifecycle.
Validation: Use params.parse for path params (/$id, /$slug) so handlers/loaders receive typed values. Use validateSearch for route search schemas (typing/navigation), but note that API server.handlers.* contexts may not expose parsed search; parse new URL(request.url).searchParams in handlers when needed. Use beforeLoad for auth/redirects. See references/loader-data-patterns.md.
Next.js Server Actions become Server Functions: RPC-style functions that run on the server and are callable from the client.
Define server function:
import { createServerFn } from '@tanstack/start';
export const updateProfile = createServerFn({ method: 'POST' })
.validator((data: { name: string }) => data)
.handler(async ({ data }) => {
await db.user.update({ where: { id: session.userId }, data: { name: data.name } });
return { ok: true };
});
Call from client:
'use client';
import { updateProfile } from './profile.server';
function Form() {
const [pending, setPending] = useState(false);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setPending(true);
await updateProfile({ data: { name: formData.name } });
setPending(false);
};
return <form onSubmit={handleSubmit}>...</form>;
}
Rules:
.server.ts or server/ file so client bundle doesn’t pull server-only code.method: 'POST' for mutations; GET for idempotent reads (if needed).onSubmit and handle loading/error state (no formAction prop like Next.js).See references/server-functions.md for validation, errors, and invalidation.
createAPIFile or server route in app/server/) that returns Response. Map pages/api/foo to one such handler.Do not recreate every API route as a REST endpoint; prefer server functions for app-to-server calls.
TanStack Start does not have the same RSC boundary model. Equivalent pattern:
useLoaderData().When migrating RSC pages: move async page component’s data fetching into the route’s loader; replace await fetch() in component with useLoaderData().
app/routes/ with createFileRoute('/path/$param'); route tree generated.createRoute / createRootRoute and pass to router.Params: path: '/users/$userId' → params.userId; validate/coerce with route params.parse. Search: validateSearch on route. Use Link, useNavigate, useParams, useSearch from @tanstack/react-router. See project’s react-dev/references/tanstack-router.md for types and patterns.
app/.getServerSideProps/RSC fetch with route loaders; use useLoaderData() in components.pages/api/* with server functions or Vinxi API handlers.process.env → import.meta.env; expose client vars with VITE_ prefix.next/head / metadata → use Start/Vinxi document/head APIs if needed.next/image (optional third-party or custom).beforeLoad for auth/redirects; no Next.js middleware.| Topic | File | |-------|------| | Next.js ↔ Start concept mapping, file layout | nextjs-to-start-mapping.md | | Loaders, loaderDeps, errors, prefetch | loader-data-patterns.md | | createServerFn, validation, errors, invalidation | server-functions.md |
External: TanStack Start docs — Migrate from Next.js, Server Functions, Getting Started. Community cookbooks (e.g. TanStack-Start-React-Cookbook, skills.sh TanStack Start skills) align with the above; use official docs as source of truth for API.
tools
Build type-safe global state in React with Zustand. Supports TypeScript, persist middleware, devtools, slices pattern, and Next.js SSR with hydration handling. Prevents 6 documented errors. Use when setting up React state, migrating from Redux/Context, or troubleshooting hydration errors, TypeScript inference, infinite render loops, or persist race conditions.
testing
Run local Docker processing in reference then fixed diffing mode to validate Lua/SQL topic changes via public.*_diff tables. From app/, use `processing-generate-command` to print a copy-paste shell line (interactive Clack on a TTY); agents/CI pass the full non-interactive flag set (see --help). Triggers on processing verification, bbox/topic-limited runs, or diff regression after editing processing/topics.
development
React useEffect best practices from official docs and naming discipline. Use when writing/reviewing useEffect, naming effects, useState for derived values, data fetching, or state synchronization. Strong recommendation to name every effect; teaches when NOT to use Effect and better alternatives.
development
This skill should be used when building React components with TypeScript, typing hooks, handling events, or when React TypeScript, React 19, Server Components are mentioned. Covers type-safe patterns for React 18-19 including generic components, proper event typing, and routing integration (TanStack Router, React Router).