@kood/claude-code 0.3.9 → 0.3.10

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.
@@ -1,67 +1,1368 @@
1
1
  # TanStack Start
2
2
 
3
- > 1.x | Full-stack React Framework
3
+ > v1 | Full-stack React Framework
4
4
 
5
- @setup.md
6
- @server-functions.md
7
- @middleware.md
8
- @routing.md
9
- @auth-patterns.md
5
+ ---
6
+
7
+ <context>
8
+
9
+ **Purpose:** TanStack Start를 사용한 Full-stack React 애플리케이션 개발 가이드
10
+
11
+ **Scope:**
12
+ - Server Functions (타입 안전 API)
13
+ - File-based Routing (TanStack Router)
14
+ - Middleware 체계
15
+ - SSR + Streaming
16
+ - TanStack Query 통합
17
+
18
+ **Key Features:**
19
+ - Type-safe Server Functions
20
+ - Zero-config file-based routing
21
+ - Built-in middleware system
22
+ - First-class SSR support
23
+ - Seamless TanStack Query integration
24
+ - Multiple deployment targets (Vercel, Cloudflare, Nitro)
25
+
26
+ **Version:** v1.x
27
+
28
+ </context>
10
29
 
11
30
  ---
12
31
 
13
- ## ⛔ Required Rules
32
+ <forbidden>
14
33
 
15
- | Forbidden | Use Instead |
16
- |------|------|
17
- | /api routes | Server Functions |
18
- | Manual validation in handler | inputValidator |
19
- | Manual auth in handler | middleware |
34
+ | 분류 | 금지 | 이유 |
35
+ |------|---------|------|
36
+ | **API 라우터** | `/api` 경로에 라우터 생성 | Server Functions 사용 |
37
+ | **수동 검증** | handler 내부에서 Zod 수동 검증 | `.inputValidator()` 사용 |
38
+ | **수동 인증** | handler 내부에서 세션 체크 | `.middleware()` 사용 |
39
+ | **deprecated API** | `.validator()` 메서드 | `.inputValidator()` 사용 (v1) |
40
+ | **직접 호출** | 컴포넌트에서 Server Function 직접 호출 | TanStack Query 사용 필수 |
41
+ | **환경변수 노출** | loader에서 `process.env` 직접 사용 | Server Function에서만 |
42
+ | **헬퍼 export** | 내부 헬퍼 함수 export | Server Function만 export |
43
+ | **any 타입** | Server Function 파라미터에 any | 명시적 타입/Zod 스키마 |
44
+ | **일반 함수** | 일반 async 함수를 API로 사용 | `createServerFn()` 사용 |
20
45
 
21
- ✅ POST/PUT/PATCH → inputValidator required
22
- ✅ Auth needed → middleware required
46
+ </forbidden>
23
47
 
24
48
  ---
25
49
 
26
- ## Quick Reference
50
+ <required>
51
+
52
+ | 분류 | ✅ 필수 | 상세 |
53
+ |------|---------|------|
54
+ | **POST/PUT/PATCH** | `.inputValidator()` 사용 | Zod 스키마로 검증 |
55
+ | **인증 필요** | `.middleware()` 사용 | authMiddleware 적용 |
56
+ | **클라이언트 호출** | TanStack Query 사용 | useQuery/useMutation |
57
+ | **Server Function** | `createServerFn()` 사용 | method 명시 (GET/POST/etc) |
58
+ | **타입 안전성** | 명시적 return type | TypeScript strict 모드 |
59
+ | **파일 구조** | `-functions/` 폴더 사용 | 페이지 전용 함수 분리 |
60
+ | **공통 함수** | `@/functions/` 사용 | 재사용 가능한 함수 |
61
+ | **환경변수** | Server Function 내부에서만 | 클라이언트 노출 방지 |
62
+
63
+ </required>
64
+
65
+ ---
66
+
67
+ <setup>
68
+
69
+ ## 설치
70
+
71
+ ```bash
72
+ yarn add @tanstack/react-start @tanstack/react-router vinxi
73
+ yarn add -D vite @vitejs/plugin-react vite-tsconfig-paths
74
+ ```
75
+
76
+ ## 설정
77
+
78
+ ```typescript
79
+ // vite.config.ts
80
+ import { defineConfig } from 'vite'
81
+ import tsConfigPaths from 'vite-tsconfig-paths'
82
+ import { tanstackStart } from '@tanstack/react-start/plugin/vite'
83
+ import viteReact from '@vitejs/plugin-react'
84
+
85
+ export default defineConfig({
86
+ server: { port: 3000 },
87
+ plugins: [tsConfigPaths(), tanstackStart(), viteReact()],
88
+ })
89
+ ```
90
+
91
+ ```json
92
+ // tsconfig.json
93
+ {
94
+ "compilerOptions": {
95
+ "target": "ES2022",
96
+ "module": "ESNext",
97
+ "moduleResolution": "bundler",
98
+ "strict": true,
99
+ "jsx": "react-jsx",
100
+ "paths": { "@/*": ["./src/*"] }
101
+ }
102
+ }
103
+ ```
104
+
105
+ ## 환경 변수 검증
27
106
 
