@kood/claude-code 0.6.6 → 0.7.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.
Files changed (170) hide show
  1. package/dist/index.js +7 -1
  2. package/package.json +1 -1
  3. package/templates/.claude/agents/analyst.md +5 -0
  4. package/templates/.claude/agents/architect.md +5 -0
  5. package/templates/.claude/agents/build-fixer.md +1 -0
  6. package/templates/.claude/agents/code-reviewer.md +1 -0
  7. package/templates/.claude/agents/critic.md +4 -0
  8. package/templates/.claude/agents/deep-executor.md +1 -0
  9. package/templates/.claude/agents/dependency-manager.md +2 -0
  10. package/templates/.claude/agents/deployment-validator.md +2 -0
  11. package/templates/.claude/agents/designer.md +2 -0
  12. package/templates/.claude/agents/document-writer.md +3 -0
  13. package/templates/.claude/agents/explore.md +1 -0
  14. package/templates/.claude/agents/git-operator.md +2 -0
  15. package/templates/.claude/agents/implementation-executor.md +2 -0
  16. package/templates/.claude/agents/ko-to-en-translator.md +3 -0
  17. package/templates/.claude/agents/lint-fixer.md +2 -0
  18. package/templates/.claude/agents/planner.md +3 -0
  19. package/templates/.claude/agents/pm.md +349 -0
  20. package/templates/.claude/agents/qa-tester.md +1 -0
  21. package/templates/.claude/agents/refactor-advisor.md +4 -0
  22. package/templates/.claude/agents/researcher.md +9 -1
  23. package/templates/.claude/agents/scientist.md +1 -0
  24. package/templates/.claude/agents/security-reviewer.md +1 -0
  25. package/templates/.claude/agents/tdd-guide.md +1 -0
  26. package/templates/.claude/agents/vision.md +1 -0
  27. package/templates/.claude/instructions/agent-patterns/agent-teams-usage.md +376 -0
  28. package/templates/.claude/instructions/sourcing/reliable-search.md +49 -2
  29. package/templates/.claude/scripts/agent-teams/check-availability.sh +238 -0
  30. package/templates/.claude/scripts/agent-teams/setup-tmux.sh +125 -0
  31. package/templates/.claude/skills/agent-teams-setup/SKILL.md +460 -0
  32. package/templates/.claude/skills/brainstorm/SKILL.md +1 -0
  33. package/templates/.claude/skills/bug-fix/SKILL.md +1 -0
  34. package/templates/.claude/skills/crawler/SKILL.md +2 -0
  35. package/templates/.claude/skills/docs-creator/SKILL.md +1 -0
  36. package/templates/.claude/skills/docs-fetch/SKILL.md +6 -4
  37. package/templates/.claude/skills/docs-refactor/SKILL.md +1 -0
  38. package/templates/.claude/skills/elon-musk/SKILL.md +1 -0
  39. package/templates/.claude/skills/execute/SKILL.md +1 -0
  40. package/templates/.claude/skills/feedback/SKILL.md +1 -0
  41. package/templates/.claude/skills/figma-to-code/SKILL.md +1 -0
  42. package/templates/.claude/skills/genius-thinking/SKILL.md +1 -0
  43. package/templates/.claude/skills/global-uiux-design/SKILL.md +1 -0
  44. package/templates/.claude/skills/korea-uiux-design/SKILL.md +1 -0
  45. package/templates/.claude/skills/nextjs-react-best-practices/SKILL.md +1 -0
  46. package/templates/.claude/skills/plan/SKILL.md +1 -0
  47. package/templates/.claude/skills/prd/SKILL.md +1 -0
  48. package/templates/.claude/skills/project-optimizer/AGENTS.md +275 -0
  49. package/templates/.claude/skills/project-optimizer/SKILL.md +375 -0
  50. package/templates/.claude/skills/project-optimizer/rules/arch-config-centralize.md +66 -0
  51. package/templates/.claude/skills/project-optimizer/rules/arch-hot-path.md +35 -0
  52. package/templates/.claude/skills/project-optimizer/rules/arch-interface-segregation.md +51 -0
  53. package/templates/.claude/skills/project-optimizer/rules/arch-module-boundary.md +42 -0
  54. package/templates/.claude/skills/project-optimizer/rules/build-cache.md +57 -0
  55. package/templates/.claude/skills/project-optimizer/rules/build-code-split.md +56 -0
  56. package/templates/.claude/skills/project-optimizer/rules/build-incremental.md +65 -0
  57. package/templates/.claude/skills/project-optimizer/rules/build-minify.md +61 -0
  58. package/templates/.claude/skills/project-optimizer/rules/build-tree-shake.md +60 -0
  59. package/templates/.claude/skills/project-optimizer/rules/code-complexity.md +65 -0
  60. package/templates/.claude/skills/project-optimizer/rules/code-dead-elimination.md +32 -0
  61. package/templates/.claude/skills/project-optimizer/rules/code-duplication.md +54 -0
  62. package/templates/.claude/skills/project-optimizer/rules/code-error-handling.md +75 -0
  63. package/templates/.claude/skills/project-optimizer/rules/code-naming.md +52 -0
  64. package/templates/.claude/skills/project-optimizer/rules/concurrency-defer-await.md +54 -0
  65. package/templates/.claude/skills/project-optimizer/rules/concurrency-parallel.md +90 -0
  66. package/templates/.claude/skills/project-optimizer/rules/concurrency-pipeline.md +68 -0
  67. package/templates/.claude/skills/project-optimizer/rules/concurrency-pool.md +68 -0
  68. package/templates/.claude/skills/project-optimizer/rules/deps-lightweight-alt.md +37 -0
  69. package/templates/.claude/skills/project-optimizer/rules/deps-peer-align.md +44 -0
  70. package/templates/.claude/skills/project-optimizer/rules/deps-security-audit.md +45 -0
  71. package/templates/.claude/skills/project-optimizer/rules/deps-unused-removal.md +25 -0
  72. package/templates/.claude/skills/project-optimizer/rules/deps-version-pin.md +40 -0
  73. package/templates/.claude/skills/project-optimizer/rules/dx-ci-speed.md +47 -0
  74. package/templates/.claude/skills/project-optimizer/rules/dx-dev-server.md +35 -0
  75. package/templates/.claude/skills/project-optimizer/rules/dx-lint-config.md +36 -0
  76. package/templates/.claude/skills/project-optimizer/rules/dx-test-coverage.md +34 -0
  77. package/templates/.claude/skills/project-optimizer/rules/dx-type-safety.md +49 -0
  78. package/templates/.claude/skills/project-optimizer/rules/io-batch-queries.md +67 -0
  79. package/templates/.claude/skills/project-optimizer/rules/io-cache-layer.md +67 -0
  80. package/templates/.claude/skills/project-optimizer/rules/io-connection-reuse.md +67 -0
  81. package/templates/.claude/skills/project-optimizer/rules/io-serialize-minimal.md +61 -0
  82. package/templates/.claude/skills/project-optimizer/rules/io-stream.md +75 -0
  83. package/templates/.claude/skills/project-optimizer/rules/memory-bounded-cache.md +65 -0
  84. package/templates/.claude/skills/project-optimizer/rules/memory-large-data.md +64 -0
  85. package/templates/.claude/skills/project-optimizer/rules/memory-lazy-init.md +78 -0
  86. package/templates/.claude/skills/project-optimizer/rules/memory-leak-prevention.md +79 -0
  87. package/templates/.claude/skills/project-optimizer/rules/memory-pool-reuse.md +70 -0
  88. package/templates/.claude/skills/ralph/SKILL.md +1 -0
  89. package/templates/.claude/skills/refactor/SKILL.md +1 -0
  90. package/templates/.claude/skills/research/SKILL.md +1 -0
  91. package/templates/.claude/skills/sql-optimizer/SKILL.md +438 -0
  92. package/templates/.claude/skills/sql-optimizer/orm-patterns.md +218 -0
  93. package/templates/.claude/skills/startup-validator/SKILL.md +1 -0
  94. package/templates/.claude/skills/tanstack-start-react-best-practices/AGENTS.md +53 -14
  95. package/templates/.claude/skills/tanstack-start-react-best-practices/SKILL.md +94 -27
  96. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/bundle-defer-third-party.md +42 -19
  97. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/client-optimistic-updates.md +109 -0
  98. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/client-suspense-query.md +74 -0
  99. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/client-use-hook.md +81 -0
  100. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/rerender-react-compiler.md +81 -0
  101. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-beforeload-auth.md +121 -0
  102. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-file-conventions.md +104 -0
  103. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-link-navigation.md +119 -0
  104. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-nested-layouts.md +155 -0
  105. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-path-params.md +89 -0
  106. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-pending-component.md +110 -0
  107. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-preload-strategy.md +91 -0
  108. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-router-context.md +120 -0
  109. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/routing-search-params.md +114 -0
  110. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-deferred-data.md +1 -1
  111. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-error-boundaries.md +79 -0
  112. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-middleware.md +85 -0
  113. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-serialization.md +56 -21
  114. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-streaming.md +84 -0
  115. package/templates/.claude/skills/tanstack-start-react-best-practices/rules/server-validator.md +71 -0
  116. package/templates/.claude/skills/tauri-react-best-practices/AGENTS.md +527 -0
  117. package/templates/.claude/skills/tauri-react-best-practices/SKILL.md +571 -0
  118. package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-barrel-imports.md +140 -0
  119. package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-cargo-profile.md +96 -0
  120. package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-frontend-treeshake.md +242 -0
  121. package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-lazy-components.md +255 -0
  122. package/templates/.claude/skills/tauri-react-best-practices/rules/bundle-remove-unused-commands.md +160 -0
  123. package/templates/.claude/skills/tauri-react-best-practices/rules/deploy-ci-pipeline.md +269 -0
  124. package/templates/.claude/skills/tauri-react-best-practices/rules/deploy-signing.md +207 -0
  125. package/templates/.claude/skills/tauri-react-best-practices/rules/deploy-updater.md +226 -0
  126. package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-async-commands.md +172 -0
  127. package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-batch-commands.md +133 -0
  128. package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-binary-response.md +198 -0
  129. package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-channel-streaming.md +186 -0
  130. package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-error-handling.md +250 -0
  131. package/templates/.claude/skills/tauri-react-best-practices/rules/ipc-type-safe.md +227 -0
  132. package/templates/.claude/skills/tauri-react-best-practices/rules/perf-derived-state.md +231 -0
  133. package/templates/.claude/skills/tauri-react-best-practices/rules/perf-functional-setstate.md +191 -0
  134. package/templates/.claude/skills/tauri-react-best-practices/rules/perf-index-maps.md +276 -0
  135. package/templates/.claude/skills/tauri-react-best-practices/rules/perf-lazy-state-init.md +196 -0
  136. package/templates/.claude/skills/tauri-react-best-practices/rules/plugin-lifecycle.md +265 -0
  137. package/templates/.claude/skills/tauri-react-best-practices/rules/plugin-mobile-compat.md +199 -0
  138. package/templates/.claude/skills/tauri-react-best-practices/rules/plugin-permission-scope.md +193 -0
  139. package/templates/.claude/skills/tauri-react-best-practices/rules/react-error-boundary.md +239 -0
  140. package/templates/.claude/skills/tauri-react-best-practices/rules/react-event-listener.md +151 -0
  141. package/templates/.claude/skills/tauri-react-best-practices/rules/react-file-src.md +155 -0
  142. package/templates/.claude/skills/tauri-react-best-practices/rules/react-invoke-hook.md +139 -0
  143. package/templates/.claude/skills/tauri-react-best-practices/rules/react-optimistic-update.md +211 -0
  144. package/templates/.claude/skills/tauri-react-best-practices/rules/security-capability-split.md +205 -0
  145. package/templates/.claude/skills/tauri-react-best-practices/rules/security-csp.md +207 -0
  146. package/templates/.claude/skills/tauri-react-best-practices/rules/security-least-privilege.md +106 -0
  147. package/templates/.claude/skills/tauri-react-best-practices/rules/security-no-wildcard.md +253 -0
  148. package/templates/.claude/skills/tauri-react-best-practices/rules/security-scope-paths.md +160 -0
  149. package/templates/.claude/skills/tauri-react-best-practices/rules/state-async-mutex.md +270 -0
  150. package/templates/.claude/skills/tauri-react-best-practices/rules/state-mutex-pattern.md +265 -0
  151. package/templates/.claude/skills/tauri-react-best-practices/rules/state-react-sync.md +375 -0
  152. package/templates/.claude/skills/tauri-react-best-practices/rules/state-single-container.md +275 -0
  153. package/templates/tanstack-start/docs/architecture.md +238 -167
  154. package/templates/tanstack-start/docs/library/tanstack-router/error-handling.md +777 -38
  155. package/templates/tanstack-start/docs/library/tanstack-router/hooks.md +549 -37
  156. package/templates/tanstack-start/docs/library/tanstack-router/index.md +895 -111
  157. package/templates/tanstack-start/docs/library/tanstack-router/navigation.md +641 -43
  158. package/templates/tanstack-start/docs/library/tanstack-router/route-context.md +889 -38
  159. package/templates/tanstack-start/docs/library/tanstack-router/search-params.md +891 -29
  160. package/templates/tanstack-start/docs/library/tanstack-start/auth-patterns.md +972 -36
  161. package/templates/tanstack-start/docs/library/tanstack-start/index.md +1525 -881
  162. package/templates/tanstack-start/docs/library/tanstack-start/middleware.md +1099 -20
  163. package/templates/tanstack-start/docs/library/tanstack-start/routing.md +796 -30
  164. package/templates/tanstack-start/docs/library/tanstack-start/server-functions.md +953 -35
  165. package/templates/tanstack-start/docs/library/tanstack-start/setup.md +371 -15
  166. package/templates/tauri/CLAUDE.md +189 -0
  167. package/templates/tauri/docs/guides/distribution.md +261 -0
  168. package/templates/tauri/docs/guides/getting-started.md +302 -0
  169. package/templates/tauri/docs/guides/mobile.md +288 -0
  170. package/templates/tauri/docs/library/tauri/index.md +510 -0
