claudify-agent 1.0.1 → 1.0.2
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/.claude/QUICK-REFERENCE.md +213 -213
- package/.claude/cost-dashboard.md +162 -162
- package/.claude/docs/COST-OPTIMIZATION.md +402 -403
- package/.claude/docs/MCP-INTEGRATION-GUIDE.md +348 -348
- package/.claude/docs/PLAN-MODE-OPTIMIZATION.md +835 -836
- package/.claude/hooks/auto-learning-review.ts +200 -195
- package/.claude/hooks/auto-lint-fix.ts +60 -104
- package/.claude/hooks/auto-security-review.ts +314 -354
- package/.claude/hooks/auto-validate.ts +249 -348
- package/.claude/hooks/lib/claude-hook.ts +168 -0
- package/.claude/hooks/plan-quality-tracker.ts +598 -719
- package/.claude/rules/anti-patterns.md +22 -22
- package/.claude/rules/context-tier-1.md +41 -41
- package/.claude/rules/context-tier-2.md +285 -285
- package/.claude/rules/general.md +25 -25
- package/.claude/rules/mcp-auto-use.md +46 -46
- package/.claude/rules/parallel-exploration-auto.md +30 -31
- package/.claude/rules/plan-mode-execution.md +289 -289
- package/.claude/rules/plan-mode-research.md +34 -34
- package/.claude/rules/plan-mode-structure.md +166 -166
- package/.claude/rules/plan-mode-validation.md +129 -130
- package/.claude/rules/security-critical.md +19 -19
- package/.claude/rules/tool-auto-selection.md +45 -45
- package/.claude/rules/tool-strategy.md +22 -22
- package/.claude/scripts/security-scan.js +1 -1
- package/.claude/security-dashboard.md +142 -142
- package/.claude/settings.json +50 -0
- package/.claude/skills/debug-optimizer/SKILL.md +420 -399
- package/.claude/skills/learning-loop/SKILL.md +313 -308
- package/.claude/skills/mcp-mastery/SKILL.md +218 -214
- package/.claude/skills/nextjs-guide/SKILL.md +687 -687
- package/.claude/skills/parallel-exploration/SKILL.md +197 -193
- package/.claude/skills/plan-mode-mastery/SKILL.md +268 -263
- package/.claude/skills/python-guide/SKILL.md +822 -822
- package/.claude/skills/react-guide/SKILL.md +762 -762
- package/.claude/skills/{security.md → security-guide/SKILL.md} +10 -0
- package/.claude/skills/solidity-guide/SKILL.md +5 -0
- package/.claude/skills/tool-selection/SKILL.md +433 -428
- package/.claude/skills/vite-guide/SKILL.md +445 -445
- package/CLAUDE.md +1 -0
- package/README.md +74 -35
- package/README.vi.md +106 -0
- package/bin/cli.js +13 -4
- package/package.json +9 -1
- package/.claude/skills/debug-optimizer.md +0 -12
- package/.claude/skills/nextjs.md +0 -13
- package/.claude/skills/react.md +0 -12
|
@@ -1,687 +1,687 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: nextjs-guide
|
|
3
|
-
description: Apply Next.js 14-15 best practices including App Router, Server Components, data fetching, and project structure. Use when building, reviewing, or optimizing Next.js applications.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Next.js Best Practices
|
|
7
|
-
|
|
8
|
-
## Quick Start
|
|
9
|
-
|
|
10
|
-
When working with Next.js 14-15:
|
|
11
|
-
|
|
12
|
-
1. **Server Components by default**: Only use `"use client"` when necessary
|
|
13
|
-
2. **App Router structure**: Use route groups, layouts, and proper file organization
|
|
14
|
-
3. **Data fetching**: Fetch directly in Server Components, use proper caching
|
|
15
|
-
4. **Component composition**: Keep client boundary small, compose Server Components inside Client Components
|
|
16
|
-
5. **Project structure**: Use `src/` directory with proper separation (components, lib, utils)
|
|
17
|
-
6. **Performance**: Leverage automatic optimizations (code-splitting, prefetching, caching)
|
|
18
|
-
|
|
19
|
-
## Server vs Client Components
|
|
20
|
-
|
|
21
|
-
### Server Components (Default)
|
|
22
|
-
|
|
23
|
-
Use Server Components by default for data fetching and rendering:
|
|
24
|
-
|
|
25
|
-
```tsx
|
|
26
|
-
// ✅ Good: Server Component (no "use client")
|
|
27
|
-
async function ProductPage({ params }: { params: { id: string } }) {
|
|
28
|
-
const product = await fetch(`https://api.example.com/products/${params.id}`, {
|
|
29
|
-
next: { revalidate: 60 }
|
|
30
|
-
}).then(res => res.json());
|
|
31
|
-
|
|
32
|
-
return <div>{product.name}</div>;
|
|
33
|
-
}
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
**Use Server Components when**:
|
|
37
|
-
- Fetching data from APIs or databases
|
|
38
|
-
- Accessing environment variables
|
|
39
|
-
- Rendering static or semi-static content
|
|
40
|
-
- Keeping bundle size small
|
|
41
|
-
|
|
42
|
-
### Client Components
|
|
43
|
-
|
|
44
|
-
Add `"use client"` only when genuinely needed:
|
|
45
|
-
|
|
46
|
-
```tsx
|
|
47
|
-
// ✅ Good: Client Component with "use client"
|
|
48
|
-
"use client";
|
|
49
|
-
|
|
50
|
-
import { useState } from 'react';
|
|
51
|
-
|
|
52
|
-
export function Counter() {
|
|
53
|
-
const [count, setCount] = useState(0);
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<button onClick={() => setCount(count + 1)}>
|
|
57
|
-
Count: {count}
|
|
58
|
-
</button>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
**Use Client Components when**:
|
|
64
|
-
- Event handlers (`onClick`, `onChange`, `onSubmit`)
|
|
65
|
-
- Browser APIs (`window`, `localStorage`, `IntersectionObserver`)
|
|
66
|
-
- React state and effects (`useState`, `useEffect`, `useRef`)
|
|
67
|
-
- Third-party libraries that require client-side execution
|
|
68
|
-
|
|
69
|
-
### Composition Pattern
|
|
70
|
-
|
|
71
|
-
Keep client boundary small by composing Server Components inside Client Components:
|
|
72
|
-
|
|
73
|
-
```tsx
|
|
74
|
-
// ✅ Good: Client Component receiving Server Component as children
|
|
75
|
-
// components/dashboard/DashboardLayout.tsx
|
|
76
|
-
"use client";
|
|
77
|
-
|
|
78
|
-
import { useState } from 'react';
|
|
79
|
-
|
|
80
|
-
export function DashboardLayout({ children }: { children: React.ReactNode }) {
|
|
81
|
-
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<div>
|
|
85
|
-
<button onClick={() => setSidebarOpen(!sidebarOpen)}>Toggle</button>
|
|
86
|
-
{children}
|
|
87
|
-
</div>
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
```tsx
|
|
93
|
-
// ✅ Good: Server Component passing content to Client Component
|
|
94
|
-
// app/dashboard/page.tsx
|
|
95
|
-
import { DashboardLayout } from '@/components/dashboard/DashboardLayout';
|
|
96
|
-
import { StatsCard } from '@/components/dashboard/StatsCard'; // Server Component
|
|
97
|
-
|
|
98
|
-
export default async function DashboardPage() {
|
|
99
|
-
const stats = await fetchStats(); // Server-side data fetching
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<DashboardLayout>
|
|
103
|
-
<StatsCard data={stats} />
|
|
104
|
-
</DashboardLayout>
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## Data Fetching
|
|
110
|
-
|
|
111
|
-
### Direct Fetching in Server Components
|
|
112
|
-
|
|
113
|
-
Fetch data directly without `useEffect`:
|
|
114
|
-
|
|
115
|
-
```tsx
|
|
116
|
-
// ✅ Good: Direct data fetching
|
|
117
|
-
async function ProductList() {
|
|
118
|
-
const products = await fetch('https://api.example.com/products', {
|
|
119
|
-
next: { revalidate: 60 } // Revalidate every 60 seconds
|
|
120
|
-
}).then(res => res.json());
|
|
121
|
-
|
|
122
|
-
return (
|
|
123
|
-
<ul>
|
|
124
|
-
{products.map(product => (
|
|
125
|
-
<li key={product.id}>{product.name}</li>
|
|
126
|
-
))}
|
|
127
|
-
</ul>
|
|
128
|
-
);
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### Parallel Fetching
|
|
133
|
-
|
|
134
|
-
Use `Promise.all()` for multiple resources:
|
|
135
|
-
|
|
136
|
-
```tsx
|
|
137
|
-
// ✅ Good: Parallel data fetching
|
|
138
|
-
async function Dashboard() {
|
|
139
|
-
const [users, products, orders] = await Promise.all([
|
|
140
|
-
fetch('https://api.example.com/users').then(res => res.json()),
|
|
141
|
-
fetch('https://api.example.com/products').then(res => res.json()),
|
|
142
|
-
fetch('https://api.example.com/orders').then(res => res.json())
|
|
143
|
-
]);
|
|
144
|
-
|
|
145
|
-
return (
|
|
146
|
-
<div>
|
|
147
|
-
<UsersSection users={users} />
|
|
148
|
-
<ProductsSection products={products} />
|
|
149
|
-
<OrdersSection orders={orders} />
|
|
150
|
-
</div>
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
### Caching Strategies
|
|
156
|
-
|
|
157
|
-
```tsx
|
|
158
|
-
// Time-based revalidation
|
|
159
|
-
const data = await fetch(url, {
|
|
160
|
-
next: { revalidate: 3600 } // Revalidate every hour
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// No caching (always fresh)
|
|
164
|
-
const data = await fetch(url, { cache: 'no-store' });
|
|
165
|
-
|
|
166
|
-
// On-demand revalidation
|
|
167
|
-
// In your route handler:
|
|
168
|
-
import { revalidateTag } from 'next/cache';
|
|
169
|
-
|
|
170
|
-
revalidateTag('products'); // Revalidate all data tagged with 'products'
|
|
171
|
-
|
|
172
|
-
// In your fetch:
|
|
173
|
-
const data = await fetch(url, {
|
|
174
|
-
next: { tags: ['products'] }
|
|
175
|
-
});
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
## Project Structure
|
|
179
|
-
|
|
180
|
-
### Core Directory Layout
|
|
181
|
-
|
|
182
|
-
```
|
|
183
|
-
src/
|
|
184
|
-
├── app/ # App Router (routes)
|
|
185
|
-
│ ├── (auth)/ # Route group (doesn't affect URL)
|
|
186
|
-
│ │ ├── login/
|
|
187
|
-
│ │ └── register/
|
|
188
|
-
│ ├── (dashboard)/ # Another route group
|
|
189
|
-
│ │ ├── layout.tsx # Shared layout for dashboard routes
|
|
190
|
-
│ │ ├── page.tsx # /dashboard
|
|
191
|
-
│ │ └── settings/
|
|
192
|
-
│ ├── api/ # API routes
|
|
193
|
-
│ │ └── users/
|
|
194
|
-
│ │ └── route.ts
|
|
195
|
-
│ ├── globals.css
|
|
196
|
-
│ ├── layout.tsx # Root layout
|
|
197
|
-
│ └── page.tsx # Home page
|
|
198
|
-
├── components/
|
|
199
|
-
│ ├── ui/ # Reusable UI components (Button, Card, Modal)
|
|
200
|
-
│ ├── layout/ # Layout components (Header, Footer, Sidebar)
|
|
201
|
-
│ └── features/ # Feature-specific components
|
|
202
|
-
│ ├── auth/
|
|
203
|
-
│ ├── dashboard/
|
|
204
|
-
│ └── products/
|
|
205
|
-
├── lib/ # Business logic, API clients, auth
|
|
206
|
-
│ ├── api.ts
|
|
207
|
-
│ ├── auth.ts
|
|
208
|
-
│ └── db.ts
|
|
209
|
-
├── utils/ # Pure utility functions
|
|
210
|
-
│ ├── cn.ts
|
|
211
|
-
│ └── formatters.ts
|
|
212
|
-
├── types/ # TypeScript types
|
|
213
|
-
│ └── index.ts
|
|
214
|
-
└── styles/ # Additional styles
|
|
215
|
-
└── variables.css
|
|
216
|
-
|
|
217
|
-
public/ # Static assets
|
|
218
|
-
├── images/
|
|
219
|
-
└── fonts/
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
### Route Organization
|
|
223
|
-
|
|
224
|
-
Use route groups for organization without affecting URL structure:
|
|
225
|
-
|
|
226
|
-
```
|
|
227
|
-
app/
|
|
228
|
-
├── (marketing)/ # Marketing pages: /, /about, /contact
|
|
229
|
-
│ ├── page.tsx # /
|
|
230
|
-
│ ├── about/
|
|
231
|
-
│ │ └── page.tsx # /about
|
|
232
|
-
│ └── contact/
|
|
233
|
-
│ └── page.tsx # /contact
|
|
234
|
-
├── (auth)/ # Auth pages: /login, /register
|
|
235
|
-
│ ├── login/
|
|
236
|
-
│ │ └── page.tsx # /login
|
|
237
|
-
│ └── register/
|
|
238
|
-
│ └── page.tsx # /register
|
|
239
|
-
└── (app)/ # App routes: /dashboard, /settings
|
|
240
|
-
├── dashboard/
|
|
241
|
-
│ └── page.tsx # /dashboard
|
|
242
|
-
└── settings/
|
|
243
|
-
└── page.tsx # /settings
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
### Component Organization
|
|
247
|
-
|
|
248
|
-
Each component in its own folder:
|
|
249
|
-
|
|
250
|
-
```
|
|
251
|
-
components/ui/Button/
|
|
252
|
-
├── Button.tsx # Main component
|
|
253
|
-
├── Button.test.tsx # Tests
|
|
254
|
-
├── Button.stories.tsx # Storybook (optional)
|
|
255
|
-
└── index.ts # Barrel export
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
```tsx
|
|
259
|
-
// components/ui/Button/Button.tsx
|
|
260
|
-
import { clsx } from 'clsx';
|
|
261
|
-
|
|
262
|
-
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
263
|
-
variant?: 'primary' | 'secondary' | 'outline';
|
|
264
|
-
size?: 'sm' | 'md' | 'lg';
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
export function Button({
|
|
268
|
-
variant = 'primary',
|
|
269
|
-
size = 'md',
|
|
270
|
-
className,
|
|
271
|
-
children,
|
|
272
|
-
...props
|
|
273
|
-
}: ButtonProps) {
|
|
274
|
-
return (
|
|
275
|
-
<button
|
|
276
|
-
className={clsx(
|
|
277
|
-
'font-medium rounded transition-colors',
|
|
278
|
-
{
|
|
279
|
-
'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
|
|
280
|
-
'bg-gray-200 text-gray-900 hover:bg-gray-300': variant === 'secondary',
|
|
281
|
-
'border border-gray-300 hover:bg-gray-100': variant === 'outline',
|
|
282
|
-
'px-3 py-1.5 text-sm': size === 'sm',
|
|
283
|
-
'px-4 py-2 text-base': size === 'md',
|
|
284
|
-
'px-6 py-3 text-lg': size === 'lg',
|
|
285
|
-
},
|
|
286
|
-
className
|
|
287
|
-
)}
|
|
288
|
-
{...props}
|
|
289
|
-
>
|
|
290
|
-
{children}
|
|
291
|
-
</button>
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
## Utils vs Lib
|
|
297
|
-
|
|
298
|
-
Distinguish between utils and lib:
|
|
299
|
-
|
|
300
|
-
```typescript
|
|
301
|
-
// ✅ utils/formatters.ts - Pure functions, no side effects
|
|
302
|
-
export function formatCurrency(amount: number): string {
|
|
303
|
-
return new Intl.NumberFormat('en-US', {
|
|
304
|
-
style: 'currency',
|
|
305
|
-
currency: 'USD'
|
|
306
|
-
}).format(amount);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// ✅ lib/api.ts - Business logic, external dependencies
|
|
310
|
-
import { ENV } from './env';
|
|
311
|
-
|
|
312
|
-
export async function fetchUser(id: string) {
|
|
313
|
-
const response = await fetch(`${ENV.API_URL}/users/${id}`, {
|
|
314
|
-
headers: {
|
|
315
|
-
'Authorization': `Bearer ${ENV.API_KEY}`
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
if (!response.ok) {
|
|
320
|
-
throw new Error('Failed to fetch user');
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return response.json();
|
|
324
|
-
}
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
## Performance Optimizations
|
|
328
|
-
|
|
329
|
-
### Automatic Optimizations (Built-in)
|
|
330
|
-
|
|
331
|
-
Next.js provides by default:
|
|
332
|
-
- **Code-splitting**: By route segments
|
|
333
|
-
- **Link prefetching**: `<Link>` components prefetch in viewport
|
|
334
|
-
- **Prerendering**: At build time
|
|
335
|
-
- **Intelligent caching**: Data requests and rendered components
|
|
336
|
-
|
|
337
|
-
### Image Optimization
|
|
338
|
-
|
|
339
|
-
```tsx
|
|
340
|
-
import Image from 'next/image';
|
|
341
|
-
|
|
342
|
-
// ✅ Good: Optimized image
|
|
343
|
-
<Image
|
|
344
|
-
src="/hero.jpg"
|
|
345
|
-
alt="Hero banner"
|
|
346
|
-
width={1200}
|
|
347
|
-
height={630}
|
|
348
|
-
priority // For above-the-fold images
|
|
349
|
-
quality={85}
|
|
350
|
-
/>
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
### Font Optimization
|
|
354
|
-
|
|
355
|
-
```tsx
|
|
356
|
-
// ✅ Good: Optimized fonts
|
|
357
|
-
import { Inter } from 'next/font/google';
|
|
358
|
-
|
|
359
|
-
const inter = Inter({
|
|
360
|
-
subsets: ['latin'],
|
|
361
|
-
display: 'swap',
|
|
362
|
-
variable: '--font-inter'
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
export default function RootLayout({ children }) {
|
|
366
|
-
return (
|
|
367
|
-
<html lang="en" className={inter.variable}>
|
|
368
|
-
<body>{children}</body>
|
|
369
|
-
</html>
|
|
370
|
-
);
|
|
371
|
-
}
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
## Error Handling
|
|
375
|
-
|
|
376
|
-
### Error Boundaries
|
|
377
|
-
|
|
378
|
-
```tsx
|
|
379
|
-
// app/error.tsx
|
|
380
|
-
"use client";
|
|
381
|
-
|
|
382
|
-
import { useEffect } from 'react';
|
|
383
|
-
|
|
384
|
-
export default function Error({
|
|
385
|
-
error,
|
|
386
|
-
reset,
|
|
387
|
-
}: {
|
|
388
|
-
error: Error & { digest?: string };
|
|
389
|
-
reset: () => void;
|
|
390
|
-
}) {
|
|
391
|
-
useEffect(() => {
|
|
392
|
-
console.error('Application error:', error);
|
|
393
|
-
}, [error]);
|
|
394
|
-
|
|
395
|
-
return (
|
|
396
|
-
<div>
|
|
397
|
-
<h2>Something went wrong!</h2>
|
|
398
|
-
<button onClick={() => reset()}>Try again</button>
|
|
399
|
-
</div>
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
### Not Found Handling
|
|
405
|
-
|
|
406
|
-
```tsx
|
|
407
|
-
// app/not-found.tsx
|
|
408
|
-
import Link from 'next/link';
|
|
409
|
-
|
|
410
|
-
export default function NotFound() {
|
|
411
|
-
return (
|
|
412
|
-
<div>
|
|
413
|
-
<h2>Page Not Found</h2>
|
|
414
|
-
<p>Could not find requested resource</p>
|
|
415
|
-
<Link href="/">Return Home</Link>
|
|
416
|
-
</div>
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
### Loading States
|
|
422
|
-
|
|
423
|
-
```tsx
|
|
424
|
-
// app/dashboard/loading.tsx
|
|
425
|
-
export default function Loading() {
|
|
426
|
-
return (
|
|
427
|
-
<div className="flex items-center justify-center min-h-screen">
|
|
428
|
-
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600" />
|
|
429
|
-
</div>
|
|
430
|
-
);
|
|
431
|
-
}
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
## API Routes
|
|
435
|
-
|
|
436
|
-
### RESTful API Structure
|
|
437
|
-
|
|
438
|
-
```typescript
|
|
439
|
-
// app/api/users/route.ts
|
|
440
|
-
import { NextRequest, NextResponse } from 'next/server';
|
|
441
|
-
import { db } from '@/lib/db';
|
|
442
|
-
|
|
443
|
-
export async function GET() {
|
|
444
|
-
try {
|
|
445
|
-
const users = await db.user.findMany();
|
|
446
|
-
return NextResponse.json(users);
|
|
447
|
-
} catch (error) {
|
|
448
|
-
console.error('Failed to fetch users:', error);
|
|
449
|
-
return NextResponse.json(
|
|
450
|
-
{ error: 'Failed to fetch users' },
|
|
451
|
-
{ status: 500 }
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
export async function POST(request: NextRequest) {
|
|
457
|
-
try {
|
|
458
|
-
const body = await request.json();
|
|
459
|
-
const user = await db.user.create({ data: body });
|
|
460
|
-
return NextResponse.json(user, { status: 201 });
|
|
461
|
-
} catch (error) {
|
|
462
|
-
return NextResponse.json(
|
|
463
|
-
{ error: 'Failed to create user' },
|
|
464
|
-
{ status: 400 }
|
|
465
|
-
);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
## Testing
|
|
471
|
-
|
|
472
|
-
### Component Testing with Vitest
|
|
473
|
-
|
|
474
|
-
```tsx
|
|
475
|
-
// __tests__/Button.test.tsx
|
|
476
|
-
import { render, screen } from '@testing-library/react';
|
|
477
|
-
import { describe, it, expect } from 'vitest';
|
|
478
|
-
import { Button } from '@/components/ui/Button';
|
|
479
|
-
|
|
480
|
-
describe('Button', () => {
|
|
481
|
-
it('renders with default props', () => {
|
|
482
|
-
render(<Button>Click me</Button>);
|
|
483
|
-
expect(screen.getByRole('button')).toHaveTextContent('Click me');
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
it('handles click events', () => {
|
|
487
|
-
const handleClick = vi.fn();
|
|
488
|
-
render(<Button onClick={handleClick}>Click me</Button>);
|
|
489
|
-
screen.getByRole('button').click();
|
|
490
|
-
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
it('applies variant classes', () => {
|
|
494
|
-
const { container } = render(<Button variant="primary">Test</Button>);
|
|
495
|
-
expect(container.firstChild).toHaveClass('bg-blue-600');
|
|
496
|
-
});
|
|
497
|
-
});
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
## Commands
|
|
501
|
-
|
|
502
|
-
### Essential Next.js Commands
|
|
503
|
-
|
|
504
|
-
```bash
|
|
505
|
-
npm run dev # Start development server
|
|
506
|
-
npm run build # Build for production
|
|
507
|
-
npm run start # Start production server
|
|
508
|
-
npm run lint # Run ESLint
|
|
509
|
-
npm run typecheck # Run TypeScript checker
|
|
510
|
-
npm run test # Run test suite
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
## Anti-Patterns to Avoid
|
|
514
|
-
|
|
515
|
-
### ❌ Bad: Unnecessary Client Components
|
|
516
|
-
|
|
517
|
-
```tsx
|
|
518
|
-
// ❌ Bad: Using Client Component when Server Component suffices
|
|
519
|
-
"use client";
|
|
520
|
-
|
|
521
|
-
export async function ProductPage({ params }) {
|
|
522
|
-
const product = await fetchProduct(params.id);
|
|
523
|
-
return <div>{product.name}</div>;
|
|
524
|
-
}
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
```tsx
|
|
528
|
-
// ✅ Good: Server Component
|
|
529
|
-
export async function ProductPage({ params }) {
|
|
530
|
-
const product = await fetchProduct(params.id);
|
|
531
|
-
return <div>{product.name}</div>;
|
|
532
|
-
}
|
|
533
|
-
```
|
|
534
|
-
|
|
535
|
-
### ❌ Bad: Large Client Components
|
|
536
|
-
|
|
537
|
-
```tsx
|
|
538
|
-
// ❌ Bad: Everything in one Client Component
|
|
539
|
-
"use client";
|
|
540
|
-
|
|
541
|
-
export function Dashboard() {
|
|
542
|
-
const [data, setData] = useState(null);
|
|
543
|
-
|
|
544
|
-
useEffect(() => {
|
|
545
|
-
fetchData().then(setData);
|
|
546
|
-
}, []);
|
|
547
|
-
|
|
548
|
-
return <div>...</div>;
|
|
549
|
-
}
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
```tsx
|
|
553
|
-
// ✅ Good: Split Server and Client
|
|
554
|
-
async function Dashboard() {
|
|
555
|
-
const data = await fetchData();
|
|
556
|
-
return <DashboardClient initialData={data} />;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
"use client";
|
|
560
|
-
|
|
561
|
-
function DashboardClient({ initialData }) {
|
|
562
|
-
const [data, setData] = useState(initialData);
|
|
563
|
-
return <div>...</div>;
|
|
564
|
-
}
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
### ❌ Bad: Waterfall Fetching
|
|
568
|
-
|
|
569
|
-
```tsx
|
|
570
|
-
// ❌ Bad: Sequential fetching
|
|
571
|
-
async function Page() {
|
|
572
|
-
const user = await fetchUser();
|
|
573
|
-
const posts = await fetchPosts(user.id);
|
|
574
|
-
const comments = await fetchComments(posts[0].id);
|
|
575
|
-
// ...
|
|
576
|
-
}
|
|
577
|
-
```
|
|
578
|
-
|
|
579
|
-
```tsx
|
|
580
|
-
// ✅ Good: Parallel fetching
|
|
581
|
-
async function Page() {
|
|
582
|
-
const [user, posts, comments] = await Promise.all([
|
|
583
|
-
fetchUser(),
|
|
584
|
-
fetchPosts(user.id),
|
|
585
|
-
fetchComments(posts[0].id)
|
|
586
|
-
]);
|
|
587
|
-
}
|
|
588
|
-
```
|
|
589
|
-
|
|
590
|
-
## Examples
|
|
591
|
-
|
|
592
|
-
### Example 1: E-commerce Product Page
|
|
593
|
-
|
|
594
|
-
```tsx
|
|
595
|
-
// app/products/[id]/page.tsx
|
|
596
|
-
import { notFound } from 'next/navigation';
|
|
597
|
-
import { ProductImages } from './ProductImages';
|
|
598
|
-
import { AddToCart } from './AddToCart';
|
|
599
|
-
|
|
600
|
-
async function getProduct(id: string) {
|
|
601
|
-
const res = await fetch(`https://api.example.com/products/${id}`, {
|
|
602
|
-
next: { revalidate: 3600, tags: ['products'] }
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
if (!res.ok) return null;
|
|
606
|
-
return res.json();
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
export default async function ProductPage({
|
|
610
|
-
params
|
|
611
|
-
}: {
|
|
612
|
-
params: { id: string }
|
|
613
|
-
}) {
|
|
614
|
-
const product = await getProduct(params.id);
|
|
615
|
-
|
|
616
|
-
if (!product) {
|
|
617
|
-
notFound();
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
return (
|
|
621
|
-
<div>
|
|
622
|
-
<ProductImages images={product.images} />
|
|
623
|
-
<div>
|
|
624
|
-
<h1>{product.name}</h1>
|
|
625
|
-
<p>{product.description}</p>
|
|
626
|
-
<p>${product.price}</p>
|
|
627
|
-
<AddToCart productId={product.id} />
|
|
628
|
-
</div>
|
|
629
|
-
</div>
|
|
630
|
-
);
|
|
631
|
-
}
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
```tsx
|
|
635
|
-
// app/products/[id]/AddToCart.tsx
|
|
636
|
-
"use client";
|
|
637
|
-
|
|
638
|
-
import { useState } from 'react';
|
|
639
|
-
|
|
640
|
-
export function AddToCart({ productId }: { productId: string }) {
|
|
641
|
-
const [quantity, setQuantity] = useState(1);
|
|
642
|
-
|
|
643
|
-
return (
|
|
644
|
-
<div>
|
|
645
|
-
<input
|
|
646
|
-
type="number"
|
|
647
|
-
value={quantity}
|
|
648
|
-
onChange={(e) => setQuantity(Number(e.target.value))}
|
|
649
|
-
min={1}
|
|
650
|
-
/>
|
|
651
|
-
<button onClick={() => addToCart(productId, quantity)}>
|
|
652
|
-
Add to Cart
|
|
653
|
-
</button>
|
|
654
|
-
</div>
|
|
655
|
-
);
|
|
656
|
-
}
|
|
657
|
-
```
|
|
658
|
-
|
|
659
|
-
### Example 2: Dashboard with Authentication
|
|
660
|
-
|
|
661
|
-
```tsx
|
|
662
|
-
// app/(dashboard)/dashboard/page.tsx
|
|
663
|
-
import { redirect } from 'next/navigation';
|
|
664
|
-
import { getSession } from '@/lib/auth';
|
|
665
|
-
import { DashboardLayout } from '@/components/layout/DashboardLayout';
|
|
666
|
-
import { StatsCards } from './StatsCards';
|
|
667
|
-
|
|
668
|
-
export default async function DashboardPage() {
|
|
669
|
-
const session = await getSession();
|
|
670
|
-
|
|
671
|
-
if (!session) {
|
|
672
|
-
redirect('/login');
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
return (
|
|
676
|
-
<DashboardLayout user={session.user}>
|
|
677
|
-
<StatsCards userId={session.user.id} />
|
|
678
|
-
</DashboardLayout>
|
|
679
|
-
);
|
|
680
|
-
}
|
|
681
|
-
```
|
|
682
|
-
|
|
683
|
-
## Additional Resources
|
|
684
|
-
|
|
685
|
-
- For complete API details, see [Next.js Documentation](https://nextjs.org/docs)
|
|
686
|
-
- For App Router patterns, see [Next.js App Router Guide](https://nextjs.org/docs/app)
|
|
687
|
-
- For production checklist, see [Next.js Production Guide](https://nextjs.org/docs/app/guides/production-checklist)
|
|
1
|
+
---
|
|
2
|
+
name: nextjs-guide
|
|
3
|
+
description: Apply Next.js 14-15 best practices including App Router, Server Components, data fetching, and project structure. Use when building, reviewing, or optimizing Next.js applications.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Next.js Best Practices
|
|
7
|
+
|
|
8
|
+
## Quick Start
|
|
9
|
+
|
|
10
|
+
When working with Next.js 14-15:
|
|
11
|
+
|
|
12
|
+
1. **Server Components by default**: Only use `"use client"` when necessary
|
|
13
|
+
2. **App Router structure**: Use route groups, layouts, and proper file organization
|
|
14
|
+
3. **Data fetching**: Fetch directly in Server Components, use proper caching
|
|
15
|
+
4. **Component composition**: Keep client boundary small, compose Server Components inside Client Components
|
|
16
|
+
5. **Project structure**: Use `src/` directory with proper separation (components, lib, utils)
|
|
17
|
+
6. **Performance**: Leverage automatic optimizations (code-splitting, prefetching, caching)
|
|
18
|
+
|
|
19
|
+
## Server vs Client Components
|
|
20
|
+
|
|
21
|
+
### Server Components (Default)
|
|
22
|
+
|
|
23
|
+
Use Server Components by default for data fetching and rendering:
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
// ✅ Good: Server Component (no "use client")
|
|
27
|
+
async function ProductPage({ params }: { params: { id: string } }) {
|
|
28
|
+
const product = await fetch(`https://api.example.com/products/${params.id}`, {
|
|
29
|
+
next: { revalidate: 60 }
|
|
30
|
+
}).then(res => res.json());
|
|
31
|
+
|
|
32
|
+
return <div>{product.name}</div>;
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Use Server Components when**:
|
|
37
|
+
- Fetching data from APIs or databases
|
|
38
|
+
- Accessing environment variables
|
|
39
|
+
- Rendering static or semi-static content
|
|
40
|
+
- Keeping bundle size small
|
|
41
|
+
|
|
42
|
+
### Client Components
|
|
43
|
+
|
|
44
|
+
Add `"use client"` only when genuinely needed:
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// ✅ Good: Client Component with "use client"
|
|
48
|
+
"use client";
|
|
49
|
+
|
|
50
|
+
import { useState } from 'react';
|
|
51
|
+
|
|
52
|
+
export function Counter() {
|
|
53
|
+
const [count, setCount] = useState(0);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<button onClick={() => setCount(count + 1)}>
|
|
57
|
+
Count: {count}
|
|
58
|
+
</button>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Use Client Components when**:
|
|
64
|
+
- Event handlers (`onClick`, `onChange`, `onSubmit`)
|
|
65
|
+
- Browser APIs (`window`, `localStorage`, `IntersectionObserver`)
|
|
66
|
+
- React state and effects (`useState`, `useEffect`, `useRef`)
|
|
67
|
+
- Third-party libraries that require client-side execution
|
|
68
|
+
|
|
69
|
+
### Composition Pattern
|
|
70
|
+
|
|
71
|
+
Keep client boundary small by composing Server Components inside Client Components:
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
// ✅ Good: Client Component receiving Server Component as children
|
|
75
|
+
// components/dashboard/DashboardLayout.tsx
|
|
76
|
+
"use client";
|
|
77
|
+
|
|
78
|
+
import { useState } from 'react';
|
|
79
|
+
|
|
80
|
+
export function DashboardLayout({ children }: { children: React.ReactNode }) {
|
|
81
|
+
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div>
|
|
85
|
+
<button onClick={() => setSidebarOpen(!sidebarOpen)}>Toggle</button>
|
|
86
|
+
{children}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
// ✅ Good: Server Component passing content to Client Component
|
|
94
|
+
// app/dashboard/page.tsx
|
|
95
|
+
import { DashboardLayout } from '@/components/dashboard/DashboardLayout';
|
|
96
|
+
import { StatsCard } from '@/components/dashboard/StatsCard'; // Server Component
|
|
97
|
+
|
|
98
|
+
export default async function DashboardPage() {
|
|
99
|
+
const stats = await fetchStats(); // Server-side data fetching
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<DashboardLayout>
|
|
103
|
+
<StatsCard data={stats} />
|
|
104
|
+
</DashboardLayout>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Data Fetching
|
|
110
|
+
|
|
111
|
+
### Direct Fetching in Server Components
|
|
112
|
+
|
|
113
|
+
Fetch data directly without `useEffect`:
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
// ✅ Good: Direct data fetching
|
|
117
|
+
async function ProductList() {
|
|
118
|
+
const products = await fetch('https://api.example.com/products', {
|
|
119
|
+
next: { revalidate: 60 } // Revalidate every 60 seconds
|
|
120
|
+
}).then(res => res.json());
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<ul>
|
|
124
|
+
{products.map(product => (
|
|
125
|
+
<li key={product.id}>{product.name}</li>
|
|
126
|
+
))}
|
|
127
|
+
</ul>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Parallel Fetching
|
|
133
|
+
|
|
134
|
+
Use `Promise.all()` for multiple resources:
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
// ✅ Good: Parallel data fetching
|
|
138
|
+
async function Dashboard() {
|
|
139
|
+
const [users, products, orders] = await Promise.all([
|
|
140
|
+
fetch('https://api.example.com/users').then(res => res.json()),
|
|
141
|
+
fetch('https://api.example.com/products').then(res => res.json()),
|
|
142
|
+
fetch('https://api.example.com/orders').then(res => res.json())
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<div>
|
|
147
|
+
<UsersSection users={users} />
|
|
148
|
+
<ProductsSection products={products} />
|
|
149
|
+
<OrdersSection orders={orders} />
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Caching Strategies
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
// Time-based revalidation
|
|
159
|
+
const data = await fetch(url, {
|
|
160
|
+
next: { revalidate: 3600 } // Revalidate every hour
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// No caching (always fresh)
|
|
164
|
+
const data = await fetch(url, { cache: 'no-store' });
|
|
165
|
+
|
|
166
|
+
// On-demand revalidation
|
|
167
|
+
// In your route handler:
|
|
168
|
+
import { revalidateTag } from 'next/cache';
|
|
169
|
+
|
|
170
|
+
revalidateTag('products'); // Revalidate all data tagged with 'products'
|
|
171
|
+
|
|
172
|
+
// In your fetch:
|
|
173
|
+
const data = await fetch(url, {
|
|
174
|
+
next: { tags: ['products'] }
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Project Structure
|
|
179
|
+
|
|
180
|
+
### Core Directory Layout
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
src/
|
|
184
|
+
├── app/ # App Router (routes)
|
|
185
|
+
│ ├── (auth)/ # Route group (doesn't affect URL)
|
|
186
|
+
│ │ ├── login/
|
|
187
|
+
│ │ └── register/
|
|
188
|
+
│ ├── (dashboard)/ # Another route group
|
|
189
|
+
│ │ ├── layout.tsx # Shared layout for dashboard routes
|
|
190
|
+
│ │ ├── page.tsx # /dashboard
|
|
191
|
+
│ │ └── settings/
|
|
192
|
+
│ ├── api/ # API routes
|
|
193
|
+
│ │ └── users/
|
|
194
|
+
│ │ └── route.ts
|
|
195
|
+
│ ├── globals.css
|
|
196
|
+
│ ├── layout.tsx # Root layout
|
|
197
|
+
│ └── page.tsx # Home page
|
|
198
|
+
├── components/
|
|
199
|
+
│ ├── ui/ # Reusable UI components (Button, Card, Modal)
|
|
200
|
+
│ ├── layout/ # Layout components (Header, Footer, Sidebar)
|
|
201
|
+
│ └── features/ # Feature-specific components
|
|
202
|
+
│ ├── auth/
|
|
203
|
+
│ ├── dashboard/
|
|
204
|
+
│ └── products/
|
|
205
|
+
├── lib/ # Business logic, API clients, auth
|
|
206
|
+
│ ├── api.ts
|
|
207
|
+
│ ├── auth.ts
|
|
208
|
+
│ └── db.ts
|
|
209
|
+
├── utils/ # Pure utility functions
|
|
210
|
+
│ ├── cn.ts
|
|
211
|
+
│ └── formatters.ts
|
|
212
|
+
├── types/ # TypeScript types
|
|
213
|
+
│ └── index.ts
|
|
214
|
+
└── styles/ # Additional styles
|
|
215
|
+
└── variables.css
|
|
216
|
+
|
|
217
|
+
public/ # Static assets
|
|
218
|
+
├── images/
|
|
219
|
+
└── fonts/
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Route Organization
|
|
223
|
+
|
|
224
|
+
Use route groups for organization without affecting URL structure:
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
app/
|
|
228
|
+
├── (marketing)/ # Marketing pages: /, /about, /contact
|
|
229
|
+
│ ├── page.tsx # /
|
|
230
|
+
│ ├── about/
|
|
231
|
+
│ │ └── page.tsx # /about
|
|
232
|
+
│ └── contact/
|
|
233
|
+
│ └── page.tsx # /contact
|
|
234
|
+
├── (auth)/ # Auth pages: /login, /register
|
|
235
|
+
│ ├── login/
|
|
236
|
+
│ │ └── page.tsx # /login
|
|
237
|
+
│ └── register/
|
|
238
|
+
│ └── page.tsx # /register
|
|
239
|
+
└── (app)/ # App routes: /dashboard, /settings
|
|
240
|
+
├── dashboard/
|
|
241
|
+
│ └── page.tsx # /dashboard
|
|
242
|
+
└── settings/
|
|
243
|
+
└── page.tsx # /settings
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Component Organization
|
|
247
|
+
|
|
248
|
+
Each component in its own folder:
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
components/ui/Button/
|
|
252
|
+
├── Button.tsx # Main component
|
|
253
|
+
├── Button.test.tsx # Tests
|
|
254
|
+
├── Button.stories.tsx # Storybook (optional)
|
|
255
|
+
└── index.ts # Barrel export
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
// components/ui/Button/Button.tsx
|
|
260
|
+
import { clsx } from 'clsx';
|
|
261
|
+
|
|
262
|
+
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
263
|
+
variant?: 'primary' | 'secondary' | 'outline';
|
|
264
|
+
size?: 'sm' | 'md' | 'lg';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function Button({
|
|
268
|
+
variant = 'primary',
|
|
269
|
+
size = 'md',
|
|
270
|
+
className,
|
|
271
|
+
children,
|
|
272
|
+
...props
|
|
273
|
+
}: ButtonProps) {
|
|
274
|
+
return (
|
|
275
|
+
<button
|
|
276
|
+
className={clsx(
|
|
277
|
+
'font-medium rounded transition-colors',
|
|
278
|
+
{
|
|
279
|
+
'bg-blue-600 text-white hover:bg-blue-700': variant === 'primary',
|
|
280
|
+
'bg-gray-200 text-gray-900 hover:bg-gray-300': variant === 'secondary',
|
|
281
|
+
'border border-gray-300 hover:bg-gray-100': variant === 'outline',
|
|
282
|
+
'px-3 py-1.5 text-sm': size === 'sm',
|
|
283
|
+
'px-4 py-2 text-base': size === 'md',
|
|
284
|
+
'px-6 py-3 text-lg': size === 'lg',
|
|
285
|
+
},
|
|
286
|
+
className
|
|
287
|
+
)}
|
|
288
|
+
{...props}
|
|
289
|
+
>
|
|
290
|
+
{children}
|
|
291
|
+
</button>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Utils vs Lib
|
|
297
|
+
|
|
298
|
+
Distinguish between utils and lib:
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// ✅ utils/formatters.ts - Pure functions, no side effects
|
|
302
|
+
export function formatCurrency(amount: number): string {
|
|
303
|
+
return new Intl.NumberFormat('en-US', {
|
|
304
|
+
style: 'currency',
|
|
305
|
+
currency: 'USD'
|
|
306
|
+
}).format(amount);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ✅ lib/api.ts - Business logic, external dependencies
|
|
310
|
+
import { ENV } from './env';
|
|
311
|
+
|
|
312
|
+
export async function fetchUser(id: string) {
|
|
313
|
+
const response = await fetch(`${ENV.API_URL}/users/${id}`, {
|
|
314
|
+
headers: {
|
|
315
|
+
'Authorization': `Bearer ${ENV.API_KEY}`
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
if (!response.ok) {
|
|
320
|
+
throw new Error('Failed to fetch user');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return response.json();
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Performance Optimizations
|
|
328
|
+
|
|
329
|
+
### Automatic Optimizations (Built-in)
|
|
330
|
+
|
|
331
|
+
Next.js provides by default:
|
|
332
|
+
- **Code-splitting**: By route segments
|
|
333
|
+
- **Link prefetching**: `<Link>` components prefetch in viewport
|
|
334
|
+
- **Prerendering**: At build time
|
|
335
|
+
- **Intelligent caching**: Data requests and rendered components
|
|
336
|
+
|
|
337
|
+
### Image Optimization
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
import Image from 'next/image';
|
|
341
|
+
|
|
342
|
+
// ✅ Good: Optimized image
|
|
343
|
+
<Image
|
|
344
|
+
src="/hero.jpg"
|
|
345
|
+
alt="Hero banner"
|
|
346
|
+
width={1200}
|
|
347
|
+
height={630}
|
|
348
|
+
priority // For above-the-fold images
|
|
349
|
+
quality={85}
|
|
350
|
+
/>
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Font Optimization
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
// ✅ Good: Optimized fonts
|
|
357
|
+
import { Inter } from 'next/font/google';
|
|
358
|
+
|
|
359
|
+
const inter = Inter({
|
|
360
|
+
subsets: ['latin'],
|
|
361
|
+
display: 'swap',
|
|
362
|
+
variable: '--font-inter'
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
export default function RootLayout({ children }) {
|
|
366
|
+
return (
|
|
367
|
+
<html lang="en" className={inter.variable}>
|
|
368
|
+
<body>{children}</body>
|
|
369
|
+
</html>
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## Error Handling
|
|
375
|
+
|
|
376
|
+
### Error Boundaries
|
|
377
|
+
|
|
378
|
+
```tsx
|
|
379
|
+
// app/error.tsx
|
|
380
|
+
"use client";
|
|
381
|
+
|
|
382
|
+
import { useEffect } from 'react';
|
|
383
|
+
|
|
384
|
+
export default function Error({
|
|
385
|
+
error,
|
|
386
|
+
reset,
|
|
387
|
+
}: {
|
|
388
|
+
error: Error & { digest?: string };
|
|
389
|
+
reset: () => void;
|
|
390
|
+
}) {
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
console.error('Application error:', error);
|
|
393
|
+
}, [error]);
|
|
394
|
+
|
|
395
|
+
return (
|
|
396
|
+
<div>
|
|
397
|
+
<h2>Something went wrong!</h2>
|
|
398
|
+
<button onClick={() => reset()}>Try again</button>
|
|
399
|
+
</div>
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Not Found Handling
|
|
405
|
+
|
|
406
|
+
```tsx
|
|
407
|
+
// app/not-found.tsx
|
|
408
|
+
import Link from 'next/link';
|
|
409
|
+
|
|
410
|
+
export default function NotFound() {
|
|
411
|
+
return (
|
|
412
|
+
<div>
|
|
413
|
+
<h2>Page Not Found</h2>
|
|
414
|
+
<p>Could not find requested resource</p>
|
|
415
|
+
<Link href="/">Return Home</Link>
|
|
416
|
+
</div>
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Loading States
|
|
422
|
+
|
|
423
|
+
```tsx
|
|
424
|
+
// app/dashboard/loading.tsx
|
|
425
|
+
export default function Loading() {
|
|
426
|
+
return (
|
|
427
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
428
|
+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600" />
|
|
429
|
+
</div>
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## API Routes
|
|
435
|
+
|
|
436
|
+
### RESTful API Structure
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
// app/api/users/route.ts
|
|
440
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
441
|
+
import { db } from '@/lib/db';
|
|
442
|
+
|
|
443
|
+
export async function GET() {
|
|
444
|
+
try {
|
|
445
|
+
const users = await db.user.findMany();
|
|
446
|
+
return NextResponse.json(users);
|
|
447
|
+
} catch (error) {
|
|
448
|
+
console.error('Failed to fetch users:', error);
|
|
449
|
+
return NextResponse.json(
|
|
450
|
+
{ error: 'Failed to fetch users' },
|
|
451
|
+
{ status: 500 }
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export async function POST(request: NextRequest) {
|
|
457
|
+
try {
|
|
458
|
+
const body = await request.json();
|
|
459
|
+
const user = await db.user.create({ data: body });
|
|
460
|
+
return NextResponse.json(user, { status: 201 });
|
|
461
|
+
} catch (error) {
|
|
462
|
+
return NextResponse.json(
|
|
463
|
+
{ error: 'Failed to create user' },
|
|
464
|
+
{ status: 400 }
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## Testing
|
|
471
|
+
|
|
472
|
+
### Component Testing with Vitest
|
|
473
|
+
|
|
474
|
+
```tsx
|
|
475
|
+
// __tests__/Button.test.tsx
|
|
476
|
+
import { render, screen } from '@testing-library/react';
|
|
477
|
+
import { describe, it, expect } from 'vitest';
|
|
478
|
+
import { Button } from '@/components/ui/Button';
|
|
479
|
+
|
|
480
|
+
describe('Button', () => {
|
|
481
|
+
it('renders with default props', () => {
|
|
482
|
+
render(<Button>Click me</Button>);
|
|
483
|
+
expect(screen.getByRole('button')).toHaveTextContent('Click me');
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('handles click events', () => {
|
|
487
|
+
const handleClick = vi.fn();
|
|
488
|
+
render(<Button onClick={handleClick}>Click me</Button>);
|
|
489
|
+
screen.getByRole('button').click();
|
|
490
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('applies variant classes', () => {
|
|
494
|
+
const { container } = render(<Button variant="primary">Test</Button>);
|
|
495
|
+
expect(container.firstChild).toHaveClass('bg-blue-600');
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
## Commands
|
|
501
|
+
|
|
502
|
+
### Essential Next.js Commands
|
|
503
|
+
|
|
504
|
+
```bash
|
|
505
|
+
npm run dev # Start development server
|
|
506
|
+
npm run build # Build for production
|
|
507
|
+
npm run start # Start production server
|
|
508
|
+
npm run lint # Run ESLint
|
|
509
|
+
npm run typecheck # Run TypeScript checker
|
|
510
|
+
npm run test # Run test suite
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
## Anti-Patterns to Avoid
|
|
514
|
+
|
|
515
|
+
### ❌ Bad: Unnecessary Client Components
|
|
516
|
+
|
|
517
|
+
```tsx
|
|
518
|
+
// ❌ Bad: Using Client Component when Server Component suffices
|
|
519
|
+
"use client";
|
|
520
|
+
|
|
521
|
+
export async function ProductPage({ params }) {
|
|
522
|
+
const product = await fetchProduct(params.id);
|
|
523
|
+
return <div>{product.name}</div>;
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
```tsx
|
|
528
|
+
// ✅ Good: Server Component
|
|
529
|
+
export async function ProductPage({ params }) {
|
|
530
|
+
const product = await fetchProduct(params.id);
|
|
531
|
+
return <div>{product.name}</div>;
|
|
532
|
+
}
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### ❌ Bad: Large Client Components
|
|
536
|
+
|
|
537
|
+
```tsx
|
|
538
|
+
// ❌ Bad: Everything in one Client Component
|
|
539
|
+
"use client";
|
|
540
|
+
|
|
541
|
+
export function Dashboard() {
|
|
542
|
+
const [data, setData] = useState(null);
|
|
543
|
+
|
|
544
|
+
useEffect(() => {
|
|
545
|
+
fetchData().then(setData);
|
|
546
|
+
}, []);
|
|
547
|
+
|
|
548
|
+
return <div>...</div>;
|
|
549
|
+
}
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
```tsx
|
|
553
|
+
// ✅ Good: Split Server and Client
|
|
554
|
+
async function Dashboard() {
|
|
555
|
+
const data = await fetchData();
|
|
556
|
+
return <DashboardClient initialData={data} />;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
"use client";
|
|
560
|
+
|
|
561
|
+
function DashboardClient({ initialData }) {
|
|
562
|
+
const [data, setData] = useState(initialData);
|
|
563
|
+
return <div>...</div>;
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
### ❌ Bad: Waterfall Fetching
|
|
568
|
+
|
|
569
|
+
```tsx
|
|
570
|
+
// ❌ Bad: Sequential fetching
|
|
571
|
+
async function Page() {
|
|
572
|
+
const user = await fetchUser();
|
|
573
|
+
const posts = await fetchPosts(user.id);
|
|
574
|
+
const comments = await fetchComments(posts[0].id);
|
|
575
|
+
// ...
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
```tsx
|
|
580
|
+
// ✅ Good: Parallel fetching
|
|
581
|
+
async function Page() {
|
|
582
|
+
const [user, posts, comments] = await Promise.all([
|
|
583
|
+
fetchUser(),
|
|
584
|
+
fetchPosts(user.id),
|
|
585
|
+
fetchComments(posts[0].id)
|
|
586
|
+
]);
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
## Examples
|
|
591
|
+
|
|
592
|
+
### Example 1: E-commerce Product Page
|
|
593
|
+
|
|
594
|
+
```tsx
|
|
595
|
+
// app/products/[id]/page.tsx
|
|
596
|
+
import { notFound } from 'next/navigation';
|
|
597
|
+
import { ProductImages } from './ProductImages';
|
|
598
|
+
import { AddToCart } from './AddToCart';
|
|
599
|
+
|
|
600
|
+
async function getProduct(id: string) {
|
|
601
|
+
const res = await fetch(`https://api.example.com/products/${id}`, {
|
|
602
|
+
next: { revalidate: 3600, tags: ['products'] }
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
if (!res.ok) return null;
|
|
606
|
+
return res.json();
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export default async function ProductPage({
|
|
610
|
+
params
|
|
611
|
+
}: {
|
|
612
|
+
params: { id: string }
|
|
613
|
+
}) {
|
|
614
|
+
const product = await getProduct(params.id);
|
|
615
|
+
|
|
616
|
+
if (!product) {
|
|
617
|
+
notFound();
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return (
|
|
621
|
+
<div>
|
|
622
|
+
<ProductImages images={product.images} />
|
|
623
|
+
<div>
|
|
624
|
+
<h1>{product.name}</h1>
|
|
625
|
+
<p>{product.description}</p>
|
|
626
|
+
<p>${product.price}</p>
|
|
627
|
+
<AddToCart productId={product.id} />
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
```tsx
|
|
635
|
+
// app/products/[id]/AddToCart.tsx
|
|
636
|
+
"use client";
|
|
637
|
+
|
|
638
|
+
import { useState } from 'react';
|
|
639
|
+
|
|
640
|
+
export function AddToCart({ productId }: { productId: string }) {
|
|
641
|
+
const [quantity, setQuantity] = useState(1);
|
|
642
|
+
|
|
643
|
+
return (
|
|
644
|
+
<div>
|
|
645
|
+
<input
|
|
646
|
+
type="number"
|
|
647
|
+
value={quantity}
|
|
648
|
+
onChange={(e) => setQuantity(Number(e.target.value))}
|
|
649
|
+
min={1}
|
|
650
|
+
/>
|
|
651
|
+
<button onClick={() => addToCart(productId, quantity)}>
|
|
652
|
+
Add to Cart
|
|
653
|
+
</button>
|
|
654
|
+
</div>
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### Example 2: Dashboard with Authentication
|
|
660
|
+
|
|
661
|
+
```tsx
|
|
662
|
+
// app/(dashboard)/dashboard/page.tsx
|
|
663
|
+
import { redirect } from 'next/navigation';
|
|
664
|
+
import { getSession } from '@/lib/auth';
|
|
665
|
+
import { DashboardLayout } from '@/components/layout/DashboardLayout';
|
|
666
|
+
import { StatsCards } from './StatsCards';
|
|
667
|
+
|
|
668
|
+
export default async function DashboardPage() {
|
|
669
|
+
const session = await getSession();
|
|
670
|
+
|
|
671
|
+
if (!session) {
|
|
672
|
+
redirect('/login');
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return (
|
|
676
|
+
<DashboardLayout user={session.user}>
|
|
677
|
+
<StatsCards userId={session.user.id} />
|
|
678
|
+
</DashboardLayout>
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
## Additional Resources
|
|
684
|
+
|
|
685
|
+
- For complete API details, see [Next.js Documentation](https://nextjs.org/docs)
|
|
686
|
+
- For App Router patterns, see [Next.js App Router Guide](https://nextjs.org/docs/app)
|
|
687
|
+
- For production checklist, see [Next.js Production Guide](https://nextjs.org/docs/app/guides/production-checklist)
|