From fad20b2d096dd12c9bd30a1fad87ef0b4f3bdb05 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Wed, 1 Apr 2026 11:50:44 +0200 Subject: [PATCH 1/6] Improve readme --- examples/react/offline-transactions/README.md | 72 +++++-------------- 1 file changed, 17 insertions(+), 55 deletions(-) diff --git a/examples/react/offline-transactions/README.md b/examples/react/offline-transactions/README.md index 90cba4aac..fd5bbf215 100644 --- a/examples/react/offline-transactions/README.md +++ b/examples/react/offline-transactions/README.md @@ -1,72 +1,34 @@ -# Welcome to TanStack.com! +# Offline Transactions Example -This site is built with TanStack Router! +A todo app demonstrating `@tanstack/offline-transactions` with three different browser storage backends: -- [TanStack Router Docs](https://tanstack.com/router) +- **IndexedDB** — persistent structured storage via `IndexedDBAdapter` +- **localStorage** — simple key-value fallback via `LocalStorageAdapter` +- **wa-sqlite OPFS** — full SQLite database in the browser via `@tanstack/browser-db-sqlite-persistence` -It's deployed automagically with Netlify! +The app uses TanStack Start in SPA mode with an in-memory server-side todo store. The server simulates network delays and random failures to demonstrate offline resilience. -- [Netlify](https://netlify.com/) +## How to run -## Development - -From your terminal: +From the root of the repository: ```sh pnpm install -pnpm dev -``` - -This starts your app in development mode, rebuilding assets on file changes. - -## Editing and previewing the docs of TanStack projects locally - -The documentations for all TanStack projects except for `React Charts` are hosted on [https://tanstack.com](https://tanstack.com), powered by this TanStack Router app. -In production, the markdown doc pages are fetched from the GitHub repos of the projects, but in development they are read from the local file system. - -Follow these steps if you want to edit the doc pages of a project (in these steps we'll assume it's [`TanStack/form`](https://github.com/tanstack/form)) and preview them locally : - -1. Create a new directory called `tanstack`. - -```sh -mkdir tanstack -``` - -2. Enter the directory and clone this repo and the repo of the project there. - -```sh -cd tanstack -git clone git@github.com:TanStack/tanstack.com.git -git clone git@github.com:TanStack/form.git +pnpm build ``` -> [!NOTE] -> Your `tanstack` directory should look like this: -> -> ``` -> tanstack/ -> | -> +-- form/ -> | -> +-- tanstack.com/ -> ``` - -> [!WARNING] -> Make sure the name of the directory in your local file system matches the name of the project's repo. For example, `tanstack/form` must be cloned into `form` (this is the default) instead of `some-other-name`, because that way, the doc pages won't be found. - -3. Enter the `tanstack/tanstack.com` directory, install the dependencies and run the app in dev mode: +Then from this directory: ```sh -cd tanstack.com -pnpm i -# The app will run on https://localhost:3000 by default pnpm dev ``` -4. Now you can visit http://localhost:3000/form/latest/docs/overview in the browser and see the changes you make in `tanstack/form/docs`. +The app runs at http://localhost:3000. -> [!NOTE] -> The updated pages need to be manually reloaded in the browser. +## What it demonstrates -> [!WARNING] -> You will need to update the `docs/config.json` file (in the project's repo) if you add a new doc page! +- **Outbox pattern** — mutations are persisted locally before syncing to the server +- **Automatic retry** — failed operations retry with exponential backoff when connectivity returns +- **Multi-tab coordination** — leader election ensures only one tab manages offline storage +- **Optimistic updates** — UI updates immediately while mutations sync in the background +- **Collection-level persistence** (wa-sqlite route) — data stored in a real SQLite database in the browser via OPFS, surviving page reloads without server sync From 4d076bca1eba2cd737b517689adbb662703dfdee Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Wed, 1 Apr 2026 12:08:27 +0200 Subject: [PATCH 2/6] Fix build errors --- .../react/offline-transactions/package.json | 3 +- .../src/routes/api/todos.$todoId.ts | 6 +- .../src/routes/api/todos.ts | 4 +- .../src/routes/api/users.$userId.ts | 28 -------- .../src/routes/api/users.ts | 64 ------------------- .../src/tanstack-start-server.d.ts | 16 +++++ .../react/offline-transactions/vite.config.ts | 3 +- 7 files changed, 23 insertions(+), 101 deletions(-) delete mode 100644 examples/react/offline-transactions/src/routes/api/users.$userId.ts delete mode 100644 examples/react/offline-transactions/src/routes/api/users.ts create mode 100644 examples/react/offline-transactions/src/tanstack-start-server.d.ts diff --git a/examples/react/offline-transactions/package.json b/examples/react/offline-transactions/package.json index ab18c6adb..0e16134af 100644 --- a/examples/react/offline-transactions/package.json +++ b/examples/react/offline-transactions/package.json @@ -5,8 +5,7 @@ "type": "module", "scripts": { "dev": "vite dev", - "build": "vite build && tsc --noEmit", - "start": "node .output/server/index.mjs" + "build": "vite build && tsc --noEmit" }, "dependencies": { "@tanstack/browser-db-sqlite-persistence": "^0.1.5", diff --git a/examples/react/offline-transactions/src/routes/api/todos.$todoId.ts b/examples/react/offline-transactions/src/routes/api/todos.$todoId.ts index 21b293645..208cbbc2c 100644 --- a/examples/react/offline-transactions/src/routes/api/todos.$todoId.ts +++ b/examples/react/offline-transactions/src/routes/api/todos.$todoId.ts @@ -4,7 +4,7 @@ import type { TodoUpdate } from '~/utils/todos' import { todoService } from '~/utils/todos' export const ServerRoute = createServerFileRoute(`/api/todos/$todoId`).methods({ - GET: async ({ params, request }) => { + GET: async ({ params, request }: { params: { todoId: string }; request: Request }) => { console.info(`GET /api/todos/${params.todoId} @`, request.url) try { @@ -24,7 +24,7 @@ export const ServerRoute = createServerFileRoute(`/api/todos/$todoId`).methods({ } }, - PUT: async ({ params, request }) => { + PUT: async ({ params, request }: { params: { todoId: string }; request: Request }) => { console.info(`PUT /api/todos/${params.todoId} @`, request.url) try { @@ -52,7 +52,7 @@ export const ServerRoute = createServerFileRoute(`/api/todos/$todoId`).methods({ } }, - DELETE: async ({ params, request }) => { + DELETE: async ({ params, request }: { params: { todoId: string }; request: Request }) => { console.info(`DELETE /api/todos/${params.todoId} @`, request.url) try { diff --git a/examples/react/offline-transactions/src/routes/api/todos.ts b/examples/react/offline-transactions/src/routes/api/todos.ts index 8b6d1f8d7..56367a139 100644 --- a/examples/react/offline-transactions/src/routes/api/todos.ts +++ b/examples/react/offline-transactions/src/routes/api/todos.ts @@ -4,7 +4,7 @@ import type { TodoInput } from '~/utils/todos' import { todoService } from '~/utils/todos' export const ServerRoute = createServerFileRoute(`/api/todos`).methods({ - GET: async ({ request }) => { + GET: async ({ request }: { request: Request }) => { console.info(`GET /api/todos @`, request.url) try { @@ -21,7 +21,7 @@ export const ServerRoute = createServerFileRoute(`/api/todos`).methods({ } }, - POST: async ({ request }) => { + POST: async ({ request }: { request: Request }) => { console.info(`POST /api/todos @`, request.url) try { diff --git a/examples/react/offline-transactions/src/routes/api/users.$userId.ts b/examples/react/offline-transactions/src/routes/api/users.$userId.ts deleted file mode 100644 index 19426fbbf..000000000 --- a/examples/react/offline-transactions/src/routes/api/users.$userId.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createServerFileRoute } from '@tanstack/react-start/server' -import { json } from '@tanstack/react-start' -import type { User } from '~/utils/users' - -export const ServerRoute = createServerFileRoute(`/api/users/$userId`).methods({ - GET: async ({ params, request }) => { - console.info(`Fetching users by id=${params.userId}... @`, request.url) - try { - const res = await fetch( - `https://jsonplaceholder.typicode.com/users/` + params.userId, - ) - if (!res.ok) { - throw new Error(`Failed to fetch user`) - } - - const user = (await res.json()) as User - - return json({ - id: user.id, - name: user.name, - email: user.email, - }) - } catch (e) { - console.error(e) - return json({ error: `User not found` }, { status: 404 }) - } - }, -}) diff --git a/examples/react/offline-transactions/src/routes/api/users.ts b/examples/react/offline-transactions/src/routes/api/users.ts deleted file mode 100644 index b627c2f1f..000000000 --- a/examples/react/offline-transactions/src/routes/api/users.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { - createServerFileRoute, - getRequestHeaders, -} from '@tanstack/react-start/server' -import { createMiddleware, json } from '@tanstack/react-start' -import type { User } from '~/utils/users' - -const userLoggerMiddleware = createMiddleware({ type: `request` }).server( - async ({ next, _request }) => { - console.info(`In: /users`) - console.info(`Request Headers:`, getRequestHeaders()) - const result = await next() - result.response.headers.set(`x-users`, `true`) - console.info(`Out: /users`) - return result - }, -) - -const testParentMiddleware = createMiddleware({ type: `request` }).server( - async ({ next, _request }) => { - console.info(`In: testParentMiddleware`) - const result = await next() - result.response.headers.set(`x-test-parent`, `true`) - console.info(`Out: testParentMiddleware`) - return result - }, -) - -const testMiddleware = createMiddleware({ type: `request` }) - .middleware([testParentMiddleware]) - .server(async ({ next, _request }) => { - console.info(`In: testMiddleware`) - const result = await next() - result.response.headers.set(`x-test`, `true`) - - // if (Math.random() > 0.5) { - // throw new Response(null, { - // status: 302, - // headers: { Location: 'https://www.google.com' }, - // }) - // } - - console.info(`Out: testMiddleware`) - return result - }) - -export const ServerRoute = createServerFileRoute(`/api/users`) - .middleware([testMiddleware, userLoggerMiddleware, testParentMiddleware]) - .methods({ - GET: async ({ request }) => { - console.info(`GET /api/users @`, request.url) - console.info(`Fetching users... @`, request.url) - const res = await fetch(`https://jsonplaceholder.typicode.com/users`) - if (!res.ok) { - throw new Error(`Failed to fetch users`) - } - - const data = (await res.json()) as Array - - const list = data.slice(0, 10) - - return json(list.map((u) => ({ id: u.id, name: u.name, email: u.email }))) - }, - }) diff --git a/examples/react/offline-transactions/src/tanstack-start-server.d.ts b/examples/react/offline-transactions/src/tanstack-start-server.d.ts new file mode 100644 index 000000000..ee2930137 --- /dev/null +++ b/examples/react/offline-transactions/src/tanstack-start-server.d.ts @@ -0,0 +1,16 @@ +// Type declarations for @tanstack/react-start/server +// The @tanstack/react-start-server package types aren't resolvable due to +// pnpm hoisting — the Vite plugin handles these at build time. +declare module '@tanstack/react-start/server' { + export function createServerFileRoute( + path: string, + ): { + methods: (methods: Record) => any>) => { + middleware: ( + middleware: Array, + ) => { methods: (methods: Record) => any>) => any } + } + } + + export function getRequestHeaders(): Record +} diff --git a/examples/react/offline-transactions/vite.config.ts b/examples/react/offline-transactions/vite.config.ts index 42992cae6..bebbe5a43 100644 --- a/examples/react/offline-transactions/vite.config.ts +++ b/examples/react/offline-transactions/vite.config.ts @@ -129,8 +129,7 @@ export default defineConfig({ projects: [`./tsconfig.json`], }), tanstackStart({ - customViteReactPlugin: true, - mode: `spa`, // SPA mode for client-side only offline features + spa: { enabled: true }, }), viteReact(), tailwindcss(), From 5686f752d1ac60db055f501b735d20e936d34251 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Wed, 1 Apr 2026 12:20:57 +0200 Subject: [PATCH 3/6] Remove stuff we don't need from tanstack start --- .../react/offline-transactions/package.json | 1 - .../src/components/DefaultCatchBoundary.tsx | 53 ------------------- .../src/components/NotFound.tsx | 25 --------- .../react/offline-transactions/src/router.tsx | 4 -- .../src/routes/__root.tsx | 38 +------------ .../src/utils/loggingMiddleware.tsx | 41 -------------- .../offline-transactions/src/utils/seo.ts | 33 ------------ 7 files changed, 1 insertion(+), 194 deletions(-) delete mode 100644 examples/react/offline-transactions/src/components/DefaultCatchBoundary.tsx delete mode 100644 examples/react/offline-transactions/src/components/NotFound.tsx delete mode 100644 examples/react/offline-transactions/src/utils/loggingMiddleware.tsx delete mode 100644 examples/react/offline-transactions/src/utils/seo.ts diff --git a/examples/react/offline-transactions/package.json b/examples/react/offline-transactions/package.json index 0e16134af..eabdbfbb8 100644 --- a/examples/react/offline-transactions/package.json +++ b/examples/react/offline-transactions/package.json @@ -19,7 +19,6 @@ "@tanstack/react-start": "^1.159.5", "react": "^19.2.4", "react-dom": "^19.2.4", - "tailwind-merge": "^2.6.1", "zod": "^3.25.76" }, "devDependencies": { diff --git a/examples/react/offline-transactions/src/components/DefaultCatchBoundary.tsx b/examples/react/offline-transactions/src/components/DefaultCatchBoundary.tsx deleted file mode 100644 index ae1b64f67..000000000 --- a/examples/react/offline-transactions/src/components/DefaultCatchBoundary.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { - ErrorComponent, - Link, - rootRouteId, - useMatch, - useRouter, -} from '@tanstack/react-router' -import type { ErrorComponentProps } from '@tanstack/react-router' - -export function DefaultCatchBoundary({ error }: ErrorComponentProps) { - const router = useRouter() - const isRoot = useMatch({ - strict: false, - select: (state) => state.id === rootRouteId, - }) - - console.error(`DefaultCatchBoundary Error:`, error) - - return ( -
- -
- - {isRoot ? ( - - Home - - ) : ( - { - e.preventDefault() - window.history.back() - }} - > - Go Back - - )} -
-
- ) -} diff --git a/examples/react/offline-transactions/src/components/NotFound.tsx b/examples/react/offline-transactions/src/components/NotFound.tsx deleted file mode 100644 index 7b54fa568..000000000 --- a/examples/react/offline-transactions/src/components/NotFound.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Link } from '@tanstack/react-router' - -export function NotFound({ children }: { children?: any }) { - return ( -
-
- {children ||

The page you are looking for does not exist.

} -
-

- - - Start Over - -

-
- ) -} diff --git a/examples/react/offline-transactions/src/router.tsx b/examples/react/offline-transactions/src/router.tsx index e9f12d870..28da0bb4d 100644 --- a/examples/react/offline-transactions/src/router.tsx +++ b/examples/react/offline-transactions/src/router.tsx @@ -1,14 +1,10 @@ import { createRouter as createTanStackRouter } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' -import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' -import { NotFound } from './components/NotFound' export function createRouter() { const router = createTanStackRouter({ routeTree, defaultPreload: `intent`, - defaultErrorComponent: DefaultCatchBoundary, - defaultNotFoundComponent: () => , scrollRestoration: true, }) diff --git a/examples/react/offline-transactions/src/routes/__root.tsx b/examples/react/offline-transactions/src/routes/__root.tsx index fb72e0806..fb0fb299a 100644 --- a/examples/react/offline-transactions/src/routes/__root.tsx +++ b/examples/react/offline-transactions/src/routes/__root.tsx @@ -8,10 +8,7 @@ import { import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' import { QueryClientProvider } from '@tanstack/react-query' import * as React from 'react' -import { DefaultCatchBoundary } from '~/components/DefaultCatchBoundary' -import { NotFound } from '~/components/NotFound' import appCss from '~/styles/app.css?url' -import { seo } from '~/utils/seo' import { queryClient } from '~/utils/queryClient' export const Route = createRootRoute({ @@ -24,42 +21,9 @@ export const Route = createRootRoute({ name: `viewport`, content: `width=device-width, initial-scale=1`, }, - ...seo({ - title: `TanStack Start | Type-Safe, Client-First, Full-Stack React Framework`, - description: `TanStack Start is a type-safe, client-first, full-stack React framework. `, - }), - ], - links: [ - { rel: `stylesheet`, href: appCss }, - { - rel: `apple-touch-icon`, - sizes: `180x180`, - href: `/apple-touch-icon.png`, - }, - { - rel: `icon`, - type: `image/png`, - sizes: `32x32`, - href: `/favicon-32x32.png`, - }, - { - rel: `icon`, - type: `image/png`, - sizes: `16x16`, - href: `/favicon-16x16.png`, - }, - { rel: `manifest`, href: `/site.webmanifest`, color: `#fffff` }, - { rel: `icon`, href: `/favicon.ico` }, - ], - scripts: [ - { - src: `/customScript.js`, - type: `text/javascript`, - }, ], + links: [{ rel: `stylesheet`, href: appCss }], }), - errorComponent: DefaultCatchBoundary, - notFoundComponent: () => , shellComponent: RootDocument, }) diff --git a/examples/react/offline-transactions/src/utils/loggingMiddleware.tsx b/examples/react/offline-transactions/src/utils/loggingMiddleware.tsx deleted file mode 100644 index 336587007..000000000 --- a/examples/react/offline-transactions/src/utils/loggingMiddleware.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { createMiddleware } from '@tanstack/react-start' - -const preLogMiddleware = createMiddleware({ type: `function` }) - .client(async (ctx) => { - const clientTime = new Date() - - return ctx.next({ - context: { - clientTime, - }, - sendContext: { - clientTime, - }, - }) - }) - .server(async (ctx) => { - const serverTime = new Date() - - return ctx.next({ - sendContext: { - serverTime, - durationToServer: - serverTime.getTime() - ctx.context.clientTime.getTime(), - }, - }) - }) - -export const logMiddleware = createMiddleware({ type: `function` }) - .middleware([preLogMiddleware]) - .client(async (ctx) => { - const res = await ctx.next() - - const now = new Date() - console.log(`Client Req/Res:`, { - duration: now.getTime() - res.context.clientTime.getTime(), - durationToServer: res.context.durationToServer, - durationFromServer: now.getTime() - res.context.serverTime.getTime(), - }) - - return res - }) diff --git a/examples/react/offline-transactions/src/utils/seo.ts b/examples/react/offline-transactions/src/utils/seo.ts deleted file mode 100644 index bbbdd34ad..000000000 --- a/examples/react/offline-transactions/src/utils/seo.ts +++ /dev/null @@ -1,33 +0,0 @@ -export const seo = ({ - title, - description, - keywords, - image, -}: { - title: string - description?: string - image?: string - keywords?: string -}) => { - const tags = [ - { title }, - { name: `description`, content: description }, - { name: `keywords`, content: keywords }, - { name: `twitter:title`, content: title }, - { name: `twitter:description`, content: description }, - { name: `twitter:creator`, content: `@tannerlinsley` }, - { name: `twitter:site`, content: `@tannerlinsley` }, - { name: `og:type`, content: `website` }, - { name: `og:title`, content: title }, - { name: `og:description`, content: description }, - ...(image - ? [ - { name: `twitter:image`, content: image }, - { name: `twitter:card`, content: `summary_large_image` }, - { name: `og:image`, content: image }, - ] - : []), - ] - - return tags -} From f5c2c9d96a563b1a15451564858da8cab7ac4d41 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Wed, 1 Apr 2026 12:22:37 +0200 Subject: [PATCH 4/6] Update lockfile after removing tailwind-merge Co-Authored-By: Claude Opus 4.6 (1M context) --- pnpm-lock.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dba49a81d..1f345f749 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -514,9 +514,6 @@ importers: react-dom: specifier: ^19.2.4 version: 19.2.4(react@19.2.4) - tailwind-merge: - specifier: ^2.6.1 - version: 2.6.1 zod: specifier: ^3.25.76 version: 3.25.76 @@ -11920,9 +11917,6 @@ packages: resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} engines: {node: ^14.18.0 || >=16.0.0} - tailwind-merge@2.6.1: - resolution: {integrity: sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==} - tailwindcss@4.1.18: resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} @@ -25583,8 +25577,6 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 - tailwind-merge@2.6.1: {} - tailwindcss@4.1.18: {} tapable@2.2.3: {} From 633f4d1e70c17d63817f326db761d3b692f69679 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:23:56 +0000 Subject: [PATCH 5/6] ci: apply automated fixes --- .../src/routes/api/todos.$todoId.ts | 24 ++++++++++++++++--- .../src/tanstack-start-server.d.ts | 10 ++++---- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/examples/react/offline-transactions/src/routes/api/todos.$todoId.ts b/examples/react/offline-transactions/src/routes/api/todos.$todoId.ts index 208cbbc2c..fd8209fe6 100644 --- a/examples/react/offline-transactions/src/routes/api/todos.$todoId.ts +++ b/examples/react/offline-transactions/src/routes/api/todos.$todoId.ts @@ -4,7 +4,13 @@ import type { TodoUpdate } from '~/utils/todos' import { todoService } from '~/utils/todos' export const ServerRoute = createServerFileRoute(`/api/todos/$todoId`).methods({ - GET: async ({ params, request }: { params: { todoId: string }; request: Request }) => { + GET: async ({ + params, + request, + }: { + params: { todoId: string } + request: Request + }) => { console.info(`GET /api/todos/${params.todoId} @`, request.url) try { @@ -24,7 +30,13 @@ export const ServerRoute = createServerFileRoute(`/api/todos/$todoId`).methods({ } }, - PUT: async ({ params, request }: { params: { todoId: string }; request: Request }) => { + PUT: async ({ + params, + request, + }: { + params: { todoId: string } + request: Request + }) => { console.info(`PUT /api/todos/${params.todoId} @`, request.url) try { @@ -52,7 +64,13 @@ export const ServerRoute = createServerFileRoute(`/api/todos/$todoId`).methods({ } }, - DELETE: async ({ params, request }: { params: { todoId: string }; request: Request }) => { + DELETE: async ({ + params, + request, + }: { + params: { todoId: string } + request: Request + }) => { console.info(`DELETE /api/todos/${params.todoId} @`, request.url) try { diff --git a/examples/react/offline-transactions/src/tanstack-start-server.d.ts b/examples/react/offline-transactions/src/tanstack-start-server.d.ts index ee2930137..db499ca8e 100644 --- a/examples/react/offline-transactions/src/tanstack-start-server.d.ts +++ b/examples/react/offline-transactions/src/tanstack-start-server.d.ts @@ -2,13 +2,11 @@ // The @tanstack/react-start-server package types aren't resolvable due to // pnpm hoisting — the Vite plugin handles these at build time. declare module '@tanstack/react-start/server' { - export function createServerFileRoute( - path: string, - ): { + export function createServerFileRoute(path: string): { methods: (methods: Record) => any>) => { - middleware: ( - middleware: Array, - ) => { methods: (methods: Record) => any>) => any } + middleware: (middleware: Array) => { + methods: (methods: Record) => any>) => any + } } } From 1f7f9365d6063498fb862ba114eac04da4c9cf82 Mon Sep 17 00:00:00 2001 From: Kevin De Porre Date: Wed, 1 Apr 2026 12:46:03 +0200 Subject: [PATCH 6/6] Fix API routes to use createFileRoute with server.handlers The API routes were using createServerFileRoute which doesn't exist in @tanstack/react-start 1.159.5. Convert to the correct API: createFileRoute with server.handlers. This fixes the 404s on /api/todos endpoints. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../offline-transactions/src/routeTree.gen.ts | 71 ++++++++- .../src/routes/api/todos.$todoId.ts | 146 ++++++++---------- .../src/routes/api/todos.ts | 96 ++++++------ .../src/tanstack-start-server.d.ts | 14 -- 4 files changed, 184 insertions(+), 143 deletions(-) delete mode 100644 examples/react/offline-transactions/src/tanstack-start-server.d.ts diff --git a/examples/react/offline-transactions/src/routeTree.gen.ts b/examples/react/offline-transactions/src/routeTree.gen.ts index c59e9b6ff..579e1a96e 100644 --- a/examples/react/offline-transactions/src/routeTree.gen.ts +++ b/examples/react/offline-transactions/src/routeTree.gen.ts @@ -13,6 +13,8 @@ import { Route as WaSqliteRouteImport } from './routes/wa-sqlite' import { Route as LocalstorageRouteImport } from './routes/localstorage' import { Route as IndexeddbRouteImport } from './routes/indexeddb' import { Route as IndexRouteImport } from './routes/index' +import { Route as ApiTodosRouteImport } from './routes/api/todos' +import { Route as ApiTodosTodoIdRouteImport } from './routes/api/todos.$todoId' const WaSqliteRoute = WaSqliteRouteImport.update({ id: '/wa-sqlite', @@ -34,18 +36,32 @@ const IndexRoute = IndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) +const ApiTodosRoute = ApiTodosRouteImport.update({ + id: '/api/todos', + path: '/api/todos', + getParentRoute: () => rootRouteImport, +} as any) +const ApiTodosTodoIdRoute = ApiTodosTodoIdRouteImport.update({ + id: '/$todoId', + path: '/$todoId', + getParentRoute: () => ApiTodosRoute, +} as any) export interface FileRoutesByFullPath { '/': typeof IndexRoute '/indexeddb': typeof IndexeddbRoute '/localstorage': typeof LocalstorageRoute '/wa-sqlite': typeof WaSqliteRoute + '/api/todos': typeof ApiTodosRouteWithChildren + '/api/todos/$todoId': typeof ApiTodosTodoIdRoute } export interface FileRoutesByTo { '/': typeof IndexRoute '/indexeddb': typeof IndexeddbRoute '/localstorage': typeof LocalstorageRoute '/wa-sqlite': typeof WaSqliteRoute + '/api/todos': typeof ApiTodosRouteWithChildren + '/api/todos/$todoId': typeof ApiTodosTodoIdRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -53,13 +69,34 @@ export interface FileRoutesById { '/indexeddb': typeof IndexeddbRoute '/localstorage': typeof LocalstorageRoute '/wa-sqlite': typeof WaSqliteRoute + '/api/todos': typeof ApiTodosRouteWithChildren + '/api/todos/$todoId': typeof ApiTodosTodoIdRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' | '/indexeddb' | '/localstorage' | '/wa-sqlite' + fullPaths: + | '/' + | '/indexeddb' + | '/localstorage' + | '/wa-sqlite' + | '/api/todos' + | '/api/todos/$todoId' fileRoutesByTo: FileRoutesByTo - to: '/' | '/indexeddb' | '/localstorage' | '/wa-sqlite' - id: '__root__' | '/' | '/indexeddb' | '/localstorage' | '/wa-sqlite' + to: + | '/' + | '/indexeddb' + | '/localstorage' + | '/wa-sqlite' + | '/api/todos' + | '/api/todos/$todoId' + id: + | '__root__' + | '/' + | '/indexeddb' + | '/localstorage' + | '/wa-sqlite' + | '/api/todos' + | '/api/todos/$todoId' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -67,6 +104,7 @@ export interface RootRouteChildren { IndexeddbRoute: typeof IndexeddbRoute LocalstorageRoute: typeof LocalstorageRoute WaSqliteRoute: typeof WaSqliteRoute + ApiTodosRoute: typeof ApiTodosRouteWithChildren } declare module '@tanstack/react-router' { @@ -99,14 +137,41 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/api/todos': { + id: '/api/todos' + path: '/api/todos' + fullPath: '/api/todos' + preLoaderRoute: typeof ApiTodosRouteImport + parentRoute: typeof rootRouteImport + } + '/api/todos/$todoId': { + id: '/api/todos/$todoId' + path: '/$todoId' + fullPath: '/api/todos/$todoId' + preLoaderRoute: typeof ApiTodosTodoIdRouteImport + parentRoute: typeof ApiTodosRoute + } } } +interface ApiTodosRouteChildren { + ApiTodosTodoIdRoute: typeof ApiTodosTodoIdRoute +} + +const ApiTodosRouteChildren: ApiTodosRouteChildren = { + ApiTodosTodoIdRoute: ApiTodosTodoIdRoute, +} + +const ApiTodosRouteWithChildren = ApiTodosRoute._addFileChildren( + ApiTodosRouteChildren, +) + const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, IndexeddbRoute: IndexeddbRoute, LocalstorageRoute: LocalstorageRoute, WaSqliteRoute: WaSqliteRoute, + ApiTodosRoute: ApiTodosRouteWithChildren, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/examples/react/offline-transactions/src/routes/api/todos.$todoId.ts b/examples/react/offline-transactions/src/routes/api/todos.$todoId.ts index fd8209fe6..47fcc7d89 100644 --- a/examples/react/offline-transactions/src/routes/api/todos.$todoId.ts +++ b/examples/react/offline-transactions/src/routes/api/todos.$todoId.ts @@ -1,98 +1,84 @@ -import { createServerFileRoute } from '@tanstack/react-start/server' +import { createFileRoute } from '@tanstack/react-router' import { json } from '@tanstack/react-start' import type { TodoUpdate } from '~/utils/todos' import { todoService } from '~/utils/todos' -export const ServerRoute = createServerFileRoute(`/api/todos/$todoId`).methods({ - GET: async ({ - params, - request, - }: { - params: { todoId: string } - request: Request - }) => { - console.info(`GET /api/todos/${params.todoId} @`, request.url) +export const Route = createFileRoute(`/api/todos/$todoId`)({ + server: { + handlers: { + GET: async ({ params, request }) => { + console.info(`GET /api/todos/${params.todoId} @`, request.url) - try { - const todo = await todoService.withDelay(() => { - todoService.simulateFailure(0.1) - return todoService.getById(params.todoId) - }) + try { + const todo = await todoService.withDelay(() => { + todoService.simulateFailure(0.1) + return todoService.getById(params.todoId) + }) - if (!todo) { - return json({ error: `Todo not found` }, { status: 404 }) - } + if (!todo) { + return json({ error: `Todo not found` }, { status: 404 }) + } - return json(todo) - } catch (error) { - console.error(`Error fetching todo:`, error) - return json({ error: `Failed to fetch todo` }, { status: 500 }) - } - }, + return json(todo) + } catch (error) { + console.error(`Error fetching todo:`, error) + return json({ error: `Failed to fetch todo` }, { status: 500 }) + } + }, - PUT: async ({ - params, - request, - }: { - params: { todoId: string } - request: Request - }) => { - console.info(`PUT /api/todos/${params.todoId} @`, request.url) + PUT: async ({ params, request }) => { + console.info(`PUT /api/todos/${params.todoId} @`, request.url) - try { - const body = (await request.json()) as TodoUpdate + try { + const body = (await request.json()) as TodoUpdate - const todo = await todoService.withDelay(() => { - todoService.simulateFailure(0.15) - return todoService.update(params.todoId, body) - }) + const todo = await todoService.withDelay(() => { + todoService.simulateFailure(0.15) + return todoService.update(params.todoId, body) + }) - if (!todo) { - return json({ error: `Todo not found` }, { status: 404 }) - } + if (!todo) { + return json({ error: `Todo not found` }, { status: 404 }) + } - return json(todo) - } catch (error) { - console.error(`Error updating todo:`, error) - if (error instanceof Error && error.message.includes(`Simulated`)) { - return json( - { error: `Network error - please try again` }, - { status: 503 }, - ) - } - return json({ error: `Failed to update todo` }, { status: 500 }) - } - }, + return json(todo) + } catch (error) { + console.error(`Error updating todo:`, error) + if (error instanceof Error && error.message.includes(`Simulated`)) { + return json( + { error: `Network error - please try again` }, + { status: 503 }, + ) + } + return json({ error: `Failed to update todo` }, { status: 500 }) + } + }, - DELETE: async ({ - params, - request, - }: { - params: { todoId: string } - request: Request - }) => { - console.info(`DELETE /api/todos/${params.todoId} @`, request.url) + DELETE: async ({ params, request }) => { + console.info(`DELETE /api/todos/${params.todoId} @`, request.url) - try { - const success = await todoService.withDelay(() => { - todoService.simulateFailure(0.15) - return todoService.delete(params.todoId) - }) + try { + const success = await todoService.withDelay(() => { + todoService.simulateFailure(0.15) + return todoService.delete(params.todoId) + }) - if (!success) { - return json({ error: `Todo not found` }, { status: 404 }) - } + if (!success) { + return json({ error: `Todo not found` }, { status: 404 }) + } - return json({ success: true }) - } catch (error) { - console.error(`Error deleting todo:`, error) - if (error instanceof Error && error.message.includes(`Simulated`)) { - return json( - { error: `Network error - please try again` }, - { status: 503 }, - ) - } - return json({ error: `Failed to delete todo` }, { status: 500 }) - } + return json({ success: true }) + } catch (error) { + console.error(`Error deleting todo:`, error) + if (error instanceof Error && error.message.includes(`Simulated`)) { + return json( + { error: `Network error - please try again` }, + { status: 503 }, + ) + } + return json({ error: `Failed to delete todo` }, { status: 500 }) + } + }, + }, }, }) diff --git a/examples/react/offline-transactions/src/routes/api/todos.ts b/examples/react/offline-transactions/src/routes/api/todos.ts index 56367a139..e5fb5691a 100644 --- a/examples/react/offline-transactions/src/routes/api/todos.ts +++ b/examples/react/offline-transactions/src/routes/api/todos.ts @@ -1,52 +1,56 @@ -import { createServerFileRoute } from '@tanstack/react-start/server' +import { createFileRoute } from '@tanstack/react-router' import { json } from '@tanstack/react-start' import type { TodoInput } from '~/utils/todos' import { todoService } from '~/utils/todos' -export const ServerRoute = createServerFileRoute(`/api/todos`).methods({ - GET: async ({ request }: { request: Request }) => { - console.info(`GET /api/todos @`, request.url) - - try { - const todos = await todoService.withDelay(() => { - // Occasionally simulate failure for demo - todoService.simulateFailure(0.1) - return todoService.getAll() - }) - - return json(todos) - } catch (error) { - console.error(`Error fetching todos:`, error) - return json({ error: `Failed to fetch todos` }, { status: 500 }) - } - }, - - POST: async ({ request }: { request: Request }) => { - console.info(`POST /api/todos @`, request.url) - - try { - const body = (await request.json()) as TodoInput - - if (!body.text || body.text.trim() === ``) { - return json({ error: `Todo text is required` }, { status: 400 }) - } - - const todo = await todoService.withDelay(() => { - // Occasionally simulate failure for demo - todoService.simulateFailure(0.15) - return todoService.create(body) - }) - - return json(todo, { status: 201 }) - } catch (error) { - console.error(`Error creating todo:`, error) - if (error instanceof Error && error.message.includes(`Simulated`)) { - return json( - { error: `Network error - please try again` }, - { status: 503 }, - ) - } - return json({ error: `Failed to create todo` }, { status: 500 }) - } +export const Route = createFileRoute(`/api/todos`)({ + server: { + handlers: { + GET: async ({ request }) => { + console.info(`GET /api/todos @`, request.url) + + try { + const todos = await todoService.withDelay(() => { + // Occasionally simulate failure for demo + todoService.simulateFailure(0.1) + return todoService.getAll() + }) + + return json(todos) + } catch (error) { + console.error(`Error fetching todos:`, error) + return json({ error: `Failed to fetch todos` }, { status: 500 }) + } + }, + + POST: async ({ request }) => { + console.info(`POST /api/todos @`, request.url) + + try { + const body = (await request.json()) as TodoInput + + if (!body.text || body.text.trim() === ``) { + return json({ error: `Todo text is required` }, { status: 400 }) + } + + const todo = await todoService.withDelay(() => { + // Occasionally simulate failure for demo + todoService.simulateFailure(0.15) + return todoService.create(body) + }) + + return json(todo, { status: 201 }) + } catch (error) { + console.error(`Error creating todo:`, error) + if (error instanceof Error && error.message.includes(`Simulated`)) { + return json( + { error: `Network error - please try again` }, + { status: 503 }, + ) + } + return json({ error: `Failed to create todo` }, { status: 500 }) + } + }, + }, }, }) diff --git a/examples/react/offline-transactions/src/tanstack-start-server.d.ts b/examples/react/offline-transactions/src/tanstack-start-server.d.ts deleted file mode 100644 index db499ca8e..000000000 --- a/examples/react/offline-transactions/src/tanstack-start-server.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Type declarations for @tanstack/react-start/server -// The @tanstack/react-start-server package types aren't resolvable due to -// pnpm hoisting — the Vite plugin handles these at build time. -declare module '@tanstack/react-start/server' { - export function createServerFileRoute(path: string): { - methods: (methods: Record) => any>) => { - middleware: (middleware: Array) => { - methods: (methods: Record) => any>) => any - } - } - } - - export function getRequestHeaders(): Record -}