autoworkflow 3.9.1 → 3.11.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.
@@ -0,0 +1,2730 @@
1
+ # Next.js Master Skill
2
+
3
+ > Comprehensive Next.js 14/15 expertise for production-grade applications.
4
+ > This skill covers App Router, Server Components, Server Actions, and advanced patterns.
5
+
6
+ ---
7
+
8
+ ## Table of Contents
9
+
10
+ 1. [Architecture Fundamentals](#architecture-fundamentals)
11
+ 2. [Server vs Client Components](#server-vs-client-components)
12
+ 3. [Data Fetching Patterns](#data-fetching-patterns)
13
+ 4. [Caching Strategies](#caching-strategies)
14
+ 5. [Server Actions](#server-actions)
15
+ 6. [Route Handlers (API)](#route-handlers-api)
16
+ 7. [Middleware](#middleware)
17
+ 8. [Streaming & Suspense](#streaming--suspense)
18
+ 9. [Error Handling](#error-handling)
19
+ 10. [Metadata & SEO](#metadata--seo)
20
+ 11. [Authentication Patterns](#authentication-patterns)
21
+ 12. [Database Integration](#database-integration)
22
+ 13. [Performance Optimization](#performance-optimization)
23
+ 14. [Security Best Practices](#security-best-practices)
24
+ 15. [Testing Strategies](#testing-strategies)
25
+ 16. [Deployment](#deployment)
26
+ 17. [Anti-Patterns to Avoid](#anti-patterns-to-avoid)
27
+ 18. [File Structure Conventions](#file-structure-conventions)
28
+
29
+ ---
30
+
31
+ ## Architecture Fundamentals
32
+
33
+ ### App Router Structure (Next.js 13+)
34
+
35
+ ```
36
+ app/
37
+ ├── layout.tsx # Root layout (required)
38
+ ├── page.tsx # Home page (/)
39
+ ├── loading.tsx # Loading UI
40
+ ├── error.tsx # Error boundary
41
+ ├── not-found.tsx # 404 page
42
+ ├── global-error.tsx # Root error boundary
43
+ ├── template.tsx # Re-renders on navigation
44
+ ├── default.tsx # Parallel route fallback
45
+
46
+ ├── (marketing)/ # Route group (no URL impact)
47
+ │ ├── layout.tsx
48
+ │ ├── about/page.tsx
49
+ │ └── contact/page.tsx
50
+
51
+ ├── dashboard/
52
+ │ ├── layout.tsx
53
+ │ ├── page.tsx
54
+ │ ├── @analytics/ # Parallel route (named slot)
55
+ │ │ └── page.tsx
56
+ │ ├── @team/ # Another parallel route
57
+ │ │ └── page.tsx
58
+ │ └── settings/
59
+ │ └── page.tsx
60
+
61
+ ├── blog/
62
+ │ ├── page.tsx # /blog
63
+ │ ├── [slug]/ # Dynamic segment
64
+ │ │ └── page.tsx # /blog/my-post
65
+ │ └── [...catchAll]/ # Catch-all segment
66
+ │ └── page.tsx # /blog/2024/01/post
67
+
68
+ ├── api/ # Route Handlers
69
+ │ └── webhook/
70
+ │ └── route.ts
71
+
72
+ └── _components/ # Private folder (not routed)
73
+ └── shared.tsx
74
+ ```
75
+
76
+ ### Route Segment Config
77
+
78
+ ```typescript
79
+ // Any page.tsx, layout.tsx, or route.ts
80
+ export const dynamic = 'auto' | 'force-dynamic' | 'error' | 'force-static'
81
+ export const dynamicParams = true | false
82
+ export const revalidate = false | 0 | number
83
+ export const fetchCache = 'auto' | 'default-cache' | 'only-cache' | 'force-cache' | 'force-no-store' | 'default-no-store' | 'only-no-store'
84
+ export const runtime = 'nodejs' | 'edge'
85
+ export const preferredRegion = 'auto' | 'global' | 'home' | string | string[]
86
+ export const maxDuration = number
87
+ ```
88
+
89
+ ---
90
+
91
+ ## Server vs Client Components
92
+
93
+ ### Decision Matrix
94
+
95
+ | Feature | Server Component | Client Component |
96
+ |---------|------------------|------------------|
97
+ | Fetch data | ✅ Direct DB/API access | ❌ Needs API route |
98
+ | Access backend resources | ✅ Filesystem, DB | ❌ No |
99
+ | Keep secrets secure | ✅ Never sent to client | ❌ Exposed |
100
+ | Reduce bundle size | ✅ Zero JS | ❌ Adds to bundle |
101
+ | useState/useEffect | ❌ No | ✅ Yes |
102
+ | Event handlers (onClick) | ❌ No | ✅ Yes |
103
+ | Browser APIs | ❌ No | ✅ Yes |
104
+ | Custom hooks with state | ❌ No | ✅ Yes |
105
+ | React Context | ❌ No | ✅ Yes |
106
+
107
+ ### Server Component (Default)
108
+
109
+ ```typescript
110
+ // app/dashboard/page.tsx
111
+ // NO "use client" directive = Server Component
112
+
113
+ import { db } from '@/lib/db'
114
+ import { headers, cookies } from 'next/headers'
115
+
116
+ export default async function DashboardPage() {
117
+ // Direct database access - no API needed
118
+ const user = await db.user.findUnique({
119
+ where: { id: cookies().get('userId')?.value }
120
+ })
121
+
122
+ // Access request headers
123
+ const headersList = headers()
124
+ const userAgent = headersList.get('user-agent')
125
+
126
+ // Environment variables (including secrets)
127
+ const apiKey = process.env.SECRET_API_KEY // Safe - never sent to client
128
+
129
+ return (
130
+ <div>
131
+ <h1>Welcome, {user?.name}</h1>
132
+ <ClientInteractiveSection initialData={user} />
133
+ </div>
134
+ )
135
+ }
136
+ ```
137
+
138
+ ### Client Component
139
+
140
+ ```typescript
141
+ 'use client'
142
+
143
+ // MUST be first line (before imports)
144
+ // This directive marks the boundary - all imports become client components
145
+
146
+ import { useState, useEffect, useTransition } from 'react'
147
+ import { useRouter, usePathname, useSearchParams } from 'next/navigation'
148
+
149
+ interface Props {
150
+ initialData: User
151
+ }
152
+
153
+ export function ClientInteractiveSection({ initialData }: Props) {
154
+ const [data, setData] = useState(initialData)
155
+ const [isPending, startTransition] = useTransition()
156
+ const router = useRouter()
157
+ const pathname = usePathname()
158
+ const searchParams = useSearchParams()
159
+
160
+ // Browser APIs available
161
+ useEffect(() => {
162
+ const handleResize = () => console.log(window.innerWidth)
163
+ window.addEventListener('resize', handleResize)
164
+ return () => window.removeEventListener('resize', handleResize)
165
+ }, [])
166
+
167
+ const handleClick = () => {
168
+ startTransition(() => {
169
+ router.refresh() // Revalidate server components
170
+ })
171
+ }
172
+
173
+ return (
174
+ <button onClick={handleClick} disabled={isPending}>
175
+ {isPending ? 'Refreshing...' : 'Refresh Data'}
176
+ </button>
177
+ )
178
+ }
179
+ ```
180
+
181
+ ### Composition Pattern (Server → Client)
182
+
183
+ ```typescript
184
+ // app/dashboard/page.tsx (Server Component)
185
+ import { ClientTabs } from './client-tabs'
186
+ import { db } from '@/lib/db'
187
+
188
+ export default async function Page() {
189
+ const data = await db.analytics.findMany()
190
+
191
+ // Pass Server Component as child to Client Component
192
+ return (
193
+ <ClientTabs>
194
+ <ServerRenderedChart data={data} />
195
+ </ClientTabs>
196
+ )
197
+ }
198
+
199
+ // client-tabs.tsx
200
+ 'use client'
201
+ import { useState, ReactNode } from 'react'
202
+
203
+ export function ClientTabs({ children }: { children: ReactNode }) {
204
+ const [activeTab, setActiveTab] = useState(0)
205
+
206
+ return (
207
+ <div>
208
+ <button onClick={() => setActiveTab(0)}>Tab 1</button>
209
+ <button onClick={() => setActiveTab(1)}>Tab 2</button>
210
+ {activeTab === 0 && children} {/* Server component rendered here */}
211
+ </div>
212
+ )
213
+ }
214
+ ```
215
+
216
+ ### Provider Pattern
217
+
218
+ ```typescript
219
+ // app/providers.tsx
220
+ 'use client'
221
+
222
+ import { ThemeProvider } from 'next-themes'
223
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
224
+ import { SessionProvider } from 'next-auth/react'
225
+ import { useState } from 'react'
226
+
227
+ export function Providers({ children }: { children: React.ReactNode }) {
228
+ // Create QueryClient inside component to avoid sharing between requests
229
+ const [queryClient] = useState(() => new QueryClient({
230
+ defaultOptions: {
231
+ queries: {
232
+ staleTime: 60 * 1000,
233
+ refetchOnWindowFocus: false,
234
+ },
235
+ },
236
+ }))
237
+
238
+ return (
239
+ <SessionProvider>
240
+ <QueryClientProvider client={queryClient}>
241
+ <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
242
+ {children}
243
+ </ThemeProvider>
244
+ </QueryClientProvider>
245
+ </SessionProvider>
246
+ )
247
+ }
248
+
249
+ // app/layout.tsx (Server Component)
250
+ import { Providers } from './providers'
251
+
252
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
253
+ return (
254
+ <html lang="en" suppressHydrationWarning>
255
+ <body>
256
+ <Providers>{children}</Providers>
257
+ </body>
258
+ </html>
259
+ )
260
+ }
261
+ ```
262
+
263
+ ---
264
+
265
+ ## Data Fetching Patterns
266
+
267
+ ### Server Component Data Fetching (Recommended)
268
+
269
+ ```typescript
270
+ // app/posts/page.tsx
271
+ import { Suspense } from 'react'
272
+
273
+ // Parallel data fetching
274
+ async function getPosts() {
275
+ const res = await fetch('https://api.example.com/posts', {
276
+ next: { revalidate: 3600 } // Cache for 1 hour
277
+ })
278
+ if (!res.ok) throw new Error('Failed to fetch posts')
279
+ return res.json()
280
+ }
281
+
282
+ async function getUser() {
283
+ const res = await fetch('https://api.example.com/user', {
284
+ cache: 'no-store' // Always fresh
285
+ })
286
+ return res.json()
287
+ }
288
+
289
+ export default async function PostsPage() {
290
+ // Parallel execution - both start immediately
291
+ const [posts, user] = await Promise.all([
292
+ getPosts(),
293
+ getUser()
294
+ ])
295
+
296
+ return (
297
+ <div>
298
+ <UserHeader user={user} />
299
+ <PostList posts={posts} />
300
+
301
+ {/* Defer non-critical data */}
302
+ <Suspense fallback={<CommentsSkeleton />}>
303
+ <Comments />
304
+ </Suspense>
305
+ </div>
306
+ )
307
+ }
308
+
309
+ // Separate component for streaming
310
+ async function Comments() {
311
+ // This can load after initial page render
312
+ const comments = await fetch('https://api.example.com/comments', {
313
+ next: { revalidate: 60 }
314
+ }).then(r => r.json())
315
+
316
+ return <CommentList comments={comments} />
317
+ }
318
+ ```
319
+
320
+ ### Fetch Deduplication
321
+
322
+ ```typescript
323
+ // Next.js automatically deduplicates fetch requests with same URL and options
324
+ // across the component tree during a single render
325
+
326
+ // component-a.tsx
327
+ async function ComponentA() {
328
+ // This request...
329
+ const data = await fetch('https://api.example.com/data')
330
+ return <div>{data.title}</div>
331
+ }
332
+
333
+ // component-b.tsx
334
+ async function ComponentB() {
335
+ // ...is deduped with this one (same URL, only 1 request made)
336
+ const data = await fetch('https://api.example.com/data')
337
+ return <div>{data.description}</div>
338
+ }
339
+
340
+ // For non-fetch requests, use React cache()
341
+ import { cache } from 'react'
342
+ import { db } from '@/lib/db'
343
+
344
+ export const getUser = cache(async (id: string) => {
345
+ return db.user.findUnique({ where: { id } })
346
+ })
347
+
348
+ // Now getUser(id) is deduped across components in same render
349
+ ```
350
+
351
+ ### Data Fetching with Preloading
352
+
353
+ ```typescript
354
+ // lib/data.ts
355
+ import { cache } from 'react'
356
+
357
+ // Wrap in cache for deduplication
358
+ export const getItem = cache(async (id: string) => {
359
+ const res = await fetch(`https://api.example.com/items/${id}`)
360
+ return res.json()
361
+ })
362
+
363
+ // Preload function (call early, don't await)
364
+ export const preloadItem = (id: string) => {
365
+ void getItem(id)
366
+ }
367
+
368
+ // page.tsx
369
+ import { getItem, preloadItem } from '@/lib/data'
370
+
371
+ export default async function Page({ params }: { params: { id: string } }) {
372
+ // Start fetching immediately
373
+ preloadItem(params.id)
374
+
375
+ // Do other work...
376
+ const otherData = await getOtherData()
377
+
378
+ // Now await - likely already cached
379
+ const item = await getItem(params.id)
380
+
381
+ return <ItemDisplay item={item} />
382
+ }
383
+ ```
384
+
385
+ ### Static Generation with Dynamic Params
386
+
387
+ ```typescript
388
+ // app/blog/[slug]/page.tsx
389
+
390
+ // Generate static paths at build time
391
+ export async function generateStaticParams() {
392
+ const posts = await fetch('https://api.example.com/posts').then(r => r.json())
393
+
394
+ return posts.map((post: Post) => ({
395
+ slug: post.slug,
396
+ }))
397
+ }
398
+
399
+ // Optional: Control behavior for paths not in generateStaticParams
400
+ export const dynamicParams = true // true = generate on-demand, false = 404
401
+
402
+ export default async function BlogPost({
403
+ params
404
+ }: {
405
+ params: { slug: string }
406
+ }) {
407
+ const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
408
+ next: { revalidate: 3600 }
409
+ }).then(r => r.json())
410
+
411
+ return <article>{post.content}</article>
412
+ }
413
+
414
+ // Generate metadata for each page
415
+ export async function generateMetadata({
416
+ params
417
+ }: {
418
+ params: { slug: string }
419
+ }): Promise<Metadata> {
420
+ const post = await getPost(params.slug) // Deduped with page fetch
421
+
422
+ return {
423
+ title: post.title,
424
+ description: post.excerpt,
425
+ }
426
+ }
427
+ ```
428
+
429
+ ---
430
+
431
+ ## Caching Strategies
432
+
433
+ ### Cache Hierarchy
434
+
435
+ ```
436
+ Request Memoization (React)
437
+
438
+ Data Cache (Next.js)
439
+
440
+ Full Route Cache (Next.js)
441
+
442
+ Router Cache (Client-side)
443
+ ```
444
+
445
+ ### Fetch Cache Options
446
+
447
+ ```typescript
448
+ // 1. Force cache (default for GET in production)
449
+ fetch(url, { cache: 'force-cache' })
450
+ fetch(url) // Same as above
451
+
452
+ // 2. No cache (always fresh)
453
+ fetch(url, { cache: 'no-store' })
454
+
455
+ // 3. Time-based revalidation
456
+ fetch(url, { next: { revalidate: 3600 } }) // Revalidate every hour
457
+
458
+ // 4. Tag-based revalidation
459
+ fetch(url, { next: { tags: ['posts', 'user-123'] } })
460
+
461
+ // Then in Server Action or Route Handler:
462
+ import { revalidateTag, revalidatePath } from 'next/cache'
463
+
464
+ export async function updatePost() {
465
+ await db.post.update(...)
466
+
467
+ revalidateTag('posts') // Revalidate all fetches with this tag
468
+ revalidatePath('/blog') // Revalidate specific path
469
+ revalidatePath('/blog', 'layout') // Revalidate layout and all pages below
470
+ revalidatePath('/blog/[slug]', 'page') // Revalidate dynamic pages
471
+ }
472
+ ```
473
+
474
+ ### unstable_cache for Non-Fetch Functions
475
+
476
+ ```typescript
477
+ import { unstable_cache } from 'next/cache'
478
+ import { db } from '@/lib/db'
479
+
480
+ // Cache database queries
481
+ const getCachedUser = unstable_cache(
482
+ async (id: string) => {
483
+ return db.user.findUnique({ where: { id } })
484
+ },
485
+ ['user-cache'], // Cache key prefix
486
+ {
487
+ tags: ['users'],
488
+ revalidate: 3600,
489
+ }
490
+ )
491
+
492
+ // Usage
493
+ const user = await getCachedUser('user-123')
494
+
495
+ // Revalidate
496
+ revalidateTag('users')
497
+ ```
498
+
499
+ ### Route Segment Caching
500
+
501
+ ```typescript
502
+ // app/dashboard/page.tsx
503
+
504
+ // Force dynamic rendering (no cache)
505
+ export const dynamic = 'force-dynamic'
506
+
507
+ // OR force static (error if dynamic features used)
508
+ export const dynamic = 'force-static'
509
+
510
+ // Set revalidation period for entire route
511
+ export const revalidate = 3600 // seconds
512
+
513
+ // Disable dynamicParams (404 for unknown params)
514
+ export const dynamicParams = false
515
+ ```
516
+
517
+ ---
518
+
519
+ ## Server Actions
520
+
521
+ ### Basic Server Action
522
+
523
+ ```typescript
524
+ // app/actions.ts
525
+ 'use server'
526
+
527
+ import { revalidatePath } from 'next/cache'
528
+ import { redirect } from 'next/navigation'
529
+ import { z } from 'zod'
530
+ import { db } from '@/lib/db'
531
+ import { auth } from '@/lib/auth'
532
+
533
+ const CreatePostSchema = z.object({
534
+ title: z.string().min(1).max(100),
535
+ content: z.string().min(1),
536
+ })
537
+
538
+ export async function createPost(formData: FormData) {
539
+ // 1. Authentication
540
+ const session = await auth()
541
+ if (!session?.user) {
542
+ throw new Error('Unauthorized')
543
+ }
544
+
545
+ // 2. Validation
546
+ const validatedFields = CreatePostSchema.safeParse({
547
+ title: formData.get('title'),
548
+ content: formData.get('content'),
549
+ })
550
+
551
+ if (!validatedFields.success) {
552
+ return {
553
+ errors: validatedFields.error.flatten().fieldErrors,
554
+ }
555
+ }
556
+
557
+ // 3. Database operation
558
+ try {
559
+ await db.post.create({
560
+ data: {
561
+ ...validatedFields.data,
562
+ authorId: session.user.id,
563
+ },
564
+ })
565
+ } catch (error) {
566
+ return {
567
+ error: 'Failed to create post',
568
+ }
569
+ }
570
+
571
+ // 4. Revalidate and redirect
572
+ revalidatePath('/posts')
573
+ redirect('/posts')
574
+ }
575
+ ```
576
+
577
+ ### Server Action with useActionState (React 19)
578
+
579
+ ```typescript
580
+ // app/posts/new/page.tsx
581
+ 'use client'
582
+
583
+ import { useActionState } from 'react'
584
+ import { createPost } from '../actions'
585
+
586
+ const initialState = {
587
+ errors: {},
588
+ error: null,
589
+ }
590
+
591
+ export default function NewPostForm() {
592
+ const [state, formAction, isPending] = useActionState(createPost, initialState)
593
+
594
+ return (
595
+ <form action={formAction}>
596
+ <div>
597
+ <label htmlFor="title">Title</label>
598
+ <input
599
+ id="title"
600
+ name="title"
601
+ type="text"
602
+ aria-describedby="title-error"
603
+ />
604
+ {state.errors?.title && (
605
+ <p id="title-error" className="text-red-500">
606
+ {state.errors.title}
607
+ </p>
608
+ )}
609
+ </div>
610
+
611
+ <div>
612
+ <label htmlFor="content">Content</label>
613
+ <textarea
614
+ id="content"
615
+ name="content"
616
+ aria-describedby="content-error"
617
+ />
618
+ {state.errors?.content && (
619
+ <p id="content-error" className="text-red-500">
620
+ {state.errors.content}
621
+ </p>
622
+ )}
623
+ </div>
624
+
625
+ {state.error && (
626
+ <p className="text-red-500">{state.error}</p>
627
+ )}
628
+
629
+ <button type="submit" disabled={isPending}>
630
+ {isPending ? 'Creating...' : 'Create Post'}
631
+ </button>
632
+ </form>
633
+ )
634
+ }
635
+ ```
636
+
637
+ ### Optimistic Updates
638
+
639
+ ```typescript
640
+ 'use client'
641
+
642
+ import { useOptimistic, useTransition } from 'react'
643
+ import { likePost } from './actions'
644
+
645
+ interface Post {
646
+ id: string
647
+ likes: number
648
+ isLiked: boolean
649
+ }
650
+
651
+ export function LikeButton({ post }: { post: Post }) {
652
+ const [isPending, startTransition] = useTransition()
653
+
654
+ const [optimisticPost, addOptimisticLike] = useOptimistic(
655
+ post,
656
+ (currentPost, newLikes: number) => ({
657
+ ...currentPost,
658
+ likes: newLikes,
659
+ isLiked: !currentPost.isLiked,
660
+ })
661
+ )
662
+
663
+ const handleLike = () => {
664
+ startTransition(async () => {
665
+ // Optimistically update UI
666
+ addOptimisticLike(optimisticPost.likes + (optimisticPost.isLiked ? -1 : 1))
667
+
668
+ // Actual server action
669
+ await likePost(post.id)
670
+ })
671
+ }
672
+
673
+ return (
674
+ <button onClick={handleLike} disabled={isPending}>
675
+ {optimisticPost.isLiked ? '❤️' : '🤍'} {optimisticPost.likes}
676
+ </button>
677
+ )
678
+ }
679
+ ```
680
+
681
+ ### Server Action Security Patterns
682
+
683
+ ```typescript
684
+ 'use server'
685
+
686
+ import { auth } from '@/lib/auth'
687
+ import { ratelimit } from '@/lib/ratelimit'
688
+ import { z } from 'zod'
689
+
690
+ // Rate limiting
691
+ async function checkRateLimit(userId: string) {
692
+ const { success, remaining } = await ratelimit.limit(userId)
693
+ if (!success) {
694
+ throw new Error('Rate limit exceeded')
695
+ }
696
+ return remaining
697
+ }
698
+
699
+ // Authorization check
700
+ async function requireRole(allowedRoles: string[]) {
701
+ const session = await auth()
702
+ if (!session?.user) {
703
+ throw new Error('Unauthorized')
704
+ }
705
+ if (!allowedRoles.includes(session.user.role)) {
706
+ throw new Error('Forbidden')
707
+ }
708
+ return session.user
709
+ }
710
+
711
+ // Secure action pattern
712
+ export async function deletePost(postId: string) {
713
+ // 1. Auth
714
+ const user = await requireRole(['admin', 'moderator'])
715
+
716
+ // 2. Rate limit
717
+ await checkRateLimit(user.id)
718
+
719
+ // 3. Validate input
720
+ const id = z.string().uuid().parse(postId)
721
+
722
+ // 4. Authorization (ownership check)
723
+ const post = await db.post.findUnique({ where: { id } })
724
+ if (!post) throw new Error('Not found')
725
+ if (post.authorId !== user.id && user.role !== 'admin') {
726
+ throw new Error('Forbidden')
727
+ }
728
+
729
+ // 5. Perform action
730
+ await db.post.delete({ where: { id } })
731
+
732
+ // 6. Revalidate
733
+ revalidatePath('/posts')
734
+ }
735
+ ```
736
+
737
+ ---
738
+
739
+ ## Route Handlers (API)
740
+
741
+ ### Basic Route Handler
742
+
743
+ ```typescript
744
+ // app/api/posts/route.ts
745
+ import { NextRequest, NextResponse } from 'next/server'
746
+ import { auth } from '@/lib/auth'
747
+ import { db } from '@/lib/db'
748
+
749
+ // GET /api/posts
750
+ export async function GET(request: NextRequest) {
751
+ const searchParams = request.nextUrl.searchParams
752
+ const page = parseInt(searchParams.get('page') ?? '1')
753
+ const limit = parseInt(searchParams.get('limit') ?? '10')
754
+
755
+ const posts = await db.post.findMany({
756
+ skip: (page - 1) * limit,
757
+ take: limit,
758
+ orderBy: { createdAt: 'desc' },
759
+ })
760
+
761
+ return NextResponse.json(posts)
762
+ }
763
+
764
+ // POST /api/posts
765
+ export async function POST(request: NextRequest) {
766
+ const session = await auth()
767
+ if (!session) {
768
+ return NextResponse.json(
769
+ { error: 'Unauthorized' },
770
+ { status: 401 }
771
+ )
772
+ }
773
+
774
+ const body = await request.json()
775
+
776
+ const post = await db.post.create({
777
+ data: {
778
+ ...body,
779
+ authorId: session.user.id,
780
+ },
781
+ })
782
+
783
+ return NextResponse.json(post, { status: 201 })
784
+ }
785
+
786
+ // Set caching behavior
787
+ export const dynamic = 'force-dynamic' // Disable cache
788
+ // OR
789
+ export const revalidate = 60 // Cache for 60 seconds
790
+ ```
791
+
792
+ ### Dynamic Route Handler
793
+
794
+ ```typescript
795
+ // app/api/posts/[id]/route.ts
796
+ import { NextRequest, NextResponse } from 'next/server'
797
+
798
+ interface RouteParams {
799
+ params: { id: string }
800
+ }
801
+
802
+ export async function GET(
803
+ request: NextRequest,
804
+ { params }: RouteParams
805
+ ) {
806
+ const post = await db.post.findUnique({
807
+ where: { id: params.id },
808
+ })
809
+
810
+ if (!post) {
811
+ return NextResponse.json(
812
+ { error: 'Not found' },
813
+ { status: 404 }
814
+ )
815
+ }
816
+
817
+ return NextResponse.json(post)
818
+ }
819
+
820
+ export async function PATCH(
821
+ request: NextRequest,
822
+ { params }: RouteParams
823
+ ) {
824
+ const body = await request.json()
825
+
826
+ const post = await db.post.update({
827
+ where: { id: params.id },
828
+ data: body,
829
+ })
830
+
831
+ return NextResponse.json(post)
832
+ }
833
+
834
+ export async function DELETE(
835
+ request: NextRequest,
836
+ { params }: RouteParams
837
+ ) {
838
+ await db.post.delete({
839
+ where: { id: params.id },
840
+ })
841
+
842
+ return new NextResponse(null, { status: 204 })
843
+ }
844
+ ```
845
+
846
+ ### Streaming Response
847
+
848
+ ```typescript
849
+ // app/api/stream/route.ts
850
+ import { NextRequest } from 'next/server'
851
+
852
+ export async function GET(request: NextRequest) {
853
+ const encoder = new TextEncoder()
854
+
855
+ const stream = new ReadableStream({
856
+ async start(controller) {
857
+ for (let i = 0; i < 10; i++) {
858
+ const chunk = encoder.encode(`data: ${JSON.stringify({ count: i })}\n\n`)
859
+ controller.enqueue(chunk)
860
+ await new Promise(resolve => setTimeout(resolve, 1000))
861
+ }
862
+ controller.close()
863
+ },
864
+ })
865
+
866
+ return new Response(stream, {
867
+ headers: {
868
+ 'Content-Type': 'text/event-stream',
869
+ 'Cache-Control': 'no-cache',
870
+ 'Connection': 'keep-alive',
871
+ },
872
+ })
873
+ }
874
+ ```
875
+
876
+ ### Webhook Handler
877
+
878
+ ```typescript
879
+ // app/api/webhook/stripe/route.ts
880
+ import { NextRequest, NextResponse } from 'next/server'
881
+ import { headers } from 'next/headers'
882
+ import Stripe from 'stripe'
883
+
884
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
885
+ const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!
886
+
887
+ export async function POST(request: NextRequest) {
888
+ const body = await request.text()
889
+ const signature = headers().get('stripe-signature')!
890
+
891
+ let event: Stripe.Event
892
+
893
+ try {
894
+ event = stripe.webhooks.constructEvent(body, signature, webhookSecret)
895
+ } catch (err) {
896
+ console.error('Webhook signature verification failed')
897
+ return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
898
+ }
899
+
900
+ switch (event.type) {
901
+ case 'checkout.session.completed':
902
+ const session = event.data.object as Stripe.Checkout.Session
903
+ await handleCheckoutComplete(session)
904
+ break
905
+ case 'customer.subscription.deleted':
906
+ const subscription = event.data.object as Stripe.Subscription
907
+ await handleSubscriptionCanceled(subscription)
908
+ break
909
+ default:
910
+ console.log(`Unhandled event type: ${event.type}`)
911
+ }
912
+
913
+ return NextResponse.json({ received: true })
914
+ }
915
+ ```
916
+
917
+ ---
918
+
919
+ ## Middleware
920
+
921
+ ### Basic Middleware
922
+
923
+ ```typescript
924
+ // middleware.ts (root of project)
925
+ import { NextResponse } from 'next/server'
926
+ import type { NextRequest } from 'next/server'
927
+
928
+ export function middleware(request: NextRequest) {
929
+ // Get pathname
930
+ const pathname = request.nextUrl.pathname
931
+
932
+ // Example: Redirect old URLs
933
+ if (pathname.startsWith('/old-blog')) {
934
+ return NextResponse.redirect(
935
+ new URL(pathname.replace('/old-blog', '/blog'), request.url)
936
+ )
937
+ }
938
+
939
+ // Example: Add custom headers
940
+ const response = NextResponse.next()
941
+ response.headers.set('x-custom-header', 'my-value')
942
+
943
+ // Example: Rewrite (internal redirect)
944
+ if (pathname === '/dashboard' && !request.cookies.get('auth')) {
945
+ return NextResponse.rewrite(new URL('/login', request.url))
946
+ }
947
+
948
+ return response
949
+ }
950
+
951
+ // Configure which paths middleware runs on
952
+ export const config = {
953
+ matcher: [
954
+ // Match all paths except static files and api
955
+ '/((?!_next/static|_next/image|favicon.ico|api).*)',
956
+ ],
957
+ }
958
+ ```
959
+
960
+ ### Authentication Middleware
961
+
962
+ ```typescript
963
+ // middleware.ts
964
+ import { NextResponse } from 'next/server'
965
+ import type { NextRequest } from 'next/server'
966
+ import { getToken } from 'next-auth/jwt'
967
+
968
+ const protectedRoutes = ['/dashboard', '/settings', '/profile']
969
+ const authRoutes = ['/login', '/register']
970
+
971
+ export async function middleware(request: NextRequest) {
972
+ const token = await getToken({
973
+ req: request,
974
+ secret: process.env.NEXTAUTH_SECRET,
975
+ })
976
+
977
+ const pathname = request.nextUrl.pathname
978
+
979
+ // Redirect authenticated users away from auth pages
980
+ if (authRoutes.some(route => pathname.startsWith(route))) {
981
+ if (token) {
982
+ return NextResponse.redirect(new URL('/dashboard', request.url))
983
+ }
984
+ return NextResponse.next()
985
+ }
986
+
987
+ // Protect routes
988
+ if (protectedRoutes.some(route => pathname.startsWith(route))) {
989
+ if (!token) {
990
+ const loginUrl = new URL('/login', request.url)
991
+ loginUrl.searchParams.set('callbackUrl', pathname)
992
+ return NextResponse.redirect(loginUrl)
993
+ }
994
+ }
995
+
996
+ // Role-based access
997
+ if (pathname.startsWith('/admin')) {
998
+ if (token?.role !== 'admin') {
999
+ return NextResponse.redirect(new URL('/unauthorized', request.url))
1000
+ }
1001
+ }
1002
+
1003
+ return NextResponse.next()
1004
+ }
1005
+
1006
+ export const config = {
1007
+ matcher: [
1008
+ '/dashboard/:path*',
1009
+ '/settings/:path*',
1010
+ '/profile/:path*',
1011
+ '/admin/:path*',
1012
+ '/login',
1013
+ '/register',
1014
+ ],
1015
+ }
1016
+ ```
1017
+
1018
+ ### Geolocation & A/B Testing
1019
+
1020
+ ```typescript
1021
+ // middleware.ts
1022
+ import { NextResponse } from 'next/server'
1023
+ import type { NextRequest } from 'next/server'
1024
+
1025
+ export function middleware(request: NextRequest) {
1026
+ // Geolocation (available on Vercel Edge)
1027
+ const country = request.geo?.country ?? 'US'
1028
+ const city = request.geo?.city ?? 'Unknown'
1029
+
1030
+ // A/B Testing
1031
+ let bucket = request.cookies.get('ab-bucket')?.value
1032
+
1033
+ if (!bucket) {
1034
+ bucket = Math.random() < 0.5 ? 'control' : 'variant'
1035
+ }
1036
+
1037
+ const response = NextResponse.next()
1038
+
1039
+ // Set headers for downstream use
1040
+ response.headers.set('x-country', country)
1041
+ response.headers.set('x-city', city)
1042
+ response.headers.set('x-ab-bucket', bucket)
1043
+
1044
+ // Set cookie for consistent bucketing
1045
+ response.cookies.set('ab-bucket', bucket, {
1046
+ maxAge: 60 * 60 * 24 * 30, // 30 days
1047
+ })
1048
+
1049
+ return response
1050
+ }
1051
+ ```
1052
+
1053
+ ---
1054
+
1055
+ ## Streaming & Suspense
1056
+
1057
+ ### Loading UI with Suspense
1058
+
1059
+ ```typescript
1060
+ // app/dashboard/page.tsx
1061
+ import { Suspense } from 'react'
1062
+ import { Skeleton } from '@/components/ui/skeleton'
1063
+
1064
+ export default function DashboardPage() {
1065
+ return (
1066
+ <div className="grid gap-4">
1067
+ {/* Critical content - renders immediately */}
1068
+ <DashboardHeader />
1069
+
1070
+ {/* Non-critical - streams in */}
1071
+ <Suspense fallback={<StatsSkeleton />}>
1072
+ <Stats />
1073
+ </Suspense>
1074
+
1075
+ <div className="grid grid-cols-2 gap-4">
1076
+ <Suspense fallback={<ChartSkeleton />}>
1077
+ <RevenueChart />
1078
+ </Suspense>
1079
+
1080
+ <Suspense fallback={<ChartSkeleton />}>
1081
+ <UserChart />
1082
+ </Suspense>
1083
+ </div>
1084
+
1085
+ <Suspense fallback={<TableSkeleton />}>
1086
+ <RecentOrders />
1087
+ </Suspense>
1088
+ </div>
1089
+ )
1090
+ }
1091
+
1092
+ // Each async component streams independently
1093
+ async function Stats() {
1094
+ const stats = await getStats() // Slow API call
1095
+ return <StatsDisplay stats={stats} />
1096
+ }
1097
+
1098
+ async function RevenueChart() {
1099
+ const data = await getRevenueData()
1100
+ return <Chart data={data} />
1101
+ }
1102
+ ```
1103
+
1104
+ ### Loading.tsx (Route-level Loading)
1105
+
1106
+ ```typescript
1107
+ // app/dashboard/loading.tsx
1108
+ // Automatically wraps page.tsx in Suspense
1109
+
1110
+ import { Skeleton } from '@/components/ui/skeleton'
1111
+
1112
+ export default function DashboardLoading() {
1113
+ return (
1114
+ <div className="space-y-4">
1115
+ <Skeleton className="h-8 w-[200px]" />
1116
+ <div className="grid grid-cols-4 gap-4">
1117
+ {Array.from({ length: 4 }).map((_, i) => (
1118
+ <Skeleton key={i} className="h-24" />
1119
+ ))}
1120
+ </div>
1121
+ <Skeleton className="h-[400px]" />
1122
+ </div>
1123
+ )
1124
+ }
1125
+ ```
1126
+
1127
+ ### Parallel Routes with Loading States
1128
+
1129
+ ```typescript
1130
+ // app/dashboard/layout.tsx
1131
+ export default function DashboardLayout({
1132
+ children,
1133
+ analytics,
1134
+ team,
1135
+ }: {
1136
+ children: React.ReactNode
1137
+ analytics: React.ReactNode // @analytics/page.tsx
1138
+ team: React.ReactNode // @team/page.tsx
1139
+ }) {
1140
+ return (
1141
+ <div className="grid grid-cols-3 gap-4">
1142
+ <div className="col-span-2">{children}</div>
1143
+ <div className="space-y-4">
1144
+ {analytics}
1145
+ {team}
1146
+ </div>
1147
+ </div>
1148
+ )
1149
+ }
1150
+
1151
+ // app/dashboard/@analytics/loading.tsx
1152
+ export default function AnalyticsLoading() {
1153
+ return <Skeleton className="h-[200px]" />
1154
+ }
1155
+
1156
+ // app/dashboard/@team/loading.tsx
1157
+ export default function TeamLoading() {
1158
+ return <Skeleton className="h-[150px]" />
1159
+ }
1160
+ ```
1161
+
1162
+ ### Streaming with AI/LLM
1163
+
1164
+ ```typescript
1165
+ // app/api/chat/route.ts
1166
+ import { OpenAIStream, StreamingTextResponse } from 'ai'
1167
+ import OpenAI from 'openai'
1168
+
1169
+ const openai = new OpenAI()
1170
+
1171
+ export async function POST(req: Request) {
1172
+ const { messages } = await req.json()
1173
+
1174
+ const response = await openai.chat.completions.create({
1175
+ model: 'gpt-4-turbo-preview',
1176
+ stream: true,
1177
+ messages,
1178
+ })
1179
+
1180
+ const stream = OpenAIStream(response)
1181
+
1182
+ return new StreamingTextResponse(stream)
1183
+ }
1184
+
1185
+ // Client component
1186
+ 'use client'
1187
+
1188
+ import { useChat } from 'ai/react'
1189
+
1190
+ export function Chat() {
1191
+ const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat()
1192
+
1193
+ return (
1194
+ <div>
1195
+ {messages.map(m => (
1196
+ <div key={m.id}>
1197
+ <strong>{m.role}:</strong> {m.content}
1198
+ </div>
1199
+ ))}
1200
+
1201
+ <form onSubmit={handleSubmit}>
1202
+ <input
1203
+ value={input}
1204
+ onChange={handleInputChange}
1205
+ placeholder="Say something..."
1206
+ disabled={isLoading}
1207
+ />
1208
+ </form>
1209
+ </div>
1210
+ )
1211
+ }
1212
+ ```
1213
+
1214
+ ---
1215
+
1216
+ ## Error Handling
1217
+
1218
+ ### Error Boundary (error.tsx)
1219
+
1220
+ ```typescript
1221
+ // app/dashboard/error.tsx
1222
+ 'use client' // Error boundaries must be client components
1223
+
1224
+ import { useEffect } from 'react'
1225
+ import { Button } from '@/components/ui/button'
1226
+
1227
+ export default function DashboardError({
1228
+ error,
1229
+ reset,
1230
+ }: {
1231
+ error: Error & { digest?: string }
1232
+ reset: () => void
1233
+ }) {
1234
+ useEffect(() => {
1235
+ // Log error to monitoring service
1236
+ console.error('Dashboard error:', error)
1237
+
1238
+ // Send to Sentry, etc.
1239
+ // Sentry.captureException(error)
1240
+ }, [error])
1241
+
1242
+ return (
1243
+ <div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
1244
+ <h2 className="text-2xl font-bold">Something went wrong!</h2>
1245
+ <p className="text-muted-foreground">
1246
+ {error.message || 'An unexpected error occurred'}
1247
+ </p>
1248
+ {error.digest && (
1249
+ <p className="text-xs text-muted-foreground">
1250
+ Error ID: {error.digest}
1251
+ </p>
1252
+ )}
1253
+ <Button onClick={reset}>Try again</Button>
1254
+ </div>
1255
+ )
1256
+ }
1257
+ ```
1258
+
1259
+ ### Global Error Boundary
1260
+
1261
+ ```typescript
1262
+ // app/global-error.tsx
1263
+ 'use client'
1264
+
1265
+ export default function GlobalError({
1266
+ error,
1267
+ reset,
1268
+ }: {
1269
+ error: Error & { digest?: string }
1270
+ reset: () => void
1271
+ }) {
1272
+ return (
1273
+ // Must include html and body tags
1274
+ <html>
1275
+ <body>
1276
+ <div className="flex flex-col items-center justify-center min-h-screen">
1277
+ <h2>Something went wrong!</h2>
1278
+ <button onClick={reset}>Try again</button>
1279
+ </div>
1280
+ </body>
1281
+ </html>
1282
+ )
1283
+ }
1284
+ ```
1285
+
1286
+ ### Not Found Handling
1287
+
1288
+ ```typescript
1289
+ // app/not-found.tsx
1290
+ import Link from 'next/link'
1291
+
1292
+ export default function NotFound() {
1293
+ return (
1294
+ <div className="flex flex-col items-center justify-center min-h-[400px]">
1295
+ <h2 className="text-2xl font-bold">Page Not Found</h2>
1296
+ <p className="text-muted-foreground">
1297
+ The page you're looking for doesn't exist.
1298
+ </p>
1299
+ <Link href="/" className="mt-4 underline">
1300
+ Go home
1301
+ </Link>
1302
+ </div>
1303
+ )
1304
+ }
1305
+
1306
+ // Trigger not-found programmatically
1307
+ import { notFound } from 'next/navigation'
1308
+
1309
+ export default async function PostPage({ params }: { params: { id: string } }) {
1310
+ const post = await getPost(params.id)
1311
+
1312
+ if (!post) {
1313
+ notFound() // Renders not-found.tsx
1314
+ }
1315
+
1316
+ return <PostDisplay post={post} />
1317
+ }
1318
+ ```
1319
+
1320
+ ### Error Handling in Server Actions
1321
+
1322
+ ```typescript
1323
+ 'use server'
1324
+
1325
+ import { z } from 'zod'
1326
+
1327
+ // Type-safe error responses
1328
+ type ActionResult<T> =
1329
+ | { success: true; data: T }
1330
+ | { success: false; error: string; fieldErrors?: Record<string, string[]> }
1331
+
1332
+ export async function createUser(formData: FormData): Promise<ActionResult<User>> {
1333
+ try {
1334
+ const validated = UserSchema.safeParse({
1335
+ email: formData.get('email'),
1336
+ name: formData.get('name'),
1337
+ })
1338
+
1339
+ if (!validated.success) {
1340
+ return {
1341
+ success: false,
1342
+ error: 'Validation failed',
1343
+ fieldErrors: validated.error.flatten().fieldErrors,
1344
+ }
1345
+ }
1346
+
1347
+ const user = await db.user.create({ data: validated.data })
1348
+
1349
+ return { success: true, data: user }
1350
+ } catch (error) {
1351
+ // Log error
1352
+ console.error('Create user error:', error)
1353
+
1354
+ // Return generic error (don't expose internal details)
1355
+ return {
1356
+ success: false,
1357
+ error: 'Failed to create user. Please try again.'
1358
+ }
1359
+ }
1360
+ }
1361
+ ```
1362
+
1363
+ ---
1364
+
1365
+ ## Metadata & SEO
1366
+
1367
+ ### Static Metadata
1368
+
1369
+ ```typescript
1370
+ // app/layout.tsx
1371
+ import type { Metadata } from 'next'
1372
+
1373
+ export const metadata: Metadata = {
1374
+ metadataBase: new URL('https://example.com'),
1375
+ title: {
1376
+ default: 'My App',
1377
+ template: '%s | My App', // Pages can override: "About | My App"
1378
+ },
1379
+ description: 'The best app ever',
1380
+ keywords: ['Next.js', 'React', 'TypeScript'],
1381
+ authors: [{ name: 'John Doe', url: 'https://johndoe.com' }],
1382
+ creator: 'John Doe',
1383
+ publisher: 'My Company',
1384
+
1385
+ // Open Graph
1386
+ openGraph: {
1387
+ type: 'website',
1388
+ locale: 'en_US',
1389
+ url: 'https://example.com',
1390
+ siteName: 'My App',
1391
+ title: 'My App',
1392
+ description: 'The best app ever',
1393
+ images: [
1394
+ {
1395
+ url: '/og-image.png',
1396
+ width: 1200,
1397
+ height: 630,
1398
+ alt: 'My App',
1399
+ },
1400
+ ],
1401
+ },
1402
+
1403
+ // Twitter
1404
+ twitter: {
1405
+ card: 'summary_large_image',
1406
+ title: 'My App',
1407
+ description: 'The best app ever',
1408
+ creator: '@johndoe',
1409
+ images: ['/twitter-image.png'],
1410
+ },
1411
+
1412
+ // Robots
1413
+ robots: {
1414
+ index: true,
1415
+ follow: true,
1416
+ googleBot: {
1417
+ index: true,
1418
+ follow: true,
1419
+ 'max-video-preview': -1,
1420
+ 'max-image-preview': 'large',
1421
+ 'max-snippet': -1,
1422
+ },
1423
+ },
1424
+
1425
+ // Icons
1426
+ icons: {
1427
+ icon: '/favicon.ico',
1428
+ shortcut: '/favicon-16x16.png',
1429
+ apple: '/apple-touch-icon.png',
1430
+ },
1431
+
1432
+ // Manifest
1433
+ manifest: '/site.webmanifest',
1434
+
1435
+ // Verification
1436
+ verification: {
1437
+ google: 'google-site-verification-code',
1438
+ yandex: 'yandex-verification-code',
1439
+ },
1440
+ }
1441
+ ```
1442
+
1443
+ ### Dynamic Metadata
1444
+
1445
+ ```typescript
1446
+ // app/blog/[slug]/page.tsx
1447
+ import type { Metadata, ResolvingMetadata } from 'next'
1448
+
1449
+ interface Props {
1450
+ params: { slug: string }
1451
+ searchParams: { [key: string]: string | string[] | undefined }
1452
+ }
1453
+
1454
+ export async function generateMetadata(
1455
+ { params, searchParams }: Props,
1456
+ parent: ResolvingMetadata
1457
+ ): Promise<Metadata> {
1458
+ const post = await getPost(params.slug)
1459
+
1460
+ // Optionally access parent metadata
1461
+ const previousImages = (await parent).openGraph?.images || []
1462
+
1463
+ return {
1464
+ title: post.title,
1465
+ description: post.excerpt,
1466
+ openGraph: {
1467
+ title: post.title,
1468
+ description: post.excerpt,
1469
+ type: 'article',
1470
+ publishedTime: post.createdAt,
1471
+ authors: [post.author.name],
1472
+ images: [
1473
+ {
1474
+ url: post.image,
1475
+ width: 1200,
1476
+ height: 630,
1477
+ },
1478
+ ...previousImages,
1479
+ ],
1480
+ },
1481
+ twitter: {
1482
+ card: 'summary_large_image',
1483
+ title: post.title,
1484
+ description: post.excerpt,
1485
+ images: [post.image],
1486
+ },
1487
+ }
1488
+ }
1489
+ ```
1490
+
1491
+ ### JSON-LD Structured Data
1492
+
1493
+ ```typescript
1494
+ // app/blog/[slug]/page.tsx
1495
+
1496
+ export default async function BlogPost({ params }: Props) {
1497
+ const post = await getPost(params.slug)
1498
+
1499
+ const jsonLd = {
1500
+ '@context': 'https://schema.org',
1501
+ '@type': 'BlogPosting',
1502
+ headline: post.title,
1503
+ description: post.excerpt,
1504
+ image: post.image,
1505
+ datePublished: post.createdAt,
1506
+ dateModified: post.updatedAt,
1507
+ author: {
1508
+ '@type': 'Person',
1509
+ name: post.author.name,
1510
+ url: post.author.url,
1511
+ },
1512
+ publisher: {
1513
+ '@type': 'Organization',
1514
+ name: 'My Company',
1515
+ logo: {
1516
+ '@type': 'ImageObject',
1517
+ url: 'https://example.com/logo.png',
1518
+ },
1519
+ },
1520
+ }
1521
+
1522
+ return (
1523
+ <>
1524
+ <script
1525
+ type="application/ld+json"
1526
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
1527
+ />
1528
+ <article>{/* content */}</article>
1529
+ </>
1530
+ )
1531
+ }
1532
+ ```
1533
+
1534
+ ### Sitemap & Robots
1535
+
1536
+ ```typescript
1537
+ // app/sitemap.ts
1538
+ import { MetadataRoute } from 'next'
1539
+
1540
+ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
1541
+ const posts = await db.post.findMany({
1542
+ select: { slug: true, updatedAt: true },
1543
+ })
1544
+
1545
+ const postUrls = posts.map((post) => ({
1546
+ url: `https://example.com/blog/${post.slug}`,
1547
+ lastModified: post.updatedAt,
1548
+ changeFrequency: 'weekly' as const,
1549
+ priority: 0.8,
1550
+ }))
1551
+
1552
+ return [
1553
+ {
1554
+ url: 'https://example.com',
1555
+ lastModified: new Date(),
1556
+ changeFrequency: 'daily',
1557
+ priority: 1,
1558
+ },
1559
+ {
1560
+ url: 'https://example.com/about',
1561
+ lastModified: new Date(),
1562
+ changeFrequency: 'monthly',
1563
+ priority: 0.5,
1564
+ },
1565
+ ...postUrls,
1566
+ ]
1567
+ }
1568
+
1569
+ // app/robots.ts
1570
+ import { MetadataRoute } from 'next'
1571
+
1572
+ export default function robots(): MetadataRoute.Robots {
1573
+ return {
1574
+ rules: [
1575
+ {
1576
+ userAgent: '*',
1577
+ allow: '/',
1578
+ disallow: ['/api/', '/admin/', '/private/'],
1579
+ },
1580
+ {
1581
+ userAgent: 'Googlebot',
1582
+ allow: '/',
1583
+ },
1584
+ ],
1585
+ sitemap: 'https://example.com/sitemap.xml',
1586
+ }
1587
+ }
1588
+ ```
1589
+
1590
+ ---
1591
+
1592
+ ## Authentication Patterns
1593
+
1594
+ ### NextAuth.js v5 Setup
1595
+
1596
+ ```typescript
1597
+ // auth.ts
1598
+ import NextAuth from 'next-auth'
1599
+ import GitHub from 'next-auth/providers/github'
1600
+ import Google from 'next-auth/providers/google'
1601
+ import Credentials from 'next-auth/providers/credentials'
1602
+ import { PrismaAdapter } from '@auth/prisma-adapter'
1603
+ import { db } from '@/lib/db'
1604
+ import bcrypt from 'bcryptjs'
1605
+
1606
+ export const { auth, handlers, signIn, signOut } = NextAuth({
1607
+ adapter: PrismaAdapter(db),
1608
+ session: { strategy: 'jwt' },
1609
+
1610
+ providers: [
1611
+ GitHub({
1612
+ clientId: process.env.GITHUB_ID,
1613
+ clientSecret: process.env.GITHUB_SECRET,
1614
+ }),
1615
+ Google({
1616
+ clientId: process.env.GOOGLE_ID,
1617
+ clientSecret: process.env.GOOGLE_SECRET,
1618
+ }),
1619
+ Credentials({
1620
+ credentials: {
1621
+ email: { label: 'Email', type: 'email' },
1622
+ password: { label: 'Password', type: 'password' },
1623
+ },
1624
+ async authorize(credentials) {
1625
+ if (!credentials?.email || !credentials?.password) {
1626
+ return null
1627
+ }
1628
+
1629
+ const user = await db.user.findUnique({
1630
+ where: { email: credentials.email as string },
1631
+ })
1632
+
1633
+ if (!user || !user.password) {
1634
+ return null
1635
+ }
1636
+
1637
+ const isValid = await bcrypt.compare(
1638
+ credentials.password as string,
1639
+ user.password
1640
+ )
1641
+
1642
+ if (!isValid) {
1643
+ return null
1644
+ }
1645
+
1646
+ return {
1647
+ id: user.id,
1648
+ email: user.email,
1649
+ name: user.name,
1650
+ role: user.role,
1651
+ }
1652
+ },
1653
+ }),
1654
+ ],
1655
+
1656
+ callbacks: {
1657
+ async jwt({ token, user, trigger, session }) {
1658
+ if (user) {
1659
+ token.id = user.id
1660
+ token.role = user.role
1661
+ }
1662
+
1663
+ // Handle session update
1664
+ if (trigger === 'update' && session) {
1665
+ token.name = session.name
1666
+ }
1667
+
1668
+ return token
1669
+ },
1670
+
1671
+ async session({ session, token }) {
1672
+ if (session.user) {
1673
+ session.user.id = token.id as string
1674
+ session.user.role = token.role as string
1675
+ }
1676
+ return session
1677
+ },
1678
+
1679
+ async signIn({ user, account, profile }) {
1680
+ // Block sign in if email not verified
1681
+ if (account?.provider === 'credentials') {
1682
+ const existingUser = await db.user.findUnique({
1683
+ where: { id: user.id },
1684
+ })
1685
+ if (!existingUser?.emailVerified) {
1686
+ return false
1687
+ }
1688
+ }
1689
+ return true
1690
+ },
1691
+ },
1692
+
1693
+ pages: {
1694
+ signIn: '/login',
1695
+ error: '/auth/error',
1696
+ verifyRequest: '/auth/verify',
1697
+ },
1698
+ })
1699
+
1700
+ // app/api/auth/[...nextauth]/route.ts
1701
+ import { handlers } from '@/auth'
1702
+ export const { GET, POST } = handlers
1703
+ ```
1704
+
1705
+ ### Protected Server Component
1706
+
1707
+ ```typescript
1708
+ // app/dashboard/page.tsx
1709
+ import { auth } from '@/auth'
1710
+ import { redirect } from 'next/navigation'
1711
+
1712
+ export default async function DashboardPage() {
1713
+ const session = await auth()
1714
+
1715
+ if (!session) {
1716
+ redirect('/login')
1717
+ }
1718
+
1719
+ // Role-based check
1720
+ if (session.user.role !== 'admin') {
1721
+ redirect('/unauthorized')
1722
+ }
1723
+
1724
+ return (
1725
+ <div>
1726
+ <h1>Welcome, {session.user.name}</h1>
1727
+ </div>
1728
+ )
1729
+ }
1730
+ ```
1731
+
1732
+ ### Protected Client Component
1733
+
1734
+ ```typescript
1735
+ 'use client'
1736
+
1737
+ import { useSession } from 'next-auth/react'
1738
+ import { useRouter } from 'next/navigation'
1739
+ import { useEffect } from 'react'
1740
+
1741
+ export function ProtectedComponent() {
1742
+ const { data: session, status } = useSession()
1743
+ const router = useRouter()
1744
+
1745
+ useEffect(() => {
1746
+ if (status === 'unauthenticated') {
1747
+ router.push('/login')
1748
+ }
1749
+ }, [status, router])
1750
+
1751
+ if (status === 'loading') {
1752
+ return <div>Loading...</div>
1753
+ }
1754
+
1755
+ if (!session) {
1756
+ return null
1757
+ }
1758
+
1759
+ return <div>Protected content for {session.user.name}</div>
1760
+ }
1761
+ ```
1762
+
1763
+ ### Sign In/Out Server Actions
1764
+
1765
+ ```typescript
1766
+ // app/actions/auth.ts
1767
+ 'use server'
1768
+
1769
+ import { signIn, signOut } from '@/auth'
1770
+ import { AuthError } from 'next-auth'
1771
+ import { redirect } from 'next/navigation'
1772
+
1773
+ export async function signInWithCredentials(formData: FormData) {
1774
+ try {
1775
+ await signIn('credentials', {
1776
+ email: formData.get('email'),
1777
+ password: formData.get('password'),
1778
+ redirectTo: '/dashboard',
1779
+ })
1780
+ } catch (error) {
1781
+ if (error instanceof AuthError) {
1782
+ switch (error.type) {
1783
+ case 'CredentialsSignin':
1784
+ return { error: 'Invalid credentials' }
1785
+ default:
1786
+ return { error: 'Something went wrong' }
1787
+ }
1788
+ }
1789
+ throw error
1790
+ }
1791
+ }
1792
+
1793
+ export async function signInWithGitHub() {
1794
+ await signIn('github', { redirectTo: '/dashboard' })
1795
+ }
1796
+
1797
+ export async function signOutAction() {
1798
+ await signOut({ redirectTo: '/' })
1799
+ }
1800
+ ```
1801
+
1802
+ ---
1803
+
1804
+ ## Database Integration
1805
+
1806
+ ### Prisma Setup
1807
+
1808
+ ```typescript
1809
+ // lib/db.ts
1810
+ import { PrismaClient } from '@prisma/client'
1811
+
1812
+ const globalForPrisma = globalThis as unknown as {
1813
+ prisma: PrismaClient | undefined
1814
+ }
1815
+
1816
+ export const db = globalForPrisma.prisma ?? new PrismaClient({
1817
+ log: process.env.NODE_ENV === 'development'
1818
+ ? ['query', 'error', 'warn']
1819
+ : ['error'],
1820
+ })
1821
+
1822
+ if (process.env.NODE_ENV !== 'production') {
1823
+ globalForPrisma.prisma = db
1824
+ }
1825
+ ```
1826
+
1827
+ ### Drizzle ORM Setup
1828
+
1829
+ ```typescript
1830
+ // lib/db.ts
1831
+ import { drizzle } from 'drizzle-orm/postgres-js'
1832
+ import postgres from 'postgres'
1833
+ import * as schema from './schema'
1834
+
1835
+ const connectionString = process.env.DATABASE_URL!
1836
+
1837
+ // For query purposes
1838
+ const queryClient = postgres(connectionString)
1839
+ export const db = drizzle(queryClient, { schema })
1840
+
1841
+ // lib/schema.ts
1842
+ import { pgTable, text, timestamp, uuid, boolean } from 'drizzle-orm/pg-core'
1843
+
1844
+ export const users = pgTable('users', {
1845
+ id: uuid('id').primaryKey().defaultRandom(),
1846
+ email: text('email').notNull().unique(),
1847
+ name: text('name'),
1848
+ emailVerified: timestamp('email_verified'),
1849
+ image: text('image'),
1850
+ createdAt: timestamp('created_at').defaultNow().notNull(),
1851
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
1852
+ })
1853
+
1854
+ export const posts = pgTable('posts', {
1855
+ id: uuid('id').primaryKey().defaultRandom(),
1856
+ title: text('title').notNull(),
1857
+ content: text('content'),
1858
+ published: boolean('published').default(false).notNull(),
1859
+ authorId: uuid('author_id').references(() => users.id).notNull(),
1860
+ createdAt: timestamp('created_at').defaultNow().notNull(),
1861
+ updatedAt: timestamp('updated_at').defaultNow().notNull(),
1862
+ })
1863
+ ```
1864
+
1865
+ ### Database Query Patterns
1866
+
1867
+ ```typescript
1868
+ // lib/queries/posts.ts
1869
+ import { cache } from 'react'
1870
+ import { db } from '@/lib/db'
1871
+ import { unstable_cache } from 'next/cache'
1872
+
1873
+ // Request-level caching (deduplication)
1874
+ export const getPost = cache(async (id: string) => {
1875
+ return db.post.findUnique({
1876
+ where: { id },
1877
+ include: { author: true },
1878
+ })
1879
+ })
1880
+
1881
+ // Time-based caching
1882
+ export const getPublishedPosts = unstable_cache(
1883
+ async () => {
1884
+ return db.post.findMany({
1885
+ where: { published: true },
1886
+ orderBy: { createdAt: 'desc' },
1887
+ include: { author: { select: { name: true, image: true } } },
1888
+ })
1889
+ },
1890
+ ['published-posts'],
1891
+ { revalidate: 3600, tags: ['posts'] }
1892
+ )
1893
+
1894
+ // Pagination
1895
+ export async function getPaginatedPosts(page: number, limit: number = 10) {
1896
+ const [posts, total] = await Promise.all([
1897
+ db.post.findMany({
1898
+ skip: (page - 1) * limit,
1899
+ take: limit,
1900
+ orderBy: { createdAt: 'desc' },
1901
+ }),
1902
+ db.post.count(),
1903
+ ])
1904
+
1905
+ return {
1906
+ posts,
1907
+ total,
1908
+ pages: Math.ceil(total / limit),
1909
+ hasMore: page * limit < total,
1910
+ }
1911
+ }
1912
+ ```
1913
+
1914
+ ---
1915
+
1916
+ ## Performance Optimization
1917
+
1918
+ ### Image Optimization
1919
+
1920
+ ```typescript
1921
+ import Image from 'next/image'
1922
+
1923
+ // Basic usage
1924
+ <Image
1925
+ src="/hero.jpg"
1926
+ alt="Hero image"
1927
+ width={1200}
1928
+ height={600}
1929
+ priority // Load immediately (above the fold)
1930
+ />
1931
+
1932
+ // Fill container
1933
+ <div className="relative h-64 w-full">
1934
+ <Image
1935
+ src="/background.jpg"
1936
+ alt="Background"
1937
+ fill
1938
+ className="object-cover"
1939
+ sizes="100vw"
1940
+ />
1941
+ </div>
1942
+
1943
+ // Responsive images
1944
+ <Image
1945
+ src="/photo.jpg"
1946
+ alt="Photo"
1947
+ width={800}
1948
+ height={600}
1949
+ sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
1950
+ placeholder="blur"
1951
+ blurDataURL="data:image/jpeg;base64,..." // Or use static import for auto blur
1952
+ />
1953
+
1954
+ // Remote images (configure in next.config.js)
1955
+ // next.config.js
1956
+ module.exports = {
1957
+ images: {
1958
+ remotePatterns: [
1959
+ {
1960
+ protocol: 'https',
1961
+ hostname: 'cdn.example.com',
1962
+ pathname: '/images/**',
1963
+ },
1964
+ ],
1965
+ },
1966
+ }
1967
+ ```
1968
+
1969
+ ### Font Optimization
1970
+
1971
+ ```typescript
1972
+ // app/layout.tsx
1973
+ import { Inter, Roboto_Mono } from 'next/font/google'
1974
+ import localFont from 'next/font/local'
1975
+
1976
+ // Google Fonts
1977
+ const inter = Inter({
1978
+ subsets: ['latin'],
1979
+ display: 'swap',
1980
+ variable: '--font-inter',
1981
+ })
1982
+
1983
+ const robotoMono = Roboto_Mono({
1984
+ subsets: ['latin'],
1985
+ display: 'swap',
1986
+ variable: '--font-roboto-mono',
1987
+ })
1988
+
1989
+ // Local fonts
1990
+ const customFont = localFont({
1991
+ src: [
1992
+ {
1993
+ path: '../fonts/CustomFont-Regular.woff2',
1994
+ weight: '400',
1995
+ style: 'normal',
1996
+ },
1997
+ {
1998
+ path: '../fonts/CustomFont-Bold.woff2',
1999
+ weight: '700',
2000
+ style: 'normal',
2001
+ },
2002
+ ],
2003
+ variable: '--font-custom',
2004
+ })
2005
+
2006
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
2007
+ return (
2008
+ <html lang="en" className={`${inter.variable} ${robotoMono.variable} ${customFont.variable}`}>
2009
+ <body className="font-sans">{children}</body>
2010
+ </html>
2011
+ )
2012
+ }
2013
+
2014
+ // tailwind.config.ts
2015
+ module.exports = {
2016
+ theme: {
2017
+ extend: {
2018
+ fontFamily: {
2019
+ sans: ['var(--font-inter)'],
2020
+ mono: ['var(--font-roboto-mono)'],
2021
+ custom: ['var(--font-custom)'],
2022
+ },
2023
+ },
2024
+ },
2025
+ }
2026
+ ```
2027
+
2028
+ ### Bundle Analysis
2029
+
2030
+ ```bash
2031
+ # Install analyzer
2032
+ npm install @next/bundle-analyzer
2033
+
2034
+ # next.config.js
2035
+ const withBundleAnalyzer = require('@next/bundle-analyzer')({
2036
+ enabled: process.env.ANALYZE === 'true',
2037
+ })
2038
+
2039
+ module.exports = withBundleAnalyzer({
2040
+ // your config
2041
+ })
2042
+
2043
+ # Run analysis
2044
+ ANALYZE=true npm run build
2045
+ ```
2046
+
2047
+ ### Dynamic Imports
2048
+
2049
+ ```typescript
2050
+ import dynamic from 'next/dynamic'
2051
+
2052
+ // Basic dynamic import
2053
+ const DynamicComponent = dynamic(() => import('./HeavyComponent'), {
2054
+ loading: () => <p>Loading...</p>,
2055
+ })
2056
+
2057
+ // Disable SSR (for browser-only components)
2058
+ const MapComponent = dynamic(() => import('./Map'), {
2059
+ ssr: false,
2060
+ loading: () => <MapSkeleton />,
2061
+ })
2062
+
2063
+ // Named exports
2064
+ const Modal = dynamic(() =>
2065
+ import('./Modal').then((mod) => mod.Modal)
2066
+ )
2067
+
2068
+ // Usage
2069
+ export default function Page() {
2070
+ return (
2071
+ <div>
2072
+ <DynamicComponent />
2073
+ <MapComponent />
2074
+ </div>
2075
+ )
2076
+ }
2077
+ ```
2078
+
2079
+ ---
2080
+
2081
+ ## Security Best Practices
2082
+
2083
+ ### Environment Variables
2084
+
2085
+ ```typescript
2086
+ // ❌ Wrong - exposes to client
2087
+ const apiKey = process.env.API_KEY // undefined on client
2088
+
2089
+ // ✅ Correct - server only
2090
+ const apiKey = process.env.API_KEY // Only in Server Components/Actions/Route Handlers
2091
+
2092
+ // ✅ Correct - exposed to client (prefix with NEXT_PUBLIC_)
2093
+ const publicKey = process.env.NEXT_PUBLIC_STRIPE_KEY // Available everywhere
2094
+
2095
+ // lib/env.ts - Validate environment variables
2096
+ import { z } from 'zod'
2097
+
2098
+ const envSchema = z.object({
2099
+ DATABASE_URL: z.string().url(),
2100
+ NEXTAUTH_SECRET: z.string().min(32),
2101
+ STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
2102
+ NEXT_PUBLIC_APP_URL: z.string().url(),
2103
+ })
2104
+
2105
+ export const env = envSchema.parse(process.env)
2106
+ ```
2107
+
2108
+ ### Content Security Policy
2109
+
2110
+ ```typescript
2111
+ // next.config.js
2112
+ const ContentSecurityPolicy = `
2113
+ default-src 'self';
2114
+ script-src 'self' 'unsafe-eval' 'unsafe-inline' https://cdn.example.com;
2115
+ style-src 'self' 'unsafe-inline';
2116
+ img-src 'self' blob: data: https://cdn.example.com;
2117
+ font-src 'self';
2118
+ connect-src 'self' https://api.example.com;
2119
+ frame-ancestors 'none';
2120
+ `
2121
+
2122
+ const securityHeaders = [
2123
+ {
2124
+ key: 'Content-Security-Policy',
2125
+ value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim(),
2126
+ },
2127
+ {
2128
+ key: 'X-Frame-Options',
2129
+ value: 'DENY',
2130
+ },
2131
+ {
2132
+ key: 'X-Content-Type-Options',
2133
+ value: 'nosniff',
2134
+ },
2135
+ {
2136
+ key: 'Referrer-Policy',
2137
+ value: 'strict-origin-when-cross-origin',
2138
+ },
2139
+ {
2140
+ key: 'Permissions-Policy',
2141
+ value: 'camera=(), microphone=(), geolocation=()',
2142
+ },
2143
+ ]
2144
+
2145
+ module.exports = {
2146
+ async headers() {
2147
+ return [
2148
+ {
2149
+ source: '/:path*',
2150
+ headers: securityHeaders,
2151
+ },
2152
+ ]
2153
+ },
2154
+ }
2155
+ ```
2156
+
2157
+ ### Input Validation
2158
+
2159
+ ```typescript
2160
+ // lib/validations/post.ts
2161
+ import { z } from 'zod'
2162
+
2163
+ export const createPostSchema = z.object({
2164
+ title: z
2165
+ .string()
2166
+ .min(1, 'Title is required')
2167
+ .max(100, 'Title must be less than 100 characters')
2168
+ .transform((val) => val.trim()),
2169
+ content: z
2170
+ .string()
2171
+ .min(1, 'Content is required')
2172
+ .max(10000, 'Content must be less than 10000 characters'),
2173
+ tags: z
2174
+ .array(z.string().max(20))
2175
+ .max(5, 'Maximum 5 tags allowed')
2176
+ .optional(),
2177
+ })
2178
+
2179
+ export type CreatePostInput = z.infer<typeof createPostSchema>
2180
+
2181
+ // Server Action
2182
+ 'use server'
2183
+
2184
+ import { createPostSchema } from '@/lib/validations/post'
2185
+ import { auth } from '@/auth'
2186
+ import DOMPurify from 'isomorphic-dompurify'
2187
+
2188
+ export async function createPost(input: unknown) {
2189
+ const session = await auth()
2190
+ if (!session) throw new Error('Unauthorized')
2191
+
2192
+ // Validate input
2193
+ const validated = createPostSchema.parse(input)
2194
+
2195
+ // Sanitize HTML content if allowing rich text
2196
+ const sanitizedContent = DOMPurify.sanitize(validated.content, {
2197
+ ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li'],
2198
+ ALLOWED_ATTR: ['href'],
2199
+ })
2200
+
2201
+ return db.post.create({
2202
+ data: {
2203
+ ...validated,
2204
+ content: sanitizedContent,
2205
+ authorId: session.user.id,
2206
+ },
2207
+ })
2208
+ }
2209
+ ```
2210
+
2211
+ ### CSRF Protection
2212
+
2213
+ ```typescript
2214
+ // Server Actions have built-in CSRF protection via the Origin header check
2215
+
2216
+ // For API Routes, implement manually:
2217
+ // app/api/webhook/route.ts
2218
+ import { headers } from 'next/headers'
2219
+ import crypto from 'crypto'
2220
+
2221
+ export async function POST(request: Request) {
2222
+ const headersList = headers()
2223
+ const signature = headersList.get('x-webhook-signature')
2224
+
2225
+ if (!signature) {
2226
+ return new Response('Missing signature', { status: 401 })
2227
+ }
2228
+
2229
+ const body = await request.text()
2230
+ const expectedSignature = crypto
2231
+ .createHmac('sha256', process.env.WEBHOOK_SECRET!)
2232
+ .update(body)
2233
+ .digest('hex')
2234
+
2235
+ if (!crypto.timingSafeEqual(
2236
+ Buffer.from(signature),
2237
+ Buffer.from(expectedSignature)
2238
+ )) {
2239
+ return new Response('Invalid signature', { status: 401 })
2240
+ }
2241
+
2242
+ // Process webhook...
2243
+ }
2244
+ ```
2245
+
2246
+ ---
2247
+
2248
+ ## Testing Strategies
2249
+
2250
+ ### Unit Testing with Vitest
2251
+
2252
+ ```typescript
2253
+ // __tests__/utils.test.ts
2254
+ import { describe, it, expect } from 'vitest'
2255
+ import { formatDate, slugify } from '@/lib/utils'
2256
+
2257
+ describe('formatDate', () => {
2258
+ it('formats date correctly', () => {
2259
+ const date = new Date('2024-01-15')
2260
+ expect(formatDate(date)).toBe('January 15, 2024')
2261
+ })
2262
+ })
2263
+
2264
+ describe('slugify', () => {
2265
+ it('converts string to slug', () => {
2266
+ expect(slugify('Hello World')).toBe('hello-world')
2267
+ expect(slugify('Test 123!')).toBe('test-123')
2268
+ })
2269
+ })
2270
+ ```
2271
+
2272
+ ### Component Testing with React Testing Library
2273
+
2274
+ ```typescript
2275
+ // __tests__/components/Button.test.tsx
2276
+ import { render, screen, fireEvent } from '@testing-library/react'
2277
+ import { describe, it, expect, vi } from 'vitest'
2278
+ import { Button } from '@/components/ui/button'
2279
+
2280
+ describe('Button', () => {
2281
+ it('renders children correctly', () => {
2282
+ render(<Button>Click me</Button>)
2283
+ expect(screen.getByRole('button')).toHaveTextContent('Click me')
2284
+ })
2285
+
2286
+ it('calls onClick when clicked', () => {
2287
+ const handleClick = vi.fn()
2288
+ render(<Button onClick={handleClick}>Click me</Button>)
2289
+
2290
+ fireEvent.click(screen.getByRole('button'))
2291
+ expect(handleClick).toHaveBeenCalledTimes(1)
2292
+ })
2293
+
2294
+ it('is disabled when disabled prop is true', () => {
2295
+ render(<Button disabled>Click me</Button>)
2296
+ expect(screen.getByRole('button')).toBeDisabled()
2297
+ })
2298
+ })
2299
+ ```
2300
+
2301
+ ### Integration Testing with Playwright
2302
+
2303
+ ```typescript
2304
+ // tests/e2e/auth.spec.ts
2305
+ import { test, expect } from '@playwright/test'
2306
+
2307
+ test.describe('Authentication', () => {
2308
+ test('should redirect to login when accessing protected route', async ({ page }) => {
2309
+ await page.goto('/dashboard')
2310
+ await expect(page).toHaveURL('/login?callbackUrl=%2Fdashboard')
2311
+ })
2312
+
2313
+ test('should login successfully with valid credentials', async ({ page }) => {
2314
+ await page.goto('/login')
2315
+
2316
+ await page.fill('input[name="email"]', 'test@example.com')
2317
+ await page.fill('input[name="password"]', 'password123')
2318
+ await page.click('button[type="submit"]')
2319
+
2320
+ await expect(page).toHaveURL('/dashboard')
2321
+ await expect(page.locator('h1')).toContainText('Welcome')
2322
+ })
2323
+
2324
+ test('should show error with invalid credentials', async ({ page }) => {
2325
+ await page.goto('/login')
2326
+
2327
+ await page.fill('input[name="email"]', 'wrong@example.com')
2328
+ await page.fill('input[name="password"]', 'wrongpassword')
2329
+ await page.click('button[type="submit"]')
2330
+
2331
+ await expect(page.locator('[data-testid="error-message"]')).toBeVisible()
2332
+ })
2333
+ })
2334
+ ```
2335
+
2336
+ ### API Route Testing
2337
+
2338
+ ```typescript
2339
+ // __tests__/api/posts.test.ts
2340
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2341
+ import { GET, POST } from '@/app/api/posts/route'
2342
+ import { db } from '@/lib/db'
2343
+
2344
+ vi.mock('@/lib/db')
2345
+ vi.mock('@/auth', () => ({
2346
+ auth: vi.fn(() => Promise.resolve({ user: { id: '1' } })),
2347
+ }))
2348
+
2349
+ describe('POST /api/posts', () => {
2350
+ beforeEach(() => {
2351
+ vi.clearAllMocks()
2352
+ })
2353
+
2354
+ it('creates a post successfully', async () => {
2355
+ const mockPost = { id: '1', title: 'Test', content: 'Content' }
2356
+ vi.mocked(db.post.create).mockResolvedValue(mockPost)
2357
+
2358
+ const request = new Request('http://localhost/api/posts', {
2359
+ method: 'POST',
2360
+ headers: { 'Content-Type': 'application/json' },
2361
+ body: JSON.stringify({ title: 'Test', content: 'Content' }),
2362
+ })
2363
+
2364
+ const response = await POST(request)
2365
+ const data = await response.json()
2366
+
2367
+ expect(response.status).toBe(201)
2368
+ expect(data).toEqual(mockPost)
2369
+ })
2370
+ })
2371
+ ```
2372
+
2373
+ ---
2374
+
2375
+ ## Deployment
2376
+
2377
+ ### Vercel Configuration
2378
+
2379
+ ```json
2380
+ // vercel.json
2381
+ {
2382
+ "buildCommand": "prisma generate && next build",
2383
+ "framework": "nextjs",
2384
+ "regions": ["iad1"],
2385
+ "env": {
2386
+ "DATABASE_URL": "@database-url",
2387
+ "NEXTAUTH_SECRET": "@nextauth-secret"
2388
+ },
2389
+ "crons": [
2390
+ {
2391
+ "path": "/api/cron/cleanup",
2392
+ "schedule": "0 0 * * *"
2393
+ }
2394
+ ]
2395
+ }
2396
+ ```
2397
+
2398
+ ### Docker Deployment
2399
+
2400
+ ```dockerfile
2401
+ # Dockerfile
2402
+ FROM node:20-alpine AS base
2403
+
2404
+ # Install dependencies only when needed
2405
+ FROM base AS deps
2406
+ RUN apk add --no-cache libc6-compat
2407
+ WORKDIR /app
2408
+
2409
+ COPY package.json package-lock.json* ./
2410
+ RUN npm ci
2411
+
2412
+ # Rebuild the source code only when needed
2413
+ FROM base AS builder
2414
+ WORKDIR /app
2415
+ COPY --from=deps /app/node_modules ./node_modules
2416
+ COPY . .
2417
+
2418
+ ENV NEXT_TELEMETRY_DISABLED 1
2419
+
2420
+ RUN npx prisma generate
2421
+ RUN npm run build
2422
+
2423
+ # Production image
2424
+ FROM base AS runner
2425
+ WORKDIR /app
2426
+
2427
+ ENV NODE_ENV production
2428
+ ENV NEXT_TELEMETRY_DISABLED 1
2429
+
2430
+ RUN addgroup --system --gid 1001 nodejs
2431
+ RUN adduser --system --uid 1001 nextjs
2432
+
2433
+ COPY --from=builder /app/public ./public
2434
+ COPY --from=builder /app/.next/standalone ./
2435
+ COPY --from=builder /app/.next/static ./.next/static
2436
+
2437
+ USER nextjs
2438
+
2439
+ EXPOSE 3000
2440
+
2441
+ ENV PORT 3000
2442
+ ENV HOSTNAME "0.0.0.0"
2443
+
2444
+ CMD ["node", "server.js"]
2445
+ ```
2446
+
2447
+ ```javascript
2448
+ // next.config.js for standalone output
2449
+ module.exports = {
2450
+ output: 'standalone',
2451
+ }
2452
+ ```
2453
+
2454
+ ---
2455
+
2456
+ ## Anti-Patterns to Avoid
2457
+
2458
+ ### 1. Fetching in Client Components Unnecessarily
2459
+
2460
+ ```typescript
2461
+ // ❌ Bad - unnecessary client-side fetch
2462
+ 'use client'
2463
+ export function Posts() {
2464
+ const [posts, setPosts] = useState([])
2465
+
2466
+ useEffect(() => {
2467
+ fetch('/api/posts').then(r => r.json()).then(setPosts)
2468
+ }, [])
2469
+
2470
+ return <PostList posts={posts} />
2471
+ }
2472
+
2473
+ // ✅ Good - fetch in Server Component
2474
+ export default async function Posts() {
2475
+ const posts = await db.post.findMany()
2476
+ return <PostList posts={posts} />
2477
+ }
2478
+ ```
2479
+
2480
+ ### 2. Waterfall Requests
2481
+
2482
+ ```typescript
2483
+ // ❌ Bad - sequential fetches
2484
+ async function Page() {
2485
+ const user = await getUser()
2486
+ const posts = await getPosts() // Waits for user
2487
+ const comments = await getComments() // Waits for posts
2488
+ return <Content user={user} posts={posts} comments={comments} />
2489
+ }
2490
+
2491
+ // ✅ Good - parallel fetches
2492
+ async function Page() {
2493
+ const [user, posts, comments] = await Promise.all([
2494
+ getUser(),
2495
+ getPosts(),
2496
+ getComments(),
2497
+ ])
2498
+ return <Content user={user} posts={posts} comments={comments} />
2499
+ }
2500
+ ```
2501
+
2502
+ ### 3. Passing Functions as Props to Client Components
2503
+
2504
+ ```typescript
2505
+ // ❌ Bad - can't serialize functions
2506
+ async function Page() {
2507
+ async function handleSubmit(data: FormData) {
2508
+ 'use server'
2509
+ await db.post.create({ data })
2510
+ }
2511
+
2512
+ return <Form onSubmit={handleSubmit} /> // Error!
2513
+ }
2514
+
2515
+ // ✅ Good - import Server Action
2516
+ import { createPost } from './actions'
2517
+
2518
+ export default function Page() {
2519
+ return <Form action={createPost} />
2520
+ }
2521
+ ```
2522
+
2523
+ ### 4. Using useEffect for Data Fetching
2524
+
2525
+ ```typescript
2526
+ // ❌ Bad - client-side fetch with useEffect
2527
+ 'use client'
2528
+ function Dashboard() {
2529
+ const [data, setData] = useState(null)
2530
+
2531
+ useEffect(() => {
2532
+ fetchData().then(setData)
2533
+ }, [])
2534
+
2535
+ if (!data) return <Loading />
2536
+ return <DashboardContent data={data} />
2537
+ }
2538
+
2539
+ // ✅ Good - Server Component with Suspense
2540
+ async function Dashboard() {
2541
+ const data = await fetchData()
2542
+ return <DashboardContent data={data} />
2543
+ }
2544
+
2545
+ // Wrap in Suspense at parent level
2546
+ <Suspense fallback={<Loading />}>
2547
+ <Dashboard />
2548
+ </Suspense>
2549
+ ```
2550
+
2551
+ ### 5. Overusing 'use client'
2552
+
2553
+ ```typescript
2554
+ // ❌ Bad - entire page is client
2555
+ 'use client'
2556
+ export default function Page() {
2557
+ const [count, setCount] = useState(0)
2558
+ const data = useData() // Client-side fetch
2559
+
2560
+ return (
2561
+ <div>
2562
+ <Header />
2563
+ <Navigation />
2564
+ <Content data={data} />
2565
+ <Counter count={count} setCount={setCount} />
2566
+ <Footer />
2567
+ </div>
2568
+ )
2569
+ }
2570
+
2571
+ // ✅ Good - only interactive parts are client
2572
+ export default async function Page() {
2573
+ const data = await getData() // Server fetch
2574
+
2575
+ return (
2576
+ <div>
2577
+ <Header />
2578
+ <Navigation />
2579
+ <Content data={data} />
2580
+ <Counter /> {/* Only this is 'use client' */}
2581
+ <Footer />
2582
+ </div>
2583
+ )
2584
+ }
2585
+ ```
2586
+
2587
+ ### 6. Not Handling Loading States
2588
+
2589
+ ```typescript
2590
+ // ❌ Bad - no loading state
2591
+ export default async function Page() {
2592
+ const data = await slowFetch() // User sees blank page
2593
+ return <Content data={data} />
2594
+ }
2595
+
2596
+ // ✅ Good - with loading state
2597
+ // loading.tsx
2598
+ export default function Loading() {
2599
+ return <Skeleton />
2600
+ }
2601
+
2602
+ // page.tsx
2603
+ export default async function Page() {
2604
+ const data = await slowFetch()
2605
+ return <Content data={data} />
2606
+ }
2607
+ ```
2608
+
2609
+ ---
2610
+
2611
+ ## File Structure Conventions
2612
+
2613
+ ### Recommended Structure
2614
+
2615
+ ```
2616
+ src/
2617
+ ├── app/ # App Router
2618
+ │ ├── (auth)/ # Auth route group
2619
+ │ │ ├── login/
2620
+ │ │ └── register/
2621
+ │ ├── (marketing)/ # Marketing route group
2622
+ │ │ ├── about/
2623
+ │ │ └── pricing/
2624
+ │ ├── (dashboard)/ # Dashboard route group
2625
+ │ │ ├── layout.tsx
2626
+ │ │ ├── dashboard/
2627
+ │ │ └── settings/
2628
+ │ ├── api/ # Route Handlers
2629
+ │ ├── layout.tsx
2630
+ │ ├── page.tsx
2631
+ │ ├── loading.tsx
2632
+ │ ├── error.tsx
2633
+ │ ├── not-found.tsx
2634
+ │ └── providers.tsx
2635
+
2636
+ ├── components/
2637
+ │ ├── ui/ # Shadcn/UI components
2638
+ │ │ ├── button.tsx
2639
+ │ │ ├── input.tsx
2640
+ │ │ └── dialog.tsx
2641
+ │ ├── forms/ # Form components
2642
+ │ │ ├── login-form.tsx
2643
+ │ │ └── post-form.tsx
2644
+ │ ├── layouts/ # Layout components
2645
+ │ │ ├── header.tsx
2646
+ │ │ ├── footer.tsx
2647
+ │ │ └── sidebar.tsx
2648
+ │ └── [feature]/ # Feature-specific components
2649
+ │ ├── post-card.tsx
2650
+ │ └── post-list.tsx
2651
+
2652
+ ├── lib/
2653
+ │ ├── db.ts # Database client
2654
+ │ ├── auth.ts # Auth configuration
2655
+ │ ├── utils.ts # Utility functions
2656
+ │ └── validations/ # Zod schemas
2657
+ │ ├── auth.ts
2658
+ │ └── post.ts
2659
+
2660
+ ├── hooks/ # Custom React hooks
2661
+ │ ├── use-media-query.ts
2662
+ │ └── use-debounce.ts
2663
+
2664
+ ├── actions/ # Server Actions
2665
+ │ ├── auth.ts
2666
+ │ ├── posts.ts
2667
+ │ └── users.ts
2668
+
2669
+ ├── types/ # TypeScript types
2670
+ │ ├── index.d.ts
2671
+ │ └── next-auth.d.ts
2672
+
2673
+ ├── styles/
2674
+ │ └── globals.css
2675
+
2676
+ └── config/
2677
+ ├── site.ts # Site configuration
2678
+ └── dashboard.ts # Dashboard navigation
2679
+ ```
2680
+
2681
+ ---
2682
+
2683
+ ## Quick Reference
2684
+
2685
+ ### Common Imports
2686
+
2687
+ ```typescript
2688
+ // Navigation
2689
+ import { redirect, notFound } from 'next/navigation'
2690
+ import { useRouter, usePathname, useSearchParams } from 'next/navigation'
2691
+ import Link from 'next/link'
2692
+
2693
+ // Headers/Cookies
2694
+ import { headers, cookies } from 'next/headers'
2695
+
2696
+ // Caching
2697
+ import { revalidatePath, revalidateTag } from 'next/cache'
2698
+ import { unstable_cache } from 'next/cache'
2699
+ import { cache } from 'react'
2700
+
2701
+ // Images/Fonts
2702
+ import Image from 'next/image'
2703
+ import { Inter } from 'next/font/google'
2704
+
2705
+ // Dynamic
2706
+ import dynamic from 'next/dynamic'
2707
+
2708
+ // Metadata
2709
+ import type { Metadata, ResolvingMetadata } from 'next'
2710
+ ```
2711
+
2712
+ ### Essential Patterns Cheat Sheet
2713
+
2714
+ | Pattern | Use Case |
2715
+ |---------|----------|
2716
+ | `async function Component()` | Server Component data fetching |
2717
+ | `'use client'` | Interactive components |
2718
+ | `'use server'` | Server Actions |
2719
+ | `<Suspense>` | Streaming/loading states |
2720
+ | `loading.tsx` | Route-level loading UI |
2721
+ | `error.tsx` | Route-level error boundary |
2722
+ | `generateStaticParams` | Static generation for dynamic routes |
2723
+ | `generateMetadata` | Dynamic SEO metadata |
2724
+ | `revalidatePath/Tag` | On-demand revalidation |
2725
+ | `unstable_cache` | Cache non-fetch functions |
2726
+ | `cache()` | Request memoization |
2727
+
2728
+ ---
2729
+
2730
+ *This skill provides comprehensive Next.js expertise. Reference specific sections as needed during development.*