@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.
@@ -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 '@tanstack/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 '@tanstack/react-router'
45
+ import { createServerFn } from '@tanstack/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 '@tanstack/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 '@tanstack/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 '@tanstack/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 '@tanstack/react-start'
195
+ import {
196
+ getRequest,
197
+ getRequestHeader,
198
+ setResponseHeaders,
199
+ setResponseStatus,
200
+ } from '@tanstack/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 '@tanstack/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
@@ -0,0 +1,280 @@
1
+ ---
2
+ name: start-core/server-routes
3
+ description: >-
4
+ Server-side API endpoints using the server property on
5
+ createFileRoute, HTTP method handlers (GET, POST, PUT, DELETE),
6
+ createHandlers for per-handler middleware, handler context
7
+ (request, params, context), request body parsing, response
8
+ helpers, file naming for API routes.
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-routes.md
16
+ ---
17
+
18
+ # Server Routes
19
+
20
+ Server routes are API endpoints defined alongside app routes in the `src/routes` directory. They use the `server` property on `createFileRoute` and handle raw HTTP requests.
21
+
22
+ ## Basic Server Route
23
+
24
+ ```ts
25
+ // src/routes/api/hello.ts
26
+ import { createFileRoute } from '@tanstack/react-router'
27
+
28
+ export const Route = createFileRoute('/api/hello')({
29
+ server: {
30
+ handlers: {
31
+ GET: async ({ request }) => {
32
+ return new Response('Hello, World!')
33
+ },
34
+ },
35
+ },
36
+ })
37
+ ```
38
+
39
+ ## Combining Server Route and App Route
40
+
41
+ The same file can define both a server route and a UI route:
42
+
43
+ ```tsx
44
+ // src/routes/hello.tsx
45
+ import { createFileRoute } from '@tanstack/react-router'
46
+ import { useState } from 'react'
47
+
48
+ export const Route = createFileRoute('/hello')({
49
+ server: {
50
+ handlers: {
51
+ POST: async ({ request }) => {
52
+ const body = await request.json()
53
+ return Response.json({ message: `Hello, ${body.name}!` })
54
+ },
55
+ },
56
+ },
57
+ component: HelloComponent,
58
+ })
59
+
60
+ function HelloComponent() {
61
+ const [reply, setReply] = useState('')
62
+ return (
63
+ <button
64
+ onClick={() => {
65
+ fetch('/hello', {
66
+ method: 'POST',
67
+ headers: { 'Content-Type': 'application/json' },
68
+ body: JSON.stringify({ name: 'Tanner' }),
69
+ })
70
+ .then((res) => res.json())
71
+ .then((data) => setReply(data.message))
72
+ }}
73
+ >
74
+ Say Hello {reply && `- ${reply}`}
75
+ </button>
76
+ )
77
+ }
78
+ ```
79
+
80
+ ## File Route Conventions
81
+
82
+ Server routes follow TanStack Router file-based routing conventions:
83
+
84
+ | File | Route |
85
+ | --------------------------- | ----------------------------- |
86
+ | `routes/users.ts` | `/users` |
87
+ | `routes/users/$id.ts` | `/users/$id` |
88
+ | `routes/users/$id/posts.ts` | `/users/$id/posts` |
89
+ | `routes/api/file/$.ts` | `/api/file/$` (splat) |
90
+ | `routes/my-script[.]js.ts` | `/my-script.js` (escaped dot) |
91
+
92
+ ## Unique Route Paths
93
+
94
+ Each route can only have a single handler file. These would conflict:
95
+
96
+ - `routes/users.ts`
97
+ - `routes/users.index.ts`
98
+ - `routes/users/index.ts`
99
+
100
+ ## Handler Context
101
+
102
+ Each handler receives:
103
+
104
+ - `request` — the incoming [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object
105
+ - `params` — dynamic path parameters
106
+ - `context` — context from middleware
107
+ - `pathname` — the matched pathname
108
+ - `next` — call to fall through to SSR (returns a `Response`)
109
+
110
+ ## Dynamic Path Params
111
+
112
+ ```ts
113
+ // routes/users/$id.ts
114
+ import { createFileRoute } from '@tanstack/react-router'
115
+
116
+ export const Route = createFileRoute('/users/$id')({
117
+ server: {
118
+ handlers: {
119
+ GET: async ({ params }) => {
120
+ return new Response(`User ID: ${params.id}`)
121
+ },
122
+ },
123
+ },
124
+ })
125
+ ```
126
+
127
+ ## Splat/Wildcard Params
128
+
129
+ ```ts
130
+ // routes/file/$.ts
131
+ import { createFileRoute } from '@tanstack/react-router'
132
+
133
+ export const Route = createFileRoute('/file/$')({
134
+ server: {
135
+ handlers: {
136
+ GET: async ({ params }) => {
137
+ return new Response(`File: ${params._splat}`)
138
+ },
139
+ },
140
+ },
141
+ })
142
+ ```
143
+
144
+ ## Request Body Handling
145
+
146
+ ```ts
147
+ export const Route = createFileRoute('/api/users')({
148
+ server: {
149
+ handlers: {
150
+ POST: async ({ request }) => {
151
+ const body = await request.json()
152
+ return Response.json({ created: body.name })
153
+ },
154
+ },
155
+ },
156
+ })
157
+ ```
158
+
159
+ Other body methods: `request.text()`, `request.formData()`.
160
+
161
+ ## JSON Responses
162
+
163
+ ```ts
164
+ // Using Response.json helper
165
+ handlers: {
166
+ GET: async () => {
167
+ return Response.json({ message: 'Hello!' })
168
+ },
169
+ }
170
+ ```
171
+
172
+ ## Status Codes and Headers
173
+
174
+ ```ts
175
+ handlers: {
176
+ GET: async ({ params }) => {
177
+ const user = await findUser(params.id)
178
+ if (!user) {
179
+ return new Response('Not found', { status: 404 })
180
+ }
181
+ return Response.json(user)
182
+ },
183
+ }
184
+ ```
185
+
186
+ ```ts
187
+ handlers: {
188
+ GET: async () => {
189
+ return new Response('Hello', {
190
+ headers: { 'Content-Type': 'text/plain' },
191
+ })
192
+ },
193
+ }
194
+ ```
195
+
196
+ ## Middleware on Server Routes
197
+
198
+ ### All handlers
199
+
200
+ ```tsx
201
+ export const Route = createFileRoute('/api/admin')({
202
+ server: {
203
+ middleware: [authMiddleware, loggerMiddleware],
204
+ handlers: {
205
+ GET: async ({ context }) => Response.json(context.user),
206
+ POST: async ({ request, context }) => {
207
+ /* ... */
208
+ },
209
+ },
210
+ },
211
+ })
212
+ ```
213
+
214
+ ### Specific handlers with createHandlers
215
+
216
+ ```tsx
217
+ export const Route = createFileRoute('/api/data')({
218
+ server: {
219
+ handlers: ({ createHandlers }) =>
220
+ createHandlers({
221
+ GET: async () => Response.json({ public: true }),
222
+ POST: {
223
+ middleware: [authMiddleware],
224
+ handler: async ({ context }) => {
225
+ return Response.json({ user: context.session.user })
226
+ },
227
+ },
228
+ }),
229
+ },
230
+ })
231
+ ```
232
+
233
+ ### Combined route-level and handler-specific
234
+
235
+ ```tsx
236
+ export const Route = createFileRoute('/api/posts')({
237
+ server: {
238
+ middleware: [authMiddleware], // runs first for all
239
+ handlers: ({ createHandlers }) =>
240
+ createHandlers({
241
+ GET: async () => Response.json([]),
242
+ POST: {
243
+ middleware: [validationMiddleware], // runs after auth, POST only
244
+ handler: async ({ request }) => {
245
+ const body = await request.json()
246
+ return Response.json({ created: true })
247
+ },
248
+ },
249
+ }),
250
+ },
251
+ })
252
+ ```
253
+
254
+ ## Common Mistakes
255
+
256
+ ### 1. MEDIUM: Duplicate route paths
257
+
258
+ ```text
259
+ # WRONG — both resolve to /users, causes error
260
+ routes/users.ts
261
+ routes/users/index.ts
262
+
263
+ # CORRECT — pick one
264
+ routes/users.ts
265
+ ```
266
+
267
+ ### 2. MEDIUM: Forgetting to await request body methods
268
+
269
+ ```ts
270
+ // WRONG — body is a Promise, not the actual data
271
+ const body = request.json()
272
+
273
+ // CORRECT — await the promise
274
+ const body = await request.json()
275
+ ```
276
+
277
+ ## Cross-References
278
+
279
+ - [start-core/middleware](../middleware/SKILL.md) — middleware for server routes
280
+ - [start-core/server-functions](../server-functions/SKILL.md) — alternative for RPC-style calls