@tanstack/start-client-core 1.166.11 → 1.166.13
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,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
|