@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,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 '@benjavicente/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 '@benjavicente/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 '@benjavicente/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 '@benjavicente/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
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createSerializationAdapter } from '@benjavicente/router-core'
|
|
2
|
+
import { TSS_SERVER_FUNCTION } from '../constants'
|
|
3
|
+
import { createClientRpc } from '../client-rpc/createClientRpc'
|
|
4
|
+
|
|
5
|
+
export const ServerFunctionSerializationAdapter = createSerializationAdapter({
|
|
6
|
+
key: '$TSS/serverfn',
|
|
7
|
+
test: (v): v is { serverFnMeta: { id: string } } => {
|
|
8
|
+
if (typeof v !== 'function') return false
|
|
9
|
+
|
|
10
|
+
if (!(TSS_SERVER_FUNCTION in v)) return false
|
|
11
|
+
|
|
12
|
+
return !!v[TSS_SERVER_FUNCTION]
|
|
13
|
+
},
|
|
14
|
+
toSerializable: ({ serverFnMeta }) => ({ functionId: serverFnMeta.id }),
|
|
15
|
+
fromSerializable: ({ functionId }) => createClientRpc(functionId),
|
|
16
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { hydrate } from '@benjavicente/router-core/ssr/client'
|
|
2
|
+
|
|
3
|
+
import { ServerFunctionSerializationAdapter } from './ServerFunctionSerializationAdapter'
|
|
4
|
+
import type { AnyStartInstanceOptions } from '../createStart'
|
|
5
|
+
import type { AnyRouter, AnySerializationAdapter } from '@benjavicente/router-core'
|
|
6
|
+
// eslint-disable-next-line import/no-duplicates,import/order
|
|
7
|
+
import { getRouter } from '#tanstack-router-entry'
|
|
8
|
+
// eslint-disable-next-line import/no-duplicates,import/order
|
|
9
|
+
import { startInstance } from '#tanstack-start-entry'
|
|
10
|
+
|
|
11
|
+
export async function hydrateStart(): Promise<AnyRouter> {
|
|
12
|
+
const router = await getRouter()
|
|
13
|
+
|
|
14
|
+
let serializationAdapters: Array<AnySerializationAdapter>
|
|
15
|
+
if (startInstance) {
|
|
16
|
+
const startOptions = await startInstance.getOptions()
|
|
17
|
+
startOptions.serializationAdapters =
|
|
18
|
+
startOptions.serializationAdapters ?? []
|
|
19
|
+
window.__TSS_START_OPTIONS__ = startOptions as AnyStartInstanceOptions
|
|
20
|
+
serializationAdapters = startOptions.serializationAdapters
|
|
21
|
+
router.options.defaultSsr = startOptions.defaultSsr
|
|
22
|
+
} else {
|
|
23
|
+
serializationAdapters = []
|
|
24
|
+
window.__TSS_START_OPTIONS__ = {
|
|
25
|
+
serializationAdapters,
|
|
26
|
+
} as AnyStartInstanceOptions
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
serializationAdapters.push(ServerFunctionSerializationAdapter)
|
|
30
|
+
if (router.options.serializationAdapters) {
|
|
31
|
+
serializationAdapters.push(...router.options.serializationAdapters)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
router.update({
|
|
35
|
+
basepath: process.env.TSS_ROUTER_BASEPATH,
|
|
36
|
+
...{ serializationAdapters },
|
|
37
|
+
})
|
|
38
|
+
if (!router.stores.matchesId.state.length) {
|
|
39
|
+
await hydrate(router)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return router
|
|
43
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { TSS_SERVER_FUNCTION } from '../constants'
|
|
2
|
+
import { getStartOptions } from '../getStartOptions'
|
|
3
|
+
import { serverFnFetcher } from './serverFnFetcher'
|
|
4
|
+
import type { ClientFnMeta } from '../constants'
|
|
5
|
+
|
|
6
|
+
export function createClientRpc(functionId: string) {
|
|
7
|
+
const url = process.env.TSS_SERVER_FN_BASE + functionId
|
|
8
|
+
const serverFnMeta: ClientFnMeta = { id: functionId }
|
|
9
|
+
|
|
10
|
+
const clientFn = (...args: Array<any>) => {
|
|
11
|
+
const startFetch = getStartOptions()?.serverFns?.fetch
|
|
12
|
+
return serverFnFetcher(url, args, startFetch ?? fetch)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return Object.assign(clientFn, {
|
|
16
|
+
url,
|
|
17
|
+
serverFnMeta,
|
|
18
|
+
[TSS_SERVER_FUNCTION]: true,
|
|
19
|
+
})
|
|
20
|
+
}
|