@@ -1,29 +1,34 @@
1
- # TanStack Start
2
-
3
- > v1 | Full-stack React Framework
4
-
5
- ---
1
+ # TanStack Start Documentation
6
2
 
7
3
  <context>
8
4
 
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 통합
5
+ **목적**: TanStack Start 애플리케이션 개발을 위한 AI Agent 최적화 문서
17
6
 
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)
7
+ **버전**: v1.x (RC, ~v1.159.x)
25
8
 
26
- **Version:** v1.x
9
+ **핵심 특징**:
10
+ - Type-safe, full-stack React framework
11
+ - File-based routing with Vite-powered build
12
+ - Server Functions로 type-safe API 구축
13
+ - Built-in SWR caching (staleTime, gcTime, shouldReload)
14
+ - Streaming SSR 지원
15
+ - TanStack Query 통합
16
+ - Multiple deployment targets (Cloudflare, Netlify, Vercel, Railway, Node, Bun)
17
+ - React Server Components 지원 예정 (non-breaking v1.x addition)
18
+
19
+ **주요 패키지 변경사항**:
20
+ - `@tanstack/start` → `@tanstack/react-start` (v1.121.0+)
21
+ - Build system: Vinxi → Vite
22
+ - `.validator()` → `.inputValidator()` (deprecated)
23
+ - `createAPIFileRoute()` → `createServerFileRoute()` with `.methods()`
24
+
25
+ **런타임 요구사항**:
26
+ | 패키지 | 버전 |
27
+ |--------|------|
28
+ | Node.js | >=22.12.0 |
29
+ | React | 18+ or 19+ |
30
+ | Vite | 7+ |
31
+ | TypeScript | 5.x |
27
32
 
28
33
  </context>
29
34
 
@@ -31,17 +36,20 @@
31
36
 
32
37
  <forbidden>
33
38
 
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()` 사용 |
39
+ | 패턴 | 이유 |
40
+ |------|------|
41
+ | `@tanstack/start` 패키지 사용 | v1.121.0+부터 `@tanstack/react-start`로 변경됨 |
42
+ | `.validator()` 사용 | Deprecated. `.inputValidator()` 사용 필수 |
43
+ | `createAPIFileRoute()` 사용 | Deprecated. `createServerFileRoute().methods()` 사용 |
44
+ | Vinxi config 참조 | Vite로 마이그레이션됨 |
45
+ | Server Function `import.meta.env` | 클라이언트 전용. `process.env` 사용 |
46
+ | Client에서 `process.env` | 서버 전용. `import.meta.env.VITE_*` 사용 |
47
+ | `tsconfig.json`에 `verbatimModuleSyntax: true` | TanStack Start와 호환성 문제 발생 |
48
+ | Server Function에 민감 정보 직접 노출 | 클라이언트 번들에 포함됨. Helper function 분리 필수 |
49
+ | `any` 타입 사용 | Type safety 상실. `unknown` 또는 명시적 타입 사용 |
50
+ | Middleware 없이 인증 로직 | 보안 취약. `createMiddleware` 사용 필수 |
51
+ | Route 파일에 비즈니스 로직 | 관심사 분리. `-functions/` 디렉토리 사용 |
52
+ | `gcTime: 0` 남발 | 캐시 무효화. 필요한 경우만 사용 |
45
53
 
46
54
  </forbidden>
47
55
 
@@ -49,16 +57,20 @@
49
57
 
50
58
  <required>
51
59
 
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 내부에서만 | 클라이언트 노출 방지 |
60
+ | 패턴 | 세부사항 |
61
+ |------|----------|
62
+ | `@tanstack/react-start` 패키지 | v1.121.0+ 필수 패키지명 |
63
+ | `.inputValidator()` | POST/PUT/PATCH/DELETE Server Function에 필수 |
64
+ | Zod schema 검증 | 모든 입력값 검증 (`z.email()`, `z.url()` 등) |
65
+ | Middleware로 인증 처리 | `createMiddleware`로 auth 로직 중앙화 |
66
+ | Helper functions 분리 | Server Function 외부에 민감 로직 격리 |
67
+ | 명시적 return type | 모든 함수에 타입 명시 |
68
+ | `strictNullChecks: true` | TypeScript config에 권장 |
69
+ | Server Routes는 `createServerFileRoute()` | API 엔드포인트용 API |
70
+ | Environment variables 분리 | Server: `process.env`, Client: `import.meta.env.VITE_*` |
71
+ | TanStack Query로 Server Function 호출 | `useSuspenseQuery`, `useMutation` 사용 |
72
+ | `-components/`, `-hooks/`, `-functions/` 컨벤션 | 파일 구조화 규칙 |
73
+ | `beforeLoad`로 인증 가드 | Route-level 접근 제어 |
62
74
 
63
75
  </required>
64
76
 
@@ -69,52 +81,118 @@
69
81
  ## 설치
70
82
 
71
83
  ```bash
72
- yarn add @tanstack/react-start @tanstack/react-router vinxi
73
- yarn add -D vite @vitejs/plugin-react vite-tsconfig-paths
84
+ # New project
85
+ npm create @tanstack/start@latest
86
+
87
+ # Dependencies
88
+ npm install @tanstack/react-start@latest @tanstack/react-router@latest
89
+ npm install -D @tanstack/router-plugin vite typescript
74
90
  ```
75
91
 
76
- ## 설정
92
+ ## vite.config.ts
77
93
 
78
94
  ```typescript
79
- // vite.config.ts
80
95
  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'
96
+ import react from '@vitejs/plugin-react'
97
+ import { tanstackRouterPlugin } from '@tanstack/router-plugin/vite'
84
98
 
85
99
  export default defineConfig({
86
- server: { port: 3000 },
87
- plugins: [tsConfigPaths(), tanstackStart(), viteReact()],
100
+ plugins: [
101
+ tanstackRouterPlugin(),
102
+ react(),
103
+ ],
104
+ server: {
105
+ port: 3000,
106
+ },
88
107
  })
89
108
  ```
90
109
 
110
+ ## tsconfig.json
111
+
91
112
  ```json
92
- // tsconfig.json
93
113
  {
94
114
  "compilerOptions": {
95
115
  "target": "ES2022",
116
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
96
117
  "module": "ESNext",
97
- "moduleResolution": "bundler",
98
- "strict": true,
118
+ "moduleResolution": "Bundler",
99
119
  "jsx": "react-jsx",
100
- "paths": { "@/*": ["./src/*"] }
120
+ "strict": false,
121
+ "strictNullChecks": true,
122
+ "esModuleInterop": true,
123
+ "skipLibCheck": true,
124
+ "resolveJsonModule": true,
125
+ "isolatedModules": true,
126
+ "noEmit": true
127
+ },
128
+ "include": ["app/**/*"],
129
+ "exclude": ["node_modules"]
130
+ }
131
+ ```
132
+
133
+ **주의**: `verbatimModuleSyntax: true` 사용 금지 (호환성 문제)
134
+
135
+ ## Environment Variables
136
+
137
+ ### Server-only (.env)
138
+
139
+ ```bash
140
+ # Server-only (process.env)
141
+ DATABASE_URL="postgresql://..."
142
+ AUTH_SECRET="secret-key"
143
+ STRIPE_SECRET_KEY="sk_test_..."
144
+ ```
145
+
146
+ ### Client-exposed (.env)
147
+
148
+ ```bash
149
+ # Client-exposed (import.meta.env.VITE_*)
150
+ VITE_API_URL="https://api.example.com"
151
+ VITE_PUBLIC_KEY="pk_test_..."
152
+ ```
153
+
154
+ ### 사용 예시
155
+
156
+ ```typescript
157
+ // ✅ Server Function
158
+ export const getSecretData = createServerFn({ method: 'GET' }).handler(
159
+ async () => {
160
+ const secret = process.env.AUTH_SECRET // OK
161
+ return { data: 'protected' }
101
162
  }
163
+ )
164
+
165
+ // ✅ Client Component
166
+ function App() {
167
+ const apiUrl = import.meta.env.VITE_API_URL // OK
168
+ return <div>{apiUrl}</div>
102
169
  }
170
+
171
+ // ❌ Wrong
172
+ export const wrongFn = createServerFn({ method: 'GET' }).handler(async () => {
173
+ const key = import.meta.env.VITE_SECRET // ❌ 서버에서 import.meta.env 사용 금지
174
+ return key
175
+ })
103
176
  ```
104
177
 
105
- ## 환경 변수 검증
178
+ ### Validation (app/env.ts)
106
179
 
107
180
  ```typescript
108
- // lib/env.ts
109
181
  import { z } from 'zod'
110
182
 