28
107
  ```typescript
29
- // GET + Auth
108
+ // lib/env.ts
109
+ import { z } from 'zod'
110
+
111
+ const envSchema = z.object({
112
+ NODE_ENV: z.enum(['development', 'production', 'test']),
113
+ DATABASE_URL: z.string().url(),
114
+ API_SECRET: z.string().min(32),
115
+ })
116
+
117
+ export const env = envSchema.parse(process.env)
118
+ ```
119
+
120
+ </setup>
121
+
122
+ ---
123
+
124
+ <server_functions>
125
+
126
+ ## Server Functions
127
+
128
+ 서버에서만 실행되는 타입 안전한 함수.
129
+
130
+ ### 기본 패턴
131
+
132
+ | 메서드 | 사용 시점 | inputValidator | middleware |
133
+ |--------|----------|---------------|-----------|
134
+ | **GET** | 데이터 조회 | ❌ 선택 | ✅ 인증 시 필수 |
135
+ | **POST** | 데이터 생성 | ✅ 필수 | ✅ 인증 시 필수 |
136
+ | **PUT** | 전체 수정 | ✅ 필수 | ✅ 인증 시 필수 |
137
+ | **PATCH** | 부분 수정 | ✅ 필수 | ✅ 인증 시 필수 |
138
+ | **DELETE** | 삭제 | ❌ 선택 | ✅ 인증 시 필수 |
139
+
140
+ ### GET: 데이터 조회
141
+
142
+ ```typescript
143
+ // ✅ 기본 GET
30
144
  export const getUsers = createServerFn({ method: 'GET' })
145
+ .handler(async () => {
146
+ return prisma.user.findMany()
147
+ })
148
+
149
+ // ✅ GET + 인증
150
+ export const getMyProfile = createServerFn({ method: 'GET' })
31
151
  .middleware([authMiddleware])
32
- .handler(async () => prisma.user.findMany())
152
+ .handler(async ({ context }) => {
153
+ return prisma.user.findUnique({
154
+ where: { id: context.user.id },
155
+ })
156
+ })
157
+
158
+ // ✅ GET + Query Params (선택적 검증)
159
+ export const getUserById = createServerFn({ method: 'GET' })
160
+ .inputValidator(z.object({ id: z.string() }))
161
+ .handler(async ({ data }) => {
162
+ return prisma.user.findUnique({
163
+ where: { id: data.id },
164
+ })
165
+ })
166
+ ```
167
+
168
+ ### POST: 데이터 생성
169
+
170
+ ```typescript
171
+ // ✅ POST + inputValidator 필수
172
+ const createUserSchema = z.object({
173
+ email: z.email(),
174
+ name: z.string().min(1).max(100).trim(),
175
+ age: z.number().int().min(0).optional(),
176
+ })
33
177
 
34
- // POST + Validation + Auth
35
178
  export const createUser = createServerFn({ method: 'POST' })
179
+ .inputValidator(createUserSchema)
180
+ .handler(async ({ data }) => {
181
+ // data는 자동으로 검증됨
182
+ return prisma.user.create({ data })
183
+ })
184
+
185
+ // ✅ POST + inputValidator + 인증
186
+ export const createPost = createServerFn({ method: 'POST' })
36
187
  .middleware([authMiddleware])
188
+ .inputValidator(z.object({
189
+ title: z.string().min(1).max(200),
190
+ content: z.string().min(1),
191
+ }))
192
+ .handler(async ({ data, context }) => {
193
+ return prisma.post.create({
194
+ data: {
195
+ ...data,
196
+ authorId: context.user.id,
197
+ },
198
+ })
199
+ })
200
+
201
+ // ❌ inputValidator 없이 POST (금지)
202
+ export const badCreate = createServerFn({ method: 'POST' })
203
+ .handler(async ({ data }) => {
204
+ // data 타입 불안전, 검증 없음
205
+ return prisma.user.create({ data })
206
+ })
207
+ ```
208
+
209
+ ### PUT/PATCH: 데이터 수정
210
+
211
+ ```typescript
212
+ // ✅ PUT (전체 수정) + inputValidator 필수
213
+ const updateUserSchema = z.object({
214
+ id: z.string(),
215
+ email: z.email(),
216
+ name: z.string().min(1).max(100),
217
+ })
218
+
219
+ export const updateUser = createServerFn({ method: 'PUT' })
220
+ .middleware([authMiddleware])
221
+ .inputValidator(updateUserSchema)
222
+ .handler(async ({ data }) => {
223
+ return prisma.user.update({
224
+ where: { id: data.id },
225
+ data: { email: data.email, name: data.name },
226
+ })
227
+ })
228
+
229
+ // ✅ PATCH (부분 수정) + inputValidator 필수
230
+ const patchUserSchema = z.object({
231
+ id: z.string(),
232
+ name: z.string().min(1).max(100).optional(),
233
+ age: z.number().int().min(0).optional(),
234
+ })
235
+
236
+ export const patchUser = createServerFn({ method: 'PATCH' })
237
+ .middleware([authMiddleware])
238
+ .inputValidator(patchUserSchema)
239
+ .handler(async ({ data }) => {
240
+ const { id, ...updateData } = data
241
+ return prisma.user.update({
242
+ where: { id },
243
+ data: updateData,
244
+ })
245
+ })
246
+ ```
247
+
248
+ ### DELETE: 데이터 삭제
249
+
250
+ ```typescript
251
+ // ✅ DELETE + 인증
252
+ export const deletePost = createServerFn({ method: 'DELETE' })
253
+ .middleware([authMiddleware])
254
+ .inputValidator(z.object({ id: z.string() }))
255
+ .handler(async ({ data, context }) => {
256
+ // 권한 체크
257
+ const post = await prisma.post.findUnique({
258
+ where: { id: data.id },
259
+ })
260
+
261
+ if (post?.authorId !== context.user.id) {
262
+ throw new Error('Unauthorized')
263
+ }
264
+
265
+ return prisma.post.delete({
266
+ where: { id: data.id },
267
+ })
268
+ })
269
+ ```
270
+
271
+ ### 클라이언트에서 호출 (TanStack Query 필수)
272
+
273
+ ```tsx
274
+ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
275
+
276
+ // ✅ useQuery (조회)
277
+ const UsersPage = (): JSX.Element => {
278
+ const { data, isLoading, error } = useQuery({
279
+ queryKey: ['users'],
280
+ queryFn: () => getUsers(),
281
+ })
282
+
283
+ if (isLoading) return <div>Loading...</div>
284
+ if (error) return <div>Error: {error.message}</div>
285
+
286
+ return (
287
+ <ul>
288
+ {data?.map((user) => (
289
+ <li key={user.id}>{user.name}</li>
290
+ ))}
291
+ </ul>
292
+ )
293
+ }
294
+
295
+ // ✅ useMutation (생성/수정/삭제)
296
+ const CreateUserForm = (): JSX.Element => {
297
+ const queryClient = useQueryClient()
298
+
299
+ const mutation = useMutation({
300
+ mutationFn: createUser,
301
+ onSuccess: () => {
302
+ // 캐시 무효화 → 자동 리페치
303
+ queryClient.invalidateQueries({ queryKey: ['users'] })
304
+ },
305
+ })
306
+
307
+ const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
308
+ e.preventDefault()
309
+ const formData = new FormData(e.currentTarget)
310
+
311
+ mutation.mutate({
312
+ email: formData.get('email') as string,
313
+ name: formData.get('name') as string,
314
+ })
315
+ }
316
+
317
+ return (
318
+ <form onSubmit={handleSubmit}>
319
+ <input name="email" type="email" required />
320
+ <input name="name" required />
321
+ <button type="submit" disabled={mutation.isPending}>
322
+ {mutation.isPending ? 'Creating...' : 'Create'}
323
+ </button>
324
+ {mutation.error && <p>Error: {mutation.error.message}</p>}
325
+ </form>
326
+ )
327
+ }
328
+
329
+ // ❌ 직접 호출 금지 (캐싱 없음, 동기화 안됨)
330
+ const BadComponent = (): JSX.Element => {
331
+ const [users, setUsers] = useState([])
332
+
333
+ useEffect(() => {
334
+ getUsers().then(setUsers) // ❌ 직접 호출
335
+ }, [])
336
+
337
+ return <div>{/* ... */}</div>
338
+ }
339
+ ```
340
+
341
+ ### 함수 분리 규칙
342
+
343
+ ```typescript
344
+ // ❌ 잘못된 구조: 헬퍼 함수 export
345
+ export const validateUserData = async (email: string) => {
346
+ const exists = await prisma.user.findUnique({ where: { email } })
347
+ if (exists) throw new Error('Email already exists')
348
+ }
349
+
350
+ export const createUser = createServerFn({ method: 'POST' })
37
351
  .inputValidator(createUserSchema)
352
+ .handler(async ({ data }) => {
353
+ await validateUserData(data.email) // ❌ export된 헬퍼 사용
354
+ return prisma.user.create({ data })
355
+ })
356
+
357
+ // ✅ 올바른 구조: 헬퍼는 export 금지
358
+ const validateUserData = async (email: string) => {
359
+ const exists = await prisma.user.findUnique({ where: { email } })
360
+ if (exists) throw new Error('Email already exists')
361
+ }
362
+
363
+ export const createUser = createServerFn({ method: 'POST' })
364
+ .inputValidator(createUserSchema)
365
+ .handler(async ({ data }) => {
366
+ await validateUserData(data.email) // ✅ 내부 헬퍼만 사용
367
+ return prisma.user.create({ data })
368
+ })
369
+
370
+ // index.ts: Server Function만 export
371
+ export { getUsers, createUser, updateUser } from './user-functions'
372
+ // ❌ export { validateUserData } 금지
373
+ ```
374
+
375
+ ### 보안: 환경 변수
376
+
377
+ ```typescript
378
+ // ❌ loader에서 환경변수 직접 사용 (클라이언트 노출)
379
+ export const Route = createFileRoute('/config')({
380
+ loader: () => {
381
+ const secret = process.env.API_SECRET // ❌ 클라이언트에 노출됨!
382
+ return { secret }
383
+ },
384
+ component: ConfigPage,
385
+ })
386
+
387
+ // ✅ Server Function에서만 사용
388
+ const getConfig = createServerFn({ method: 'GET' })
389
+ .middleware([authMiddleware])
390
+ .handler(async () => {
391
+ const secret = process.env.API_SECRET // ✅ 서버에서만 실행
392
+ return { secret }
393
+ })
394
+
395
+ export const Route = createFileRoute('/config')({
396
+ loader: async () => {
397
+ const config = await getConfig()
398
+ return config
399
+ },
400
+ component: ConfigPage,
401
+ })
402
+ ```
403
+
404
+ </server_functions>
405
+
406
+ ---
407
+
408
+ <middleware>
409
+
410
+ ## Middleware
411
+
412
+ Server Function 및 라우트에 공통 로직 적용.
413
+
414
+ ### 기본 패턴
415
+
416
+ ```typescript
417
+ // 미들웨어 정의
418
+ const loggingMiddleware = createMiddleware({ type: 'function' })
419
+ .server(({ next }) => {
420
+ console.log('Processing request')
421
+ return next()
422
+ })
423
+
424
+ // Server Function에 적용
425
+ const fn = createServerFn({ method: 'GET' })
426
+ .middleware([loggingMiddleware])
427
+ .handler(async () => ({ message: 'Hello' }))
428
+ ```
429
+
430
+ ### 인증 미들웨어
431
+
432
+ ```typescript
433
+ // 세션 기반 인증
434
+ const authMiddleware = createMiddleware({ type: 'function' })
435
+ .server(async ({ next }) => {
436
+ const session = await getSession()
437
+ if (!session?.user) {
438
+ throw redirect({ to: '/login' })
439
+ }
440
+ return next({ context: { user: session.user } })
441
+ })
442
+
443
+ // Server Function에 적용
444
+ export const getMyPosts = createServerFn({ method: 'GET' })
445
+ .middleware([authMiddleware])
446
+ .handler(async ({ context }) => {
447
+ return prisma.post.findMany({
448
+ where: { authorId: context.user.id },
449
+ })
450
+ })
451
+
452
+ // 권한 체크 미들웨어
453
+ const adminMiddleware = createMiddleware({ type: 'function' })
454
+ .server(async ({ next }) => {
455
+ const session = await getSession()
456
+ if (!session?.user || session.user.role !== 'ADMIN') {
457
+ throw new Error('Forbidden: Admin only')
458
+ }
459
+ return next({ context: { user: session.user } })
460
+ })
461
+
462
+ // 적용
463
+ export const deleteAnyPost = createServerFn({ method: 'DELETE' })
464
+ .middleware([adminMiddleware])
465
+ .inputValidator(z.object({ id: z.string() }))
466
+ .handler(async ({ data }) => {
467
+ return prisma.post.delete({ where: { id: data.id } })
468
+ })
469
+ ```
470
+
471
+ ### Zod Validation Middleware
472
+
473
+ ```typescript
474
+ // 재사용 가능한 검증 미들웨어
475
+ const workspaceMiddleware = createMiddleware({ type: 'function' })
476
+ .inputValidator(z.object({ workspaceId: z.string() }))
477
+ .server(async ({ next, data }) => {
478
+ const workspace = await prisma.workspace.findUnique({
479
+ where: { id: data.workspaceId },
480
+ })
481
+
482
+ if (!workspace) {
483
+ throw new Error('Workspace not found')
484
+ }
485
+
486
+ return next({ context: { workspace } })
487
+ })
488
+
489
+ // 적용
490
+ export const getWorkspaceData = createServerFn({ method: 'GET' })
491
+ .middleware([authMiddleware, workspaceMiddleware])
492
+ .handler(async ({ context }) => {
493
+ return {
494
+ user: context.user,
495
+ workspace: context.workspace,
496
+ }
497
+ })
498
+ ```
499
+
500
+ ### 미들웨어 체이닝
501
+
502
+ ```typescript
503
+ // 여러 미들웨어 조합
504
+ export const protectedWorkspaceFn = createServerFn({ method: 'POST' })
505
+ .middleware([
506
+ loggingMiddleware, // 1. 로깅
507
+ authMiddleware, // 2. 인증
508
+ workspaceMiddleware, // 3. Workspace 검증
509
+ ])
510
+ .inputValidator(taskSchema)
511
+ .handler(async ({ data, context }) => {
512
+ return prisma.task.create({
513
+ data: {
514
+ ...data,
515
+ workspaceId: context.workspace.id,
516
+ createdById: context.user.id,
517
+ },
518
+ })
519
+ })
520
+ ```
521
+
522
+ ### Global Middleware
523
+
524
+ ```typescript
525
+ // src/start.ts
526
+ export const startInstance = createStart(() => ({
527
+ requestMiddleware: [corsMiddleware], // 모든 요청
528
+ functionMiddleware: [loggingMiddleware], // 모든 Server Function
529
+ }))
530
+ ```
531
+
532
+ ### Route-Level Middleware
533
+
534
+ ```typescript
535
+ // 특정 라우트에만 적용
536
+ export const Route = createFileRoute('/admin/dashboard')({
537
+ server: {
538
+ middleware: [adminMiddleware], // 이 라우트의 모든 핸들러에 적용
539
+ handlers: {
540
+ GET: async () => new Response('Admin Dashboard'),
541
+ },
542
+ },
543
+ })
544
+ ```
545
+
546
+ </middleware>
547
+
548
+ ---
549
+
550
+ <routing>
551
+
552
+ ## Routing
553
+
554
+ File-based routing with TanStack Router.
555
+
556
+ ### 파일 구조 → URL 매핑
557
+
558
+ | 파일 경로 | URL | 설명 |
559
+ |----------|-----|------|
560
+ | `routes/index.tsx` | `/` | 홈 페이지 |
561
+ | `routes/about.tsx` | `/about` | 정적 라우트 |
562
+ | `routes/users/$id.tsx` | `/users/:id` | 동적 파라미터 |
563
+ | `routes/users/index.tsx` | `/users` | 사용자 목록 |
564
+ | `routes/$.tsx` | `/*` | Catch-all (404) |
565
+ | `routes/__root.tsx` | - | Root layout |
566
+
567
+ ### 기본 라우트
568
+
569
+ ```tsx
570
+ // routes/about.tsx
571
+ export const Route = createFileRoute('/about')({
572
+ component: AboutPage,
573
+ })
574
+
575
+ const AboutPage = (): JSX.Element => {
576
+ return <div>About Page</div>
577
+ }
578
+ ```
579
+
580
+ ### Loader: 데이터 사전 로드
581
+
582
+ ```tsx
583
+ // routes/posts/index.tsx
584
+ export const Route = createFileRoute('/posts')({
585
+ component: PostsPage,
586
+ loader: async () => {
587
+ const posts = await getPosts()
588
+ return { posts }
589
+ },
590
+ })
591
+
592
+ const PostsPage = (): JSX.Element => {
593
+ const { posts } = Route.useLoaderData()
594
+
595
+ return (
596
+ <ul>
597
+ {posts.map((post) => (
598
+ <li key={post.id}>{post.title}</li>
599
+ ))}
600
+ </ul>
601
+ )
602
+ }
603
+ ```
604
+
605
+ ### 동적 라우트
606
+
607
+ ```tsx
608
+ // routes/users/$id.tsx
609
+ export const Route = createFileRoute('/users/$id')({
610
+ loader: async ({ params }) => {
611
+ const user = await getUserById(params.id)
612
+ return { user }
613
+ },
614
+ component: UserPage,
615
+ })
616
+
617
+ const UserPage = (): JSX.Element => {
618
+ const { user } = Route.useLoaderData()
619
+
620
+ return (
621
+ <div>
622
+ <h1>{user.name}</h1>
623
+ <p>{user.email}</p>
624
+ </div>
625
+ )
626
+ }
627
+ ```
628
+
629
+ ### SSR 옵션
630
+
631
+ | 옵션 | 동작 | 사용 시점 |
632
+ |------|------|----------|
633
+ | `ssr: true` | 전체 SSR (기본값) | 일반 페이지 |
634
+ | `ssr: false` | 클라이언트만 렌더링 | 인증 필요 페이지 |
635
+ | `ssr: 'data-only'` | 데이터만 서버에서 로드 | 데이터 + 클라이언트 렌더링 |
636
+
637
+ ```tsx
638
+ // ssr 옵션 예시
639
+ export const Route = createFileRoute('/dashboard')({
640
+ ssr: false, // 클라이언트에서만 렌더링
641
+ component: DashboardPage,
642
+ })
643
+ ```
644
+
645
+ ### beforeLoad: 라우트 접근 전 체크
646
+
647
+ ```tsx
648
+ // routes/dashboard.tsx
649
+ export const Route = createFileRoute('/dashboard')({
650
+ beforeLoad: async () => {
651
+ const user = await getCurrentUser()
652
+ if (!user) {
653
+ throw redirect({ to: '/login' })
654
+ }
655
+ return { user }
656
+ },
657
+ component: DashboardPage,
658
+ })
659
+
660
+ const DashboardPage = (): JSX.Element => {
661
+ const { user } = Route.useRouteContext()
662
+ return <h1>Welcome, {user.name}!</h1>
663
+ }
664
+ ```
665
+
666
+ ### Server Routes (API 엔드포인트)
667
+
668
+ ```tsx
669
+ // routes/api/hello.tsx
670
+ export const Route = createFileRoute('/api/hello')({
671
+ server: {
672
+ handlers: {
673
+ GET: async () => {
674
+ return new Response('Hello World')
675
+ },
676
+ POST: async ({ request }) => {
677
+ const body = await request.json()
678
+ return Response.json({ name: body.name })
679
+ },
680
+ },
681
+ },
682
+ })
683
+ ```
684
+
685
+ ### Catch-all Route (404)
686
+
687
+ ```tsx
688
+ // routes/$.tsx
689
+ export const Route = createFileRoute('/$')({
690
+ component: NotFoundPage,
691
+ })
692
+
693
+ const NotFoundPage = (): JSX.Element => {
694
+ return (
695
+ <div>
696
+ <h1>404 - Page Not Found</h1>
697
+ <a href="/">Go Home</a>
698
+ </div>
699
+ )
700
+ }
701
+ ```
702
+
703
+ </routing>
704
+
705
+ ---
706
+
707
+ <auth_patterns>
708
+
709
+ ## 인증 패턴
710
+
711
+ Better Auth 통합 및 인증 패턴.
712
+
713
+ ### Better Auth 설정
714
+
715
+ ```typescript
716
+ // lib/auth.ts
717
+ import { betterAuth } from 'better-auth'
718
+ import { prismaAdapter } from 'better-auth/adapters/prisma'
719
+ import { prisma } from '@/database/prisma'
720
+
721
+ export const auth = betterAuth({
722
+ database: prismaAdapter(prisma),
723
+ emailAndPassword: { enabled: true },
724
+ session: {
725
+ expiresIn: 60 * 60 * 24 * 7, // 7일
726
+ updateAge: 60 * 60 * 24, // 1일마다 갱신
727
+ },
728
+ })
729
+
730
+ export type Session = typeof auth.$Infer.Session
731
+ ```
732
+
733
+ ### 인증 Server Functions
734
+
735
+ ```typescript
736
+ // functions/auth.ts
737
+
738
+ // 로그인
739
+ const loginSchema = z.object({
740
+ email: z.email(),
741
+ password: z.string().min(8),
742
+ })
743
+
744
+ export const login = createServerFn({ method: 'POST' })
745
+ .inputValidator(loginSchema)
746
+ .handler(async ({ data, request }) => {
747
+ const result = await auth.api.signInEmail({
748
+ email: data.email,
749
+ password: data.password,
750
+ headers: request.headers,
751
+ })
752
+
753
+ if (!result.user) {
754
+ throw new Error('Invalid credentials')
755
+ }
756
+
757
+ throw redirect({ to: '/dashboard' })
758
+ })
759
+
760
+ // 로그아웃
761
+ export const logout = createServerFn({ method: 'POST' })
762
+ .handler(async ({ request }) => {
763
+ await auth.api.signOut({ headers: request.headers })
764
+ throw redirect({ to: '/' })
765
+ })
766
+
767
+ // 현재 사용자
768
+ export const getCurrentUser = createServerFn({ method: 'GET' })
769
+ .handler(async ({ request }) => {
770
+ const session = await auth.api.getSession({
771
+ headers: request.headers,
772
+ })
773
+ return session?.user ?? null
774
+ })
775
+
776
+ // 회원가입
777
+ const registerSchema = z.object({
778
+ email: z.email(),
779
+ password: z.string().min(8),
780
+ name: z.string().min(1),
781
+ })
782
+
783
+ export const register = createServerFn({ method: 'POST' })
784
+ .inputValidator(registerSchema)
785
+ .handler(async ({ data, request }) => {
786
+ const result = await auth.api.signUpEmail({
787
+ email: data.email,
788
+ password: data.password,
789
+ name: data.name,
790
+ headers: request.headers,
791
+ })
792
+
793
+ if (!result.user) {
794
+ throw new Error('Registration failed')
795
+ }
796
+
797
+ throw redirect({ to: '/dashboard' })
798
+ })
799
+ ```
800
+
801
+ ### 인증 미들웨어
802
+
803
+ ```typescript
804
+ // middleware/auth.ts
805
+ export const authMiddleware = createMiddleware({ type: 'function' })
806
+ .server(async ({ next, request }) => {
807
+ const session = await auth.api.getSession({
808
+ headers: request.headers,
809
+ })
810
+
811
+ if (!session?.user) {
812
+ throw redirect({ to: '/login' })
813
+ }
814
+
815
+ return next({ context: { user: session.user } })
816
+ })
817
+ ```
818
+
819
+ ### 보호된 Server Function
820
+
821
+ ```typescript
822
+ // functions/posts.ts
823
+ export const getMyPosts = createServerFn({ method: 'GET' })
824
+ .middleware([authMiddleware])
825
+ .handler(async ({ context }) => {
826
+ return prisma.post.findMany({
827
+ where: { authorId: context.user.id },
828
+ })
829
+ })
830
+
831
+ export const createPost = createServerFn({ method: 'POST' })
832
+ .middleware([authMiddleware])
833
+ .inputValidator(z.object({
834
+ title: z.string().min(1),
835
+ content: z.string().min(1),
836
+ }))
837
+ .handler(async ({ data, context }) => {
838
+ return prisma.post.create({
839
+ data: {
840
+ ...data,
841
+ authorId: context.user.id,
842
+ },
843
+ })
844
+ })
845
+ ```
846
+
847
+ ### 보호된 라우트
848
+
849
+ ```tsx
850
+ // routes/dashboard.tsx
851
+ export const Route = createFileRoute('/dashboard')({
852
+ beforeLoad: async () => {
853
+ const user = await getCurrentUser()
854
+ if (!user) {
855
+ throw redirect({ to: '/login' })
856
+ }
857
+ return { user }
858
+ },
859
+ component: DashboardPage,
860
+ })
861
+
862
+ const DashboardPage = (): JSX.Element => {
863
+ const { user } = Route.useRouteContext()
864
+
865
+ return (
866
+ <div>
867
+ <h1>Welcome, {user.name}!</h1>
868
+ </div>
869
+ )
870
+ }
871
+ ```
872
+
873
+ ### 로그인 폼 (TanStack Query)
874
+
875
+ ```tsx
876
+ // routes/login.tsx
877
+ const LoginPage = (): JSX.Element => {
878
+ const mutation = useMutation({
879
+ mutationFn: login,
880
+ })
881
+
882
+ const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
883
+ e.preventDefault()
884
+ const formData = new FormData(e.currentTarget)
885
+
886
+ mutation.mutate({
887
+ email: formData.get('email') as string,
888
+ password: formData.get('password') as string,
889
+ })
890
+ }
891
+
892
+ return (
893
+ <form onSubmit={handleSubmit}>
894
+ <input name="email" type="email" required />
895
+ <input name="password" type="password" required />
896
+ <button type="submit" disabled={mutation.isPending}>
897
+ {mutation.isPending ? 'Logging in...' : 'Login'}
898
+ </button>
899
+ {mutation.error && <p>Error: {mutation.error.message}</p>}
900
+ </form>
901
+ )
902
+ }
903
+ ```
904
+
905
+ </auth_patterns>
906
+
907
+ ---
908
+
909
+ <file_structure>
910
+
911
+ ## 파일 구조
912
+
913
+ ### 디렉터리 레이아웃
914
+
915
+ ```
916
+ src/
917
+ ├── routes/ # File-based routes
918
+ │ ├── __root.tsx # Root layout (모든 라우트의 부모)
919
+ │ ├── index.tsx # / (홈)
920
+ │ ├── about.tsx # /about
921
+ │ ├── users/
922
+ │ │ ├── index.tsx # /users
923
+ │ │ ├── $id.tsx # /users/:id
924
+ │ │ ├── -components/ # 페이지 전용 컴포넌트 (필수)
925
+ │ │ │ └── user-card.tsx
926
+ │ │ ├── -hooks/ # 페이지 전용 Custom Hooks (필수)
927
+ │ │ │ └── use-user-data.ts
928
+ │ │ └── -functions/ # 페이지 전용 Server Functions (필수)
929
+ │ │ └── user-mutations.ts
930
+ │ └── dashboard/
931
+ │ ├── index.tsx
932
+ │ ├── -components/
933
+ │ ├── -hooks/
934
+ │ └── -functions/
935
+
936
+ ├── functions/ # 공통 Server Functions
937
+ │ ├── auth.ts
938
+ │ ├── posts.ts
939
+ │ └── users.ts
940
+
941
+ ├── middleware/ # 공통 Middleware
942
+ │ ├── auth.ts
943
+ │ └── logging.ts
944
+
945
+ ├── components/ # 공통 컴포넌트
946
+ │ └── ui/
947
+ │ ├── button.tsx
948
+ │ └── input.tsx
949
+
950
+ ├── lib/ # 유틸리티
951
+ │ ├── env.ts
952
+ │ └── utils.ts
953
+
954
+ └── database/ # Prisma
955
+ └── prisma.ts
956
+ ```
957
+
958
+ ### 파일 분리 규칙
959
+
960
+ | 위치 | 사용 시점 | 예시 |
961
+ |------|----------|------|
962
+ | `routes/[path]/-components/` | 해당 페이지에서만 사용 | `user-card.tsx` |
963
+ | `routes/[path]/-hooks/` | 해당 페이지 전용 Hook | `use-user-data.ts` |
964
+ | `routes/[path]/-functions/` | 해당 페이지 전용 Server Function | `user-mutations.ts` |
965
+ | `@/functions/` | 여러 페이지에서 재사용 | `auth.ts`, `posts.ts` |
966
+ | `@/components/` | 공통 UI 컴포넌트 | `button.tsx`, `input.tsx` |
967
+ | `@/middleware/` | 공통 미들웨어 | `auth.ts`, `logging.ts` |
968
+
969
+ ### 필수 규칙
970
+
971
+ ✅ **페이지당 `-components/`, `-hooks/`, `-functions/` 폴더 필수**
972
+ - Custom Hook은 페이지 크기와 무관하게 **반드시** `-hooks/` 폴더에 분리
973
+ - 줄 수 무관: 10줄이든 100줄이든 분리 필수
974
+
975
+ ✅ **공통 함수 → `@/functions/`**
976
+ - 여러 페이지에서 사용하는 Server Function
977
+
978
+ ✅ **라우트 전용 → `routes/[경로]/-functions/`**
979
+ - 해당 라우트에서만 사용하는 Server Function
980
+
981
+ ❌ **index.ts에서 내부 헬퍼 함수 export 금지**
982
+ - Server Function만 export
983
+
984
+ </file_structure>
985
+
986
+ ---
987
+
988
+ <dos_donts>
989
+
990
+ ## Do's & Don'ts
991
+
992
+ ### Server Functions
993
+
994
+ | ✅ Do | ❌ Don't |
995
+ |-------|----------|
996
+ | `createServerFn({ method: 'POST' })` 사용 | 일반 async 함수를 API로 사용 |
997
+ | POST/PUT/PATCH에 `.inputValidator()` 필수 | handler 내부에서 수동 검증 |
998
+ | 인증 필요 시 `.middleware([authMiddleware])` | handler 내부에서 세션 체크 |
999
+ | `.inputValidator(zodSchema)` 사용 (v1) | `.validator()` 사용 (deprecated) |
1000
+ | 명시적 return type 선언 | any 타입 사용 |
1001
+ | 내부 헬퍼는 export 금지 | 헬퍼 함수 export |
1002
+ | Server Function에서만 `process.env` 사용 | loader에서 환경변수 직접 사용 |
1003
+
1004
+ ### 예시: Server Functions
1005
+
1006
+ ```typescript
1007
+ // ✅ 올바른 패턴
1008
+ const schema = z.object({
1009
+ email: z.email(),
1010
+ name: z.string().min(1),
1011
+ })
1012
+
1013
+ export const createUser = createServerFn({ method: 'POST' })
1014
+ .middleware([authMiddleware])
1015
+ .inputValidator(schema)
1016
+ .handler(async ({ data, context }): Promise<User> => {
1017
+ return prisma.user.create({
1018
+ data: {
1019
+ ...data,
1020
+ createdBy: context.user.id,
1021
+ },
1022
+ })
1023
+ })
1024
+
1025
+ // ❌ 잘못된 패턴
1026
+ export const badCreate = async (data: any) => { // ❌ createServerFn 없음, any 타입
1027
+ // ❌ 수동 검증
1028
+ if (!data.email || !data.name) {
1029
+ throw new Error('Invalid data')
1030
+ }
1031
+
1032
+ // ❌ 수동 인증 체크
1033
+ const session = await getSession()
1034
+ if (!session) {
1035
+ throw new Error('Unauthorized')
1036
+ }
1037
+
1038
+ return prisma.user.create({ data })
1039
+ }
1040
+ ```
1041
+
1042
+ ### 클라이언트 호출
1043
+
1044
+ | ✅ Do | ❌ Don't |
1045
+ |-------|----------|
1046
+ | TanStack Query 사용 (`useQuery`/`useMutation`) | Server Function 직접 호출 |
1047
+ | `queryClient.invalidateQueries()` 로 동기화 | 수동 상태 관리 |
1048
+ | `isLoading`, `error` 상태 활용 | try-catch로 에러 처리 |
1049
+ | `queryKey`로 캐싱 관리 | useEffect + useState |
1050
+
1051
+ ### 예시: 클라이언트 호출
1052
+
1053
+ ```tsx
1054
+ // ✅ 올바른 패턴 (useQuery)
1055
+ const UsersPage = (): JSX.Element => {
1056
+ const { data, isLoading, error } = useQuery({
1057
+ queryKey: ['users'],
1058
+ queryFn: () => getUsers(),
1059
+ })
1060
+
1061
+ if (isLoading) return <div>Loading...</div>
1062
+ if (error) return <div>Error: {error.message}</div>
1063
+
1064
+ return <ul>{data?.map(u => <li key={u.id}>{u.name}</li>)}</ul>
1065
+ }
1066
+
1067
+ // ✅ 올바른 패턴 (useMutation)
1068
+ const CreateForm = (): JSX.Element => {
1069
+ const queryClient = useQueryClient()
1070
+
1071
+ const mutation = useMutation({
1072
+ mutationFn: createUser,
1073
+ onSuccess: () => {
1074
+ queryClient.invalidateQueries({ queryKey: ['users'] })
1075
+ },
1076
+ })
1077
+
1078
+ return (
1079
+ <form onSubmit={(e) => {
1080
+ e.preventDefault()
1081
+ const formData = new FormData(e.currentTarget)
1082
+ mutation.mutate({
1083
+ email: formData.get('email') as string,
1084
+ name: formData.get('name') as string,
1085
+ })
1086
+ }}>
1087
+ <input name="email" type="email" />
1088
+ <input name="name" />
1089
+ <button type="submit">Create</button>
1090
+ </form>
1091
+ )
1092
+ }
1093
+
1094
+ // ❌ 잘못된 패턴
1095
+ const BadComponent = (): JSX.Element => {
1096
+ const [users, setUsers] = useState([])
1097
+ const [loading, setLoading] = useState(false)
1098
+
1099
+ useEffect(() => {
1100
+ setLoading(true)
1101
+ getUsers() // ❌ 직접 호출 (캐싱 없음)
1102
+ .then(setUsers)
1103
+ .catch(console.error)
1104
+ .finally(() => setLoading(false))
1105
+ }, [])
1106
+
1107
+ return <div>{/* ... */}</div>
1108
+ }
1109
+ ```
1110
+
1111
+ ### Middleware
1112
+
1113
+ | ✅ Do | ❌ Don't |
1114
+ |-------|----------|
1115
+ | 인증/권한 체크를 미들웨어로 분리 | handler 내부에서 체크 |
1116
+ | `context`로 데이터 전달 | 전역 변수 사용 |
1117
+ | 여러 미들웨어 체이닝 가능 | 하나의 미들웨어에 모든 로직 |
1118
+
1119
+ ### 예시: Middleware
1120
+
1121
+ ```typescript
1122
+ // ✅ 올바른 패턴
1123
+ const authMiddleware = createMiddleware({ type: 'function' })
1124
+ .server(async ({ next }) => {
1125
+ const session = await getSession()
1126
+ if (!session) throw redirect({ to: '/login' })
1127
+ return next({ context: { user: session.user } })
1128
+ })
1129
+
1130
+ export const fn = createServerFn({ method: 'GET' })
1131
+ .middleware([authMiddleware])
1132
+ .handler(async ({ context }) => {
1133
+ return { user: context.user }
1134
+ })
1135
+
1136
+ // ❌ 잘못된 패턴
1137
+ export const badFn = createServerFn({ method: 'GET' })
1138
+ .handler(async () => {
1139
+ // ❌ handler 내부에서 인증 체크
1140
+ const session = await getSession()
1141
+ if (!session) throw redirect({ to: '/login' })
1142
+
1143
+ return { user: session.user }
1144
+ })
1145
+ ```
1146
+
1147
+ ### 파일 구조
1148
+
1149
+ | ✅ Do | ❌ Don't |
1150
+ |-------|----------|
1151
+ | 페이지 전용: `routes/[path]/-functions/` | 모든 함수를 `@/functions/`에 |
1152
+ | 공통 함수: `@/functions/` | 라우트 파일에 함수 직접 작성 |
1153
+ | Custom Hook: `-hooks/` 폴더에 분리 (필수) | 라우트 파일에 Hook 작성 |
1154
+
1155
+ ### 예시: 파일 구조
1156
+
1157
+ ```typescript
1158
+ // ✅ 올바른 구조
1159
+ // routes/users/-functions/user-mutations.ts
1160
+ export const createUser = createServerFn({ method: 'POST' })
1161
+ .inputValidator(schema)
38
1162
  .handler(async ({ data }) => prisma.user.create({ data }))
39
1163
 
40
- // Route with Loader
1164
+ // routes/users/-hooks/use-user-form.ts
1165
+ export const useUserForm = () => {
1166
+ const mutation = useMutation({ mutationFn: createUser })
1167
+ return { mutation }
1168
+ }
1169
+
1170
+ // routes/users/index.tsx
1171
+ import { useUserForm } from './-hooks/use-user-form'
1172
+ import { createUser } from './-functions/user-mutations'
1173
+
1174
+ // ❌ 잘못된 구조
1175
+ // routes/users/index.tsx (모든 로직이 한 파일에)
1176
+ const createUser = createServerFn({ method: 'POST' })
1177
+ .inputValidator(schema)
1178
+ .handler(async ({ data }) => prisma.user.create({ data }))
1179
+
1180
+ const useUserForm = () => {
1181
+ const mutation = useMutation({ mutationFn: createUser })
1182
+ return { mutation }
1183
+ }
1184
+
41
1185
  export const Route = createFileRoute('/users')({
42
1186
  component: UsersPage,
43
- loader: async () => ({ users: await getUsers() }),
1187
+ })
1188
+ ```
1189
+
1190
+ ### 환경 변수
1191
+
1192
+ | ✅ Do | ❌ Don't |
1193
+ |-------|----------|
1194
+ | Server Function에서만 `process.env` 사용 | loader에서 직접 사용 |
1195
+ | Zod로 환경 변수 검증 | 검증 없이 사용 |
1196
+
1197
+ ### 예시: 환경 변수
1198
+
1199
+ ```typescript
1200
+ // ✅ 올바른 패턴
1201
+ // lib/env.ts
1202
+ const envSchema = z.object({
1203
+ DATABASE_URL: z.string().url(),
1204
+ API_SECRET: z.string().min(32),
1205
+ })
1206
+
1207
+ export const env = envSchema.parse(process.env)
1208
+
1209
+ // functions/config.ts
1210
+ const getConfig = createServerFn({ method: 'GET' })
1211
+ .middleware([authMiddleware])
1212
+ .handler(async () => {
1213
+ return {
1214
+ secret: env.API_SECRET, // ✅ 서버에서만 실행
1215
+ }
1216
+ })
1217
+
1218
+ // ❌ 잘못된 패턴
1219
+ export const Route = createFileRoute('/config')({
1220
+ loader: () => {
1221
+ const secret = process.env.API_SECRET // ❌ 클라이언트에 노출됨!
1222
+ return { secret }
1223
+ },
1224
+ })
1225
+ ```
1226
+
1227
+ </dos_donts>
1228
+
1229
+ ---
1230
+
1231
+ <quick_reference>
1232
+
1233
+ ## Quick Reference
1234
+
1235
+ ### GET: 데이터 조회
1236
+
1237
+ ```typescript
1238
+ export const getUsers = createServerFn({ method: 'GET' })
1239
+ .handler(async () => prisma.user.findMany())
1240
+
1241
+ export const getMyProfile = createServerFn({ method: 'GET' })
1242
+ .middleware([authMiddleware])
1243
+ .handler(async ({ context }) => context.user)
1244
+ ```
1245
+
1246
+ ### POST: 데이터 생성 (inputValidator 필수)
1247
+
1248
+ ```typescript
1249
+ const schema = z.object({
1250
+ email: z.email(),
1251
+ name: z.string().min(1),
1252
+ })
1253
+
1254
+ export const createUser = createServerFn({ method: 'POST' })
1255
+ .middleware([authMiddleware])
1256
+ .inputValidator(schema)
1257
+ .handler(async ({ data, context }) => {
1258
+ return prisma.user.create({
1259
+ data: { ...data, createdBy: context.user.id },
1260
+ })
1261
+ })
1262
+ ```
1263
+
1264
+ ### PUT/PATCH: 데이터 수정 (inputValidator 필수)
1265
+
1266
+ ```typescript
1267
+ export const updateUser = createServerFn({ method: 'PUT' })
1268
+ .middleware([authMiddleware])
1269
+ .inputValidator(z.object({ id: z.string(), name: z.string() }))
1270
+ .handler(async ({ data }) => {
1271
+ return prisma.user.update({
1272
+ where: { id: data.id },
1273
+ data: { name: data.name },
1274
+ })
1275
+ })
1276
+ ```
1277
+
1278
+ ### DELETE: 데이터 삭제
1279
+
1280
+ ```typescript
1281
+ export const deletePost = createServerFn({ method: 'DELETE' })
1282
+ .middleware([authMiddleware])
1283
+ .inputValidator(z.object({ id: z.string() }))
1284
+ .handler(async ({ data }) => {
1285
+ return prisma.post.delete({ where: { id: data.id } })
1286
+ })
1287
+ ```
1288
+
1289
+ ### Loader: 데이터 사전 로드
1290
+
1291
+ ```typescript
1292
+ export const Route = createFileRoute('/users')({
1293
+ component: UsersPage,
1294
+ loader: async () => {
1295
+ const users = await getUsers()
1296
+ return { users }
1297
+ },
44
1298
  })
45
1299
 
46
- // Using Loader data
47
1300
  const UsersPage = (): JSX.Element => {
48
1301
  const { users } = Route.useLoaderData()
49
- return <div>{/* render */}</div>
1302
+ return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
50
1303
  }
51
1304
  ```
