@benjavicente/start-client-core 1.167.9
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/LICENSE +21 -0
- package/README.md +12 -0
- package/bin/intent.js +25 -0
- package/dist/esm/client/ServerFunctionSerializationAdapter.d.ts +7 -0
- package/dist/esm/client/ServerFunctionSerializationAdapter.js +18 -0
- package/dist/esm/client/ServerFunctionSerializationAdapter.js.map +1 -0
- package/dist/esm/client/hydrateStart.d.ts +2 -0
- package/dist/esm/client/hydrateStart.js +31 -0
- package/dist/esm/client/hydrateStart.js.map +1 -0
- package/dist/esm/client/index.d.ts +2 -0
- package/dist/esm/client/index.js +2 -0
- package/dist/esm/client-rpc/createClientRpc.d.ts +6 -0
- package/dist/esm/client-rpc/createClientRpc.js +21 -0
- package/dist/esm/client-rpc/createClientRpc.js.map +1 -0
- package/dist/esm/client-rpc/frame-decoder.d.ts +23 -0
- package/dist/esm/client-rpc/frame-decoder.js +231 -0
- package/dist/esm/client-rpc/frame-decoder.js.map +1 -0
- package/dist/esm/client-rpc/index.d.ts +1 -0
- package/dist/esm/client-rpc/index.js +2 -0
- package/dist/esm/client-rpc/serverFnFetcher.d.ts +1 -0
- package/dist/esm/client-rpc/serverFnFetcher.js +231 -0
- package/dist/esm/client-rpc/serverFnFetcher.js.map +1 -0
- package/dist/esm/constants.d.ts +53 -0
- package/dist/esm/constants.js +46 -0
- package/dist/esm/constants.js.map +1 -0
- package/dist/esm/createMiddleware.d.ts +195 -0
- package/dist/esm/createMiddleware.js +26 -0
- package/dist/esm/createMiddleware.js.map +1 -0
- package/dist/esm/createServerFn.d.ts +131 -0
- package/dist/esm/createServerFn.js +200 -0
- package/dist/esm/createServerFn.js.map +1 -0
- package/dist/esm/createStart.d.ts +50 -0
- package/dist/esm/createStart.js +29 -0
- package/dist/esm/createStart.js.map +1 -0
- package/dist/esm/fake-start-entry.d.ts +2 -0
- package/dist/esm/fake-start-entry.js +7 -0
- package/dist/esm/fake-start-entry.js.map +1 -0
- package/dist/esm/getDefaultSerovalPlugins.d.ts +1 -0
- package/dist/esm/getDefaultSerovalPlugins.js +10 -0
- package/dist/esm/getDefaultSerovalPlugins.js.map +1 -0
- package/dist/esm/getGlobalStartContext.d.ts +3 -0
- package/dist/esm/getGlobalStartContext.js +12 -0
- package/dist/esm/getGlobalStartContext.js.map +1 -0
- package/dist/esm/getRouterInstance.d.ts +2 -0
- package/dist/esm/getRouterInstance.js +8 -0
- package/dist/esm/getRouterInstance.js.map +1 -0
- package/dist/esm/getStartContextServerOnly.d.ts +2 -0
- package/dist/esm/getStartContextServerOnly.js +8 -0
- package/dist/esm/getStartContextServerOnly.js.map +1 -0
- package/dist/esm/getStartOptions.d.ts +2 -0
- package/dist/esm/getStartOptions.js +8 -0
- package/dist/esm/getStartOptions.js.map +1 -0
- package/dist/esm/global.d.ts +7 -0
- package/dist/esm/index.d.ts +20 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/safeObjectMerge.d.ts +10 -0
- package/dist/esm/safeObjectMerge.js +30 -0
- package/dist/esm/safeObjectMerge.js.map +1 -0
- package/dist/esm/serverRoute.d.ts +65 -0
- package/dist/esm/startEntry.d.ts +8 -0
- package/dist/esm/tests/createServerFn.test-d.d.ts +1 -0
- package/dist/esm/tests/createServerMiddleware.test-d.d.ts +1 -0
- package/package.json +98 -0
- 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
- package/src/client/ServerFunctionSerializationAdapter.ts +16 -0
- package/src/client/hydrateStart.ts +43 -0
- package/src/client/index.ts +2 -0
- package/src/client-rpc/createClientRpc.ts +20 -0
- package/src/client-rpc/frame-decoder.ts +389 -0
- package/src/client-rpc/index.ts +1 -0
- package/src/client-rpc/serverFnFetcher.ts +416 -0
- package/src/constants.ts +90 -0
- package/src/createMiddleware.ts +824 -0
- package/src/createServerFn.ts +813 -0
- package/src/createStart.ts +166 -0
- package/src/fake-start-entry.ts +2 -0
- package/src/getDefaultSerovalPlugins.ts +17 -0
- package/src/getGlobalStartContext.ts +18 -0
- package/src/getRouterInstance.ts +8 -0
- package/src/getStartContextServerOnly.ts +4 -0
- package/src/getStartOptions.ts +8 -0
- package/src/global.ts +9 -0
- package/src/index.tsx +119 -0
- package/src/safeObjectMerge.ts +38 -0
- package/src/serverRoute.ts +509 -0
- package/src/start-entry.d.ts +11 -0
- package/src/startEntry.ts +10 -0
- package/src/tests/createServerFn.test-d.ts +866 -0
- package/src/tests/createServerMiddleware.test-d.ts +810 -0
|
@@ -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 '@benjavicente/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 '@benjavicente/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 '@benjavicente/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 '@benjavicente/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 '@benjavicente/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 '@benjavicente/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
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: start-core/server-functions
|
|
3
|
+
description: >-
|
|
4
|
+
createServerFn (GET/POST), inputValidator (Zod or function),
|
|
5
|
+
useServerFn hook, server context utilities (getRequest,
|
|
6
|
+
getRequestHeader, setResponseHeader, setResponseStatus), error
|
|
7
|
+
handling (throw errors, redirect, notFound), streaming, FormData
|
|
8
|
+
handling, file organization (.functions.ts, .server.ts).
|
|
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/server-functions.md
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Server Functions
|
|
19
|
+
|
|
20
|
+
Server functions are type-safe RPCs created with `createServerFn`. They run exclusively on the server but can be called from anywhere — loaders, components, hooks, event handlers, or other server functions.
|
|
21
|
+
|
|
22
|
+
> **CRITICAL**: Loaders are ISOMORPHIC — they run on BOTH client and server. Database queries, file system access, and secret API keys MUST go inside `createServerFn`, NOT in loaders directly.
|
|
23
|
+
> **CRITICAL**: Do not use `"use server"` directives, `getServerSideProps`, or any Next.js/Remix server patterns. TanStack Start uses `createServerFn` exclusively.
|
|
24
|
+
|
|
25
|
+
## Basic Usage
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { createServerFn } from '@benjavicente/react-start'
|
|
29
|
+
|
|
30
|
+
// GET (default)
|
|
31
|
+
const getData = createServerFn().handler(async () => {
|
|
32
|
+
return { message: 'Hello from server!' }
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// POST
|
|
36
|
+
const saveData = createServerFn({ method: 'POST' }).handler(async () => {
|
|
37
|
+
return { success: true }
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Calling from Loaders
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import { createFileRoute } from '@benjavicente/react-router'
|
|
45
|
+
import { createServerFn } from '@benjavicente/react-start'
|
|
46
|
+
|
|
47
|
+
const getPosts = createServerFn({ method: 'GET' }).handler(async () => {
|
|
48
|
+
const posts = await db.query('SELECT * FROM posts')
|
|
49
|
+
return { posts }
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
export const Route = createFileRoute('/posts')({
|
|
53
|
+
loader: () => getPosts(),
|
|
54
|
+
component: PostList,
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
function PostList() {
|
|
58
|
+
const { posts } = Route.useLoaderData()
|
|
59
|
+
return (
|
|
60
|
+
<ul>
|
|
61
|
+
{posts.map((p) => (
|
|
62
|
+
<li key={p.id}>{p.title}</li>
|
|
63
|
+
))}
|
|
64
|
+
</ul>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Calling from Components
|
|
70
|
+
|
|
71
|
+
Use the `useServerFn` hook to call server functions from event handlers:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
import { useServerFn } from '@benjavicente/react-start'
|
|
75
|
+
|
|
76
|
+
const deletePost = createServerFn({ method: 'POST' })
|
|
77
|
+
.inputValidator((data: { id: string }) => data)
|
|
78
|
+
.handler(async ({ data }) => {
|
|
79
|
+
await db.delete('posts').where({ id: data.id })
|
|
80
|
+
return { success: true }
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
function DeleteButton({ postId }: { postId: string }) {
|
|
84
|
+
const deletePostFn = useServerFn(deletePost)
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<button onClick={() => deletePostFn({ data: { id: postId } })}>
|
|
88
|
+
Delete
|
|
89
|
+
</button>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Input Validation
|
|
95
|
+
|
|
96
|
+
### Basic Validator
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
const greetUser = createServerFn({ method: 'GET' })
|
|
100
|
+
.inputValidator((data: { name: string }) => data)
|
|
101
|
+
.handler(async ({ data }) => {
|
|
102
|
+
return `Hello, ${data.name}!`
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
await greetUser({ data: { name: 'John' } })
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Zod Validator
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
import { z } from 'zod'
|
|
112
|
+
|
|
113
|
+
const createUser = createServerFn({ method: 'POST' })
|
|
114
|
+
.inputValidator(
|
|
115
|
+
z.object({
|
|
116
|
+
name: z.string().min(1),
|
|
117
|
+
age: z.number().min(0),
|
|
118
|
+
}),
|
|
119
|
+
)
|
|
120
|
+
.handler(async ({ data }) => {
|
|
121
|
+
return `Created user: ${data.name}, age ${data.age}`
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### FormData
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
const submitForm = createServerFn({ method: 'POST' })
|
|
129
|
+
.inputValidator((data) => {
|
|
130
|
+
if (!(data instanceof FormData)) {
|
|
131
|
+
throw new Error('Expected FormData')
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
name: data.get('name')?.toString() || '',
|
|
135
|
+
email: data.get('email')?.toString() || '',
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
.handler(async ({ data }) => {
|
|
139
|
+
return { success: true }
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Error Handling
|
|
144
|
+
|
|
145
|
+
### Errors
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
const riskyFunction = createServerFn().handler(async () => {
|
|
149
|
+
throw new Error('Something went wrong!')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
await riskyFunction()
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.log(error.message) // "Something went wrong!"
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Redirects
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
import { redirect } from '@benjavicente/react-router'
|
|
163
|
+
|
|
164
|
+
const requireAuth = createServerFn().handler(async () => {
|
|
165
|
+
const user = await getCurrentUser()
|
|
166
|
+
if (!user) {
|
|
167
|
+
throw redirect({ to: '/login' })
|
|
168
|
+
}
|
|
169
|
+
return user
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Not Found
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
import { notFound } from '@benjavicente/react-router'
|
|
177
|
+
|
|
178
|
+
const getPost = createServerFn()
|
|
179
|
+
.inputValidator((data: { id: string }) => data)
|
|
180
|
+
.handler(async ({ data }) => {
|
|
181
|
+
const post = await db.findPost(data.id)
|
|
182
|
+
if (!post) {
|
|
183
|
+
throw notFound()
|
|
184
|
+
}
|
|
185
|
+
return post
|
|
186
|
+
})
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Server Context Utilities
|
|
190
|
+
|
|
191
|
+
Access request/response details inside server function handlers:
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
import { createServerFn } from '@benjavicente/react-start'
|
|
195
|
+
import {
|
|
196
|
+
getRequest,
|
|
197
|
+
getRequestHeader,
|
|
198
|
+
setResponseHeaders,
|
|
199
|
+
setResponseStatus,
|
|
200
|
+
} from '@benjavicente/react-start/server'
|
|
201
|
+
|
|
202
|
+
const getCachedData = createServerFn({ method: 'GET' }).handler(async () => {
|
|
203
|
+
const request = getRequest()
|
|
204
|
+
const authHeader = getRequestHeader('Authorization')
|
|
205
|
+
|
|
206
|
+
setResponseHeaders({
|
|
207
|
+
'Cache-Control': 'public, max-age=300',
|
|
208
|
+
})
|
|
209
|
+
setResponseStatus(200)
|
|
210
|
+
|
|
211
|
+
return fetchData()
|
|
212
|
+
})
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Available utilities:
|
|
216
|
+
|
|
217
|
+
- `getRequest()` — full Request object
|
|
218
|
+
- `getRequestHeader(name)` — single request header
|
|
219
|
+
- `setResponseHeader(name, value)` — single response header
|
|
220
|
+
- `setResponseHeaders(headers)` — multiple response headers
|
|
221
|
+
- `setResponseStatus(code)` — HTTP status code
|
|
222
|
+
|
|
223
|
+
## File Organization
|
|
224
|
+
|
|
225
|
+
```text
|
|
226
|
+
src/utils/
|
|
227
|
+
├── users.functions.ts # createServerFn wrappers (safe to import anywhere)
|
|
228
|
+
├── users.server.ts # Server-only helpers (DB queries, internal logic)
|
|
229
|
+
└── schemas.ts # Shared validation schemas (client-safe)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
// users.server.ts — server-only helpers
|
|
234
|
+
import { db } from '~/db'
|
|
235
|
+
|
|
236
|
+
export async function findUserById(id: string) {
|
|
237
|
+
return db.query.users.findFirst({ where: eq(users.id, id) })
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
```tsx
|
|
242
|
+
// users.functions.ts — server functions
|
|
243
|
+
import { createServerFn } from '@benjavicente/react-start'
|
|
244
|
+
import { findUserById } from './users.server'
|
|
245
|
+
|
|
246
|
+
export const getUser = createServerFn({ method: 'GET' })
|
|
247
|
+
.inputValidator((data: { id: string }) => data)
|
|
248
|
+
.handler(async ({ data }) => {
|
|
249
|
+
return findUserById(data.id)
|
|
250
|
+
})
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Static imports of server functions are safe — the build replaces implementations with RPC stubs in client bundles.
|
|
254
|
+
|
|
255
|
+
## Common Mistakes
|
|
256
|
+
|
|
257
|
+
### 1. CRITICAL: Putting server-only code in loaders
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
// WRONG — loader is ISOMORPHIC, runs on BOTH client and server
|
|
261
|
+
export const Route = createFileRoute('/posts')({
|
|
262
|
+
loader: async () => {
|
|
263
|
+
const posts = await db.query('SELECT * FROM posts')
|
|
264
|
+
return { posts }
|
|
265
|
+
},
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
// CORRECT — use createServerFn for server-only logic
|
|
269
|
+
const getPosts = createServerFn({ method: 'GET' }).handler(async () => {
|
|
270
|
+
const posts = await db.query('SELECT * FROM posts')
|
|
271
|
+
return { posts }
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
export const Route = createFileRoute('/posts')({
|
|
275
|
+
loader: () => getPosts(),
|
|
276
|
+
})
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### 2. CRITICAL: Using Next.js/Remix server patterns
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
// WRONG — "use server" is a React directive, not used in TanStack Start
|
|
283
|
+
'use server'
|
|
284
|
+
export async function getUser() { ... }
|
|
285
|
+
|
|
286
|
+
// WRONG — getServerSideProps is Next.js
|
|
287
|
+
export async function getServerSideProps() { ... }
|
|
288
|
+
|
|
289
|
+
// CORRECT — TanStack Start uses createServerFn
|
|
290
|
+
const getUser = createServerFn({ method: 'GET' })
|
|
291
|
+
.handler(async () => { ... })
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### 3. HIGH: Dynamic imports for server functions
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
// WRONG — can cause bundler issues
|
|
298
|
+
const { getUser } = await import('~/utils/users.functions')
|
|
299
|
+
|
|
300
|
+
// CORRECT — static imports are safe, build handles environment shaking
|
|
301
|
+
import { getUser } from '~/utils/users.functions'
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### 4. HIGH: Awaiting server function without calling it
|
|
305
|
+
|
|
306
|
+
`createServerFn` returns a function — it must be invoked with `()`:
|
|
307
|
+
|
|
308
|
+
```tsx
|
|
309
|
+
// WRONG — getItems is a function, not a Promise
|
|
310
|
+
const data = await getItems
|
|
311
|
+
|
|
312
|
+
// CORRECT — call the function
|
|
313
|
+
const data = await getItems()
|
|
314
|
+
|
|
315
|
+
// With validated input
|
|
316
|
+
const data = await getItems({ data: { id: '1' } })
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### 5. MEDIUM: Not using useServerFn for component calls
|
|
320
|
+
|
|
321
|
+
When calling server functions from event handlers in components, use `useServerFn` to get proper React integration:
|
|
322
|
+
|
|
323
|
+
```tsx
|
|
324
|
+
// WRONG — direct call doesn't integrate with React lifecycle
|
|
325
|
+
<button onClick={() => deletePost({ data: { id } })}>Delete</button>
|
|
326
|
+
|
|
327
|
+
// CORRECT — useServerFn integrates with React
|
|
328
|
+
const deletePostFn = useServerFn(deletePost)
|
|
329
|
+
<button onClick={() => deletePostFn({ data: { id } })}>Delete</button>
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Cross-References
|
|
333
|
+
|
|
334
|
+
- [start-core/execution-model](../execution-model/SKILL.md) — understanding where code runs
|
|
335
|
+
- [start-core/middleware](../middleware/SKILL.md) — composing server functions with middleware
|