111
- const envSchema = z.object({
112
- NODE_ENV: z.enum(['development', 'production', 'test']),
183
+ const serverEnvSchema = z.object({
113
184
  DATABASE_URL: z.string().url(),
114
- API_SECRET: z.string().min(32),
185
+ AUTH_SECRET: z.string().min(32),
186
+ NODE_ENV: z.enum(['development', 'production', 'test']),
187
+ })
188
+
189
+ const clientEnvSchema = z.object({
190
+ VITE_API_URL: z.string().url(),
115
191
  })
116
192
 
117
- export const env = envSchema.parse(process.env)
193
+ export const serverEnv = serverEnvSchema.parse(process.env)
194
+
195
+ export const clientEnv = clientEnvSchema.parse(import.meta.env)
118
196
  ```
119
197
 
120
198
  </setup>
@@ -123,184 +201,164 @@ export const env = envSchema.parse(process.env)
123
201
 
124
202
  <server_functions>
125
203
 
126
- ## Server Functions
204
+ ## Server Functions 개요
127
205
 
128
- 서버에서만 실행되는 타입 안전한 함수.
206
+ Server Functions는 type-safe한 API 엔드포인트를 생성하는 TanStack Start의 핵심 기능입니다.
129
207
 
130
- ### 기본 패턴
208
+ **주요 특징**:
209
+ - Type-safe client-server communication
210
+ - Automatic serialization/deserialization
211
+ - Middleware support
212
+ - Input validation with Zod
213
+ - Method-based routing (GET, POST, PUT, PATCH, DELETE)
131
214
 
132
- | 메서드 | 사용 시점 | inputValidator | middleware |
133
- |--------|----------|---------------|-----------|
134
- | **GET** | 데이터 조회 | ❌ 선택 | ✅ 인증 시 필수 |
135
- | **POST** | 데이터 생성 | ✅ 필수 | ✅ 인증 시 필수 |
136
- | **PUT** | 전체 수정 | ✅ 필수 | ✅ 인증 시 필수 |
137
- | **PATCH** | 부분 수정 | ✅ 필수 | ✅ 인증 시 필수 |
138
- | **DELETE** | 삭제 | ❌ 선택 | ✅ 인증 시 필수 |
215
+ ## createServerFn 패턴
139
216
 
140
- ### GET: 데이터 조회
217
+ ### GET (Query)
141
218
 
142
219
  ```typescript
143
- // ✅ 기본 GET
144
- export const getUsers = createServerFn({ method: 'GET' })
145
- .handler(async () => {
146
- return prisma.user.findMany()
147
- })
220
+ // app/functions/users.ts
221
+ import { createServerFn } from '@tanstack/react-start'
222
+ import { z } from 'zod'
148
223
 
149
- // GET + 인증
150
- export const getMyProfile = createServerFn({ method: 'GET' })
151
- .middleware([authMiddleware])
152
- .handler(async ({ context }) => {
153
- return prisma.user.findUnique({
154
- where: { id: context.user.id },
155
- })
156
- })
224
+ const getUserSchema = z.object({
225
+ id: z.string().uuid(),
226
+ })
157
227
 
158
- // GET + Query Params (선택적 검증)
159
- export const getUserById = createServerFn({ method: 'GET' })
160
- .inputValidator(z.object({ id: z.string() }))
228
+ export const getUser = createServerFn({ method: 'GET' })
229
+ .inputValidator(getUserSchema)
161
230
  .handler(async ({ data }) => {
162
- return prisma.user.findUnique({
163
- where: { id: data.id },
164
- })
231
+ const user = await db.user.findUnique({ where: { id: data.id } })
232
+ if (!user) throw new Error('User not found')
233
+ return user
165
234
  })
166
235
  ```
167
236
 
168
- ### POST: 데이터 생성
237
+ ### POST (Create)
169
238
 
170
239
  ```typescript
171
- // ✅ POST + inputValidator 필수
240
+ // app/functions/users.ts
241
+ import { createServerFn } from '@tanstack/react-start'
242
+ import { z } from 'zod'
243
+
172
244
  const createUserSchema = z.object({
173
245
  email: z.email(),
174
- name: z.string().min(1).max(100).trim(),
175
- age: z.number().int().min(0).optional(),
246
+ name: z.string().min(2),
247
+ website: z.url().optional(),
176
248
  })
177
249
 
178
250
  export const createUser = createServerFn({ method: 'POST' })
179
251
  .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' })
187
252
  .middleware([authMiddleware])
188
- .inputValidator(z.object({
189
- title: z.string().min(1).max(200),
190
- content: z.string().min(1),
191
- }))
192
253
  .handler(async ({ data, context }) => {
193
- return prisma.post.create({
254
+ const user = await db.user.create({
194
255
  data: {
195
256
  ...data,
196
- authorId: context.user.id,
257
+ createdBy: context.user.id,
197
258
  },
198
259
  })
199
- })
200
-
201
- // ❌ inputValidator 없이 POST (금지)
202
- export const badCreate = createServerFn({ method: 'POST' })
203
- .handler(async ({ data }) => {
204
- // data 타입 불안전, 검증 없음
205
- return prisma.user.create({ data })
260
+ return user
206
261
  })
207
262
  ```
208
263
 
209
- ### PUT/PATCH: 데이터 수정
264
+ ### PUT/PATCH (Update)
210
265
 
211
266
  ```typescript
212
- // ✅ PUT (전체 수정) + inputValidator 필수
267
+ // app/functions/users.ts
213
268
  const updateUserSchema = z.object({
214
- id: z.string(),
215
- email: z.email(),
216
- name: z.string().min(1).max(100),
269
+ id: z.string().uuid(),
270
+ email: z.email().optional(),
271
+ name: z.string().min(2).optional(),
217
272
  })
218
273
 
219
274
  export const updateUser = createServerFn({ method: 'PUT' })
220
- .middleware([authMiddleware])
221
275
  .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
- })
276
+ .middleware([authMiddleware])
277
+ .handler(async ({ data, context }) => {
278
+ const { id, ...updates } = data
228
279
 
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
- })
280
+ // Authorization check
281
+ if (context.user.id !== id && context.user.role !== 'admin') {
282
+ throw new Error('Unauthorized')
283
+ }
235
284
 
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({
285
+ const user = await db.user.update({
242
286
  where: { id },
243
- data: updateData,
287
+ data: updates,
244
288
  })
289
+ return user
245
290
  })
246
291
  ```
247
292
 
248
- ### DELETE: 데이터 삭제
293
+ ### DELETE
249
294
 
250
295
  ```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
- }
296
+ // app/functions/users.ts
297
+ const deleteUserSchema = z.object({
298
+ id: z.string().uuid(),
299
+ })
264
300
 
265
- return prisma.post.delete({
266
- where: { id: data.id },
267
- })
301
+ export const deleteUser = createServerFn({ method: 'DELETE' })
302
+ .inputValidator(deleteUserSchema)
303
+ .middleware([authMiddleware, adminMiddleware])
304
+ .handler(async ({ data }) => {
305
+ await db.user.delete({ where: { id: data.id } })
306
+ return { success: true }
268
307
  })
269
308
  ```
270
309
 
271
- ### 클라이언트에서 호출 (TanStack Query 필수)
310
+ ## Client에서 호출 (TanStack Query)
272
311
 
273
- ```tsx
274
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
312
+ ### useSuspenseQuery (GET)
275
313
 
276
- // ✅ useQuery (조회)
277
- const UsersPage = (): JSX.Element => {
278
- const { data, isLoading, error } = useQuery({
279
- queryKey: ['users'],
280
- queryFn: () => getUsers(),
281
- })
314
+ ```typescript
315
+ // app/routes/users.$userId.tsx
316
+ import { createFileRoute } from '@tanstack/react-router'
317
+ import { useSuspenseQuery } from '@tanstack/react-query'
318
+ import { getUser } from '~/functions/users'
319
+
320
+ export const Route = createFileRoute('/users/$userId')({
321
+ component: UserDetail,
322
+ })
323
+
324
+ function UserDetail() {
325
+ const { userId } = Route.useParams()
282
326
 
283
- if (isLoading) return <div>Loading...</div>
284
- if (error) return <div>Error: {error.message}</div>
327
+ const { data: user } = useSuspenseQuery({
328
+ queryKey: ['user', userId],
329
+ queryFn: () => getUser({ data: { id: userId } }),
330
+ })
285
331
 
286
332
  return (
287
- <ul>
288
- {data?.map((user) => (
289
- <li key={user.id}>{user.name}</li>
290
- ))}
291
- </ul>
333
+ <div>
334
+ <h1>{user.name}</h1>
335
+ <p>{user.email}</p>
336
+ </div>
292
337
  )
293
338
  }
339
+ ```
340
+
341
+ ### useMutation (POST/PUT/DELETE)
342
+
343
+ ```typescript
344
+ // app/routes/users.new.tsx
345
+ import { createFileRoute } from '@tanstack/react-router'
346
+ import { useMutation, useQueryClient } from '@tanstack/react-query'
347
+ import { createUser } from '~/functions/users'
294
348
 
295
- // useMutation (생성/수정/삭제)
296
- const CreateUserForm = (): JSX.Element => {
349
+ export const Route = createFileRoute('/users/new')({
350
+ component: NewUser,
351
+ })
352
+
353
+ function NewUser() {
297
354
  const queryClient = useQueryClient()
355
+ const navigate = Route.useNavigate()
298
356
 
299
357
  const mutation = useMutation({
300
358
  mutationFn: createUser,
301
- onSuccess: () => {
302
- // 캐시 무효화 → 자동 리페치
359
+ onSuccess: (user) => {
303
360
  queryClient.invalidateQueries({ queryKey: ['users'] })
361
+ navigate({ to: '/users/$userId', params: { userId: user.id } })
304
362
  },
305
363
  })
306
364
 
@@ -309,96 +367,123 @@ const CreateUserForm = (): JSX.Element => {
309
367
  const formData = new FormData(e.currentTarget)
310
368
 
311
369
  mutation.mutate({
312
- email: formData.get('email') as string,
313
- name: formData.get('name') as string,
370
+ data: {
371
+ email: formData.get('email') as string,
372
+ name: formData.get('name') as string,
373
+ },
314
374
  })
315
375
  }
316
376
 
317
377
  return (
318
378
  <form onSubmit={handleSubmit}>
319
379
  <input name="email" type="email" required />
320
- <input name="name" required />
380
+ <input name="name" type="text" required />
321
381
  <button type="submit" disabled={mutation.isPending}>
322
- {mutation.isPending ? 'Creating...' : 'Create'}
382
+ {mutation.isPending ? 'Creating...' : 'Create User'}
323
383
  </button>
324
- {mutation.error && <p>Error: {mutation.error.message}</p>}
384
+ {mutation.isError && <p>Error: {mutation.error.message}</p>}
325
385
  </form>
326
386
  )
327
387
  }
388
+ ```
328
389
 
329
- // 직접 호출 금지 (캐싱 없음, 동기화 안됨)
330
- const BadComponent = (): JSX.Element => {
331
- const [users, setUsers] = useState([])
332
-
333
- useEffect(() => {
334
- getUsers().then(setUsers) // ❌ 직접 호출
335
- }, [])
390
+ ## Helper Functions 패턴
336
391
 
337
- return <div>{/* ... */}</div>
338
- }
339
- ```
392
+ **규칙**: 민감한 로직(DB 쿼리, 인증, 비즈니스 로직)은 Server Function 외부에 분리
340
393
 
341
- ### 함수 분리 규칙
394
+ ### 올바른 패턴
342
395
 
343
396
  ```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')
397
+ // app/functions/users-helpers.ts
398
+ import 'server-only' // Ensures this file is never bundled for client
399
+
400
+ export async function getUserFromDB(id: string) {
401
+ const user = await db.user.findUnique({ where: { id } })
402
+ if (!user) throw new Error('User not found')
403
+ return user
348
404
  }
349
405
 
406
+ export async function createUserInDB(data: {
407
+ email: string
408
+ name: string
409
+ passwordHash: string
410
+ }) {
411
+ return db.user.create({ data })
412
+ }
413
+
414
+ // app/functions/users.ts
415
+ import { createServerFn } from '@tanstack/react-start'
416
+ import { getUserFromDB, createUserInDB } from './users-helpers'
417
+
418
+ export const getUser = createServerFn({ method: 'GET' })
419
+ .inputValidator(getUserSchema)
420
+ .handler(async ({ data }) => getUserFromDB(data.id))
421
+
350
422
  export const createUser = createServerFn({ method: 'POST' })
351
423
  .inputValidator(createUserSchema)
424
+ .middleware([authMiddleware])
352
425
  .handler(async ({ data }) => {
353
- await validateUserData(data.email) // ❌ export된 헬퍼 사용
354
- return prisma.user.create({ data })
426
+ const passwordHash = await hashPassword(data.password)
427
+ return createUserInDB({ ...data, passwordHash })
355
428
  })
429
+ ```
356
430
 
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
- }
431
+ ### 잘못된 패턴
362
432
 
363
- export const createUser = createServerFn({ method: 'POST' })
364
- .inputValidator(createUserSchema)
433
+ ```typescript
434
+ // ❌ Server Function 내부에 직접 DB 로직
435
+ export const getUser = createServerFn({ method: 'GET' })
436
+ .inputValidator(getUserSchema)
365
437
  .handler(async ({ data }) => {
366
- await validateUserData(data.email) // 내부 헬퍼만 사용
367
- return prisma.user.create({ data })
438
+ // 민감 로직이 클라이언트 번들에 포함될 수 있음
439
+ const user = await db.user.findUnique({ where: { id: data.id } })
440
+ return user
368
441
  })
369
-
370
- // index.ts: Server Function만 export
371
- export { getUsers, createUser, updateUser } from './user-functions'
372
- // ❌ export { validateUserData } 금지
373
442
  ```
374
443
 
375
- ### 보안: 환경 변수
444
+ ## Environment Variables 보안
376
445
 
377
446
  ```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,
447
+ // Helper function에서 env 사용
448
+ // app/functions/stripe-helpers.ts
449
+ import 'server-only'
450
+ import Stripe from 'stripe'
451
+
452
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
453
+ apiVersion: '2023-10-16',
385
454
  })
386
455
 
387
- // Server Function에서만 사용
388
- const getConfig = createServerFn({ method: 'GET' })
456
+ export async function createPaymentIntent(amount: number) {
457
+ return stripe.paymentIntents.create({
458
+ amount,
459
+ currency: 'usd',
460
+ })
461
+ }
462
+
463
+ // app/functions/stripe.ts
464
+ import { createServerFn } from '@tanstack/react-start'
465
+ import { createPaymentIntent } from './stripe-helpers'
466
+
467
+ export const createPayment = createServerFn({ method: 'POST' })
468
+ .inputValidator(z.object({ amount: z.number().positive() }))
389
469
  .middleware([authMiddleware])
470
+ .handler(async ({ data }) => createPaymentIntent(data.amount))
471
+ ```
472
+
473
+ ## Static Server Functions (Experimental)
474
+
475
+ 정적 사이트 생성 시 Server Functions를 빌드 타임에 실행:
476
+
477
+ ```typescript
478
+ import { createServerFn } from '@tanstack/react-start'
479
+ import { staticFunctionMiddleware } from '@tanstack/react-start/static'
480
+
481
+ export const getStaticPosts = createServerFn({ method: 'GET' })
482
+ .middleware([staticFunctionMiddleware])
390
483
  .handler(async () => {
391
- const secret = process.env.API_SECRET // ✅ 서버에서만 실행
392
- return { secret }
484
+ const posts = await fetchAllPosts()
485
+ return posts
393
486
  })
394
-
395
- export const Route = createFileRoute('/config')({
396
- loader: async () => {
397
- const config = await getConfig()
398
- return config
399
- },
400
- component: ConfigPage,
401
- })
402
487
  ```
403
488
 
404
489
  </server_functions>
@@ -407,189 +492,272 @@ export const Route = createFileRoute('/config')({
407
492
 
408
493
  <middleware>
409
494
 
410
- ## Middleware
411
-
412
- Server Function 및 라우트에 공통 로직 적용.
495
+ ## Middleware 개요
413
496
 
414
- ### 기본 패턴
415
-
416
- ```typescript
417
- // 미들웨어 정의
418
- const loggingMiddleware = createMiddleware({ type: 'function' })
419
- .server(({ next }) => {
420
- console.log('Processing request')
421
- return next()
422
- })
497
+ Middleware는 Server Functions와 Routes에서 재사용 가능한 로직을 제공합니다.
423
498
 
424
- // Server Function에 적용
425
- const fn = createServerFn({ method: 'GET' })
426
- .middleware([loggingMiddleware])
427
- .handler(async () => ({ message: 'Hello' }))
428
- ```
499
+ **사용 사례**:
500
+ - 인증/인가
501
+ - 입력 검증
502
+ - 로깅
503
+ - Rate limiting
504
+ - CORS 설정
429
505
 
430
- ### 인증 미들웨어
506
+ ## createMiddleware
431
507
 
432
508
  ```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
