@tanstack/start-client-core 1.166.11 → 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.
- package/bin/intent.js +25 -0
- package/package.json +12 -5
- package/skills/start-core/SKILL.md +210 -0
- package/skills/start-core/deployment/SKILL.md +306 -0
- package/skills/start-core/execution-model/SKILL.md +302 -0
- package/skills/start-core/middleware/SKILL.md +365 -0
- package/skills/start-core/server-functions/SKILL.md +335 -0
- package/skills/start-core/server-routes/SKILL.md +280 -0
|
@@ -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
|