52
1305
 
53
- ### Structure
1306
+ ### TanStack Query: useQuery
54
1307
 
1308
+ ```typescript
1309
+ const { data, isLoading, error } = useQuery({
1310
+ queryKey: ['posts'],
1311
+ queryFn: () => getPosts(),
1312
+ })
55
1313
  ```
56
- routes/
57
- ├── __root.tsx # Root layout
58
- ├── index.tsx # /
59
- ├── users/
60
- │ ├── index.tsx # /users
61
- │ ├── $id.tsx # /users/:id
62
- │ ├── -components/ # Page-specific
63
- │ └── -functions/ # Page-specific Server Functions
64
1314
 
65
- Shared functions @/functions/
66
- Route-specific → routes/[path]/-functions/
1315
+ ### TanStack Query: useMutation
1316
+
1317
+ ```typescript
1318
+ const queryClient = useQueryClient()
1319
+
1320
+ const mutation = useMutation({
1321
+ mutationFn: createPost,
1322
+ onSuccess: () => {
1323
+ queryClient.invalidateQueries({ queryKey: ['posts'] })
1324
+ },
1325
+ })
1326
+
1327
+ mutation.mutate({ title: 'New Post', content: 'Content' })
1328
+ ```
1329
+
1330
+ ### 인증 미들웨어
1331
+
1332
+ ```typescript
1333
+ const authMiddleware = createMiddleware({ type: 'function' })
1334
+ .server(async ({ next }) => {
1335
+ const session = await getSession()
1336
+ if (!session) throw redirect({ to: '/login' })
1337
+ return next({ context: { user: session.user } })
1338
+ })
67
1339
  ```
1340
+
1341
+ ### 보호된 라우트
1342
+
1343
+ ```typescript
1344
+ export const Route = createFileRoute('/dashboard')({
1345
+ beforeLoad: async () => {
1346
+ const user = await getCurrentUser()
1347
+ if (!user) throw redirect({ to: '/login' })
1348
+ return { user }
1349
+ },
1350
+ component: DashboardPage,
1351
+ })
1352
+ ```
1353
+
1354
+ </quick_reference>
1355
+
1356
+ ---
1357
+
1358
+ <version_info>
1359
+
1360
+ **Version:** v1.x
1361
+
1362
+ **Key Changes in v1:**
1363
+ - `.inputValidator()` (v1) replaces `.validator()` (deprecated)
1364
+ - Enhanced middleware system with context passing
1365
+ - Improved type safety for Server Functions
1366
+ - Better TanStack Query integration
1367
+
1368
+ </version_info>