- })
509
+ // app/middleware/auth.ts
510
+ import { createMiddleware } from '@tanstack/react-start'
511
+ import { getSessionFromCookie } from '~/auth-helpers'
442
512
 
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
- })
513
+ export const authMiddleware = createMiddleware().server(async ({ next }) => {
514
+ const session = await getSessionFromCookie()
451
515
 
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
- })
516
+ if (!session) {
517
+ throw new Error('Unauthorized')
518
+ }
461
519
 
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 } })
520
+ return next({
521
+ context: {
522
+ user: session.user,
523
+ sessionId: session.id,
524
+ },
468
525
  })
526
+ })
469
527
  ```
470
528
 
471
- ### Zod Validation Middleware
529
+ ## Zod Validation Middleware
472
530
 
473
531
  ```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
- })
532
+ // app/middleware/validation.ts
533
+ import { createMiddleware } from '@tanstack/react-start'
534
+ import { z } from 'zod'
481
535
 
482
- if (!workspace) {
483
- throw new Error('Workspace not found')
484
- }
536
+ export const createValidationMiddleware = <T extends z.ZodType>(
537
+ schema: T
538
+ ) => {
539
+ return createMiddleware().server(async ({ next, data }) => {
540
+ const validated = schema.parse(data)
485
541
 
486
- return next({ context: { workspace } })
542
+ return next({
543
+ context: {
544
+ validated: validated as z.infer<T>,
545
+ },
546
+ })
487
547
  })
548
+ }
549
+
550
+ // Usage
551
+ const userSchema = z.object({ email: z.email() })
552
+ const validateUser = createValidationMiddleware(userSchema)
488
553
 
489
- // 적용
490
- export const getWorkspaceData = createServerFn({ method: 'GET' })
491
- .middleware([authMiddleware, workspaceMiddleware])
554
+ export const createUser = createServerFn({ method: 'POST' })
555
+ .middleware([validateUser])
492
556
  .handler(async ({ context }) => {
493
- return {
494
- user: context.user,
495
- workspace: context.workspace,
496
- }
557
+ const { validated } = context // Type-safe
558
+ return db.user.create({ data: validated })
497
559
  })
498
560
  ```
499
561
 
500
- ### 미들웨어 체이닝
562
+ ## Middleware Chaining
501
563
 
502
564
  ```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,
565
+ // app/middleware/admin.ts
566
+ import { createMiddleware } from '@tanstack/react-start'
567
+
568
+ export const adminMiddleware = createMiddleware().server(
569
+ async ({ next, context }) => {
570
+ // authMiddleware에서 context.user 주입됨
571
+ if (context.user.role !== 'admin') {
572
+ throw new Error('Forbidden: Admin access required')
573
+ }
574
+
575
+ return next({
576
+ context: {
577
+ ...context,
578
+ isAdmin: true,
517
579
  },
518
580
  })
581
+ }
582
+ )
583
+
584
+ // Usage: authMiddleware → adminMiddleware 순서로 실행
585
+ export const deleteUser = createServerFn({ method: 'DELETE' })
586
+ .inputValidator(deleteUserSchema)
587
+ .middleware([authMiddleware, adminMiddleware])
588
+ .handler(async ({ data, context }) => {
589
+ console.log(context.isAdmin) // true
590
+ await db.user.delete({ where: { id: data.id } })
591
+ return { success: true }
519
592
  })
520
593
  ```
521
594
 
522
- ### Global Middleware
595
+ ## Global Middleware
523
596
 
524
597
  ```typescript
525
- // src/start.ts
526
- export const startInstance = createStart(() => ({
527
- requestMiddleware: [corsMiddleware], // 모든 요청
528
- functionMiddleware: [loggingMiddleware], // 모든 Server Function
529
- }))
598
+ // app/router.tsx
599
+ import { createRouter } from '@tanstack/react-router'
600
+ import { loggingMiddleware } from '~/middleware/logging'
601
+
602
+ export const router = createRouter({
603
+ routeTree,
604
+ defaultPreload: 'intent',
605
+ context: {
606
+ // Global context
607
+ },
608
+ middleware: [loggingMiddleware], // 모든 routes에 적용
609
+ })
530
610
  ```
531
611
 
532
- ### Route-Level Middleware
612
+ ## Route-level Middleware
533
613
 
534
614
  ```typescript
535
- // 특정 라우트에만 적용
536
- export const Route = createFileRoute('/admin/dashboard')({
537
- server: {
538
- middleware: [adminMiddleware], // 이 라우트의 모든 핸들러에 적용
539
- handlers: {
540
- GET: async () => new Response('Admin Dashboard'),
541
- },
615
+ // app/routes/_authenticated.tsx
616
+ import { createFileRoute } from '@tanstack/react-router'
617
+ import { authMiddleware } from '~/middleware/auth'
618
+
619
+ export const Route = createFileRoute('/_authenticated')({
620
+ beforeLoad: async ({ context }) => {
621
+ // authMiddleware 실행
622
+ const session = await getSession()
623
+ if (!session) {
624
+ throw redirect({ to: '/login' })
625
+ }
626
+ return {
627
+ user: session.user,
628
+ }
542
629
  },
543
630
  })
544
631
  ```
545
632
 
633
+ ## Logging Middleware
634
+
635
+ ```typescript
636
+ // app/middleware/logging.ts
637
+ import { createMiddleware } from '@tanstack/react-start'
638
+
639
+ export const loggingMiddleware = createMiddleware().server(
640
+ async ({ next, method, url }) => {
641
+ const start = Date.now()
642
+
643
+ console.log(`[${method}] ${url} - Start`)
644
+
645
+ try {
646
+ const result = await next()
647
+ const duration = Date.now() - start
648
+ console.log(`[${method}] ${url} - Success (${duration}ms)`)
649
+ return result
650
+ } catch (error) {
651
+ const duration = Date.now() - start
652
+ console.error(`[${method}] ${url} - Error (${duration}ms)`, error)
653
+ throw error
654
+ }
655
+ }
656
+ )
657
+ ```
658
+
659
+ ## Rate Limiting Middleware
660
+
661
+ ```typescript
662
+ // app/middleware/rate-limit.ts
663
+ import { createMiddleware } from '@tanstack/react-start'
664
+ import { Ratelimit } from '@upstash/ratelimit'
665
+ import { Redis } from '@upstash/redis'
666
+
667
+ const ratelimit = new Ratelimit({
668
+ redis: Redis.fromEnv(),
669
+ limiter: Ratelimit.slidingWindow(10, '10 s'),
670
+ })
671
+
672
+ export const rateLimitMiddleware = createMiddleware().server(
673
+ async ({ next, context }) => {
674
+ const identifier = context.user?.id || 'anonymous'
675
+ const { success } = await ratelimit.limit(identifier)
676
+
677
+ if (!success) {
678
+ throw new Error('Rate limit exceeded')
679
+ }
680
+
681
+ return next()
682
+ }
683
+ )
684
+ ```
685
+
546
686
  </middleware>
547
687
 
548
688
  ---
549
689
 
550
690
  <routing>
551
691
 
552
- ## Routing
692
+ ## File-based Routing
553
693
 
554
- File-based routing with TanStack Router.
694
+ TanStack Start는 `app/routes/` 디렉토리 구조를 URL로 변환합니다.
555
695
 
556
696
  ### 파일 구조 → URL 매핑
557
697
 
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 |
698
+ | 파일 경로 | URL |
699
+ |-----------|-----|
700
+ | `routes/index.tsx` | `/` |
701
+ | `routes/about.tsx` | `/about` |
702
+ | `routes/posts.index.tsx` | `/posts` |
703
+ | `routes/posts.$postId.tsx` | `/posts/:postId` |
704
+ | `routes/posts.$postId.edit.tsx` | `/posts/:postId/edit` |
705
+ | `routes/blog._layout.tsx` | Layout (no URL) |
706
+ | `routes/blog._layout.posts.tsx` | `/blog/posts` |
707
+ | `routes/admin._.tsx` | `/admin/*` (catch-all) |
708
+
709
+ ## Basic Routes
710
+
711
+ ### Index Route
712
+
713
+ ```typescript
714
+ // app/routes/index.tsx
715
+ import { createFileRoute } from '@tanstack/react-router'
716
+
717
+ export const Route = createFileRoute('/')({
718
+ component: Home,
719
+ })
720
+
721
+ function Home() {
722
+ return (
723
+ <div>
724
+ <h1>Welcome to TanStack Start</h1>
725
+ </div>
726
+ )
727
+ }
728
+ ```
729
+
730
+ ### Static Route
566
731
 
567
- ### 기본 라우트
732
+ ```typescript
733
+ // app/routes/about.tsx
734
+ import { createFileRoute } from '@tanstack/react-router'
568
735
 
569
- ```tsx
570
- // routes/about.tsx
571
736
  export const Route = createFileRoute('/about')({
572
- component: AboutPage,
737
+ component: About,
573
738
  })
574
739
 
575
- const AboutPage = (): JSX.Element => {
740
+ function About() {
576
741
  return <div>About Page</div>
577
742
  }
578
743
  ```
579
744
 
580
- ### Loader: 데이터 사전 로드
745
+ ## Loader Pattern
746
+
747
+ ```typescript
748
+ // app/routes/posts.index.tsx
749
+ import { createFileRoute } from '@tanstack/react-router'
750
+ import { getPosts } from '~/functions/posts'
581
751
 
582
- ```tsx
583
- // routes/posts/index.tsx
584
- export const Route = createFileRoute('/posts')({
585
- component: PostsPage,
752
+ export const Route = createFileRoute('/posts/')({
586
753
  loader: async () => {
587
754
  const posts = await getPosts()
588
755
  return { posts }
589
756
  },
757
+ component: PostsList,
590
758
  })
591
759
 
592
- const PostsPage = (): JSX.Element => {
760
+ function PostsList() {
593
761
  const { posts } = Route.useLoaderData()
594
762
 
595
763
  return (
@@ -602,767 +770,1243 @@ const PostsPage = (): JSX.Element => {
602
770
  }
603
771
  ```
604
772
 
605
- ### 동적 라우트
773
+ ## Dynamic Routes
774
+
775
+ ### Path Parameters
606
776
 
607
- ```tsx
608
- // routes/users/$id.tsx
609
- export const Route = createFileRoute('/users/$id')({
777
+ ```typescript
778
+ // app/routes/posts.$postId.tsx
779
+ import { createFileRoute } from '@tanstack/react-router'
780
+ import { getPost } from '~/functions/posts'
781
+
782
+ export const Route = createFileRoute('/posts/$postId')({
610
783
  loader: async ({ params }) => {
611
- const user = await getUserById(params.id)
612
- return { user }
784
+ const post = await getPost({ data: { id: params.postId } })
785
+ return { post }
613
786
  },
614
- component: UserPage,
787
+ component: PostDetail,
615
788
  })
616
789
 
617
- const UserPage = (): JSX.Element => {
618
- const { user } = Route.useLoaderData()
790
+ function PostDetail() {
791
+ const { postId } = Route.useParams()
792
+ const { post } = Route.useLoaderData()
619
793
 
620
794
  return (
621
- <div>
622
- <h1>{user.name}</h1>
623
- <p>{user.email}</p>
624
- </div>
795
+ <article>
796
+ <h1>{post.title}</h1>
797
+ <p>{post.content}</p>
798
+ </article>
625
799
  )
626
800
  }
627
801
  ```
628
802
 
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
- ```
803
+ ### Search Parameters
644
804
 
645
- ### beforeLoad: 라우트 접근 전 체크
805
+ ```typescript
806
+ // app/routes/search.tsx
807
+ import { createFileRoute } from '@tanstack/react-router'
808
+ import { z } from 'zod'
646
809
 
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,
810
+ const searchSchema = z.object({
811
+ q: z.string().optional(),
812
+ page: z.number().int().positive().default(1),
813
+ sort: z.enum(['asc', 'desc']).default('asc'),
658
814
  })
659
815
 
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
- },
816
+ export const Route = createFileRoute('/search')({
817
+ validateSearch: searchSchema,
818
+ loaderDeps: ({ search }) => ({ search }),
819
+ loader: async ({ deps }) => {
820
+ const results = await searchPosts(deps.search)
821
+ return { results }
681
822
  },
823
+ component: SearchPage,
682
824
  })
683
- ```
684
825
 
685
- ### Catch-all Route (404)
826
+ function SearchPage() {
827
+ const { q, page, sort } = Route.useSearch()
828
+ const { results } = Route.useLoaderData()
829
+ const navigate = Route.useNavigate()
686
830
 
687
- ```tsx
688
- // routes/$.tsx
689
- export const Route = createFileRoute('/$')({
690
- component: NotFoundPage,
691
- })
831
+ const updateSearch = (newQ: string) => {
832
+ navigate({
833
+ search: (prev) => ({ ...prev, q: newQ, page: 1 }),
834
+ })
835
+ }
692
836
 
693
- const NotFoundPage = (): JSX.Element => {
694
837
  return (
695
838
  <div>
696
- <h1>404 - Page Not Found</h1>
697
- <a href="/">Go Home</a>
839
+ <input
840
+ value={q || ''}
841
+ onChange={(e) => updateSearch(e.target.value)}
842
+ />
843
+ <ul>
844
+ {results.map((result) => (
845
+ <li key={result.id}>{result.title}</li>
846
+ ))}
847
+ </ul>
698
848
  </div>
699
849
  )
700
850
  }
701
851
  ```
702
852
 
703
- </routing>
704
-
705
- ---
706
-
707
- <auth_patterns>
708
-
709
- ## 인증 패턴
853
+ ## SSR Options
710
854
 
711
- Better Auth 통합 및 인증 패턴.
855
+ ```typescript
856
+ // app/routes/posts.$postId.tsx
857
+ export const Route = createFileRoute('/posts/$postId')({
858
+ loader: async ({ params }) => {
859
+ const post = await getPost({ data: { id: params.postId } })
860
+ return { post }
861
+ },
862
+ staleTime: 1000 * 60 * 5, // 5분간 캐시
863
+ gcTime: 1000 * 60 * 10, // 10분 후 가비지 컬렉션
864
+ shouldReload: false, // 재방문 시 reload 안함
865
+ component: PostDetail,
866
+ })
867
+ ```
868
+
869
+ **SWR Caching 옵션**:
870
+ | 옵션 | 설명 | 기본값 |
871
+ |------|------|--------|
872
+ | `staleTime` | 데이터가 fresh한 시간 (ms) | 0 |
873
+ | `gcTime` | 메모리에서 제거되기까지 시간 (ms) | 30분 |
874
+ | `shouldReload` | 재방문 시 reload 여부 | true |
712
875
 
713
- ### Better Auth 설정
876
+ ## beforeLoad (Auth Guard)
714
877
 
715
878
  ```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'
879
+ // app/routes/_authenticated.tsx
880
+ import { createFileRoute, redirect } from '@tanstack/react-router'
881
+ import { getSession } from '~/auth-helpers'
720
882
 
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일마다 갱신
883
+ export const Route = createFileRoute('/_authenticated')({
884
+ beforeLoad: async ({ context, location }) => {
885
+ const session = await getSession()
886
+
887
+ if (!session) {
888
+ throw redirect({
889
+ to: '/login',
890
+ search: {
891
+ redirect: location.href,
892
+ },
893
+ })
894
+ }
895
+
896
+ return {
897
+ user: session.user,
898
+ }
727
899
  },
728
900
  })
729
901
 
730
- export type Session = typeof auth.$Infer.Session
902
+ // app/routes/_authenticated.profile.tsx
903
+ export const Route = createFileRoute('/_authenticated/profile')({
904
+ component: Profile,
905
+ })
906
+
907
+ function Profile() {
908
+ const { user } = Route.useRouteContext()
909
+ return <div>Welcome, {user.name}</div>
910
+ }
731
911
  ```
732
912
 
733
- ### 인증 Server Functions
913
+ ## Nested Layouts
734
914
 
735
915
  ```typescript
736
- // functions/auth.ts
916
+ // app/routes/_layout.tsx
917
+ import { createFileRoute, Outlet } from '@tanstack/react-router'
737
918
 
738
- // 로그인
739
- const loginSchema = z.object({
740
- email: z.email(),
741
- password: z.string().min(8),
919
+ export const Route = createFileRoute('/_layout')({
920
+ component: Layout,
742
921
  })
743
922
 
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
- })
923
+ function Layout() {
924
+ return (
925
+ <div>
926
+ <nav>Navigation</nav>
927
+ <main>
928
+ <Outlet /> {/* Child routes render here */}
929
+ </main>
930
+ <footer>Footer</footer>
931
+ </div>
932
+ )
933
+ }
752
934
 
