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,421 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tanstack-start-api-routes
|
|
3
|
+
description: Create API routes (server routes) in TanStack Start for handling HTTP requests. Use when building REST APIs, webhooks, or any HTTP endpoint that returns data rather than rendering a page.
|
|
4
|
+
license: Apache-2.0
|
|
5
|
+
metadata:
|
|
6
|
+
author: tanstack
|
|
7
|
+
version: "1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# TanStack Start API Routes (Server Routes)
|
|
11
|
+
|
|
12
|
+
TanStack Start allows you to create API endpoints using the `server` property on routes. These run server-side and handle raw HTTP requests.
|
|
13
|
+
|
|
14
|
+
## When to Use
|
|
15
|
+
|
|
16
|
+
- Building REST API endpoints
|
|
17
|
+
- Handling webhooks
|
|
18
|
+
- File uploads/downloads
|
|
19
|
+
- Any endpoint that returns data (not HTML)
|
|
20
|
+
|
|
21
|
+
**Note**: For RPC-style server logic callable from components, use **server functions** instead. Server routes are for traditional HTTP endpoints.
|
|
22
|
+
|
|
23
|
+
## Basic Server Route
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// src/routes/api/hello.ts
|
|
27
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
28
|
+
|
|
29
|
+
export const Route = createFileRoute('/api/hello')({
|
|
30
|
+
server: {
|
|
31
|
+
handlers: {
|
|
32
|
+
GET: async ({ request }) => {
|
|
33
|
+
return new Response(JSON.stringify({ message: 'Hello, World!' }), {
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Multiple HTTP Methods
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// src/routes/api/users.ts
|
|
46
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
47
|
+
|
|
48
|
+
export const Route = createFileRoute('/api/users')({
|
|
49
|
+
server: {
|
|
50
|
+
handlers: {
|
|
51
|
+
GET: async ({ request }) => {
|
|
52
|
+
const users = await fetchUsers();
|
|
53
|
+
return Response.json(users);
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
POST: async ({ request }) => {
|
|
57
|
+
const body = await request.json();
|
|
58
|
+
const newUser = await createUser(body);
|
|
59
|
+
return Response.json(newUser, { status: 201 });
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Dynamic Parameters
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// src/routes/api/users/$id.ts
|
|
70
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
71
|
+
|
|
72
|
+
export const Route = createFileRoute('/api/users/$id')({
|
|
73
|
+
server: {
|
|
74
|
+
handlers: {
|
|
75
|
+
GET: async ({ params }) => {
|
|
76
|
+
const { id } = params;
|
|
77
|
+
const user = await getUser(id);
|
|
78
|
+
|
|
79
|
+
if (!user) {
|
|
80
|
+
return new Response('User not found', { status: 404 });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return Response.json(user);
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
PUT: async ({ request, params }) => {
|
|
87
|
+
const { id } = params;
|
|
88
|
+
const body = await request.json();
|
|
89
|
+
const updatedUser = await updateUser(id, body);
|
|
90
|
+
return Response.json(updatedUser);
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
DELETE: async ({ params }) => {
|
|
94
|
+
const { id } = params;
|
|
95
|
+
await deleteUser(id);
|
|
96
|
+
return new Response(null, { status: 204 });
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Multiple Dynamic Parameters
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// src/routes/api/users/$userId/posts/$postId.ts
|
|
107
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
108
|
+
|
|
109
|
+
export const Route = createFileRoute('/api/users/$userId/posts/$postId')({
|
|
110
|
+
server: {
|
|
111
|
+
handlers: {
|
|
112
|
+
GET: async ({ params }) => {
|
|
113
|
+
const { userId, postId } = params;
|
|
114
|
+
const post = await getUserPost(userId, postId);
|
|
115
|
+
return Response.json(post);
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Handler Context
|
|
123
|
+
|
|
124
|
+
The handler receives a context object with:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
export const Route = createFileRoute('/api/example')({
|
|
128
|
+
server: {
|
|
129
|
+
handlers: {
|
|
130
|
+
GET: async (context) => {
|
|
131
|
+
// Request object (Web API Request)
|
|
132
|
+
const { request } = context;
|
|
133
|
+
|
|
134
|
+
// URL parameters from route
|
|
135
|
+
const { params } = context;
|
|
136
|
+
|
|
137
|
+
// Get headers
|
|
138
|
+
const authHeader = request.headers.get('Authorization');
|
|
139
|
+
|
|
140
|
+
// Get query parameters
|
|
141
|
+
const url = new URL(request.url);
|
|
142
|
+
const searchParams = url.searchParams;
|
|
143
|
+
const page = searchParams.get('page');
|
|
144
|
+
|
|
145
|
+
return Response.json({ page });
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Request Body Handling
|
|
153
|
+
|
|
154
|
+
### JSON Body
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
POST: async ({ request }) => {
|
|
158
|
+
const body = await request.json();
|
|
159
|
+
// body is parsed JSON
|
|
160
|
+
return Response.json({ received: body });
|
|
161
|
+
},
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Form Data
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
POST: async ({ request }) => {
|
|
168
|
+
const formData = await request.formData();
|
|
169
|
+
const name = formData.get('name');
|
|
170
|
+
const email = formData.get('email');
|
|
171
|
+
|
|
172
|
+
return Response.json({ name, email });
|
|
173
|
+
},
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Raw Text/Binary
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
POST: async ({ request }) => {
|
|
180
|
+
const text = await request.text();
|
|
181
|
+
// or
|
|
182
|
+
const buffer = await request.arrayBuffer();
|
|
183
|
+
|
|
184
|
+
return new Response('Received', { status: 200 });
|
|
185
|
+
},
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Response Helpers
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
// JSON response
|
|
192
|
+
return Response.json({ data: 'value' });
|
|
193
|
+
|
|
194
|
+
// JSON with status
|
|
195
|
+
return Response.json({ error: 'Not found' }, { status: 404 });
|
|
196
|
+
|
|
197
|
+
// Plain text
|
|
198
|
+
return new Response('Hello', {
|
|
199
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// HTML
|
|
203
|
+
return new Response('<h1>Hello</h1>', {
|
|
204
|
+
headers: { 'Content-Type': 'text/html' },
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Redirect
|
|
208
|
+
return Response.redirect('https://example.com', 302);
|
|
209
|
+
|
|
210
|
+
// No content
|
|
211
|
+
return new Response(null, { status: 204 });
|
|
212
|
+
|
|
213
|
+
// Stream
|
|
214
|
+
const stream = new ReadableStream({ ... });
|
|
215
|
+
return new Response(stream, {
|
|
216
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Error Handling
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
export const Route = createFileRoute('/api/users/$id')({
|
|
224
|
+
server: {
|
|
225
|
+
handlers: {
|
|
226
|
+
GET: async ({ params }) => {
|
|
227
|
+
try {
|
|
228
|
+
const user = await getUser(params.id);
|
|
229
|
+
|
|
230
|
+
if (!user) {
|
|
231
|
+
return Response.json(
|
|
232
|
+
{ error: 'User not found' },
|
|
233
|
+
{ status: 404 }
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return Response.json(user);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.error('Error fetching user:', error);
|
|
240
|
+
|
|
241
|
+
return Response.json(
|
|
242
|
+
{ error: 'Internal server error' },
|
|
243
|
+
{ status: 500 }
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Authentication
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
// src/routes/api/protected.ts
|
|
256
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
257
|
+
import { verifyToken } from '../lib/auth';
|
|
258
|
+
|
|
259
|
+
export const Route = createFileRoute('/api/protected')({
|
|
260
|
+
server: {
|
|
261
|
+
handlers: {
|
|
262
|
+
GET: async ({ request }) => {
|
|
263
|
+
const authHeader = request.headers.get('Authorization');
|
|
264
|
+
|
|
265
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
266
|
+
return Response.json(
|
|
267
|
+
{ error: 'Missing authorization header' },
|
|
268
|
+
{ status: 401 }
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const token = authHeader.slice(7);
|
|
273
|
+
const user = await verifyToken(token);
|
|
274
|
+
|
|
275
|
+
if (!user) {
|
|
276
|
+
return Response.json(
|
|
277
|
+
{ error: 'Invalid token' },
|
|
278
|
+
{ status: 401 }
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Proceed with authenticated request
|
|
283
|
+
return Response.json({ user, message: 'Protected data' });
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
## CORS Headers
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
const corsHeaders = {
|
|
294
|
+
'Access-Control-Allow-Origin': '*',
|
|
295
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
296
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
export const Route = createFileRoute('/api/data')({
|
|
300
|
+
server: {
|
|
301
|
+
handlers: {
|
|
302
|
+
// Handle preflight
|
|
303
|
+
OPTIONS: async () => {
|
|
304
|
+
return new Response(null, {
|
|
305
|
+
status: 204,
|
|
306
|
+
headers: corsHeaders,
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
GET: async ({ request }) => {
|
|
311
|
+
const data = await fetchData();
|
|
312
|
+
|
|
313
|
+
return Response.json(data, {
|
|
314
|
+
headers: corsHeaders,
|
|
315
|
+
});
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## File Naming Conventions
|
|
323
|
+
|
|
324
|
+
| File Path | API Route |
|
|
325
|
+
|-----------|-----------|
|
|
326
|
+
| `routes/api/hello.ts` | `GET /api/hello` |
|
|
327
|
+
| `routes/api/users.ts` | `GET /api/users` |
|
|
328
|
+
| `routes/api/users/$id.ts` | `GET /api/users/:id` |
|
|
329
|
+
| `routes/api/users.index.ts` | `GET /api/users` |
|
|
330
|
+
| `routes/api/file/$.ts` | `GET /api/file/*` (catch-all) |
|
|
331
|
+
|
|
332
|
+
## Combined Route + API
|
|
333
|
+
|
|
334
|
+
A single file can handle both page rendering and API:
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
// src/routes/posts.$postId.tsx
|
|
338
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
339
|
+
|
|
340
|
+
export const Route = createFileRoute('/posts/$postId')({
|
|
341
|
+
// Server route handlers (API)
|
|
342
|
+
server: {
|
|
343
|
+
handlers: {
|
|
344
|
+
// GET /posts/123 with Accept: application/json → JSON response
|
|
345
|
+
GET: async ({ request, params }) => {
|
|
346
|
+
const accept = request.headers.get('Accept');
|
|
347
|
+
|
|
348
|
+
if (accept?.includes('application/json')) {
|
|
349
|
+
const post = await getPost(params.postId);
|
|
350
|
+
return Response.json(post);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Fall through to page rendering
|
|
354
|
+
return undefined;
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
// Page loader and component
|
|
360
|
+
loader: async ({ params }) => {
|
|
361
|
+
const post = await getPost(params.postId);
|
|
362
|
+
return { post };
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
component: PostComponent,
|
|
366
|
+
});
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
## Webhooks Example
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
// src/routes/api/webhooks/stripe.ts
|
|
373
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
374
|
+
import Stripe from 'stripe';
|
|
375
|
+
|
|
376
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
377
|
+
|
|
378
|
+
export const Route = createFileRoute('/api/webhooks/stripe')({
|
|
379
|
+
server: {
|
|
380
|
+
handlers: {
|
|
381
|
+
POST: async ({ request }) => {
|
|
382
|
+
const sig = request.headers.get('stripe-signature');
|
|
383
|
+
const body = await request.text();
|
|
384
|
+
|
|
385
|
+
let event: Stripe.Event;
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
event = stripe.webhooks.constructEvent(
|
|
389
|
+
body,
|
|
390
|
+
sig!,
|
|
391
|
+
process.env.STRIPE_WEBHOOK_SECRET!
|
|
392
|
+
);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
return Response.json(
|
|
395
|
+
{ error: 'Invalid signature' },
|
|
396
|
+
{ status: 400 }
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
switch (event.type) {
|
|
401
|
+
case 'checkout.session.completed':
|
|
402
|
+
await handleCheckoutComplete(event.data.object);
|
|
403
|
+
break;
|
|
404
|
+
// Handle other events...
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return Response.json({ received: true });
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## Best Practices
|
|
415
|
+
|
|
416
|
+
1. **Use server functions for RPC** - If calling from components, prefer `createServerFn`
|
|
417
|
+
2. **Validate input** - Always validate request bodies and parameters
|
|
418
|
+
3. **Handle errors** - Return appropriate status codes and messages
|
|
419
|
+
4. **Set correct headers** - Content-Type, CORS, caching as needed
|
|
420
|
+
5. **Keep handlers focused** - One responsibility per endpoint
|
|
421
|
+
6. **Use TypeScript** - Type your request/response bodies
|