@tanstack/start-client-core 1.166.10 → 1.166.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,302 @@
1
+ ---
2
+ name: start-core/execution-model
3
+ description: >-
4
+ Isomorphic-by-default principle, environment boundary functions
5
+ (createServerFn, createServerOnlyFn, createClientOnlyFn,
6
+ createIsomorphicFn), ClientOnly component, useHydrated hook,
7
+ import protection, dead code elimination, environment variable
8
+ safety (VITE_ prefix, process.env).
9
+ type: sub-skill
10
+ library: tanstack-start
11
+ library_version: '1.166.2'
12
+ requires:
13
+ - start-core
14
+ sources:
15
+ - TanStack/router:docs/start/framework/react/guide/execution-model.md
16
+ - TanStack/router:docs/start/framework/react/guide/environment-variables.md
17
+ ---
18
+
19
+ # Execution Model
20
+
21
+ Understanding where code runs is fundamental to TanStack Start. This skill covers the isomorphic execution model and how to control environment boundaries.
22
+
23
+ > **CRITICAL**: ALL code in TanStack Start is isomorphic by default — it runs in BOTH server and client bundles. Route loaders run on BOTH server (during SSR) AND client (during navigation). Server-only operations MUST use `createServerFn`.
24
+ > **CRITICAL**: Module-level `process.env` access runs in both environments. Secret values leak into the client bundle. Access secrets ONLY inside `createServerFn` or `createServerOnlyFn`.
25
+ > **CRITICAL**: `VITE_` prefixed environment variables are exposed to the client bundle. Server secrets must NOT have the `VITE_` prefix.
26
+
27
+ ## Execution Control APIs
28
+
29
+ | API | Use Case | Client Behavior | Server Behavior |
30
+ | ------------------------ | ------------------------- | ------------------------- | --------------------- |
31
+ | `createServerFn()` | RPC calls, data mutations | Network request to server | Direct execution |
32
+ | `createServerOnlyFn(fn)` | Utility functions | Throws error | Direct execution |
33
+ | `createClientOnlyFn(fn)` | Browser utilities | Direct execution | Throws error |
34
+ | `createIsomorphicFn()` | Different impl per env | Uses `.client()` impl | Uses `.server()` impl |
35
+ | `<ClientOnly>` | Browser-only components | Renders children | Renders fallback |
36
+ | `useHydrated()` | Hydration-dependent logic | `true` after hydration | `false` |
37
+
38
+ ## Server-Only Execution
39
+
40
+ ### createServerFn (RPC pattern)
41
+
42
+ The primary way to run server-only code. On the client, calls become fetch requests:
43
+
44
+ ```tsx
45
+ // Use @tanstack/<framework>-start for your framework (react, solid, vue)
46
+ import { createServerFn } from '@tanstack/react-start'
47
+
48
+ const fetchUser = createServerFn().handler(async () => {
49
+ const secret = process.env.API_SECRET // safe — server only
50
+ return await db.users.find()
51
+ })
52
+
53
+ // Client calls this via network request
54
+ const user = await fetchUser()
55
+ ```
56
+
57
+ ### createServerOnlyFn (throws on client)
58
+
59
+ For utility functions that must never run on client:
60
+
61
+ ```tsx
62
+ // Use @tanstack/<framework>-start for your framework (react, solid, vue)
63
+ import { createServerOnlyFn } from '@tanstack/react-start'
64
+
65
+ const getSecret = createServerOnlyFn(() => process.env.DATABASE_URL)
66
+
67
+ // Server: returns the value
68
+ // Client: THROWS an error
69
+ ```
70
+
71
+ ## Client-Only Execution
72
+
73
+ ### createClientOnlyFn
74
+
75
+ ```tsx
76
+ // Use @tanstack/<framework>-start for your framework (react, solid, vue)
77
+ import { createClientOnlyFn } from '@tanstack/react-start'
78
+
79
+ const saveToStorage = createClientOnlyFn((key: string, value: string) => {
80
+ localStorage.setItem(key, value)
81
+ })
82
+ ```
83
+
84
+ ### ClientOnly Component
85
+
86
+ ```tsx
87
+ // Use @tanstack/<framework>-router for your framework (react, solid, vue)
88
+ import { ClientOnly } from '@tanstack/react-router'
89
+
90
+ function Analytics() {
91
+ return (
92
+ <ClientOnly fallback={null}>
93
+ <GoogleAnalyticsScript />
94
+ </ClientOnly>
95
+ )
96
+ }
97
+ ```
98
+
99
+ ### useHydrated Hook
100
+
101
+ ```tsx
102
+ // Use @tanstack/<framework>-router for your framework (react, solid, vue)
103
+ import { useHydrated } from '@tanstack/react-router'
104
+
105
+ function TimeZoneDisplay() {
106
+ const hydrated = useHydrated()
107
+ const timeZone = hydrated
108
+ ? Intl.DateTimeFormat().resolvedOptions().timeZone
109
+ : 'UTC'
110
+
111
+ return <div>Your timezone: {timeZone}</div>
112
+ }
113
+ ```
114
+
115
+ Behavior: SSR → `false`, first client render → `false`, after hydration → `true` (stays `true`).
116
+
117
+ ## Environment-Specific Implementations
118
+
119
+ ```tsx
120
+ // Use @tanstack/<framework>-start for your framework (react, solid, vue)
121
+ import { createIsomorphicFn } from '@tanstack/react-start'
122
+
123
+ const getDeviceInfo = createIsomorphicFn()
124
+ .server(() => ({ type: 'server', platform: process.platform }))
125
+ .client(() => ({ type: 'client', userAgent: navigator.userAgent }))
126
+ ```
127
+
128
+ ## Environment Variables
129
+
130
+ ### Server-Side (inside createServerFn)
131
+
132
+ Access any variable via `process.env`:
133
+
134
+ ```tsx
135
+ const connectDb = createServerFn().handler(async () => {
136
+ const url = process.env.DATABASE_URL // no prefix needed
137
+ return createConnection(url)
138
+ })
139
+ ```
140
+
141
+ ### Client-Side (components)
142
+
143
+ Only `VITE_` prefixed variables are available:
144
+
145
+ ```tsx
146
+ // Framework-specific component type (React.ReactNode, JSX.Element, etc.)
147
+ function ApiProvider({ children }: { children: React.ReactNode }) {
148
+ const apiUrl = import.meta.env.VITE_API_URL // available
149
+ // import.meta.env.DATABASE_URL → undefined (security)
150
+ return (
151
+ <ApiContext.Provider value={{ apiUrl }}>{children}</ApiContext.Provider>
152
+ )
153
+ }
154
+ ```
155
+
156
+ ### Runtime Client Variables
157
+
158
+ If you need server-side variables on the client without `VITE_` prefix, pass them through a server function:
159
+
160
+ ```tsx
161
+ const getRuntimeVar = createServerFn({ method: 'GET' }).handler(() => {
162
+ return process.env.MY_RUNTIME_VAR
163
+ })
164
+
165
+ export const Route = createFileRoute('/')({
166
+ loader: async () => {
167
+ const foo = await getRuntimeVar()
168
+ return { foo }
169
+ },
170
+ component: () => {
171
+ const { foo } = Route.useLoaderData()
172
+ return <div>{foo}</div>
173
+ },
174
+ })
175
+ ```
176
+
177
+ ### Type Safety for Environment Variables
178
+
179
+ ```tsx
180
+ // src/env.d.ts
181
+ /// <reference types="vite/client" />
182
+
183
+ interface ImportMetaEnv {
184
+ readonly VITE_APP_NAME: string
185
+ readonly VITE_API_URL: string
186
+ }
187
+
188
+ interface ImportMeta {
189
+ readonly env: ImportMetaEnv
190
+ }
191
+
192
+ declare global {
193
+ namespace NodeJS {
194
+ interface ProcessEnv {
195
+ readonly DATABASE_URL: string
196
+ readonly JWT_SECRET: string
197
+ }
198
+ }
199
+ }
200
+
201
+ export {}
202
+ ```
203
+
204
+ ## Common Mistakes
205
+
206
+ ### 1. CRITICAL: Assuming loaders are server-only
207
+
208
+ ```tsx
209
+ // WRONG — loader runs on BOTH server and client
210
+ export const Route = createFileRoute('/dashboard')({
211
+ loader: async () => {
212
+ const secret = process.env.API_SECRET // LEAKED to client
213
+ return fetch(`https://api.example.com/data`, {
214
+ headers: { Authorization: secret },
215
+ })
216
+ },
217
+ })
218
+
219
+ // CORRECT — use createServerFn
220
+ const getData = createServerFn({ method: 'GET' }).handler(async () => {
221
+ const secret = process.env.API_SECRET
222
+ return fetch(`https://api.example.com/data`, {
223
+ headers: { Authorization: secret },
224
+ })
225
+ })
226
+
227
+ export const Route = createFileRoute('/dashboard')({
228
+ loader: () => getData(),
229
+ })
230
+ ```
231
+
232
+ ### 2. CRITICAL: Exposing secrets via module-level process.env
233
+
234
+ ```tsx
235
+ // WRONG — runs in both environments, value in client bundle
236
+ const apiKey = process.env.SECRET_KEY
237
+ export function fetchData() {
238
+ /* uses apiKey */
239
+ }
240
+
241
+ // CORRECT — access inside server function only
242
+ const fetchData = createServerFn({ method: 'GET' }).handler(async () => {
243
+ const apiKey = process.env.SECRET_KEY
244
+ return fetch(url, { headers: { Authorization: apiKey } })
245
+ })
246
+ ```
247
+
248
+ ### 3. CRITICAL: Using VITE\_ prefix for server secrets
249
+
250
+ ```bash
251
+ # WRONG — exposed to client bundle
252
+ VITE_SECRET_API_KEY=sk_live_xxx
253
+
254
+ # CORRECT — no prefix for server secrets
255
+ SECRET_API_KEY=sk_live_xxx
256
+
257
+ # CORRECT — VITE_ only for public client values
258
+ VITE_APP_NAME=My App
259
+ ```
260
+
261
+ ### 4. HIGH: Hydration mismatches
262
+
263
+ ```tsx
264
+ // WRONG — different content server vs client
265
+ function CurrentTime() {
266
+ return <div>{new Date().toLocaleString()}</div>
267
+ }
268
+
269
+ // CORRECT — consistent rendering
270
+ function CurrentTime() {
271
+ const [time, setTime] = useState<string>()
272
+ useEffect(() => {
273
+ setTime(new Date().toLocaleString())
274
+ }, [])
275
+ return <div>{time || 'Loading...'}</div>
276
+ }
277
+ ```
278
+
279
+ ## Architecture Decision Framework
280
+
281
+ **Server-Only** (`createServerFn` / `createServerOnlyFn`):
282
+
283
+ - Sensitive data (env vars, secrets)
284
+ - Database connections, file system
285
+ - External API keys
286
+
287
+ **Client-Only** (`createClientOnlyFn` / `<ClientOnly>`):
288
+
289
+ - DOM manipulation, browser APIs
290
+ - localStorage, geolocation
291
+ - Analytics/tracking
292
+
293
+ **Isomorphic** (default / `createIsomorphicFn`):
294
+
295
+ - Data formatting, business logic
296
+ - Shared utilities
297
+ - Route loaders (they're isomorphic by nature)
298
+
299
+ ## Cross-References
300
+
301
+ - [start-core/server-functions](../server-functions/SKILL.md) — the primary server boundary
302
+ - [start-core/deployment](../deployment/SKILL.md) — deployment target affects execution
@@ -0,0 +1,365 @@
1
+ ---
2
+ name: start-core/middleware
3
+ description: >-
4
+ createMiddleware, request middleware (.server only), server function
5
+ middleware (.client + .server), context passing via next({ context }),
6
+ sendContext for client-server transfer, global middleware via
7
+ createStart in src/start.ts, middleware factories, method order
8
+ enforcement, fetch override precedence.
9
+ type: sub-skill
10
+ library: tanstack-start
11
+ library_version: '1.166.2'
12
+ requires:
13
+ - start-core
14
+ - start-core/server-functions
15
+ sources:
16
+ - TanStack/router:docs/start/framework/react/guide/middleware.md
17
+ ---
18
+
19
+ # Middleware
20
+
21
+ Middleware customizes the behavior of server functions and server routes. It is composable — middleware can depend on other middleware to form a chain.
22
+
23
+ > **CRITICAL**: TypeScript enforces method order: `middleware()` → `inputValidator()` → `client()` → `server()`. Wrong order causes type errors.
24
+ > **CRITICAL**: Client context sent via `sendContext` is NOT validated by default. If you send dynamic user-generated data, validate it in server-side middleware before use.
25
+
26
+ ## Two Types of Middleware
27
+
28
+ | Feature | Request Middleware | Server Function Middleware |
29
+ | ----------------- | -------------------------------------------- | ---------------------------------------- |
30
+ | Scope | All server requests (SSR, routes, functions) | Server functions only |
31
+ | Methods | `.server()` | `.client()`, `.server()` |
32
+ | Input validation | No | Yes (`.inputValidator()`) |
33
+ | Client-side logic | No | Yes |
34
+ | Created with | `createMiddleware()` | `createMiddleware({ type: 'function' })` |
35
+
36
+ Request middleware cannot depend on server function middleware. Server function middleware can depend on both types.
37
+
38
+ ## Request Middleware
39
+
40
+ Runs on ALL server requests (SSR, server routes, server functions):
41
+
42
+ ```tsx
43
+ // Use @tanstack/<framework>-start for your framework (react, solid, vue)
44
+ import { createMiddleware } from '@tanstack/react-start'
45
+
46
+ const loggingMiddleware = createMiddleware().server(
47
+ async ({ next, context, request }) => {
48
+ console.log('Request:', request.url)
49
+ const result = await next()
50
+ return result
51
+ },
52
+ )
53
+ ```
54
+
55
+ ## Server Function Middleware
56
+
57
+ Has both client and server phases:
58
+
59
+ ```tsx
60
+ // Use @tanstack/<framework>-start for your framework (react, solid, vue)
61
+ import { createMiddleware } from '@tanstack/react-start'
62
+
63
+ const authMiddleware = createMiddleware({ type: 'function' })
64
+ .client(async ({ next }) => {
65
+ // Runs on client BEFORE the RPC call
66
+ const result = await next()
67
+ // Runs on client AFTER the RPC response
68
+ return result
69
+ })
70
+ .server(async ({ next, context }) => {
71
+ // Runs on server BEFORE the handler
72
+ const result = await next()
73
+ // Runs on server AFTER the handler
74
+ return result
75
+ })
76
+ ```
77
+
78
+ ## Attaching Middleware to Server Functions
79
+
80
+ ```tsx
81
+ // Use @tanstack/<framework>-start for your framework (react, solid, vue)
82
+ import { createServerFn } from '@tanstack/react-start'
83
+
84
+ const fn = createServerFn()
85
+ .middleware([authMiddleware])
86
+ .handler(async ({ context }) => {
87
+ // context contains data from middleware
88
+ return { user: context.user }
89
+ })
90
+ ```
91
+
92
+ ## Context Passing via next()
93
+
94
+ Pass context down the middleware chain:
95
+
96
+ ```tsx
97
+ const authMiddleware = createMiddleware().server(async ({ next, request }) => {
98
+ const session = await getSession(request.headers)
99
+ if (!session) throw new Error('Unauthorized')
100
+
101
+ return next({
102
+ context: { session },
103
+ })
104
+ })
105
+
106
+ const roleMiddleware = createMiddleware()
107
+ .middleware([authMiddleware])
108
+ .server(async ({ next, context }) => {
109
+ console.log('Session:', context.session) // typed!
110
+ return next()
111
+ })
112
+ ```
113
+
114
+ ## Sending Context Between Client and Server
115
+
116
+ ### Client → Server (sendContext)
117
+
118
+ ```tsx
119
+ const workspaceMiddleware = createMiddleware({ type: 'function' })
120
+ .client(async ({ next, context }) => {
121
+ return next({
122
+ sendContext: {
123
+ workspaceId: context.workspaceId,
124
+ },
125
+ })
126
+ })
127
+ .server(async ({ next, context }) => {
128
+ // workspaceId available here, but VALIDATE IT
129
+ console.log('Workspace:', context.workspaceId)
130
+ return next()
131
+ })
132
+ ```
133
+
134
+ ### Server → Client (sendContext in server)
135
+
136
+ ```tsx
137
+ const serverTimer = createMiddleware({ type: 'function' }).server(
138
+ async ({ next }) => {
139
+ return next({
140
+ sendContext: {
141
+ timeFromServer: new Date(),
142
+ },
143
+ })
144
+ },
145
+ )
146
+
147
+ const clientLogger = createMiddleware({ type: 'function' })
148
+ .middleware([serverTimer])
149
+ .client(async ({ next }) => {
150
+ const result = await next()
151
+ console.log('Server time:', result.context.timeFromServer)
152
+ return result
153
+ })
154
+ ```
155
+
156
+ ## Input Validation in Middleware
157
+
158
+ ```tsx
159
+ import { z } from 'zod'
160
+ import { zodValidator } from '@tanstack/zod-adapter'
161
+
162
+ const workspaceMiddleware = createMiddleware({ type: 'function' })
163
+ .inputValidator(zodValidator(z.object({ workspaceId: z.string() })))
164
+ .server(async ({ next, data }) => {
165
+ console.log('Workspace:', data.workspaceId)
166
+ return next()
167
+ })
168
+ ```
169
+
170
+ ## Global Middleware
171
+
172
+ Create `src/start.ts` to configure global middleware:
173
+
174
+ ```tsx
175
+ // src/start.ts
176
+ // Use @tanstack/<framework>-start for your framework (react, solid, vue)
177
+ import { createStart, createMiddleware } from '@tanstack/react-start'
178
+
179
+ const requestLogger = createMiddleware().server(async ({ next, request }) => {
180
+ console.log(`${request.method} ${request.url}`)
181
+ return next()
182
+ })
183
+
184
+ const functionAuth = createMiddleware({ type: 'function' }).server(
185
+ async ({ next }) => {
186
+ // runs for every server function
187
+ return next()
188
+ },
189
+ )
190
+
191
+ export const startInstance = createStart(() => ({
192
+ requestMiddleware: [requestLogger],
193
+ functionMiddleware: [functionAuth],
194
+ }))
195
+ ```
196
+
197
+ ## Using Middleware with Server Routes
198
+
199
+ ### All handlers in a route
200
+
201
+ ```tsx
202
+ export const Route = createFileRoute('/api/users')({
203
+ server: {
204
+ middleware: [authMiddleware],
205
+ handlers: {
206
+ GET: async ({ context }) => Response.json(context.user),
207
+ POST: async ({ request }) => {
208
+ /* ... */
209
+ },
210
+ },
211
+ },
212
+ })
213
+ ```
214
+
215
+ ### Specific handlers only
216
+
217
+ ```tsx
218
+ export const Route = createFileRoute('/api/users')({
219
+ server: {
220
+ handlers: ({ createHandlers }) =>
221
+ createHandlers({
222
+ GET: async () => Response.json({ public: true }),
223
+ POST: {
224
+ middleware: [authMiddleware],
225
+ handler: async ({ context }) => {
226
+ return Response.json({ user: context.session.user })
227
+ },
228
+ },
229
+ }),
230
+ },
231
+ })
232
+ ```
233
+
234
+ ## Middleware Factories
235
+
236
+ Create parameterized middleware for reusable patterns like authorization:
237
+
238
+ ```tsx
239
+ const authMiddleware = createMiddleware().server(async ({ next, request }) => {
240
+ const session = await auth.getSession({ headers: request.headers })
241
+ if (!session) throw new Error('Unauthorized')
242
+ return next({ context: { session } })
243
+ })
244
+
245
+ type Permissions = Record<string, string[]>
246
+
247
+ function authorizationMiddleware(permissions: Permissions) {
248
+ return createMiddleware({ type: 'function' })
249
+ .middleware([authMiddleware])
250
+ .server(async ({ next, context }) => {
251
+ const granted = await auth.hasPermission(context.session, permissions)
252
+ if (!granted) throw new Error('Forbidden')
253
+ return next()
254
+ })
255
+ }
256
+
257
+ // Usage
258
+ const getClients = createServerFn()
259
+ .middleware([authorizationMiddleware({ client: ['read'] })])
260
+ .handler(async () => {
261
+ return { message: 'The user can read clients.' }
262
+ })
263
+ ```
264
+
265
+ ## Custom Headers and Fetch
266
+
267
+ ### Setting headers from client middleware
268
+
269
+ ```tsx
270
+ const authMiddleware = createMiddleware({ type: 'function' }).client(
271
+ async ({ next }) => {
272
+ return next({
273
+ headers: { Authorization: `Bearer ${getToken()}` },
274
+ })
275
+ },
276
+ )
277
+ ```
278
+
279
+ Headers merge across middleware. Later middleware overrides earlier. Call-site headers override all middleware headers.
280
+
281
+ ### Custom fetch
282
+
283
+ ```tsx
284
+ // Use @tanstack/<framework>-start for your framework (react, solid, vue)
285
+ import type { CustomFetch } from '@tanstack/react-start'
286
+
287
+ const loggingMiddleware = createMiddleware({ type: 'function' }).client(
288
+ async ({ next }) => {
289
+ const customFetch: CustomFetch = async (url, init) => {
290
+ console.log('Request:', url)
291
+ return fetch(url, init)
292
+ }
293
+ return next({ fetch: customFetch })
294
+ },
295
+ )
296
+ ```
297
+
298
+ Fetch precedence (highest to lowest): call site → later middleware → earlier middleware → createStart global → default fetch.
299
+
300
+ ## Common Mistakes
301
+
302
+ ### 1. HIGH: Trusting client sendContext without validation
303
+
304
+ ```tsx
305
+ // WRONG — client can send arbitrary data
306
+ .server(async ({ next, context }) => {
307
+ await db.query(`SELECT * FROM workspace_${context.workspaceId}`)
308
+ return next()
309
+ })
310
+
311
+ // CORRECT — validate before use
312
+ .server(async ({ next, context }) => {
313
+ const workspaceId = z.string().uuid().parse(context.workspaceId)
314
+ await db.query('SELECT * FROM workspaces WHERE id = $1', [workspaceId])
315
+ return next()
316
+ })
317
+ ```
318
+
319
+ ### 2. MEDIUM: Confusing request vs server function middleware
320
+
321
+ Request middleware runs on ALL requests (SSR, routes, functions). Server function middleware runs only for `createServerFn` calls and has `.client()` method.
322
+
323
+ ### 3. HIGH: Browser APIs in .client() crash during SSR
324
+
325
+ During SSR, `.client()` callbacks run on the server. Browser-only APIs like `localStorage` or `window` will throw `ReferenceError`:
326
+
327
+ ```tsx
328
+ // WRONG — localStorage doesn't exist on the server during SSR
329
+ const middleware = createMiddleware({ type: 'function' }).client(
330
+ async ({ next }) => {
331
+ const token = localStorage.getItem('token')
332
+ return next({ sendContext: { token } })
333
+ },
334
+ )
335
+
336
+ // CORRECT — use cookies/headers or guard with typeof window check
337
+ const middleware = createMiddleware({ type: 'function' }).client(
338
+ async ({ next }) => {
339
+ const token =
340
+ typeof window !== 'undefined' ? localStorage.getItem('token') : null
341
+ return next({ sendContext: { token } })
342
+ },
343
+ )
344
+ ```
345
+
346
+ ### 4. MEDIUM: Wrong method order
347
+
348
+ ```tsx
349
+ // WRONG — type error
350
+ createMiddleware({ type: 'function' })
351
+ .server(() => { ... })
352
+ .client(() => { ... })
353
+
354
+ // CORRECT — middleware → inputValidator → client → server
355
+ createMiddleware({ type: 'function' })
356
+ .middleware([dep])
357
+ .inputValidator(schema)
358
+ .client(({ next }) => next())
359
+ .server(({ next }) => next())
360
+ ```
361
+
362
+ ## Cross-References
363
+
364
+ - [start-core/server-functions](../server-functions/SKILL.md) — what middleware wraps
365
+ - [start-core/server-routes](../server-routes/SKILL.md) — middleware on API endpoints