753
- if (!result.user) {
754
- throw new Error('Invalid credentials')
755
- }
935
+ // app/routes/_layout.page1.tsx
936
+ export const Route = createFileRoute('/_layout/page1')({
937
+ component: () => <div>Page 1</div>,
938
+ })
756
939
 
757
- throw redirect({ to: '/dashboard' })
758
- })
940
+ // app/routes/_layout.page2.tsx
941
+ export const Route = createFileRoute('/_layout/page2')({
942
+ component: () => <div>Page 2</div>,
943
+ })
944
+ ```
759
945
 
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
- })
946
+ ## Server Routes (New API)
766
947
 
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
- })
948
+ **중요**: `createAPIFileRoute()` → `createServerFileRoute()` + `.methods()` 로 변경됨
775
949
 
776
- // 회원가입
777
- const registerSchema = z.object({
778
- email: z.email(),
779
- password: z.string().min(8),
780
- name: z.string().min(1),
950
+ ### RESTful API Endpoint
951
+
952
+ ```typescript
953
+ // app/routes/api/users.$userId.tsx
954
+ import { createServerFileRoute } from '@tanstack/react-start'
955
+ import { z } from 'zod'
956
+
957
+ const getUserSchema = z.object({
958
+ userId: z.string().uuid(),
781
959
  })
782
960
 
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
- })
961
+ const updateUserSchema = z.object({
962
+ userId: z.string().uuid(),
963
+ name: z.string().optional(),
964
+ email: z.email().optional(),
965
+ })
792
966
 
793
- if (!result.user) {
794
- throw new Error('Registration failed')
795
- }
967
+ export const Route = createServerFileRoute('/api/users/$userId')
968
+ .methods({
969
+ GET: async ({ request, params }) => {
970
+ const { userId } = getUserSchema.parse(params)
971
+ const user = await db.user.findUnique({ where: { id: userId } })
972
+
973
+ if (!user) {
974
+ return new Response('Not Found', { status: 404 })
975
+ }
796
976
 
797
- throw redirect({ to: '/dashboard' })
977
+ return Response.json(user)
978
+ },
979
+
980
+ PUT: async ({ request, params }) => {
981
+ const body = await request.json()
982
+ const { userId, ...updates } = updateUserSchema.parse({
983
+ ...params,
984
+ ...body,
985
+ })
986
+
987
+ const user = await db.user.update({
988
+ where: { id: userId },
989
+ data: updates,
990
+ })
991
+
992
+ return Response.json(user)
993
+ },
994
+
995
+ DELETE: async ({ request, params }) => {
996
+ const { userId } = getUserSchema.parse(params)
997
+ await db.user.delete({ where: { id: userId } })
998
+
999
+ return Response.json({ success: true })
1000
+ },
798
1001
  })
799
1002
  ```
800
1003
 
801
- ### 인증 미들웨어
1004
+ ### File Upload Endpoint
802
1005
 
803
1006
  ```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
- })
1007
+ // app/routes/api/upload.tsx
1008
+ import { createServerFileRoute } from '@tanstack/react-start'
1009
+ import { writeFile } from 'fs/promises'
1010
+ import { join } from 'path'
810
1011
 
811
- if (!session?.user) {
812
- throw redirect({ to: '/login' })
813
- }
1012
+ export const Route = createServerFileRoute('/api/upload')
1013
+ .methods({
1014
+ POST: async ({ request }) => {
1015
+ const formData = await request.formData()
1016
+ const file = formData.get('file') as File
814
1017
 
815
- return next({ context: { user: session.user } })
1018
+ if (!file) {
1019
+ return new Response('No file provided', { status: 400 })
1020
+ }
1021
+
1022
+ const bytes = await file.arrayBuffer()
1023
+ const buffer = Buffer.from(bytes)
1024
+
1025
+ const path = join(process.cwd(), 'uploads', file.name)
1026
+ await writeFile(path, buffer)
1027
+
1028
+ return Response.json({ success: true, path })
1029
+ },
816
1030
  })
817
1031
  ```
818
1032
 
819
- ### 보호된 Server Function
1033
+ ## Catch-all Routes
820
1034
 
821
1035
  ```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
- })
1036
+ // app/routes/docs._.tsx
1037
+ import { createFileRoute } from '@tanstack/react-router'
830
1038
 
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
- })
1039
+ export const Route = createFileRoute('/docs/$')({
1040
+ component: DocsPage,
1041
+ })
1042
+
1043
+ function DocsPage() {
1044
+ const params = Route.useParams()
1045
+ const splat = params['_splat'] // Captures everything after /docs/
1046
+
1047
+ return <div>Docs: {splat}</div>
1048
+ }
845
1049
  ```
846
1050
 
847
- ### 보호된 라우트
1051
+ ## Link Navigation
848
1052
 
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 }
1053
+ ```typescript
1054
+ import { Link } from '@tanstack/react-router'
1055
+
1056
+ function Navigation() {
1057
+ return (
1058
+ <nav>
1059
+ <Link to="/">Home</Link>
1060
+ <Link to="/about">About</Link>
1061
+ <Link
1062
+ to="/posts/$postId"
1063
+ params={{ postId: '123' }}
1064
+ >
1065
+ Post 123
1066
+ </Link>
1067
+ <Link
1068
+ to="/search"
1069
+ search={{ q: 'tanstack', page: 1 }}
1070
+ >
1071
+ Search
1072
+ </Link>
1073
+ </nav>
1074
+ )
1075
+ }
1076
+ ```
1077
+
1078
+ ## Preload Strategy
1079
+
1080
+ ```typescript
1081
+ // app/routes/posts.$postId.tsx
1082
+ export const Route = createFileRoute('/posts/$postId')({
1083
+ loader: async ({ params }) => {
1084
+ const post = await getPost({ data: { id: params.postId } })
1085
+ return { post }
858
1086
  },
859
- component: DashboardPage,
1087
+ component: PostDetail,
860
1088
  })
861
1089
 
862
- const DashboardPage = (): JSX.Element => {
863
- const { user } = Route.useRouteContext()
864
-
1090
+ // Usage
1091
+ function PostsList() {
865
1092
  return (
866
1093
  <div>
867
- <h1>Welcome, {user.name}!</h1>
1094
+ {posts.map((post) => (
1095
+ <Link
1096
+ key={post.id}
1097
+ to="/posts/$postId"
1098
+ params={{ postId: post.id }}
1099
+ preload="intent" // Preload on hover/focus
1100
+ >
1101
+ {post.title}
1102
+ </Link>
1103
+ ))}
868
1104
  </div>
869
1105
  )
870
1106
  }
871
1107
  ```
872
1108
 
873
- ### 로그인 폼 (TanStack Query)
1109
+ **Preload 옵션**:
1110
+ | 값 | 동작 |
1111
+ |----|------|
1112
+ | `false` | Preload 안함 |
1113
+ | `intent` | Hover/focus 시 preload |
1114
+ | `render` | Link 렌더링 시 preload |
1115
+ | `viewport` | Viewport 진입 시 preload |
874
1116
 
875
- ```tsx
876
- // routes/login.tsx
877
- const LoginPage = (): JSX.Element => {
878
- const mutation = useMutation({
879
- mutationFn: login,
880
- })
1117
+ ## Pending Component
881
1118
 
882
- const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
883
- e.preventDefault()
884
- const formData = new FormData(e.currentTarget)
1119
+ ```typescript
1120
+ // app/routes/posts.$postId.tsx
1121
+ export const Route = createFileRoute('/posts/$postId')({
1122
+ loader: async ({ params }) => {
1123
+ await delay(2000) // Simulate slow loading
1124
+ const post = await getPost({ data: { id: params.postId } })
1125
+ return { post }
1126
+ },
1127
+ pendingComponent: () => <div>Loading post...</div>,
1128
+ component: PostDetail,
1129
+ })
1130
+ ```
885
1131
 
886
- mutation.mutate({
887
- email: formData.get('email') as string,
888
- password: formData.get('password') as string,
889
- })
890
- }
1132
+ ## Error Boundaries
891
1133
 
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
- }
1134
+ ```typescript
1135
+ // app/routes/posts.$postId.tsx
1136
+ export const Route = createFileRoute('/posts/$postId')({
1137
+ loader: async ({ params }) => {
1138
+ const post = await getPost({ data: { id: params.postId } })
1139
+ if (!post) {
1140
+ throw new Error('Post not found')
1141
+ }
1142
+ return { post }
1143
+ },
1144
+ errorComponent: ({ error }) => (
1145
+ <div>
1146
+ <h1>Error</h1>
1147
+ <p>{error.message}</p>
1148
+ </div>
1149
+ ),
1150
+ component: PostDetail,
1151
+ })
903
1152
  ```
904
1153
 
905
- </auth_patterns>
1154
+ ## Router Context
906
1155
 
907
- ---
1156
+ ```typescript
1157
+ // app/router.tsx
1158
+ import { createRouter } from '@tanstack/react-router'
1159
+ import { QueryClient } from '@tanstack/react-query'
908
1160
 
909
- <file_structure>
1161
+ const queryClient = new QueryClient()
910
1162
 
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
1163
+ export const router = createRouter({
1164
+ routeTree,
1165
+ context: {
1166
+ queryClient,
1167
+ },
1168
+ defaultPreload: 'intent',
1169
+ defaultStaleTime: 1000 * 60 * 5, // 5분
1170
+ })
1171
+
1172
+ // app/routes/posts.$postId.tsx
1173
+ export const Route = createFileRoute('/posts/$postId')({
1174
+ loader: async ({ params, context }) => {
1175
+ const post = await context.queryClient.ensureQueryData({
1176
+ queryKey: ['post', params.postId],
1177
+ queryFn: () => getPost({ data: { id: params.postId } }),
1178
+ })
1179
+ return { post }
1180
+ },
1181
+ component: PostDetail,
1182
+ })
956
1183
  ```
