@kood/claude-code 0.3.8 → 0.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/templates/.claude/agents/code-reviewer.md +16 -1
- package/templates/.claude/agents/dependency-manager.md +16 -1
- package/templates/.claude/agents/deployment-validator.md +16 -1
- package/templates/.claude/agents/git-operator.md +16 -1
- package/templates/.claude/agents/implementation-executor.md +16 -1
- package/templates/.claude/agents/lint-fixer.md +16 -1
- package/templates/.claude/agents/refactor-advisor.md +16 -1
- package/templates/.claude/commands/agent-creator.md +16 -1
- package/templates/.claude/commands/bug-fix.md +16 -1
- package/templates/.claude/commands/command-creator.md +17 -1
- package/templates/.claude/commands/docs-creator.md +17 -1
- package/templates/.claude/commands/docs-refactor.md +17 -1
- package/templates/.claude/commands/execute.md +17 -1
- package/templates/.claude/commands/git-all.md +16 -1
- package/templates/.claude/commands/git-session.md +17 -1
- package/templates/.claude/commands/git.md +17 -1
- package/templates/.claude/commands/lint-fix.md +17 -1
- package/templates/.claude/commands/lint-init.md +17 -1
- package/templates/.claude/commands/plan.md +17 -1
- package/templates/.claude/commands/prd.md +17 -1
- package/templates/.claude/commands/pre-deploy.md +17 -1
- package/templates/.claude/commands/refactor.md +17 -1
- package/templates/.claude/commands/version-update.md +17 -1
- package/templates/hono/CLAUDE.md +1 -0
- package/templates/nextjs/CLAUDE.md +12 -9
- package/templates/nextjs/docs/architecture.md +812 -0
- package/templates/npx/CLAUDE.md +1 -0
- package/templates/tanstack-start/CLAUDE.md +1 -0
- package/templates/tanstack-start/docs/library/better-auth/index.md +225 -185
- package/templates/tanstack-start/docs/library/prisma/index.md +1025 -41
- package/templates/tanstack-start/docs/library/t3-env/index.md +207 -40
- package/templates/tanstack-start/docs/library/tanstack-query/index.md +878 -42
- package/templates/tanstack-start/docs/library/tanstack-router/index.md +602 -54
- package/templates/tanstack-start/docs/library/tanstack-start/index.md +1334 -33
- package/templates/tanstack-start/docs/library/zod/index.md +674 -31
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
> Next.js App Router Application Architecture
|
|
4
|
+
|
|
5
|
+
<instructions>
|
|
6
|
+
@library/nextjs/app-router.md
|
|
7
|
+
@library/nextjs/server-actions.md
|
|
8
|
+
@library/nextjs/middleware.md
|
|
9
|
+
@library/prisma/index.md
|
|
10
|
+
</instructions>
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<forbidden>
|
|
15
|
+
|
|
16
|
+
| Category | Forbidden |
|
|
17
|
+
|----------|-----------|
|
|
18
|
+
| **Routes** | Flat file routes (`app/users.tsx`), Pages Router (`pages/`) |
|
|
19
|
+
| **Route Export** | Named export (`export const Page`), incorrect file names (`Users.tsx`) |
|
|
20
|
+
| **API** | Pages Router API (`pages/api/`), API Routes overuse (use Server Actions) |
|
|
21
|
+
| **Layers** | Writing business logic directly in app/ folder |
|
|
22
|
+
| **Components** | Using client-only APIs without 'use client' |
|
|
23
|
+
| **Barrel Export** | Creating `actions/index.ts` (Tree Shaking fails) |
|
|
24
|
+
|
|
25
|
+
</forbidden>
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
<required>
|
|
30
|
+
|
|
31
|
+
| Category | Required |
|
|
32
|
+
|----------|----------|
|
|
33
|
+
| **Route Structure** | Create folder per page (`app/users/page.tsx`) |
|
|
34
|
+
| **Route Export** | `export default function Page()` required |
|
|
35
|
+
| **Layer Structure** | app/ → Server Actions → lib/ → Database |
|
|
36
|
+
| **Route Group** | List pages → `(main)/`, Admin → `(admin)/` |
|
|
37
|
+
| **Page-specific Folders** | `_components/`, `_hooks/`, `_actions/` required (regardless of line count) |
|
|
38
|
+
| **Page Separation** | 100+ lines → `_components/`, 200+ lines → `_sections/` |
|
|
39
|
+
| **Server Actions** | Use Server Actions for mutations (`'use server'`) |
|
|
40
|
+
| **Validation** | Validate input with Zod schemas |
|
|
41
|
+
| **Metadata** | Export `generateMetadata` or `metadata` |
|
|
42
|
+
| **Error Handling** | `error.tsx` (route), `not-found.tsx` (404), `global-error.tsx` (global) |
|
|
43
|
+
| **Type Safety** | TypeScript strict, Prisma types |
|
|
44
|
+
|
|
45
|
+
</required>
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
<system_overview>
|
|
50
|
+
|
|
51
|
+
## System Overview
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
55
|
+
│ Client (Browser) │
|
|
56
|
+
│ ┌────────────────┐ ┌────────────────┐ ┌───────────────┐ │
|
|
57
|
+
│ │ Next.js Router │───▶│ TanStack Query │───▶│ React UI │ │
|
|
58
|
+
│ │ (Navigation) │◀───│ (Caching) │◀───│ (Components) │ │
|
|
59
|
+
│ └────────────────┘ └───────┬────────┘ └───────────────┘ │
|
|
60
|
+
└────────────────────────────────┼─────────────────────────────────┘
|
|
61
|
+
▼
|
|
62
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
63
|
+
│ Next.js Server │
|
|
64
|
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
|
65
|
+
│ │ Server Components (default) │ │
|
|
66
|
+
│ │ app/[route]/page.tsx → Server-side rendering │ │
|
|
67
|
+
│ └────────────────────────────┬───────────────────────────────┘ │
|
|
68
|
+
│ ┌────────────────────────────▼───────────────────────────────┐ │
|
|
69
|
+
│ │ Server Actions │ │
|
|
70
|
+
│ │ 'use server' → DB access, Mutations, Revalidation │ │
|
|
71
|
+
│ └────────────────────────────┬───────────────────────────────┘ │
|
|
72
|
+
│ ┌────────────────────────────▼───────────────────────────────┐ │
|
|
73
|
+
│ │ Services Layer │ │
|
|
74
|
+
│ │ Zod Validation | Business Logic | Data Transformation │ │
|
|
75
|
+
│ └────────────────────────────┬───────────────────────────────┘ │
|
|
76
|
+
└───────────────────────────────┼──────────────────────────────────┘
|
|
77
|
+
▼
|
|
78
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
79
|
+
│ Database Layer │
|
|
80
|
+
│ ┌────────────────┐ ┌────────────────┐ ┌───────────────┐ │
|
|
81
|
+
│ │ Prisma Client │───▶│ PostgreSQL │ │ Redis │ │
|
|
82
|
+
│ └────────────────┘ └────────────────┘ └───────────────┘ │
|
|
83
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
</system_overview>
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
<route_export_rule>
|
|
91
|
+
|
|
92
|
+
## Route Export Rules
|
|
93
|
+
|
|
94
|
+
> ⚠️ **`export default` required**
|
|
95
|
+
>
|
|
96
|
+
> Next.js App Router requires all page/layout files to export components as **default export**.
|
|
97
|
+
>
|
|
98
|
+
> File names must follow Next.js conventions: `page.tsx`, `layout.tsx`, `loading.tsx`, `error.tsx`, `not-found.tsx`
|
|
99
|
+
|
|
100
|
+
| ❌ Forbidden | ✅ Required |
|
|
101
|
+
|--------------|-------------|
|
|
102
|
+
| `app/users.tsx` | `app/users/page.tsx` |
|
|
103
|
+
| `export const Page = () => {}` | `export default function Page() {}` |
|
|
104
|
+
| `export default Users` (component name != file convention) | `export default function UsersPage() {}` |
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// ❌ Forbidden: Flat file
|
|
108
|
+
// app/users.tsx
|
|
109
|
+
export default function Users() {
|
|
110
|
+
return <div>Users</div>
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ❌ Forbidden: named export
|
|
114
|
+
// app/users/page.tsx
|
|
115
|
+
export const Page = () => {
|
|
116
|
+
return <div>Users</div>
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ✅ Required: folder + page.tsx + default export
|
|
120
|
+
// app/users/page.tsx
|
|
121
|
+
export default function UsersPage() {
|
|
122
|
+
return <div>Users</div>
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
</route_export_rule>
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
<layers>
|
|
131
|
+
|
|
132
|
+
## Layer Architecture
|
|
133
|
+
|
|
134
|
+
### 1. Routes Layer (app/)
|
|
135
|
+
|
|
136
|
+
> ⚠️ **Create folder per page (required)**
|
|
137
|
+
>
|
|
138
|
+
> Every page MUST be created using **folder structure**. Flat file approach (`app/users.tsx`) is forbidden.
|
|
139
|
+
>
|
|
140
|
+
> **Reason:** To systematically manage page-specific resources like `_components/`, `_hooks/`, `_actions/`.
|
|
141
|
+
>
|
|
142
|
+
> | ❌ Forbidden | ✅ Required |
|
|
143
|
+
> |--------------|-------------|
|
|
144
|
+
> | `app/users.tsx` | `app/users/page.tsx` |
|
|
145
|
+
> | `app/posts.tsx` | `app/(main)/posts/page.tsx` |
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
app/<route-name>/
|
|
149
|
+
├── (main)/ # route group (list page, not in URL)
|
|
150
|
+
│ ├── page.tsx # page component
|
|
151
|
+
│ ├── _components/ # page-specific components (required)
|
|
152
|
+
│ ├── _hooks/ # page-specific hooks (required)
|
|
153
|
+
│ ├── _sections/ # UI section separation (200+ line pages)
|
|
154
|
+
│ └── _tabs/ # tab content separation
|
|
155
|
+
├── new/ # creation page (outside route group)
|
|
156
|
+
│ └── page.tsx
|
|
157
|
+
├── [id]/ # Dynamic segment
|
|
158
|
+
│ └── page.tsx
|
|
159
|
+
├── layout.tsx # layout (shared across child routes)
|
|
160
|
+
├── loading.tsx # loading UI (Suspense boundary)
|
|
161
|
+
├── error.tsx # error UI (Error boundary)
|
|
162
|
+
└── _actions/ # page-specific Server Actions (required)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Required Rules:**
|
|
166
|
+
- Each page MUST have `_components/`, `_hooks/`, `_actions/` folders (regardless of line count)
|
|
167
|
+
- Custom Hooks MUST be separated into `_hooks/` folder regardless of page size
|
|
168
|
+
- Server Actions: global (`app/_actions/`) or page-specific (`[route]/_actions/`)
|
|
169
|
+
- Shared components → `components/ui/`, page-specific → `[route]/_components/`
|
|
170
|
+
|
|
171
|
+
| Pattern | Location | Purpose |
|
|
172
|
+
|---------|----------|---------|
|
|
173
|
+
| **Route Group** | `(main)/` | List page, not in URL |
|
|
174
|
+
| **Private Folder** | `_components/` | Ignored by routing system |
|
|
175
|
+
| **_sections/** | 200+ lines | Logical section separation |
|
|
176
|
+
| **_tabs/** | Tab UI | Tab content separation |
|
|
177
|
+
| **layout.tsx** | Layout | Shared UI for child routes |
|
|
178
|
+
|
|
179
|
+
#### Layout Routes Pattern
|
|
180
|
+
|
|
181
|
+
> ⚠️ **Compose layouts with layout.tsx**
|
|
182
|
+
>
|
|
183
|
+
> `layout.tsx` serves as the common layout for child routes.
|
|
184
|
+
> List pages should be wrapped in Route Group `(main)/`.
|
|
185
|
+
>
|
|
186
|
+
> | ❌ Forbidden | ✅ Required |
|
|
187
|
+
> |--------------|-------------|
|
|
188
|
+
> | `app/auth.tsx` | `app/(auth)/layout.tsx` + `app/(auth)/(main)/page.tsx` |
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
app/
|
|
192
|
+
├── (auth)/
|
|
193
|
+
│ ├── layout.tsx # layout (renders children)
|
|
194
|
+
│ ├── (main)/
|
|
195
|
+
│ │ └── page.tsx # /auth (main)
|
|
196
|
+
│ ├── login/
|
|
197
|
+
│ │ └── page.tsx # /auth/login
|
|
198
|
+
│ └── register/
|
|
199
|
+
│ └── page.tsx # /auth/register
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
// ❌ Forbidden: flat structure without layout
|
|
204
|
+
// app/auth/page.tsx
|
|
205
|
+
export default function AuthPage() {
|
|
206
|
+
return <div>Auth</div>
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ✅ Required: wrap common UI with layout.tsx
|
|
210
|
+
// app/(auth)/layout.tsx
|
|
211
|
+
export default function AuthLayout({
|
|
212
|
+
children,
|
|
213
|
+
}: {
|
|
214
|
+
children: React.ReactNode
|
|
215
|
+
}) {
|
|
216
|
+
return (
|
|
217
|
+
<div className="auth-container">
|
|
218
|
+
<header>Auth Header</header>
|
|
219
|
+
{children}
|
|
220
|
+
</div>
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// app/(auth)/(main)/page.tsx
|
|
225
|
+
export default function AuthMainPage() {
|
|
226
|
+
return <div>Auth Main</div>
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// app/(auth)/login/page.tsx
|
|
230
|
+
export default function LoginPage() {
|
|
231
|
+
return <div>Login Form</div>
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### 2. Server Actions Layer
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
app/_actions/ # global (reusable)
|
|
239
|
+
├── <action-name>.ts # one per file
|
|
240
|
+
└── types.ts # shared types
|
|
241
|
+
|
|
242
|
+
app/<route>/_actions/ # page-specific
|
|
243
|
+
└── <action-name>.ts
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
> ⚠️ **Do NOT create `app/_actions/index.ts`**
|
|
247
|
+
>
|
|
248
|
+
> Do not create `index.ts` (barrel export) file in `app/_actions/` folder.
|
|
249
|
+
>
|
|
250
|
+
> **Problems:**
|
|
251
|
+
> 1. **Tree Shaking fails** - bundler includes unused functions
|
|
252
|
+
> 2. **Client bundle pollution** - server-only libraries like `prisma` get included in client bundle causing build errors
|
|
253
|
+
>
|
|
254
|
+
> ```typescript
|
|
255
|
+
> // ❌ Do NOT create app/_actions/index.ts
|
|
256
|
+
> export * from './get-users'
|
|
257
|
+
> export * from './create-post' // prisma import → client build failure
|
|
258
|
+
>
|
|
259
|
+
> // ✅ Import directly from individual files
|
|
260
|
+
> import { getUsers } from '@/app/_actions/get-users'
|
|
261
|
+
> import { createPost } from '@/app/_actions/create-post'
|
|
262
|
+
> ```
|
|
263
|
+
|
|
264
|
+
### 3. Services Layer
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
lib/<domain>/
|
|
268
|
+
├── index.ts # entry point (re-export)
|
|
269
|
+
├── schemas.ts # Zod schemas
|
|
270
|
+
├── queries.ts # GET requests
|
|
271
|
+
└── mutations.ts # POST/PUT/PATCH
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### 4. Database Layer
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
// lib/db/prisma.ts
|
|
278
|
+
import { PrismaClient } from '@prisma/client'
|
|
279
|
+
|
|
280
|
+
const globalForPrisma = globalThis as unknown as {
|
|
281
|
+
prisma: PrismaClient | undefined
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
|
|
285
|
+
|
|
286
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
287
|
+
globalForPrisma.prisma = prisma
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
</layers>
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
<component_types>
|
|
296
|
+
|
|
297
|
+
## Component Types
|
|
298
|
+
|
|
299
|
+
### Server Components vs Client Components
|
|
300
|
+
|
|
301
|
+
| Item | Server Components | Client Components |
|
|
302
|
+
|------|------------------|-------------------|
|
|
303
|
+
| **Default** | ✅ Default (no declaration needed) | ❌ `'use client'` required |
|
|
304
|
+
| **Execution** | Server | Browser |
|
|
305
|
+
| **Data Fetching** | async/await direct usage | TanStack Query/SWR |
|
|
306
|
+
| **DB Access** | ✅ Possible | ❌ Impossible (use Server Actions) |
|
|
307
|
+
| **Browser API** | ❌ Impossible | ✅ Possible (window, localStorage, etc.) |
|
|
308
|
+
| **State Management** | ❌ Impossible | ✅ Possible (useState, useEffect, etc.) |
|
|
309
|
+
| **Event Handlers** | ❌ Impossible | ✅ Possible (onClick, onChange, etc.) |
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
// ✅ Server Component (default)
|
|
313
|
+
// app/users/page.tsx
|
|
314
|
+
import { prisma } from '@/lib/db/prisma'
|
|
315
|
+
|
|
316
|
+
export default async function UsersPage() {
|
|
317
|
+
// Direct DB query on server
|
|
318
|
+
const users = await prisma.user.findMany()
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<div>
|
|
322
|
+
{users.map((user) => (
|
|
323
|
+
<div key={user.id}>{user.name}</div>
|
|
324
|
+
))}
|
|
325
|
+
</div>
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ✅ Client Component
|
|
330
|
+
// app/users/_components/user-list.tsx
|
|
331
|
+
'use client'
|
|
332
|
+
|
|
333
|
+
import { useState } from 'react'
|
|
334
|
+
|
|
335
|
+
export default function UserList() {
|
|
336
|
+
const [count, setCount] = useState(0)
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<div>
|
|
340
|
+
<button onClick={() => setCount(count + 1)}>
|
|
341
|
+
Count: {count}
|
|
342
|
+
</button>
|
|
343
|
+
</div>
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Component Composition Strategy
|
|
349
|
+
|
|
350
|
+
```
|
|
351
|
+
Page (Server Component)
|
|
352
|
+
├─ Data fetching (async/await)
|
|
353
|
+
└─ Interactive UI (Client Component)
|
|
354
|
+
└─ State management, event handlers
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
</component_types>
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
<route_lifecycle>
|
|
362
|
+
|
|
363
|
+
## Route Lifecycle
|
|
364
|
+
|
|
365
|
+
### Loading & Error Handling
|
|
366
|
+
|
|
367
|
+
| File | Purpose | Required |
|
|
368
|
+
|------|---------|----------|
|
|
369
|
+
| **loading.tsx** | Loading UI (Suspense boundary) | Optional |
|
|
370
|
+
| **error.tsx** | Error UI (Error boundary) | ✅ |
|
|
371
|
+
| **not-found.tsx** | 404 UI | ✅ |
|
|
372
|
+
| **global-error.tsx** | Global error UI | Optional |
|
|
373
|
+
|
|
374
|
+
```
|
|
375
|
+
app/
|
|
376
|
+
├── layout.tsx
|
|
377
|
+
├── loading.tsx # global loading
|
|
378
|
+
├── error.tsx # global error
|
|
379
|
+
├── not-found.tsx # global 404
|
|
380
|
+
├── global-error.tsx # root error (catches layout.tsx errors too)
|
|
381
|
+
└── users/
|
|
382
|
+
├── page.tsx
|
|
383
|
+
├── loading.tsx # /users loading
|
|
384
|
+
└── error.tsx # /users error
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Code Patterns
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
// ✅ loading.tsx: loading UI
|
|
391
|
+
export default function Loading() {
|
|
392
|
+
return <div>Loading...</div>
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ✅ error.tsx: error UI (Client Component required)
|
|
396
|
+
'use client'
|
|
397
|
+
|
|
398
|
+
export default function Error({
|
|
399
|
+
error,
|
|
400
|
+
reset,
|
|
401
|
+
}: {
|
|
402
|
+
error: Error & { digest?: string }
|
|
403
|
+
reset: () => void
|
|
404
|
+
}) {
|
|
405
|
+
return (
|
|
406
|
+
<div>
|
|
407
|
+
<h2>{error.message}</h2>
|
|
408
|
+
<button onClick={reset}>Retry</button>
|
|
409
|
+
</div>
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ✅ not-found.tsx: 404 UI
|
|
414
|
+
import Link from 'next/link'
|
|
415
|
+
|
|
416
|
+
export default function NotFound() {
|
|
417
|
+
return (
|
|
418
|
+
<div>
|
|
419
|
+
<h2>Not Found</h2>
|
|
420
|
+
<Link href="/">Home</Link>
|
|
421
|
+
</div>
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
</route_lifecycle>
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
<data_flow>
|
|
431
|
+
|
|
432
|
+
## Data Flow
|
|
433
|
+
|
|
434
|
+
### Query Flow (Read)
|
|
435
|
+
|
|
436
|
+
```
|
|
437
|
+
Page (Server Component) → Prisma → Database
|
|
438
|
+
↓
|
|
439
|
+
Auto caching (fetch cache)
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
// ✅ Direct data fetching in Server Component
|
|
444
|
+
// app/users/page.tsx
|
|
445
|
+
import { prisma } from '@/lib/db/prisma'
|
|
446
|
+
|
|
447
|
+
export default async function UsersPage() {
|
|
448
|
+
const users = await prisma.user.findMany()
|
|
449
|
+
|
|
450
|
+
return (
|
|
451
|
+
<div>
|
|
452
|
+
{users.map((user) => (
|
|
453
|
+
<div key={user.id}>{user.name}</div>
|
|
454
|
+
))}
|
|
455
|
+
</div>
|
|
456
|
+
)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ✅ fetch with cache (default: 'force-cache')
|
|
460
|
+
async function getUsers() {
|
|
461
|
+
const res = await fetch('https://api.example.com/users', {
|
|
462
|
+
next: { revalidate: 3600 }, // 1 hour cache
|
|
463
|
+
})
|
|
464
|
+
return res.json()
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### Mutation Flow (Write)
|
|
469
|
+
|
|
470
|
+
```
|
|
471
|
+
Form (Client) → Server Action → Prisma → Database
|
|
472
|
+
↓
|
|
473
|
+
revalidatePath/revalidateTag
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
// ✅ Server Action
|
|
478
|
+
// app/_actions/create-user.ts
|
|
479
|
+
'use server'
|
|
480
|
+
|
|
481
|
+
import { prisma } from '@/lib/db/prisma'
|
|
482
|
+
import { revalidatePath } from 'next/cache'
|
|
483
|
+
import { z } from 'zod'
|
|
484
|
+
|
|
485
|
+
const createUserSchema = z.object({
|
|
486
|
+
name: z.string().min(1),
|
|
487
|
+
email: z.email(),
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
export async function createUser(formData: FormData) {
|
|
491
|
+
// Validation
|
|
492
|
+
const parsed = createUserSchema.safeParse({
|
|
493
|
+
name: formData.get('name'),
|
|
494
|
+
email: formData.get('email'),
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
if (!parsed.success) {
|
|
498
|
+
return { error: parsed.error.errors }
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// DB save
|
|
502
|
+
const user = await prisma.user.create({
|
|
503
|
+
data: parsed.data,
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
// Cache invalidation
|
|
507
|
+
revalidatePath('/users')
|
|
508
|
+
|
|
509
|
+
return { success: true, user }
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ✅ Usage in Client Component
|
|
513
|
+
// app/users/_components/user-form.tsx
|
|
514
|
+
'use client'
|
|
515
|
+
|
|
516
|
+
import { createUser } from '@/app/_actions/create-user'
|
|
517
|
+
|
|
518
|
+
export default function UserForm() {
|
|
519
|
+
async function handleSubmit(formData: FormData) {
|
|
520
|
+
const result = await createUser(formData)
|
|
521
|
+
|
|
522
|
+
if (result.error) {
|
|
523
|
+
console.error(result.error)
|
|
524
|
+
} else {
|
|
525
|
+
console.log('User created:', result.user)
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return (
|
|
530
|
+
<form action={handleSubmit}>
|
|
531
|
+
<input name="name" required />
|
|
532
|
+
<input name="email" type="email" required />
|
|
533
|
+
<button type="submit">Create</button>
|
|
534
|
+
</form>
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
</data_flow>
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
<server_actions_advanced>
|
|
544
|
+
|
|
545
|
+
## Server Actions (Advanced)
|
|
546
|
+
|
|
547
|
+
### Server Actions Patterns
|
|
548
|
+
|
|
549
|
+
| Pattern | Description | When to Use |
|
|
550
|
+
|---------|-------------|-------------|
|
|
551
|
+
| **Form Actions** | `<form action={...}>` | Form submission |
|
|
552
|
+
| **Programmatic** | `onClick={() => action()}` | Button click |
|
|
553
|
+
| **Progressive Enhancement** | Works without JS | Accessibility focus |
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
// ✅ Form Action (Progressive Enhancement)
|
|
557
|
+
// app/_actions/delete-user.ts
|
|
558
|
+
'use server'
|
|
559
|
+
|
|
560
|
+
import { prisma } from '@/lib/db/prisma'
|
|
561
|
+
import { revalidatePath } from 'next/cache'
|
|
562
|
+
import { redirect } from 'next/navigation'
|
|
563
|
+
|
|
564
|
+
export async function deleteUser(formData: FormData) {
|
|
565
|
+
const id = formData.get('id') as string
|
|
566
|
+
|
|
567
|
+
await prisma.user.delete({
|
|
568
|
+
where: { id },
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
revalidatePath('/users')
|
|
572
|
+
redirect('/users')
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// app/users/[id]/_components/delete-button.tsx
|
|
576
|
+
'use client'
|
|
577
|
+
|
|
578
|
+
import { deleteUser } from '@/app/_actions/delete-user'
|
|
579
|
+
|
|
580
|
+
export default function DeleteButton({ id }: { id: string }) {
|
|
581
|
+
return (
|
|
582
|
+
<form action={deleteUser}>
|
|
583
|
+
<input type="hidden" name="id" value={id} />
|
|
584
|
+
<button type="submit">Delete</button>
|
|
585
|
+
</form>
|
|
586
|
+
)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// ✅ Programmatic Action
|
|
590
|
+
// app/users/_components/user-list.tsx
|
|
591
|
+
'use client'
|
|
592
|
+
|
|
593
|
+
import { useState, useTransition } from 'react'
|
|
594
|
+
import { deleteUser } from '@/app/_actions/delete-user'
|
|
595
|
+
|
|
596
|
+
export default function UserList({ users }) {
|
|
597
|
+
const [isPending, startTransition] = useTransition()
|
|
598
|
+
|
|
599
|
+
function handleDelete(id: string) {
|
|
600
|
+
startTransition(async () => {
|
|
601
|
+
const formData = new FormData()
|
|
602
|
+
formData.append('id', id)
|
|
603
|
+
await deleteUser(formData)
|
|
604
|
+
})
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return (
|
|
608
|
+
<div>
|
|
609
|
+
{users.map((user) => (
|
|
610
|
+
<div key={user.id}>
|
|
611
|
+
{user.name}
|
|
612
|
+
<button
|
|
613
|
+
onClick={() => handleDelete(user.id)}
|
|
614
|
+
disabled={isPending}
|
|
615
|
+
>
|
|
616
|
+
Delete
|
|
617
|
+
</button>
|
|
618
|
+
</div>
|
|
619
|
+
))}
|
|
620
|
+
</div>
|
|
621
|
+
)
|
|
622
|
+
}
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
### Authentication Pattern
|
|
626
|
+
|
|
627
|
+
```typescript
|
|
628
|
+
// ✅ lib/auth/session.ts
|
|
629
|
+
import { cookies } from 'next/headers'
|
|
630
|
+
|
|
631
|
+
export async function getSession() {
|
|
632
|
+
const cookieStore = await cookies()
|
|
633
|
+
const session = cookieStore.get('session')
|
|
634
|
+
// Session validation logic
|
|
635
|
+
return session
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ✅ Authentication check in Server Action
|
|
639
|
+
// app/_actions/create-post.ts
|
|
640
|
+
'use server'
|
|
641
|
+
|
|
642
|
+
import { getSession } from '@/lib/auth/session'
|
|
643
|
+
import { prisma } from '@/lib/db/prisma'
|
|
644
|
+
import { redirect } from 'next/navigation'
|
|
645
|
+
|
|
646
|
+
export async function createPost(formData: FormData) {
|
|
647
|
+
const session = await getSession()
|
|
648
|
+
|
|
649
|
+
if (!session?.user) {
|
|
650
|
+
redirect('/login')
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const post = await prisma.post.create({
|
|
654
|
+
data: {
|
|
655
|
+
title: formData.get('title') as string,
|
|
656
|
+
authorId: session.user.id,
|
|
657
|
+
},
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
return { success: true, post }
|
|
661
|
+
}
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
</server_actions_advanced>
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
<metadata>
|
|
669
|
+
|
|
670
|
+
## Metadata
|
|
671
|
+
|
|
672
|
+
### Static Metadata
|
|
673
|
+
|
|
674
|
+
```typescript
|
|
675
|
+
// ✅ Static metadata export
|
|
676
|
+
// app/users/page.tsx
|
|
677
|
+
import { Metadata } from 'next'
|
|
678
|
+
|
|
679
|
+
export const metadata: Metadata = {
|
|
680
|
+
title: 'Users',
|
|
681
|
+
description: 'User list page',
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
export default function UsersPage() {
|
|
685
|
+
return <div>Users</div>
|
|
686
|
+
}
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
### Dynamic Metadata
|
|
690
|
+
|
|
691
|
+
```typescript
|
|
692
|
+
// ✅ Dynamic metadata with generateMetadata
|
|
693
|
+
// app/users/[id]/page.tsx
|
|
694
|
+
import { Metadata } from 'next'
|
|
695
|
+
import { prisma } from '@/lib/db/prisma'
|
|
696
|
+
|
|
697
|
+
type Props = {
|
|
698
|
+
params: Promise<{ id: string }>
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|
702
|
+
const { id } = await params
|
|
703
|
+
const user = await prisma.user.findUnique({ where: { id } })
|
|
704
|
+
|
|
705
|
+
return {
|
|
706
|
+
title: user?.name ?? 'User',
|
|
707
|
+
description: `Profile of ${user?.name}`,
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export default async function UserPage({ params }: Props) {
|
|
712
|
+
const { id } = await params
|
|
713
|
+
const user = await prisma.user.findUnique({ where: { id } })
|
|
714
|
+
|
|
715
|
+
return <div>{user?.name}</div>
|
|
716
|
+
}
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
</metadata>
|
|
720
|
+
|
|
721
|
+
---
|
|
722
|
+
|
|
723
|
+
<caching>
|
|
724
|
+
|
|
725
|
+
## Caching
|
|
726
|
+
|
|
727
|
+
### fetch() Caching
|
|
728
|
+
|
|
729
|
+
| Option | Description |
|
|
730
|
+
|--------|-------------|
|
|
731
|
+
| `{ cache: 'force-cache' }` | Default, cache indefinitely |
|
|
732
|
+
| `{ cache: 'no-store' }` | No caching |
|
|
733
|
+
| `{ next: { revalidate: 3600 } }` | Revalidate every 3600 seconds |
|
|
734
|
+
| `{ next: { tags: ['users'] } }` | Tag-based invalidation |
|
|
735
|
+
|
|
736
|
+
```typescript
|
|
737
|
+
// ✅ fetch with cache
|
|
738
|
+
async function getUsers() {
|
|
739
|
+
const res = await fetch('https://api.example.com/users', {
|
|
740
|
+
next: { revalidate: 3600, tags: ['users'] },
|
|
741
|
+
})
|
|
742
|
+
return res.json()
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// ✅ Cache invalidation
|
|
746
|
+
// app/_actions/create-user.ts
|
|
747
|
+
'use server'
|
|
748
|
+
|
|
749
|
+
import { revalidateTag, revalidatePath } from 'next/cache'
|
|
750
|
+
|
|
751
|
+
export async function createUser(data: any) {
|
|
752
|
+
// ...
|
|
753
|
+
|
|
754
|
+
// Tag-based invalidation
|
|
755
|
+
revalidateTag('users')
|
|
756
|
+
|
|
757
|
+
// Path-based invalidation
|
|
758
|
+
revalidatePath('/users')
|
|
759
|
+
}
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
### unstable_cache (Prisma, etc.)
|
|
763
|
+
|
|
764
|
+
```typescript
|
|
765
|
+
// ✅ Prisma query caching
|
|
766
|
+
import { unstable_cache } from 'next/cache'
|
|
767
|
+
import { prisma } from '@/lib/db/prisma'
|
|
768
|
+
|
|
769
|
+
const getUsers = unstable_cache(
|
|
770
|
+
async () => prisma.user.findMany(),
|
|
771
|
+
['users'],
|
|
772
|
+
{
|
|
773
|
+
revalidate: 3600,
|
|
774
|
+
tags: ['users'],
|
|
775
|
+
}
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
export default async function UsersPage() {
|
|
779
|
+
const users = await getUsers()
|
|
780
|
+
return <div>{/* ... */}</div>
|
|
781
|
+
}
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
</caching>
|
|
785
|
+
|
|
786
|
+
---
|
|
787
|
+
|
|
788
|
+
<tech_stack>
|
|
789
|
+
|
|
790
|
+
## Technology Stack
|
|
791
|
+
|
|
792
|
+
| Layer | Technology | Version |
|
|
793
|
+
|-------|------------|---------|
|
|
794
|
+
| Framework | Next.js | 15+ |
|
|
795
|
+
| Router | App Router | - |
|
|
796
|
+
| Data | TanStack Query | 5.x |
|
|
797
|
+
| ORM | Prisma | 7.x |
|
|
798
|
+
| Validation | Zod | 4.x |
|
|
799
|
+
| Database | PostgreSQL | - |
|
|
800
|
+
| UI | React 19+ | - |
|
|
801
|
+
|
|
802
|
+
</tech_stack>
|
|
803
|
+
|
|
804
|
+
---
|
|
805
|
+
|
|
806
|
+
## Sources
|
|
807
|
+
|
|
808
|
+
- [Next.js App Router Documentation](https://nextjs.org/docs/app)
|
|
809
|
+
- [Next.js Project Structure Guide](https://nextjs.org/docs/app/getting-started/project-structure)
|
|
810
|
+
- [Next.js 15 App Router Best Practices (Medium)](https://medium.com/better-dev-nextjs-react/inside-the-app-router-best-practices-for-next-js-file-and-directory-structure-2025-edition-ed6bc14a8da3)
|
|
811
|
+
- [Mastering Next.js App Router (Medium)](https://thiraphat-ps-dev.medium.com/mastering-next-js-app-router-best-practices-for-structuring-your-application-3f8cf0c76580)
|
|
812
|
+
- [Modern Full Stack Architecture with Next.js 15+](https://softwaremill.com/modern-full-stack-application-architecture-using-next-js-15/)
|