kaddidlehopper 0.1.0
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/CONTEXT.md +139 -0
- package/README.md +47 -0
- package/add-ons/ai/README.md +34 -0
- package/add-ons/ai/assets/_dot_env.local.append +13 -0
- package/add-ons/ai/assets/src/components/AIAssistant.tsx +149 -0
- package/add-ons/ai/assets/src/lib/ai-hook.ts +21 -0
- package/add-ons/ai/assets/src/lib/weather-tools.ts +30 -0
- package/add-ons/ai/assets/src/routes/api.chat.ts +94 -0
- package/add-ons/ai/assets/src/routes/chat.css +175 -0
- package/add-ons/ai/assets/src/routes/chat.tsx +141 -0
- package/add-ons/ai/info.json +27 -0
- package/add-ons/ai/package.json +17 -0
- package/add-ons/ai/small-logo.svg +8 -0
- package/dist/cli.js +251 -0
- package/dist/index.js +33 -0
- package/dist/types/cli.d.ts +8 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/types.d.ts +14 -0
- package/dist/types.js +1 -0
- package/examples/blog/README.md +60 -0
- package/examples/blog/assets/content/posts/beach.md +12 -0
- package/examples/blog/assets/content/posts/jungle.md.ejs +12 -0
- package/examples/blog/assets/content/posts/mountains.md.ejs +12 -0
- package/examples/blog/assets/content/posts/snorkeling.md.ejs +12 -0
- package/examples/blog/assets/content/posts/waterfall.md.ejs +12 -0
- package/examples/blog/assets/content-collections.ts +30 -0
- package/examples/blog/assets/public/beach.jpg +0 -0
- package/examples/blog/assets/public/jungle.jpg +0 -0
- package/examples/blog/assets/public/mountains.jpg +0 -0
- package/examples/blog/assets/public/snorkeling.jpg +0 -0
- package/examples/blog/assets/public/waterfall.jpg +0 -0
- package/examples/blog/assets/src/components/Header.tsx +52 -0
- package/examples/blog/assets/src/components/VacayAssistant.tsx +205 -0
- package/examples/blog/assets/src/components/blog-posts.tsx +78 -0
- package/examples/blog/assets/src/components/ui/card.tsx +92 -0
- package/examples/blog/assets/src/lib/blog-ai-hook.ts +25 -0
- package/examples/blog/assets/src/lib/blog-tools.ts +111 -0
- package/examples/blog/assets/src/lib/utils.ts +6 -0
- package/examples/blog/assets/src/routes/__root.tsx +57 -0
- package/examples/blog/assets/src/routes/api.blog-chat.ts +117 -0
- package/examples/blog/assets/src/routes/category.$category.tsx +19 -0
- package/examples/blog/assets/src/routes/index.tsx +19 -0
- package/examples/blog/assets/src/routes/posts.$slug.tsx +63 -0
- package/examples/blog/assets/src/styles.css +138 -0
- package/examples/blog/info.json +43 -0
- package/examples/blog/package.json +23 -0
- package/examples/events/README.md +110 -0
- package/examples/events/assets/content/speakers/andre-costa.md +22 -0
- package/examples/events/assets/content/speakers/hans-mueller.md.ejs +22 -0
- package/examples/events/assets/content/speakers/isabella-martinez.md.ejs +22 -0
- package/examples/events/assets/content/speakers/kenji-nakamura.md.ejs +22 -0
- package/examples/events/assets/content/speakers/marie-dubois.md.ejs +20 -0
- package/examples/events/assets/content/speakers/priya-sharma.md.ejs +22 -0
- package/examples/events/assets/content/talks/croissant-lamination-secrets.md +39 -0
- package/examples/events/assets/content/talks/french-macaron-mastery.md.ejs +39 -0
- package/examples/events/assets/content/talks/neapolitan-pizza-tradition-meets-innovation.md.ejs +39 -0
- package/examples/events/assets/content/talks/savory-breads-of-the-mediterranean.md.ejs +39 -0
- package/examples/events/assets/content/talks/sourdough-from-starter-to-masterpiece.md.ejs +36 -0
- package/examples/events/assets/content/talks/the-art-of-the-perfect-tart.md.ejs +32 -0
- package/examples/events/assets/content/talks/the-science-of-sugar.md.ejs +39 -0
- package/examples/events/assets/content/talks/umami-in-pastry-east-meets-west.md.ejs +39 -0
- package/examples/events/assets/content-collections.ts +56 -0
- package/examples/events/assets/public/background-1.jpg +0 -0
- package/examples/events/assets/public/background-2.jpg +0 -0
- package/examples/events/assets/public/background-3.jpg +0 -0
- package/examples/events/assets/public/background-4.jpg +0 -0
- package/examples/events/assets/public/conference-logo.png +0 -0
- package/examples/events/assets/public/favicon.ico +0 -0
- package/examples/events/assets/public/speakers/andre-costa.jpg +0 -0
- package/examples/events/assets/public/speakers/hans-mueller.jpg +0 -0
- package/examples/events/assets/public/speakers/isabella-martinez.jpg +0 -0
- package/examples/events/assets/public/speakers/kenji-nakamura.jpg +0 -0
- package/examples/events/assets/public/speakers/marie-dubois.jpg +0 -0
- package/examples/events/assets/public/speakers/priya-sharma.jpg +0 -0
- package/examples/events/assets/public/talks/croissant-lamination-secrets.jpg +0 -0
- package/examples/events/assets/public/talks/french-macaron-mastery.jpg +0 -0
- package/examples/events/assets/public/talks/neapolitan-pizza-tradition-meets-innovation.jpg +0 -0
- package/examples/events/assets/public/talks/savory-breads-of-the-mediterranean.jpg +0 -0
- package/examples/events/assets/public/talks/sourdough-from-starter-to-masterpiece.jpg +0 -0
- package/examples/events/assets/public/talks/the-art-of-the-perfect-tart.jpg +0 -0
- package/examples/events/assets/public/talks/the-science-of-sugar.jpg +0 -0
- package/examples/events/assets/public/talks/umami-in-pastry-east-meets-west.jpg +0 -0
- package/examples/events/assets/public/tanstack-circle-logo.png +0 -0
- package/examples/events/assets/public/tanstack-word-logo-white.svg +1 -0
- package/examples/events/assets/src/components/Header.tsx +59 -0
- package/examples/events/assets/src/components/HeaderNav.tsx +67 -0
- package/examples/events/assets/src/components/HeroCarousel.tsx +61 -0
- package/examples/events/assets/src/components/RemyAssistant.tsx +207 -0
- package/examples/events/assets/src/components/SpeakerCard.tsx +67 -0
- package/examples/events/assets/src/components/TalkCard.tsx +77 -0
- package/examples/events/assets/src/components/ui/card.tsx +92 -0
- package/examples/events/assets/src/lib/conference-ai-hook.ts +26 -0
- package/examples/events/assets/src/lib/conference-tools.ts +210 -0
- package/examples/events/assets/src/lib/model-selection.ts +1 -0
- package/examples/events/assets/src/lib/utils.ts +6 -0
- package/examples/events/assets/src/routes/__root.tsx +70 -0
- package/examples/events/assets/src/routes/api.remy-chat.ts +119 -0
- package/examples/events/assets/src/routes/index.tsx +192 -0
- package/examples/events/assets/src/routes/schedule.index.tsx +274 -0
- package/examples/events/assets/src/routes/speakers.$slug.tsx +122 -0
- package/examples/events/assets/src/routes/speakers.index.tsx +40 -0
- package/examples/events/assets/src/routes/talks.$slug.tsx +116 -0
- package/examples/events/assets/src/routes/talks.index.tsx +40 -0
- package/examples/events/assets/src/styles.css +182 -0
- package/examples/events/info.json +74 -0
- package/examples/events/package.json +23 -0
- package/examples/marketing/README.md +60 -0
- package/examples/marketing/assets/public/logo.png +0 -0
- package/examples/marketing/assets/public/motorcycle-adventure.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-cruiser.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-scooter.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-sport.jpg +0 -0
- package/examples/marketing/assets/public/motorcycle-supersport.jpg +0 -0
- package/examples/marketing/assets/src/components/Header.tsx +36 -0
- package/examples/marketing/assets/src/components/MotorcycleAIAssistant.tsx +162 -0
- package/examples/marketing/assets/src/components/MotorcycleRecommendation.tsx +53 -0
- package/examples/marketing/assets/src/data/motorcycles.ts.ejs +77 -0
- package/examples/marketing/assets/src/lib/motorcycle-ai-hook.ts +24 -0
- package/examples/marketing/assets/src/lib/motorcycle-tools.ts +42 -0
- package/examples/marketing/assets/src/routes/__root.tsx +57 -0
- package/examples/marketing/assets/src/routes/api.motorcycle-chat.ts +78 -0
- package/examples/marketing/assets/src/routes/index.tsx +72 -0
- package/examples/marketing/assets/src/routes/motorcycles/$motorcycleId.tsx +56 -0
- package/examples/marketing/assets/src/store/motorcycle-assistant.ts +3 -0
- package/examples/marketing/assets/src/styles.css +212 -0
- package/examples/marketing/info.json +38 -0
- package/examples/marketing/package.json +14 -0
- package/examples/resume/README.md +82 -0
- package/examples/resume/assets/content/education/code-school.md +17 -0
- package/examples/resume/assets/content/jobs/freelance.md.ejs +13 -0
- package/examples/resume/assets/content/jobs/initech-junior.md +20 -0
- package/examples/resume/assets/content/jobs/initech-lead.md.ejs +29 -0
- package/examples/resume/assets/content/jobs/initrode-senior.md.ejs +28 -0
- package/examples/resume/assets/content-collections.ts +36 -0
- package/examples/resume/assets/public/headshot-on-white.jpg +0 -0
- package/examples/resume/assets/src/components/Header.tsx +33 -0
- package/examples/resume/assets/src/components/ResumeAssistant.tsx +193 -0
- package/examples/resume/assets/src/components/ui/badge.tsx +46 -0
- package/examples/resume/assets/src/components/ui/card.tsx +92 -0
- package/examples/resume/assets/src/components/ui/checkbox.tsx +30 -0
- package/examples/resume/assets/src/components/ui/hover-card.tsx +44 -0
- package/examples/resume/assets/src/components/ui/separator.tsx +26 -0
- package/examples/resume/assets/src/lib/resume-ai-hook.ts +21 -0
- package/examples/resume/assets/src/lib/resume-tools.ts +165 -0
- package/examples/resume/assets/src/lib/utils.ts +6 -0
- package/examples/resume/assets/src/routes/api.resume-chat.ts +110 -0
- package/examples/resume/assets/src/routes/index.tsx +220 -0
- package/examples/resume/assets/src/styles.css +138 -0
- package/examples/resume/info.json +25 -0
- package/examples/resume/package.json +26 -0
- package/package.json +39 -0
- package/project/base/_dot_claude/skills/content-collections/SKILL.md +505 -0
- package/project/base/_dot_claude/skills/netlify-blobs/SKILL.md +410 -0
- package/project/base/_dot_claude/skills/netlify-db/SKILL.md +424 -0
- package/project/base/_dot_claude/skills/netlify-debugging/SKILL.md +419 -0
- package/project/base/_dot_claude/skills/netlify-forms/SKILL.md +243 -0
- package/project/base/_dot_claude/skills/netlify-functions/SKILL.md +372 -0
- package/project/base/_dot_claude/skills/tanstack-start-api-routes/SKILL.md +421 -0
- package/project/base/_dot_claude/skills/tanstack-start-loaders/SKILL.md +426 -0
- package/project/base/_dot_claude/skills/tanstack-start-project-setup/SKILL.md +493 -0
- package/project/base/_dot_claude/skills/tanstack-start-routes/SKILL.md +430 -0
- package/project/base/_dot_claude/skills/tanstack-start-server-functions/SKILL.md +445 -0
- package/project/base/_dot_claude/skills/tanstack-start-typesafe-routing/SKILL.md +494 -0
- package/project/base/_dot_gitignore +8 -0
- package/project/base/netlify.toml +7 -0
- package/project/base/package.json +33 -0
- package/project/base/public/favicon.ico +0 -0
- package/project/base/public/tanstack-circle-logo.png +0 -0
- package/project/base/public/tanstack-word-logo-white.svg +1 -0
- package/project/base/src/components/Header.tsx +17 -0
- package/project/base/src/components/HeaderNav.tsx.ejs +179 -0
- package/project/base/src/router.tsx +15 -0
- package/project/base/src/routes/__root.tsx +57 -0
- package/project/base/src/routes/index.tsx +48 -0
- package/project/base/src/styles.css +15 -0
- package/project/base/tsconfig.json +28 -0
- package/project/base/vite.config.ts.ejs +25 -0
- package/project/packages.json +22 -0
- package/scripts/check-outdated-packages.js +421 -0
- package/src/cli.ts +343 -0
- package/src/index.ts +49 -0
- package/src/types.ts +15 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tanstack-start-server-functions
|
|
3
|
+
description: Create server functions in TanStack Start for server-side logic callable from anywhere. Use for database access, API calls with secrets, mutations, or any server-only code that needs to be called from components or loaders.
|
|
4
|
+
license: Apache-2.0
|
|
5
|
+
metadata:
|
|
6
|
+
author: tanstack
|
|
7
|
+
version: "1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# TanStack Start Server Functions
|
|
11
|
+
|
|
12
|
+
Server functions are the primary way to run server-side code in TanStack Start. They provide type-safe RPC calls from client to server.
|
|
13
|
+
|
|
14
|
+
## When to Use
|
|
15
|
+
|
|
16
|
+
- Database queries and mutations
|
|
17
|
+
- API calls requiring secrets
|
|
18
|
+
- Server-only business logic
|
|
19
|
+
- Any code that needs server capabilities
|
|
20
|
+
- Called from loaders, components, or other server functions
|
|
21
|
+
|
|
22
|
+
## Basic Server Function
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// src/server/posts.functions.ts
|
|
26
|
+
import { createServerFn } from '@tanstack/react-start';
|
|
27
|
+
|
|
28
|
+
// Simple GET function
|
|
29
|
+
export const getPosts = createServerFn().handler(async () => {
|
|
30
|
+
const posts = await db.query('SELECT * FROM posts');
|
|
31
|
+
return posts;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Call it anywhere
|
|
35
|
+
const posts = await getPosts();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Function with Input
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// src/server/posts.functions.ts
|
|
42
|
+
import { createServerFn } from '@tanstack/react-start';
|
|
43
|
+
|
|
44
|
+
export const getPost = createServerFn({ method: 'GET' })
|
|
45
|
+
.inputValidator((data: { id: string }) => data)
|
|
46
|
+
.handler(async ({ data }) => {
|
|
47
|
+
const post = await db.query('SELECT * FROM posts WHERE id = $1', [data.id]);
|
|
48
|
+
return post[0] || null;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Call with input
|
|
52
|
+
const post = await getPost({ data: { id: '123' } });
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## HTTP Methods
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// GET - for reading data (default)
|
|
59
|
+
export const getData = createServerFn().handler(async () => {
|
|
60
|
+
return { data: 'value' };
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// GET with explicit method
|
|
64
|
+
export const getUser = createServerFn({ method: 'GET' })
|
|
65
|
+
.inputValidator((data: { id: string }) => data)
|
|
66
|
+
.handler(async ({ data }) => {
|
|
67
|
+
return await findUser(data.id);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// POST - for mutations
|
|
71
|
+
export const createPost = createServerFn({ method: 'POST' })
|
|
72
|
+
.inputValidator((data: { title: string; content: string }) => data)
|
|
73
|
+
.handler(async ({ data }) => {
|
|
74
|
+
const post = await db.insert('posts', data);
|
|
75
|
+
return post;
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Input Validation with Zod
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { createServerFn } from '@tanstack/react-start';
|
|
83
|
+
import { z } from 'zod';
|
|
84
|
+
|
|
85
|
+
const CreateUserSchema = z.object({
|
|
86
|
+
email: z.string().email(),
|
|
87
|
+
name: z.string().min(1).max(100),
|
|
88
|
+
age: z.number().int().min(0).optional(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export const createUser = createServerFn({ method: 'POST' })
|
|
92
|
+
.inputValidator(CreateUserSchema)
|
|
93
|
+
.handler(async ({ data }) => {
|
|
94
|
+
// data is fully typed and validated
|
|
95
|
+
// { email: string, name: string, age?: number }
|
|
96
|
+
|
|
97
|
+
const user = await db.insert('users', data);
|
|
98
|
+
return user;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Usage - type errors if invalid
|
|
102
|
+
await createUser({
|
|
103
|
+
data: {
|
|
104
|
+
email: 'alice@example.com',
|
|
105
|
+
name: 'Alice',
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Organized File Structure
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
src/
|
|
114
|
+
├── server/
|
|
115
|
+
│ ├── posts.functions.ts # Server function wrappers
|
|
116
|
+
│ ├── posts.server.ts # Server-only helpers (DB queries)
|
|
117
|
+
│ ├── users.functions.ts
|
|
118
|
+
│ ├── users.server.ts
|
|
119
|
+
│ └── schemas.ts # Shared validation schemas
|
|
120
|
+
├── routes/
|
|
121
|
+
│ └── posts.tsx
|
|
122
|
+
└── ...
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Server-Only Helpers
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// src/server/posts.server.ts
|
|
129
|
+
// These are server-only - NEVER import in client code
|
|
130
|
+
|
|
131
|
+
export async function findPostById(id: string) {
|
|
132
|
+
return db.query('SELECT * FROM posts WHERE id = $1', [id]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function insertPost(data: { title: string; content: string }) {
|
|
136
|
+
return db.query(
|
|
137
|
+
'INSERT INTO posts (title, content) VALUES ($1, $2) RETURNING *',
|
|
138
|
+
[data.title, data.content]
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Server Function Wrappers
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// src/server/posts.functions.ts
|
|
147
|
+
// These are the public API - safe to import anywhere
|
|
148
|
+
|
|
149
|
+
import { createServerFn } from '@tanstack/react-start';
|
|
150
|
+
import { findPostById, insertPost } from './posts.server';
|
|
151
|
+
import { z } from 'zod';
|
|
152
|
+
|
|
153
|
+
export const getPost = createServerFn({ method: 'GET' })
|
|
154
|
+
.inputValidator((data: { id: string }) => data)
|
|
155
|
+
.handler(async ({ data }) => {
|
|
156
|
+
return findPostById(data.id);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
export const createPost = createServerFn({ method: 'POST' })
|
|
160
|
+
.inputValidator(z.object({
|
|
161
|
+
title: z.string().min(1),
|
|
162
|
+
content: z.string(),
|
|
163
|
+
}))
|
|
164
|
+
.handler(async ({ data }) => {
|
|
165
|
+
return insertPost(data);
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Using in Components
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
// src/routes/posts.tsx
|
|
173
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
174
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
175
|
+
import { getPosts, createPost } from '../server/posts.functions';
|
|
176
|
+
import { useServerFn } from '@tanstack/react-start';
|
|
177
|
+
|
|
178
|
+
export const Route = createFileRoute('/posts')({
|
|
179
|
+
loader: async () => {
|
|
180
|
+
// Call server function in loader
|
|
181
|
+
const posts = await getPosts();
|
|
182
|
+
return { posts };
|
|
183
|
+
},
|
|
184
|
+
component: PostsComponent,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
function PostsComponent() {
|
|
188
|
+
const { posts } = Route.useLoaderData();
|
|
189
|
+
const queryClient = useQueryClient();
|
|
190
|
+
|
|
191
|
+
// Wrap for use with TanStack Query
|
|
192
|
+
const createPostFn = useServerFn(createPost);
|
|
193
|
+
|
|
194
|
+
const mutation = useMutation({
|
|
195
|
+
mutationFn: (data: { title: string; content: string }) =>
|
|
196
|
+
createPostFn({ data }),
|
|
197
|
+
onSuccess: () => {
|
|
198
|
+
// Invalidate and refetch
|
|
199
|
+
queryClient.invalidateQueries({ queryKey: ['posts'] });
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
const formData = new FormData(e.currentTarget);
|
|
206
|
+
mutation.mutate({
|
|
207
|
+
title: formData.get('title') as string,
|
|
208
|
+
content: formData.get('content') as string,
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<div>
|
|
214
|
+
<form onSubmit={handleSubmit}>
|
|
215
|
+
<input name="title" placeholder="Title" required />
|
|
216
|
+
<textarea name="content" placeholder="Content" />
|
|
217
|
+
<button type="submit" disabled={mutation.isPending}>
|
|
218
|
+
{mutation.isPending ? 'Creating...' : 'Create Post'}
|
|
219
|
+
</button>
|
|
220
|
+
</form>
|
|
221
|
+
|
|
222
|
+
<ul>
|
|
223
|
+
{posts.map((post) => (
|
|
224
|
+
<li key={post.id}>{post.title}</li>
|
|
225
|
+
))}
|
|
226
|
+
</ul>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Form Data Handling
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { createServerFn } from '@tanstack/react-start';
|
|
236
|
+
|
|
237
|
+
export const uploadFile = createServerFn({ method: 'POST' })
|
|
238
|
+
.inputValidator((formData: FormData) => formData)
|
|
239
|
+
.handler(async ({ data: formData }) => {
|
|
240
|
+
const file = formData.get('file') as File;
|
|
241
|
+
const name = formData.get('name') as string;
|
|
242
|
+
|
|
243
|
+
// Process file...
|
|
244
|
+
const buffer = await file.arrayBuffer();
|
|
245
|
+
|
|
246
|
+
return { filename: file.name, size: file.size };
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Usage in component
|
|
250
|
+
function UploadForm() {
|
|
251
|
+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
|
252
|
+
e.preventDefault();
|
|
253
|
+
const formData = new FormData(e.currentTarget);
|
|
254
|
+
|
|
255
|
+
const result = await uploadFile({ data: formData });
|
|
256
|
+
console.log('Uploaded:', result.filename);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<form onSubmit={handleSubmit}>
|
|
261
|
+
<input type="file" name="file" />
|
|
262
|
+
<input type="text" name="name" />
|
|
263
|
+
<button type="submit">Upload</button>
|
|
264
|
+
</form>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Middleware
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
import { createServerFn, createMiddleware } from '@tanstack/react-start';
|
|
273
|
+
|
|
274
|
+
// Create reusable middleware
|
|
275
|
+
const authMiddleware = createMiddleware().handler(async ({ next }) => {
|
|
276
|
+
const user = await getCurrentUser();
|
|
277
|
+
|
|
278
|
+
if (!user) {
|
|
279
|
+
throw new Error('Unauthorized');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Pass data to next middleware/handler
|
|
283
|
+
return next({ user });
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const loggingMiddleware = createMiddleware().handler(async ({ next }) => {
|
|
287
|
+
const start = Date.now();
|
|
288
|
+
const result = await next();
|
|
289
|
+
console.log(`Request took ${Date.now() - start}ms`);
|
|
290
|
+
return result;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Use middleware in server function
|
|
294
|
+
export const getSecretData = createServerFn({ method: 'GET' })
|
|
295
|
+
.middleware([loggingMiddleware, authMiddleware])
|
|
296
|
+
.handler(async ({ context }) => {
|
|
297
|
+
// context.user is available from authMiddleware
|
|
298
|
+
return { secret: 'data', user: context.user };
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Error Handling
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
import { createServerFn } from '@tanstack/react-start';
|
|
306
|
+
|
|
307
|
+
export const riskyOperation = createServerFn({ method: 'POST' })
|
|
308
|
+
.inputValidator((data: { value: number }) => data)
|
|
309
|
+
.handler(async ({ data }) => {
|
|
310
|
+
if (data.value < 0) {
|
|
311
|
+
throw new Error('Value must be positive');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const result = await dangerousOperation(data.value);
|
|
316
|
+
return { success: true, result };
|
|
317
|
+
} catch (error) {
|
|
318
|
+
// Log server-side
|
|
319
|
+
console.error('Operation failed:', error);
|
|
320
|
+
|
|
321
|
+
// Return safe error to client
|
|
322
|
+
throw new Error('Operation failed. Please try again.');
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// In component - handle errors
|
|
327
|
+
function MyComponent() {
|
|
328
|
+
const [error, setError] = useState<string | null>(null);
|
|
329
|
+
|
|
330
|
+
const handleClick = async () => {
|
|
331
|
+
try {
|
|
332
|
+
await riskyOperation({ data: { value: -1 } });
|
|
333
|
+
} catch (e) {
|
|
334
|
+
setError(e instanceof Error ? e.message : 'Unknown error');
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<div>
|
|
340
|
+
<button onClick={handleClick}>Do Thing</button>
|
|
341
|
+
{error && <p className="error">{error}</p>}
|
|
342
|
+
</div>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Redirects from Server Functions
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
import { createServerFn } from '@tanstack/react-start';
|
|
351
|
+
import { redirect } from '@tanstack/react-router';
|
|
352
|
+
|
|
353
|
+
export const loginUser = createServerFn({ method: 'POST' })
|
|
354
|
+
.inputValidator((data: { email: string; password: string }) => data)
|
|
355
|
+
.handler(async ({ data }) => {
|
|
356
|
+
const user = await authenticate(data.email, data.password);
|
|
357
|
+
|
|
358
|
+
if (!user) {
|
|
359
|
+
return { error: 'Invalid credentials' };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Set session cookie, etc.
|
|
363
|
+
await createSession(user.id);
|
|
364
|
+
|
|
365
|
+
// Redirect after successful login
|
|
366
|
+
throw redirect({ to: '/dashboard' });
|
|
367
|
+
});
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Environment Variables
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
import { createServerFn } from '@tanstack/react-start';
|
|
374
|
+
|
|
375
|
+
export const callExternalApi = createServerFn({ method: 'GET' })
|
|
376
|
+
.handler(async () => {
|
|
377
|
+
// Access secrets safely on server
|
|
378
|
+
const apiKey = process.env.EXTERNAL_API_KEY;
|
|
379
|
+
|
|
380
|
+
const response = await fetch('https://api.example.com/data', {
|
|
381
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
return response.json();
|
|
385
|
+
});
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Common Patterns
|
|
389
|
+
|
|
390
|
+
### CRUD Operations
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
// src/server/items.functions.ts
|
|
394
|
+
import { createServerFn } from '@tanstack/react-start';
|
|
395
|
+
import { z } from 'zod';
|
|
396
|
+
import { db, items } from '../db';
|
|
397
|
+
import { eq } from 'drizzle-orm';
|
|
398
|
+
|
|
399
|
+
const ItemSchema = z.object({
|
|
400
|
+
name: z.string().min(1),
|
|
401
|
+
price: z.number().positive(),
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
export const getItems = createServerFn().handler(async () => {
|
|
405
|
+
return db.select().from(items);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
export const getItem = createServerFn({ method: 'GET' })
|
|
409
|
+
.inputValidator((data: { id: string }) => data)
|
|
410
|
+
.handler(async ({ data }) => {
|
|
411
|
+
const result = await db.select().from(items).where(eq(items.id, data.id));
|
|
412
|
+
return result[0] || null;
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
export const createItem = createServerFn({ method: 'POST' })
|
|
416
|
+
.inputValidator(ItemSchema)
|
|
417
|
+
.handler(async ({ data }) => {
|
|
418
|
+
const result = await db.insert(items).values(data).returning();
|
|
419
|
+
return result[0];
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
export const updateItem = createServerFn({ method: 'POST' })
|
|
423
|
+
.inputValidator(z.object({ id: z.string() }).extend(ItemSchema.shape))
|
|
424
|
+
.handler(async ({ data }) => {
|
|
425
|
+
const { id, ...values } = data;
|
|
426
|
+
const result = await db.update(items).set(values).where(eq(items.id, id)).returning();
|
|
427
|
+
return result[0];
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
export const deleteItem = createServerFn({ method: 'POST' })
|
|
431
|
+
.inputValidator((data: { id: string }) => data)
|
|
432
|
+
.handler(async ({ data }) => {
|
|
433
|
+
await db.delete(items).where(eq(items.id, data.id));
|
|
434
|
+
return { success: true };
|
|
435
|
+
});
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
## Key Points
|
|
439
|
+
|
|
440
|
+
1. **Server functions run ONLY on the server** - Safe for secrets and DB access
|
|
441
|
+
2. **Type-safe across the network** - Input and output are fully typed
|
|
442
|
+
3. **Use `.functions.ts` suffix** - Clear convention for importable functions
|
|
443
|
+
4. **Validate input** - Use Zod or custom validators
|
|
444
|
+
5. **Keep `.server.ts` files private** - Never import directly in client code
|
|
445
|
+
6. **Use `useServerFn` hook** - When integrating with TanStack Query
|