957
1184
 
958
- ### 파일 분리 규칙
1185
+ ## Streaming SSR
959
1186
 
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` |
1187
+ ```typescript
1188
+ // app/routes/dashboard.tsx
1189
+ import { createFileRoute } from '@tanstack/react-router'
1190
+ import { Suspense } from 'react'
968
1191
 
969
- ### 필수 규칙
1192
+ export const Route = createFileRoute('/dashboard')({
1193
+ component: Dashboard,
1194
+ })
970
1195
 
971
- **페이지당 `-components/`, `-hooks/`, `-functions/` 폴더 필수**
972
- - Custom Hook은 페이지 크기와 무관하게 **반드시** `-hooks/` 폴더에 분리
973
- - 줄 수 무관: 10줄이든 100줄이든 분리 필수
1196
+ function Dashboard() {
1197
+ return (
1198
+ <div>
1199
+ <h1>Dashboard</h1>
1200
+ <Suspense fallback={<div>Loading analytics...</div>}>
1201
+ <AnalyticsPanel />
1202
+ </Suspense>
1203
+ <Suspense fallback={<div>Loading activity...</div>}>
1204
+ <ActivityFeed />
1205
+ </Suspense>
1206
+ </div>
1207
+ )
1208
+ }
974
1209
 
975
- **공통 함수 → `@/functions/`**
976
- - 여러 페이지에서 사용하는 Server Function
1210
+ function AnalyticsPanel() {
1211
+ const { data } = useSuspenseQuery({
1212
+ queryKey: ['analytics'],
1213
+ queryFn: () => getAnalytics(),
1214
+ })
1215
+ return <div>{/* Render analytics */}</div>
1216
+ }
1217
+ ```
977
1218
 
978
- ✅ **라우트 전용 → `routes/[경로]/-functions/`**
979
- - 해당 라우트에서만 사용하는 Server Function
1219
+ </routing>
980
1220
 
981
- ❌ **index.ts에서 내부 헬퍼 함수 export 금지**
982
- - Server Function만 export
1221
+ ---
983
1222
 
984
- </file_structure>
1223
+ <auth_patterns>
985
1224
 
986
- ---
1225
+ ## Better Auth 통합
987
1226
 
988
- <dos_donts>
1227
+ Better Auth는 TanStack Start와 완벽하게 호환되는 인증 라이브러리입니다.
989
1228
 
990
- ## Do's & Don'ts
1229
+ ### 설치 설정
991
1230
 
992
- ### Server Functions
1231
+ ```bash
1232
+ npm install better-auth
1233
+ ```
993
1234
 
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에서 환경변수 직접 사용 |
1235
+ ```typescript
1236
+ // app/auth.ts
1237
+ import { betterAuth } from 'better-auth'
1238
+ import { prismaAdapter } from 'better-auth/adapters/prisma'
1239
+ import { prisma } from '~/db'
1240
+
1241
+ export const auth = betterAuth({
1242
+ database: prismaAdapter(prisma, {
1243
+ provider: 'postgresql',
1244
+ }),
1245
+ emailAndPassword: {
1246
+ enabled: true,
1247
+ },
1248
+ socialProviders: {
1249
+ github: {
1250
+ clientId: process.env.GITHUB_CLIENT_ID!,
1251
+ clientSecret: process.env.GITHUB_CLIENT_SECRET!,
1252
+ },
1253
+ },
1254
+ })
1255
+ ```
1003
1256
 
1004
- ### 예시: Server Functions
1257
+ ### Auth Server Functions
1005
1258
 
1006
1259
  ```typescript
1007
- // ✅ 올바른 패턴
1008
- const schema = z.object({
1260
+ // app/functions/auth.ts
1261
+ import { createServerFn } from '@tanstack/react-start'
1262
+ import { z } from 'zod'
1263
+ import { auth } from '~/auth'
1264
+
1265
+ const signUpSchema = z.object({
1009
1266
  email: z.email(),
1010
- name: z.string().min(1),
1267
+ password: z.string().min(8),
1268
+ name: z.string().min(2),
1011
1269
  })
1012
1270
 
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
- },
1271
+ export const signUp = createServerFn({ method: 'POST' })
1272
+ .inputValidator(signUpSchema)
1273
+ .handler(async ({ data }) => {
1274
+ const user = await auth.api.signUpEmail({
1275
+ email: data.email,
1276
+ password: data.password,
1277
+ name: data.name,
1278
+ })
1279
+ return user
1280
+ })
1281
+
1282
+ const signInSchema = z.object({
1283
+ email: z.email(),
1284
+ password: z.string(),
1285
+ })
1286
+
1287
+ export const signIn = createServerFn({ method: 'POST' })
1288
+ .inputValidator(signInSchema)
1289
+ .handler(async ({ data }) => {
1290
+ const session = await auth.api.signInEmail({
1291
+ email: data.email,
1292
+ password: data.password,
1022
1293
  })
1294
+ return session
1023
1295
  })
1024
1296
 
1025
- // 잘못된 패턴
1026
- export const badCreate = async (data: any) => { // ❌ createServerFn 없음, any 타입
1027
- // ❌ 수동 검증
1028
- if (!data.email || !data.name) {
1029
- throw new Error('Invalid data')
1297
+ export const signOut = createServerFn({ method: 'POST' }).handler(
1298
+ async () => {
1299
+ await auth.api.signOut()
1300
+ return { success: true }
1030
1301
  }
1302
+ )
1303
+
1304
+ export const getSession = createServerFn({ method: 'GET' }).handler(
1305
+ async () => {
1306
+ const session = await auth.api.getSession()
1307
+ return session
1308
+ }
1309
+ )
1310
+ ```
1311
+
1312
+ ### Auth Middleware
1313
+
1314
+ ```typescript
1315
+ // app/middleware/auth.ts
1316
+ import { createMiddleware } from '@tanstack/react-start'
1317
+ import { auth } from '~/auth'
1318
+
1319
+ export const authMiddleware = createMiddleware().server(async ({ next }) => {
1320
+ const session = await auth.api.getSession()
1031
1321
 
1032
- // ❌ 수동 인증 체크
1033
- const session = await getSession()
1034
1322
  if (!session) {
1035
1323
  throw new Error('Unauthorized')
1036
1324
  }
1037
1325
 
1038
- return prisma.user.create({ data })
1039
- }
1326
+ return next({
1327
+ context: {
1328
+ user: session.user,
1329
+ sessionId: session.session.id,
1330
+ },
1331
+ })
1332
+ })
1333
+
1334
+ export const adminMiddleware = createMiddleware().server(
1335
+ async ({ next, context }) => {
1336
+ // Requires authMiddleware to run first
1337
+ if (context.user.role !== 'admin') {
1338
+ throw new Error('Forbidden: Admin access required')
1339
+ }
1340
+
1341
+ return next({
1342
+ context: {
1343
+ ...context,
1344
+ isAdmin: true,
1345
+ },
1346
+ })
1347
+ }
1348
+ )
1040
1349
  ```
1041
1350
 
1042
- ### 클라이언트 호출
1351
+ ### Protected Routes
1043
1352
 
1044
- | ✅ Do | ❌ Don't |
1045
- |-------|----------|
1046
- | TanStack Query 사용 (`useQuery`/`useMutation`) | Server Function 직접 호출 |
1047
- | `queryClient.invalidateQueries()` 동기화 | 수동 상태 관리 |
1048
- | `isLoading`, `error` 상태 활용 | try-catch로 에러 처리 |
1049
- | `queryKey`로 캐싱 관리 | useEffect + useState |
1353
+ ```typescript
1354
+ // app/routes/_authenticated.tsx
1355
+ import { createFileRoute, redirect } from '@tanstack/react-router'
1356
+ import { getSession } from '~/functions/auth'
1050
1357
 
1051
- ### 예시: 클라이언트 호출
1358
+ export const Route = createFileRoute('/_authenticated')({
1359
+ beforeLoad: async ({ context, location }) => {
1360
+ const session = await getSession()
1052
1361
 
1053
- ```tsx
1054
- // ✅ 올바른 패턴 (useQuery)
1055
- const UsersPage = (): JSX.Element => {
1056
- const { data, isLoading, error } = useQuery({
1057
- queryKey: ['users'],
1058
- queryFn: () => getUsers(),
1059
- })
1362
+ if (!session) {
1363
+ throw redirect({
1364
+ to: '/login',
1365
+ search: {
1366
+ redirect: location.href,
1367
+ },
1368
+ })
1369
+ }
1060
1370
 
1061
- if (isLoading) return <div>Loading...</div>
1062
- if (error) return <div>Error: {error.message}</div>
1371
+ return {
1372
+ user: session.user,
1373
+ }
1374
+ },
1375
+ })
1376
+
1377
+ // app/routes/_authenticated.profile.tsx
1378
+ export const Route = createFileRoute('/_authenticated/profile')({
1379
+ component: Profile,
1380
+ })
1381
+
1382
+ function Profile() {
1383
+ const { user } = Route.useRouteContext()
1063
1384
 
1064
- return <ul>{data?.map(u => <li key={u.id}>{u.name}</li>)}</ul>
1385
+ return (
1386
+ <div>
1387
+ <h1>Profile</h1>
1388
+ <p>Email: {user.email}</p>
1389
+ <p>Name: {user.name}</p>
1390
+ </div>
1391
+ )
1065
1392
  }
1066
1393
 
1067
- // ✅ 올바른 패턴 (useMutation)
1068
- const CreateForm = (): JSX.Element => {
1394
+ // app/routes/_authenticated.settings.tsx
1395
+ export const Route = createFileRoute('/_authenticated/settings')({
1396
+ component: Settings,
1397
+ })
1398
+
1399
+ function Settings() {
1400
+ const { user } = Route.useRouteContext()
1069
1401
  const queryClient = useQueryClient()
1070
1402
 
1071
- const mutation = useMutation({
1072
- mutationFn: createUser,
1403
+ const signOutMutation = useMutation({
1404
+ mutationFn: signOut,
1073
1405
  onSuccess: () => {
1074
- queryClient.invalidateQueries({ queryKey: ['users'] })
1406
+ queryClient.clear()
1407
+ window.location.href = '/login'
1075
1408
  },
1076
1409
  })
1077
1410
 
1078
1411
  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>
1412
+ <div>
1413
+ <h1>Settings</h1>
1414
+ <p>Logged in as {user.email}</p>
1415
+ <button onClick={() => signOutMutation.mutate()}>
1416
+ Sign Out
1417
+ </button>
1418
+ </div>
1091
1419
  )
1092
1420
  }
1421
+ ```
1093
1422
 
1094
- // 잘못된 패턴
1095
- const BadComponent = (): JSX.Element => {
1096
- const [users, setUsers] = useState([])
1097
- const [loading, setLoading] = useState(false)
1423
+ ### Login Form
1098
1424
 
1099
- useEffect(() => {
1100
- setLoading(true)
1101
- getUsers() // 직접 호출 (캐싱 없음)
1102
- .then(setUsers)
1103
- .catch(console.error)
1104
- .finally(() => setLoading(false))
1105
- }, [])
1425
+ ```typescript
1426
+ // app/routes/login.tsx
1427
+ import { createFileRoute } from '@tanstack/react-router'
1428
+ import { useMutation } from '@tanstack/react-query'
1429
+ import { signIn } from '~/functions/auth'
1430
+ import { z } from 'zod'
1106
1431
 
1107
- return <div>{/* ... */}</div>
1108
- }
1109
- ```
1432
+ const searchSchema = z.object({
1433
+ redirect: z.string().optional(),
1434
+ })
1435
+
1436
+ export const Route = createFileRoute('/login')({
1437
+ validateSearch: searchSchema,
1438
+ component: Login,
1439
+ })
1110
1440
 
1111
- ### Middleware
1441
+ function Login() {
1442
+ const { redirect } = Route.useSearch()
1443
+ const navigate = Route.useNavigate()
1112
1444
 
1113
- | Do | ❌ Don't |
1114
- |-------|----------|
1115
- | 인증/권한 체크를 미들웨어로 분리 | handler 내부에서 체크 |
1116
- | `context`로 데이터 전달 | 전역 변수 사용 |
1117
- | 여러 미들웨어 체이닝 가능 | 하나의 미들웨어에 모든 로직 |
1445
+ const signInMutation = useMutation({
1446
+ mutationFn: signIn,
1447
+ onSuccess: () => {
1448
+ window.location.href = redirect || '/'
1449
+ },
1450
+ })
1118
1451
 
1119
- ### 예시: Middleware
1452
+ const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
1453
+ e.preventDefault()
1454
+ const formData = new FormData(e.currentTarget)
1455
+
1456
+ signInMutation.mutate({
1457
+ data: {
1458
+ email: formData.get('email') as string,
1459
+ password: formData.get('password') as string,
1460
+ },
1461
+ })
1462
+ }
1463
+
1464
+ return (
1465
+ <div>
1466
+ <h1>Login</h1>
1467
+ <form onSubmit={handleSubmit}>
1468
+ <input
1469
+ name="email"
1470
+ type="email"
1471
+ placeholder="Email"
1472
+ required
1473
+ />
1474
+ <input
1475
+ name="password"
1476
+ type="password"
1477
+ placeholder="Password"
1478
+ required
1479
+ />
1480
+ <button type="submit" disabled={signInMutation.isPending}>
1481
+ {signInMutation.isPending ? 'Signing in...' : 'Sign In'}
1482
+ </button>
1483
+ {signInMutation.isError && (
1484
+ <p>Error: {signInMutation.error.message}</p>
1485
+ )}
1486
+ </form>
1487
+ </div>
1488
+ )
1489
+ }
1490
+ ```
1491
+
1492
+ ### Role-based Access Control
1120
1493
 
1121
1494
  ```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
- })
1495
+ // app/functions/admin.ts
1496
+ import { createServerFn } from '@tanstack/react-start'
1497
+ import { authMiddleware, adminMiddleware } from '~/middleware/auth'
1129
1498
 
1130
- export const fn = createServerFn({ method: 'GET' })
1131
- .middleware([authMiddleware])
1499
+ export const getAdminStats = createServerFn({ method: 'GET' })
1500
+ .middleware([authMiddleware, adminMiddleware])
1132
1501
  .handler(async ({ context }) => {
1133
- return { user: context.user }
1502
+ const stats = await db.stats.findMany()
1503
+ return stats
1134
1504
  })
1135
1505
 
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 }
1506
+ export const deleteUser = createServerFn({ method: 'DELETE' })
1507
+ .inputValidator(z.object({ userId: z.string().uuid() }))
1508
+ .middleware([authMiddleware, adminMiddleware])
1509
+ .handler(async ({ data }) => {
1510
+ await db.user.delete({ where: { id: data.userId } })
1511
+ return { success: true }
1144
1512
  })
1513
+
1514
+ // app/routes/_authenticated._admin.dashboard.tsx
1515
+ export const Route = createFileRoute('/_authenticated/_admin/dashboard')({
1516
+ beforeLoad: async ({ context }) => {
1517
+ if (context.user.role !== 'admin') {
1518
+ throw redirect({ to: '/' })
1519
+ }
1520
+ },
1521
+ loader: async () => {
1522
+ const stats = await getAdminStats()
1523
+ return { stats }
1524
+ },
1525
+ component: AdminDashboard,
1526
+ })
1527
+
1528
+ function AdminDashboard() {
1529
+ const { stats } = Route.useLoaderData()
1530
+ return <div>{/* Render admin dashboard */}</div>
1531
+ }
1145
1532
  ```
1146
1533
 
1147
- ### 파일 구조
1534
+ </auth_patterns>
1148
1535
 
1149
- | ✅ Do | ❌ Don't |
1150
- |-------|----------|
1151
- | 페이지 전용: `routes/[path]/-functions/` | 모든 함수를 `@/functions/`에 |
1152
- | 공통 함수: `@/functions/` | 라우트 파일에 함수 직접 작성 |
1153
- | Custom Hook: `-hooks/` 폴더에 분리 (필수) | 라우트 파일에 Hook 작성 |
1536
+ ---
1154
1537
 
1155
- ### 예시: 파일 구조
1538
+ <file_structure>
1156
1539
 
1157
- ```typescript
1158
- // ✅ 올바른 구조
1159
- // routes/users/-functions/user-mutations.ts
1160
- export const createUser = createServerFn({ method: 'POST' })
1161
- .inputValidator(schema)
1162
- .handler(async ({ data }) => prisma.user.create({ data }))
1540
+ ## 권장 디렉토리 구조
1163
1541
 
1164
- // routes/users/-hooks/use-user-form.ts
1165
- export const useUserForm = () => {
1166
- const mutation = useMutation({ mutationFn: createUser })
1167
- return { mutation }
1168
- }
1542
+ ```
1543
+ app/
1544
+ ├── routes/
1545
+ │ ├── __root.tsx
1546
+ │ ├── index.tsx
1547
+ │ ├── about.tsx
1548
+ │ ├── posts.index.tsx
1549
+ │ ├── posts.$postId.tsx
1550
+ │ ├── posts.$postId.edit.tsx
1551
+ │ ├── _authenticated.tsx
1552
+ │ ├── _authenticated.profile.tsx
1553
+ │ └── api/
1554
+ │ └── users.$userId.tsx
1555
+ ├── components/
1556
+ │ ├── ui/
1557
+ │ │ ├── Button.tsx
1558
+ │ │ └── Input.tsx
1559
+ │ └── PostCard.tsx
1560
+ ├── functions/
1561
+ │ ├── posts.ts
1562
+ │ ├── posts-helpers.ts
1563
+ │ ├── users.ts
1564
+ │ └── auth.ts
1565
+ ├── middleware/
1566
+ │ ├── auth.ts
1567
+ │ ├── logging.ts
1568
+ │ └── validation.ts
1569
+ ├── hooks/
1570
+ │ ├── useUser.ts
1571
+ │ └── usePosts.ts
1572
+ ├── lib/
1573
+ │ ├── db.ts
1574
+ │ ├── auth.ts
1575
+ │ └── utils.ts
1576
+ ├── router.tsx
1577
+ └── env.ts
1578
+ ```
1169
1579
 
1170
- // routes/users/index.tsx
1171
- import { useUserForm } from './-hooks/use-user-form'
1172
- import { createUser } from './-functions/user-mutations'
1580
+ ## Route 파일 컨벤션
1173
1581
 
1174
- // 잘못된 구조
1175
- // routes/users/index.tsx (모든 로직이 한 파일에)
1176
- const createUser = createServerFn({ method: 'POST' })
1177
- .inputValidator(schema)
1178
- .handler(async ({ data }) => prisma.user.create({ data }))
1582
+ ### Colocation Pattern
1179
1583
 
1180
- const useUserForm = () => {
1181
- const mutation = useMutation({ mutationFn: createUser })
1182
- return { mutation }
1183
- }
1584
+ Route 파일 옆에 관련 파일들을 배치할 때 `-` prefix 사용:
1184
1585
 
1185
- export const Route = createFileRoute('/users')({
1186
- component: UsersPage,
1187
- })
1586
+ ```
1587
+ routes/
1588
+ ├── posts.index.tsx
1589
+ ├── posts.index-components/
1590
+ │ ├── PostCard.tsx
1591
+ │ └── PostsList.tsx
1592
+ ├── posts.index-hooks/
1593
+ │ └── usePosts.ts
1594
+ ├── posts.$postId.tsx
1595
+ ├── posts.$postId-components/
1596
+ │ ├── PostHeader.tsx
1597
+ │ └── CommentSection.tsx
1598
+ └── posts.$postId-functions/
1599
+ ├── updatePost.ts
1600
+ └── deletePost.ts
1188
1601
  ```
1189
1602
 
1190
- ### 환경 변수
1191
-
1192
- | ✅ Do | ❌ Don't |
1193
- |-------|----------|
1194
- | Server Function에서만 `process.env` 사용 | loader에서 직접 사용 |
1195
- | Zod로 환경 변수 검증 | 검증 없이 사용 |
1603
+ **규칙**: `-components`, `-hooks`, `-functions` 접미사 사용
1196
1604
 
1197
- ### 예시: 환경 변수
1605
+ ### 올바른 패턴
1198
1606
 
1199
1607
  ```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
- })
1608
+ // posts.index-functions/getPosts.ts
1609
+ import { createServerFn } from '@tanstack/react-start'
1206
1610
 
1207
- export const env = envSchema.parse(process.env)
1611
+ export const getPosts = createServerFn({ method: 'GET' }).handler(
1612
+ async () => {
1613
+ return db.post.findMany()
1614
+ }
1615
+ )
1208
1616
 
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
- })
1617
+ // posts.index-components/PostCard.tsx
1618
+ export function PostCard({ post }: { post: Post }) {
1619
+ return <div>{post.title}</div>
1620
+ }
1217
1621
 
1218
- // ❌ 잘못된 패턴
1219
- export const Route = createFileRoute('/config')({
1220
- loader: () => {
1221
- const secret = process.env.API_SECRET // ❌ 클라이언트에 노출됨!
1222
- return { secret }
1622
+ // posts.index.tsx
1623
+ import { createFileRoute } from '@tanstack/react-router'
1624
+ import { getPosts } from './posts.index-functions/getPosts'
1625
+ import { PostCard } from './posts.index-components/PostCard'
1626
+
1627
+ export const Route = createFileRoute('/posts/')({
1628
+ loader: async () => {
1629
+ const posts = await getPosts()
1630
+ return { posts }
1223
1631
  },
1632
+ component: PostsList,
1224
1633
  })
1634
+
1635
+ function PostsList() {
1636
+ const { posts } = Route.useLoaderData()
1637
+ return (
1638
+ <div>
1639
+ {posts.map((post) => (
1640
+ <PostCard key={post.id} post={post} />
1641
+ ))}
1642
+ </div>
1643
+ )
1644
+ }
1225
1645
  ```
1226
1646
 
1227
- </dos_donts>
1647
+ ### ❌ 잘못된 패턴
1648
+
1649
+ ```
1650
+ routes/
1651
+ ├── posts.index.tsx
1652
+ ├── PostCard.tsx ❌ Route 파일과 혼재
1653
+ └── getPosts.ts ❌ 명확한 분류 없음
1654
+ ```
1655
+
1656
+ </file_structure>
1228
1657
 
1229
1658
  ---
1230
1659
 
1231
- <quick_reference>
1660
+ <dos_donts>
1232
1661
 
1233
- ## Quick Reference
1662
+ ## Do's
1663
+
1664
+ | 패턴 | 예시 |
1665
+ |------|------|
1666
+ | **✅ .inputValidator() 사용** | `createServerFn({ method: 'POST' }).inputValidator(schema)` |
1667
+ | **✅ Middleware로 인증** | `.middleware([authMiddleware])` |
1668
+ | **✅ Helper functions 분리** | `import 'server-only'` + 별도 파일 |
1669
+ | **✅ TanStack Query로 호출** | `useSuspenseQuery`, `useMutation` |
1670
+ | **✅ beforeLoad로 guard** | `beforeLoad: async () => { if (!session) throw redirect() }` |
1671
+ | **✅ Zod로 입력 검증** | `z.email()`, `z.url()`, `z.string().min()` |
1672
+ | **✅ 명시적 return type** | `function getData(): Promise<User[]>` |
1673
+ | **✅ Server env 분리** | Server: `process.env`, Client: `import.meta.env.VITE_*` |
1674
+ | **✅ SWR caching 활용** | `staleTime`, `gcTime`, `shouldReload` 설정 |
1675
+ | **✅ Streaming SSR** | `<Suspense>` + `useSuspenseQuery` |
1676
+
1677
+ ## Don'ts
1678
+
1679
+ | 패턴 | 이유 |
1680
+ |------|------|
1681
+ | **❌ .validator() 사용** | Deprecated. `.inputValidator()` 사용 필수 |
1682
+ | **❌ createAPIFileRoute()** | Deprecated. `createServerFileRoute().methods()` 사용 |
1683
+ | **❌ @tanstack/start** | 패키지명 변경됨. `@tanstack/react-start` 사용 |
1684
+ | **❌ Server Function에 민감 로직** | 클라이언트 번들 포함 위험. Helper 분리 |
1685
+ | **❌ Server에서 import.meta.env** | 서버에서는 `process.env` 사용 |
1686
+ | **❌ Client에서 process.env** | 클라이언트에서는 `import.meta.env.VITE_*` 사용 |
1687
+ | **❌ verbatimModuleSyntax: true** | TanStack Start 호환성 문제 |
1688
+ | **❌ gcTime: 0 남발** | 캐시 무효화. 신중히 사용 |
1689
+ | **❌ any 타입** | Type safety 상실. `unknown` 또는 명시적 타입 |
1690
+ | **❌ Route 파일에 비즈니스 로직** | 관심사 분리. `-functions/` 사용 |
1691
+
1692
+ ## 마이그레이션 체크리스트
1693
+
1694
+ ### v1.121.0+ 업데이트
1695
+
1696
+ | 변경 전 | 변경 후 |
1697
+ |---------|---------|
1698
+ | `import { createServerFn } from '@tanstack/start'` | `import { createServerFn } from '@tanstack/react-start'` |
1699
+ | `.validator(schema)` | `.inputValidator(schema)` |
1700
+ | `createAPIFileRoute('/api/users')` | `createServerFileRoute('/api/users').methods({ GET, POST })` |
1701
+ | Vinxi config | Vite config (`vite.config.ts`) |
1234
1702
 
1235
- ### GET: 데이터 조회
1703
+ </dos_donts>
1236
1704
 
1237
- ```typescript
1238
- export const getUsers = createServerFn({ method: 'GET' })
1239
- .handler(async () => prisma.user.findMany())
1705
+ ---
1240
1706
 
1241
- export const getMyProfile = createServerFn({ method: 'GET' })
1242
- .middleware([authMiddleware])
1243
- .handler(async ({ context }) => context.user)
1244
- ```
1707
+ <quick_reference>
1245
1708
 
1246
- ### POST: 데이터 생성 (inputValidator 필수)
1709
+ ## Server Function 템플릿
1247
1710
 
1248
1711
  ```typescript
1249
- const schema = z.object({
1250
- email: z.email(),
1251
- name: z.string().min(1),
1252
- })
1712
+ // GET
1713
+ export const getData = createServerFn({ method: 'GET' })
1714
+ .inputValidator(z.object({ id: z.string() }))
1715
+ .handler(async ({ data }) => {
1716
+ return await getDataFromDB(data.id)
1717
+ })
1253
1718
 
1254
- export const createUser = createServerFn({ method: 'POST' })
1719
+ // POST
1720
+ export const createData = createServerFn({ method: 'POST' })
1721
+ .inputValidator(z.object({ name: z.string() }))
1255
1722
  .middleware([authMiddleware])
1256
- .inputValidator(schema)
1257
1723
  .handler(async ({ data, context }) => {
1258
- return prisma.user.create({
1259
- data: { ...data, createdBy: context.user.id },
1724
+ return await createDataInDB({
1725
+ ...data,
1726
+ userId: context.user.id,
1260
1727
  })
1261
1728
  })
1262
- ```
1263
1729
 
1264
- ### PUT/PATCH: 데이터 수정 (inputValidator 필수)
1265
-
1266
- ```typescript
1267
- export const updateUser = createServerFn({ method: 'PUT' })
1268
- .middleware([authMiddleware])
1730
+ // PUT
1731
+ export const updateData = createServerFn({ method: 'PUT' })
1269
1732
  .inputValidator(z.object({ id: z.string(), name: z.string() }))
1733
+ .middleware([authMiddleware])
1270
1734
  .handler(async ({ data }) => {
1271
- return prisma.user.update({
1272
- where: { id: data.id },
1273
- data: { name: data.name },
1274
- })
1735
+ return await updateDataInDB(data.id, data)
1275
1736
  })
1276
- ```
1277
-
1278
- ### DELETE: 데이터 삭제
1279
1737
 
1280
- ```typescript
1281
- export const deletePost = createServerFn({ method: 'DELETE' })
1282
- .middleware([authMiddleware])
1738
+ // DELETE
1739
+ export const deleteData = createServerFn({ method: 'DELETE' })
1283
1740
  .inputValidator(z.object({ id: z.string() }))
1741
+ .middleware([authMiddleware, adminMiddleware])
1284
1742
  .handler(async ({ data }) => {
1285
- return prisma.post.delete({ where: { id: data.id } })
1743
+ await deleteDataFromDB(data.id)
1744
+ return { success: true }
1286
1745
  })
1287
1746
  ```
1288
1747
 
1289
- ### Loader: 데이터 사전 로드
1748
+ ## Route 템플릿
1290
1749
 
1291
1750
  ```typescript
1292
- export const Route = createFileRoute('/users')({
1293
- component: UsersPage,
1751
+ // Basic Route
1752
+ export const Route = createFileRoute('/path')({
1753
+ component: Component,
1754
+ })
1755
+
1756
+ // Route with Loader
1757
+ export const Route = createFileRoute('/path')({
1294
1758
  loader: async () => {
1295
- const users = await getUsers()
1296
- return { users }
1759
+ const data = await getData()
1760
+ return { data }
1297
1761
  },
1762
+ component: Component,
1298
1763
  })
1299
1764
 
1300
- const UsersPage = (): JSX.Element => {
1301
- const { users } = Route.useLoaderData()
1302
- return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
1303
- }
1765
+ // Route with Params
1766
+ export const Route = createFileRoute('/path/$id')({
1767
+ loader: async ({ params }) => {
1768
+ const data = await getData({ data: { id: params.id } })
1769
+ return { data }
1770
+ },
1771
+ component: Component,
1772
+ })
1773
+
1774
+ // Route with Search
1775
+ export const Route = createFileRoute('/path')({
1776
+ validateSearch: z.object({
1777
+ q: z.string().optional(),
1778
+ }),
1779
+ loaderDeps: ({ search }) => ({ search }),
1780
+ loader: async ({ deps }) => {
1781
+ const results = await search(deps.search.q)
1782
+ return { results }
1783
+ },
1784
+ component: Component,
1785
+ })
1786
+
1787
+ // Protected Route
1788
+ export const Route = createFileRoute('/_auth/path')({
1789
+ beforeLoad: async ({ context }) => {
1790
+ const session = await getSession()
1791
+ if (!session) throw redirect({ to: '/login' })
1792
+ return { user: session.user }
1793
+ },
1794
+ component: Component,
1795
+ })
1304
1796
  ```
1305
1797
 
1306
- ### TanStack Query: useQuery
1798
+ ## Middleware 템플릿
1307
1799
 
1308
1800
  ```typescript
1309
- const { data, isLoading, error } = useQuery({
1310
- queryKey: ['posts'],
1311
- queryFn: () => getPosts(),
1312
- })
1801
+ // Auth Middleware
1802
+ export const authMiddleware = createMiddleware().server(
1803
+ async ({ next }) => {
1804
+ const session = await getSession()
1805
+ if (!session) throw new Error('Unauthorized')
1806
+ return next({ context: { user: session.user } })
1807
+ }
1808
+ )
1809
+
1810
+ // Validation Middleware
1811
+ export const validateMiddleware = <T extends z.ZodType>(schema: T) =>
1812
+ createMiddleware().server(async ({ next, data }) => {
1813
+ const validated = schema.parse(data)
1814
+ return next({ context: { validated } })
1815
+ })
1816
+
1817
+ // Logging Middleware
1818
+ export const loggingMiddleware = createMiddleware().server(
1819
+ async ({ next, method, url }) => {
1820
+ console.log(`[${method}] ${url}`)
1821
+ const result = await next()
1822
+ console.log(`[${method}] ${url} - Success`)
1823
+ return result
1824
+ }
1825
+ )
1313
1826
  ```
1314
1827
 
1315
- ### TanStack Query: useMutation
1828
+ ## TanStack Query 템플릿
1316
1829
 
1317
1830
  ```typescript
1318
- const queryClient = useQueryClient()
1831
+ // useSuspenseQuery (GET)
1832
+ const { data } = useSuspenseQuery({
1833
+ queryKey: ['key', id],
1834
+ queryFn: () => getData({ data: { id } }),
1835
+ })
1319
1836
 
1837
+ // useMutation (POST/PUT/DELETE)
1320
1838
  const mutation = useMutation({
1321
- mutationFn: createPost,
1322
- onSuccess: () => {
1323
- queryClient.invalidateQueries({ queryKey: ['posts'] })
1839
+ mutationFn: createData,
1840
+ onSuccess: (data) => {
1841
+ queryClient.invalidateQueries({ queryKey: ['key'] })
1324
1842
  },
1325
1843
  })
1326
1844
 
1327
- mutation.mutate({ title: 'New Post', content: 'Content' })
1845
+ mutation.mutate({ data: { name: 'value' } })
1328
1846
  ```
1329
1847
 
1330
- ### 인증 미들웨어
1848
+ ## Server Routes 템플릿
1331
1849
 
1332
1850
  ```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 } })
1851
+ // RESTful API
1852
+ export const Route = createServerFileRoute('/api/resource/$id')
1853
+ .methods({
1854
+ GET: async ({ params }) => {
1855
+ const data = await getResource(params.id)
1856
+ return Response.json(data)
1857
+ },
1858
+ PUT: async ({ request, params }) => {
1859
+ const body = await request.json()
1860
+ const data = await updateResource(params.id, body)
1861
+ return Response.json(data)
1862
+ },
1863
+ DELETE: async ({ params }) => {
1864
+ await deleteResource(params.id)
1865
+ return Response.json({ success: true })
1866
+ },
1338
1867
  })
1339
1868
  ```
1340
1869
 
1341
- ### 보호된 라우트
1870
+ ## Auth 템플릿
1342
1871
 
1343
1872
  ```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 }
1873
+ // Auth Server Functions
1874
+ export const signIn = createServerFn({ method: 'POST' })
1875
+ .inputValidator(z.object({ email: z.email(), password: z.string() }))
1876
+ .handler(async ({ data }) => {
1877
+ return await auth.api.signInEmail(data)
1878
+ })
1879
+
1880
+ export const getSession = createServerFn({ method: 'GET' })
1881
+ .handler(async () => {
1882
+ return await auth.api.getSession()
1883
+ })
1884
+
1885
+ // Protected Route
1886
+ export const Route = createFileRoute('/_auth')({
1887
+ beforeLoad: async ({ location }) => {
1888
+ const session = await getSession()
1889
+ if (!session) {
1890
+ throw redirect({ to: '/login', search: { redirect: location.href } })
1891
+ }
1892
+ return { user: session.user }
1349
1893
  },
1350
- component: DashboardPage,
1351
1894
  })
1352
1895
  ```
1353
1896
 
1897
+ ## Deployment 템플릿
1898
+
1899
+ ### Cloudflare Workers
1900
+
1901
+ ```typescript
1902
+ // app/entry.server.tsx
1903
+ export default {
1904
+ async fetch(request: Request, env: Env) {
1905
+ return handleRequest(request, { env })
1906
+ },
1907
+ }
1908
+ ```
1909
+
1910
+ ### Vercel
1911
+
1912
+ ```json
1913
+ // vercel.json
1914
+ {
1915
+ "buildCommand": "npm run build",
1916
+ "framework": "vite"
1917
+ }
1918
+ ```
1919
+
1920
+ ### Netlify
1921
+
1922
+ ```toml
1923
+ # netlify.toml
1924
+ [build]
1925
+ command = "npm run build"
1926
+ publish = "dist/public"
1927
+
1928
+ [[redirects]]
1929
+ from = "/*"
1930
+ to = "/.netlify/functions/server"
1931
+ status = 200
1932
+ ```
1933
+
1354
1934
  </quick_reference>
1355
1935
 
1356
1936
  ---
1357
1937
 
1358
1938
  <version_info>
1359
1939
 
1360
- **Version:** v1.x
1940
+ ## 버전 정보
1941
+
1942
+ **현재 버전**: v1.x (RC, ~v1.159.x)
1943
+
1944
+ **주요 변경사항**:
1945
+
1946
+ | 버전 | 변경 내용 |
1947
+ |------|-----------|
1948
+ | v1.121.0+ | `@tanstack/start` → `@tanstack/react-start` 패키지명 변경 |
1949
+ | v1.x | Vinxi → Vite 빌드 시스템 마이그레이션 |
1950
+ | v1.x | `.validator()` → `.inputValidator()` (deprecated) |
1951
+ | v1.x | `createAPIFileRoute()` → `createServerFileRoute().methods()` |
1952
+ | v1.x | React 18+ or 19+ 지원 |
1953
+ | v1.x | Vite 7+ 필수 |
1954
+ | v1.x | Node.js >=22.12.0 필수 |
1955
+
1956
+ **Breaking Changes**:
1957
+
1958
+ 1. **패키지명 변경**: `@tanstack/start` → `@tanstack/react-start`
1959
+ 2. **빌드 시스템**: Vinxi config → Vite config
1960
+ 3. **Validator API**: `.validator()` deprecated, `.inputValidator()` 사용
1961
+ 4. **Server Routes API**: `createAPIFileRoute()` deprecated, `createServerFileRoute().methods()` 사용
1962
+
1963
+ **마이그레이션 가이드**:
1964
+
1965
+ ```bash
1966
+ # 1. 패키지 업데이트
1967
+ npm uninstall @tanstack/start
1968
+ npm install @tanstack/react-start@latest
1969
+
1970
+ # 2. Import 변경
1971
+ # Before: import { createServerFn } from '@tanstack/start'
1972
+ # After: import { createServerFn } from '@tanstack/react-start'
1973
+
1974
+ # 3. Validator 변경
1975
+ # Before: .validator(schema)
1976
+ # After: .inputValidator(schema)
1977
+
1978
+ # 4. Server Routes 변경
1979
+ # Before: createAPIFileRoute('/api/users')
1980
+ # After: createServerFileRoute('/api/users').methods({ GET, POST })
1981
+
1982
+ # 5. Vite config 생성
1983
+ # vinxi.config.ts 삭제 → vite.config.ts 생성
1984
+ ```
1985
+
1986
+ **향후 계획**:
1987
+
1988
+ - **React Server Components**: v1.x에 non-breaking 방식으로 추가 예정
1989
+ - **Static Server Functions**: Experimental 단계, 정식 지원 예정
1990
+ - **v2.0**: Breaking changes 예정 (정확한 일정 미정)
1991
+
1992
+ **호환성**:
1993
+
1994
+ | 패키지 | 최소 버전 | 권장 버전 |
1995
+ |--------|-----------|-----------|
1996
+ | React | 18.0.0 | 19.x |
1997
+ | Vite | 7.0.0 | 7.x |
1998
+ | Node.js | 22.12.0 | 22.x LTS |
1999
+ | TypeScript | 5.0.0 | 5.x |
2000
+
2001
+ **알려진 이슈**:
2002
+
2003
+ - `verbatimModuleSyntax: true` 사용 시 호환성 문제
2004
+ - Full `strict` mode 대신 `strictNullChecks: true` 권장
2005
+
2006
+ **참고 자료**:
1361
2007
 
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
2008
+ - Official Docs: https://tanstack.com/start
2009
+ - GitHub: https://github.com/TanStack/router
2010
+ - Discord: https://tanstack.com/discord
1367
2011
 
1368
2012
  </version_info>