Initial commit
This commit is contained in:
282
templates/app-router-async-params.tsx
Normal file
282
templates/app-router-async-params.tsx
Normal file
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* Next.js 16 - Async Route Parameters
|
||||
*
|
||||
* BREAKING CHANGE: params, searchParams, cookies(), headers(), draftMode()
|
||||
* are now async and must be awaited in Next.js 16.
|
||||
*
|
||||
* This template shows the correct patterns for accessing route parameters,
|
||||
* search parameters, cookies, and headers in Next.js 16.
|
||||
*/
|
||||
|
||||
import { cookies, headers, draftMode } from 'next/headers'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
// ============================================================================
|
||||
// Example 1: Page with Async Params
|
||||
// ============================================================================
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ slug: string }>
|
||||
searchParams: Promise<{ q?: string; page?: string }>
|
||||
}
|
||||
|
||||
export default async function BlogPostPage({ params, searchParams }: PageProps) {
|
||||
// ✅ Await params and searchParams in Next.js 16
|
||||
const { slug } = await params
|
||||
const { q, page } = await searchParams
|
||||
|
||||
// Fetch post data
|
||||
const post = await fetch(`https://api.example.com/posts/${slug}`)
|
||||
.then(r => r.json())
|
||||
.catch(() => null)
|
||||
|
||||
if (!post) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
return (
|
||||
<article>
|
||||
<h1>{post.title}</h1>
|
||||
<div dangerouslySetInnerHTML={{ __html: post.content }} />
|
||||
|
||||
{/* Show search query if present */}
|
||||
{q && <p>Search query: {q}</p>}
|
||||
|
||||
{/* Show page number if present */}
|
||||
{page && <p>Page: {page}</p>}
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 2: Layout with Async Params
|
||||
// ============================================================================
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode
|
||||
params: Promise<{ category: string }>
|
||||
}
|
||||
|
||||
export async function ProductLayout({ children, params }: LayoutProps) {
|
||||
// ✅ Await params in layouts too
|
||||
const { category } = await params
|
||||
|
||||
return (
|
||||
<div>
|
||||
<nav>
|
||||
<h2>Category: {category}</h2>
|
||||
</nav>
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 3: Accessing Cookies (Async in Next.js 16)
|
||||
// ============================================================================
|
||||
|
||||
export async function UserGreeting() {
|
||||
// ✅ Await cookies() in Next.js 16
|
||||
const cookieStore = await cookies()
|
||||
const userId = cookieStore.get('userId')?.value
|
||||
const theme = cookieStore.get('theme')?.value || 'light'
|
||||
|
||||
if (!userId) {
|
||||
return <p>Welcome, Guest!</p>
|
||||
}
|
||||
|
||||
const user = await fetch(`https://api.example.com/users/${userId}`)
|
||||
.then(r => r.json())
|
||||
|
||||
return (
|
||||
<div data-theme={theme}>
|
||||
<p>Welcome back, {user.name}!</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 4: Accessing Headers (Async in Next.js 16)
|
||||
// ============================================================================
|
||||
|
||||
export async function RequestInfo() {
|
||||
// ✅ Await headers() in Next.js 16
|
||||
const headersList = await headers()
|
||||
const userAgent = headersList.get('user-agent') || 'Unknown'
|
||||
const referer = headersList.get('referer') || 'Direct'
|
||||
const ip = headersList.get('x-forwarded-for') || 'Unknown'
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>User Agent: {userAgent}</p>
|
||||
<p>Referrer: {referer}</p>
|
||||
<p>IP: {ip}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 5: Draft Mode (Async in Next.js 16)
|
||||
// ============================================================================
|
||||
|
||||
export async function DraftBanner() {
|
||||
// ✅ Await draftMode() in Next.js 16
|
||||
const { isEnabled } = await draftMode()
|
||||
|
||||
if (!isEnabled) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ background: 'yellow', padding: '1rem' }}>
|
||||
<p>🚧 Draft Mode Enabled</p>
|
||||
<a href="/api/disable-draft">Exit Draft Mode</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 6: Generate Metadata with Async Params
|
||||
// ============================================================================
|
||||
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
// ✅ Await params in generateMetadata
|
||||
const { slug } = await params
|
||||
|
||||
const post = await fetch(`https://api.example.com/posts/${slug}`)
|
||||
.then(r => r.json())
|
||||
|
||||
return {
|
||||
title: post.title,
|
||||
description: post.excerpt,
|
||||
openGraph: {
|
||||
title: post.title,
|
||||
description: post.excerpt,
|
||||
images: [post.coverImage],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 7: Generate Static Params (Async)
|
||||
// ============================================================================
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const posts = await fetch('https://api.example.com/posts')
|
||||
.then(r => r.json())
|
||||
|
||||
return posts.map((post: { slug: string }) => ({
|
||||
slug: post.slug,
|
||||
}))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 8: Route Handler with Async Params
|
||||
// ============================================================================
|
||||
|
||||
// File: app/api/posts/[id]/route.ts
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
// ✅ Await params in route handlers
|
||||
const { id } = await params
|
||||
|
||||
const post = await fetch(`https://api.example.com/posts/${id}`)
|
||||
.then(r => r.json())
|
||||
|
||||
return NextResponse.json(post)
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
// ✅ Await params in route handlers
|
||||
const { id } = await params
|
||||
|
||||
await fetch(`https://api.example.com/posts/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
return NextResponse.json({ message: 'Post deleted' })
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Migration Guide: Next.js 15 → Next.js 16
|
||||
// ============================================================================
|
||||
|
||||
// ❌ BEFORE (Next.js 15):
|
||||
/*
|
||||
export default function Page({ params, searchParams }) {
|
||||
const slug = params.slug // ❌ Sync access
|
||||
const query = searchParams.q // ❌ Sync access
|
||||
}
|
||||
|
||||
export function MyComponent() {
|
||||
const cookieStore = cookies() // ❌ Sync access
|
||||
const headersList = headers() // ❌ Sync access
|
||||
}
|
||||
*/
|
||||
|
||||
// ✅ AFTER (Next.js 16):
|
||||
/*
|
||||
export default async function Page({ params, searchParams }) {
|
||||
const { slug } = await params // ✅ Async access
|
||||
const { q: query } = await searchParams // ✅ Async access
|
||||
}
|
||||
|
||||
export async function MyComponent() {
|
||||
const cookieStore = await cookies() // ✅ Async access
|
||||
const headersList = await headers() // ✅ Async access
|
||||
}
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// TypeScript Types
|
||||
// ============================================================================
|
||||
|
||||
// Correct types for Next.js 16:
|
||||
type Params<T = Record<string, string>> = Promise<T>
|
||||
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>
|
||||
|
||||
// Usage:
|
||||
type ProductPageProps = {
|
||||
params: Params<{ id: string }>
|
||||
searchParams: SearchParams
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Codemod (Automatic Migration)
|
||||
// ============================================================================
|
||||
|
||||
// Run this command to automatically migrate your code:
|
||||
// npx @next/codemod@canary upgrade latest
|
||||
|
||||
/**
|
||||
* Summary:
|
||||
*
|
||||
* 1. ALL route parameters are now async:
|
||||
* - params → await params
|
||||
* - searchParams → await searchParams
|
||||
*
|
||||
* 2. ALL next/headers functions are now async:
|
||||
* - cookies() → await cookies()
|
||||
* - headers() → await headers()
|
||||
* - draftMode() → await draftMode()
|
||||
*
|
||||
* 3. Components using these must be async:
|
||||
* - export default async function Page({ params }) { ... }
|
||||
* - export async function Layout({ params }) { ... }
|
||||
* - export async function generateMetadata({ params }) { ... }
|
||||
*
|
||||
* 4. Route handlers must await params:
|
||||
* - export async function GET(request, { params }) {
|
||||
* const { id } = await params
|
||||
* }
|
||||
*/
|
||||
343
templates/cache-component-use-cache.tsx
Normal file
343
templates/cache-component-use-cache.tsx
Normal file
@@ -0,0 +1,343 @@
|
||||
/**
|
||||
* Next.js 16 - Cache Components with "use cache" Directive
|
||||
*
|
||||
* NEW in Next.js 16: Explicit opt-in caching with "use cache" directive.
|
||||
* Replaces implicit caching from Next.js 15.
|
||||
*
|
||||
* This template shows component-level, function-level, and page-level caching.
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Example 1: Component-Level Caching
|
||||
// ============================================================================
|
||||
|
||||
'use cache'
|
||||
|
||||
// This entire component will be cached
|
||||
export async function CachedProductList() {
|
||||
const products = await fetch('https://api.example.com/products')
|
||||
.then(r => r.json())
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Products</h2>
|
||||
<ul>
|
||||
{products.map((product: { id: string; name: string; price: number }) => (
|
||||
<li key={product.id}>
|
||||
{product.name} - ${product.price}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 2: Function-Level Caching
|
||||
// ============================================================================
|
||||
|
||||
// File: lib/data.ts
|
||||
'use cache'
|
||||
|
||||
export async function getExpensiveData(id: string) {
|
||||
console.log('Fetching expensive data...') // Only logs on cache miss
|
||||
|
||||
// Simulate expensive operation
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
const data = await fetch(`https://api.example.com/items/${id}`)
|
||||
.then(r => r.json())
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// Usage in component (not cached itself):
|
||||
import { getExpensiveData } from '@/lib/data'
|
||||
|
||||
export async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
const product = await getExpensiveData(id) // Cached by function
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>{product.name}</h1>
|
||||
<p>{product.description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 3: Page-Level Caching
|
||||
// ============================================================================
|
||||
|
||||
// File: app/blog/[slug]/page.tsx
|
||||
'use cache'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const posts = await fetch('https://api.example.com/posts')
|
||||
.then(r => r.json())
|
||||
|
||||
return posts.map((post: { slug: string }) => ({
|
||||
slug: post.slug,
|
||||
}))
|
||||
}
|
||||
|
||||
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params
|
||||
|
||||
const post = await fetch(`https://api.example.com/posts/${slug}`)
|
||||
.then(r => r.json())
|
||||
|
||||
return (
|
||||
<article>
|
||||
<h1>{post.title}</h1>
|
||||
<div dangerouslySetInnerHTML={{ __html: post.content }} />
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 4: Partial Prerendering (PPR) - Mix Static & Dynamic
|
||||
// ============================================================================
|
||||
|
||||
// File: app/dashboard/page.tsx
|
||||
|
||||
// Static component (cached)
|
||||
'use cache'
|
||||
async function StaticHeader() {
|
||||
return (
|
||||
<header>
|
||||
<h1>My Dashboard</h1>
|
||||
<nav>
|
||||
<a href="/dashboard">Overview</a>
|
||||
<a href="/dashboard/settings">Settings</a>
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
// Dynamic component (NOT cached) - separate file without "use cache"
|
||||
// File: components/dynamic-user-info.tsx
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
export async function DynamicUserInfo() {
|
||||
const cookieStore = await cookies()
|
||||
const userId = cookieStore.get('userId')?.value
|
||||
|
||||
if (!userId) {
|
||||
return <div>Please log in</div>
|
||||
}
|
||||
|
||||
const user = await fetch(`https://api.example.com/users/${userId}`)
|
||||
.then(r => r.json())
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Welcome, {user.name}</p>
|
||||
<p>Balance: ${user.balance}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Page combines static + dynamic (Partial Prerendering)
|
||||
import { DynamicUserInfo } from '@/components/dynamic-user-info'
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<div>
|
||||
<StaticHeader /> {/* Cached (static) */}
|
||||
<DynamicUserInfo /> {/* Not cached (dynamic) */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 5: Selective Caching with Multiple Functions
|
||||
// ============================================================================
|
||||
|
||||
// Cache expensive operations, skip cheap ones
|
||||
|
||||
// Cached function
|
||||
'use cache'
|
||||
export async function getPopularPosts() {
|
||||
const posts = await fetch('https://api.example.com/posts/popular')
|
||||
.then(r => r.json())
|
||||
|
||||
return posts
|
||||
}
|
||||
|
||||
// NOT cached (changes frequently)
|
||||
export async function getRealtimeMetrics() {
|
||||
const metrics = await fetch('https://api.example.com/metrics/realtime')
|
||||
.then(r => r.json())
|
||||
|
||||
return metrics
|
||||
}
|
||||
|
||||
// Component uses both
|
||||
export async function Dashboard() {
|
||||
const popularPosts = await getPopularPosts() // Cached
|
||||
const metrics = await getRealtimeMetrics() // NOT cached
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<h2>Popular Posts</h2>
|
||||
<ul>
|
||||
{popularPosts.map((post: { id: string; title: string }) => (
|
||||
<li key={post.id}>{post.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Realtime Metrics</h2>
|
||||
<p>Active users: {metrics.activeUsers}</p>
|
||||
<p>Requests/min: {metrics.requestsPerMinute}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 6: Cache with Revalidation (using tags)
|
||||
// ============================================================================
|
||||
|
||||
// File: app/actions.ts
|
||||
'use server'
|
||||
|
||||
import { revalidateTag } from 'next/cache'
|
||||
|
||||
export async function createPost(formData: FormData) {
|
||||
const title = formData.get('title') as string
|
||||
const content = formData.get('content') as string
|
||||
|
||||
await fetch('https://api.example.com/posts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ title, content }),
|
||||
})
|
||||
|
||||
// Revalidate cached posts
|
||||
revalidateTag('posts', 'max')
|
||||
}
|
||||
|
||||
// File: lib/posts.ts
|
||||
'use cache'
|
||||
|
||||
export async function getPosts() {
|
||||
const response = await fetch('https://api.example.com/posts', {
|
||||
next: { tags: ['posts'] }, // Tag for revalidation
|
||||
})
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 7: Conditional Caching (Cache Based on User Role)
|
||||
// ============================================================================
|
||||
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
export async function getContent() {
|
||||
const cookieStore = await cookies()
|
||||
const userRole = cookieStore.get('role')?.value
|
||||
|
||||
if (userRole === 'admin') {
|
||||
// Don't cache admin content (changes frequently)
|
||||
return fetch('https://api.example.com/admin/content').then(r => r.json())
|
||||
}
|
||||
|
||||
// Cache public content
|
||||
return getCachedPublicContent()
|
||||
}
|
||||
|
||||
'use cache'
|
||||
async function getCachedPublicContent() {
|
||||
return fetch('https://api.example.com/public/content').then(r => r.json())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 8: Inline "use cache" (Granular Control)
|
||||
// ============================================================================
|
||||
|
||||
export async function MixedCachingComponent() {
|
||||
// This function call is cached
|
||||
const cachedData = await (async function() {
|
||||
'use cache'
|
||||
return fetch('https://api.example.com/slow-data').then(r => r.json())
|
||||
})()
|
||||
|
||||
// This function call is NOT cached
|
||||
const freshData = await fetch('https://api.example.com/fresh-data').then(r => r.json())
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>Cached: {cachedData.value}</div>
|
||||
<div>Fresh: {freshData.value}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Migration Guide: Next.js 15 → Next.js 16
|
||||
// ============================================================================
|
||||
|
||||
// ❌ BEFORE (Next.js 15 - Implicit Caching):
|
||||
/*
|
||||
// All Server Components were cached by default
|
||||
export async function MyComponent() {
|
||||
const data = await fetch('https://api.example.com/data')
|
||||
return <div>{data.value}</div>
|
||||
}
|
||||
|
||||
// To opt-out of caching:
|
||||
export const revalidate = 0 // or export const dynamic = 'force-dynamic'
|
||||
*/
|
||||
|
||||
// ✅ AFTER (Next.js 16 - Explicit Opt-In Caching):
|
||||
/*
|
||||
// Components are NOT cached by default
|
||||
export async function MyComponent() {
|
||||
const data = await fetch('https://api.example.com/data')
|
||||
return <div>{data.value}</div>
|
||||
}
|
||||
|
||||
// To opt-IN to caching, add "use cache"
|
||||
'use cache'
|
||||
export async function MyCachedComponent() {
|
||||
const data = await fetch('https://api.example.com/data')
|
||||
return <div>{data.value}</div>
|
||||
}
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Cache Behavior Summary
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* "use cache" can be added to:
|
||||
* 1. ✅ Components (entire component cached)
|
||||
* 2. ✅ Functions (function output cached)
|
||||
* 3. ✅ Pages (entire page cached)
|
||||
* 4. ✅ Layouts (layout cached)
|
||||
* 5. ✅ Inline async functions (granular caching)
|
||||
*
|
||||
* Default behavior (without "use cache"):
|
||||
* - Server Components: NOT cached (change from Next.js 15)
|
||||
* - fetch() calls: Cached by default (unchanged)
|
||||
*
|
||||
* Revalidation:
|
||||
* - Use revalidateTag() to invalidate cache by tag
|
||||
* - Use updateTag() for immediate read-your-writes
|
||||
* - Use refresh() for uncached data only
|
||||
*
|
||||
* When to use "use cache":
|
||||
* ✅ Expensive computations (database queries, API calls)
|
||||
* ✅ Stable data (product catalogs, blog posts)
|
||||
* ✅ Partial Prerendering (static header + dynamic user info)
|
||||
*
|
||||
* When NOT to use "use cache":
|
||||
* ❌ Real-time data (metrics, notifications)
|
||||
* ❌ User-specific data (unless using cookies/headers for cache keys)
|
||||
* ❌ Frequently changing data (stock prices, live scores)
|
||||
*/
|
||||
32
templates/package.json
Normal file
32
templates/package.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "nextjs-16-app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^16.0.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"typescript": "^5.7.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"zod": "^3.24.0",
|
||||
"@tailwindcss/vite": "^4.1.0",
|
||||
"tailwindcss": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.9.0",
|
||||
"npm": ">=10.0.0"
|
||||
}
|
||||
}
|
||||
437
templates/parallel-routes-with-default.tsx
Normal file
437
templates/parallel-routes-with-default.tsx
Normal file
@@ -0,0 +1,437 @@
|
||||
/**
|
||||
* Next.js 16 - Parallel Routes with Required default.js
|
||||
*
|
||||
* BREAKING CHANGE: Parallel routes now REQUIRE explicit default.js files.
|
||||
* Without them, routes will fail during soft navigation.
|
||||
*
|
||||
* Directory structure:
|
||||
* app/
|
||||
* ├── @modal/
|
||||
* │ ├── login/
|
||||
* │ │ └── page.tsx
|
||||
* │ └── default.tsx ← REQUIRED in Next.js 16
|
||||
* ├── @feed/
|
||||
* │ ├── trending/
|
||||
* │ │ └── page.tsx
|
||||
* │ └── default.tsx ← REQUIRED in Next.js 16
|
||||
* └── layout.tsx
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Example 1: Modal + Main Content (Common Pattern)
|
||||
// ============================================================================
|
||||
|
||||
// File: app/layout.tsx
|
||||
export default function RootLayout({
|
||||
children,
|
||||
modal,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
modal: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
{modal}
|
||||
<main>{children}</main>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/@modal/login/page.tsx
|
||||
export default function LoginModal() {
|
||||
return (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal">
|
||||
<h2>Login</h2>
|
||||
<form>
|
||||
<input type="email" placeholder="Email" />
|
||||
<input type="password" placeholder="Password" />
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/@modal/default.tsx (REQUIRED)
|
||||
export default function ModalDefault() {
|
||||
return null // No modal shown by default
|
||||
}
|
||||
|
||||
// File: app/page.tsx
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Home Page</h1>
|
||||
<a href="/login">Open Login Modal</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 2: Dashboard with Multiple Panels
|
||||
// ============================================================================
|
||||
|
||||
// File: app/dashboard/layout.tsx
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
analytics,
|
||||
notifications,
|
||||
activity,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
analytics: React.ReactNode
|
||||
notifications: React.ReactNode
|
||||
activity: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="dashboard-layout">
|
||||
<aside className="sidebar">
|
||||
{notifications}
|
||||
</aside>
|
||||
|
||||
<main className="main-content">
|
||||
{children}
|
||||
{analytics}
|
||||
</main>
|
||||
|
||||
<aside className="activity-sidebar">
|
||||
{activity}
|
||||
</aside>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/dashboard/@analytics/overview/page.tsx
|
||||
export default async function AnalyticsOverview() {
|
||||
const stats = await fetch('https://api.example.com/stats').then(r => r.json())
|
||||
|
||||
return (
|
||||
<div className="analytics-panel">
|
||||
<h2>Analytics</h2>
|
||||
<div>
|
||||
<p>Page Views: {stats.pageViews}</p>
|
||||
<p>Unique Visitors: {stats.uniqueVisitors}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/dashboard/@analytics/default.tsx (REQUIRED)
|
||||
export default function AnalyticsDefault() {
|
||||
return (
|
||||
<div className="analytics-panel">
|
||||
<h2>Analytics</h2>
|
||||
<p>No analytics data available</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/dashboard/@notifications/default.tsx (REQUIRED)
|
||||
export default function NotificationsDefault() {
|
||||
return (
|
||||
<div className="notifications-panel">
|
||||
<h3>Notifications</h3>
|
||||
<p>No new notifications</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/dashboard/@activity/default.tsx (REQUIRED)
|
||||
export default function ActivityDefault() {
|
||||
return (
|
||||
<div className="activity-panel">
|
||||
<h3>Recent Activity</h3>
|
||||
<p>No recent activity</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 3: E-commerce with Product + Reviews
|
||||
// ============================================================================
|
||||
|
||||
// File: app/products/[id]/layout.tsx
|
||||
export default function ProductLayout({
|
||||
children,
|
||||
reviews,
|
||||
recommendations,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
reviews: React.ReactNode
|
||||
recommendations: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="product-layout">
|
||||
<div className="product-main">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<div className="product-sidebar">
|
||||
{reviews}
|
||||
{recommendations}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/products/[id]/@reviews/page.tsx
|
||||
export default async function ProductReviews({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
const reviews = await fetch(`https://api.example.com/products/${id}/reviews`)
|
||||
.then(r => r.json())
|
||||
|
||||
return (
|
||||
<div className="reviews">
|
||||
<h3>Reviews</h3>
|
||||
<ul>
|
||||
{reviews.map((review: { id: string; rating: number; comment: string }) => (
|
||||
<li key={review.id}>
|
||||
<p>⭐ {review.rating}/5</p>
|
||||
<p>{review.comment}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/products/[id]/@reviews/default.tsx (REQUIRED)
|
||||
export default function ReviewsDefault() {
|
||||
return (
|
||||
<div className="reviews">
|
||||
<h3>Reviews</h3>
|
||||
<p>No reviews yet</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/products/[id]/@recommendations/default.tsx (REQUIRED)
|
||||
export default function RecommendationsDefault() {
|
||||
return (
|
||||
<div className="recommendations">
|
||||
<h3>Recommendations</h3>
|
||||
<p>Loading recommendations...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 4: Auth-Gated Content
|
||||
// ============================================================================
|
||||
|
||||
import { cookies } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
// File: app/@auth/default.tsx (REQUIRED)
|
||||
export default async function AuthDefault() {
|
||||
const cookieStore = await cookies()
|
||||
const isAuthenticated = cookieStore.get('auth')?.value
|
||||
|
||||
if (!isAuthenticated) {
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// File: app/@auth/profile/page.tsx
|
||||
export default async function ProfilePage() {
|
||||
const cookieStore = await cookies()
|
||||
const userId = cookieStore.get('userId')?.value
|
||||
|
||||
const user = await fetch(`https://api.example.com/users/${userId}`)
|
||||
.then(r => r.json())
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Profile</h2>
|
||||
<p>Name: {user.name}</p>
|
||||
<p>Email: {user.email}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 5: Conditional Rendering Based on Slot
|
||||
// ============================================================================
|
||||
|
||||
// File: app/layout.tsx
|
||||
export default function Layout({
|
||||
children,
|
||||
banner,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
banner: React.ReactNode
|
||||
}) {
|
||||
// Only show banner on specific pages
|
||||
const showBanner = true // Determine based on route
|
||||
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
{showBanner && banner}
|
||||
<main>{children}</main>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/@banner/sale/page.tsx
|
||||
export default function SaleBanner() {
|
||||
return (
|
||||
<div className="banner sale-banner">
|
||||
🎉 50% OFF SALE! Use code SALE50
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/@banner/default.tsx (REQUIRED)
|
||||
export default function BannerDefault() {
|
||||
return null // No banner by default
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 6: Loading States with Parallel Routes
|
||||
// ============================================================================
|
||||
|
||||
// File: app/dashboard/@analytics/loading.tsx
|
||||
export default function AnalyticsLoading() {
|
||||
return (
|
||||
<div className="analytics-panel">
|
||||
<h2>Analytics</h2>
|
||||
<p>Loading analytics...</p>
|
||||
<div className="skeleton-loader" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// File: app/dashboard/@notifications/loading.tsx
|
||||
export default function NotificationsLoading() {
|
||||
return (
|
||||
<div className="notifications-panel">
|
||||
<h3>Notifications</h3>
|
||||
<div className="skeleton-loader" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 7: Error Boundaries with Parallel Routes
|
||||
// ============================================================================
|
||||
|
||||
// File: app/dashboard/@analytics/error.tsx
|
||||
'use client'
|
||||
|
||||
export default function AnalyticsError({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error
|
||||
reset: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="analytics-panel error">
|
||||
<h2>Analytics</h2>
|
||||
<p>Failed to load analytics</p>
|
||||
<button onClick={reset}>Try Again</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Migration Guide: Next.js 15 → Next.js 16
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* BREAKING CHANGE: default.js is now REQUIRED for all parallel routes
|
||||
*
|
||||
* ❌ BEFORE (Next.js 15):
|
||||
* app/
|
||||
* ├── @modal/
|
||||
* │ └── login/
|
||||
* │ └── page.tsx
|
||||
* └── layout.tsx
|
||||
*
|
||||
* This worked in Next.js 15. If no matching route, Next.js rendered nothing.
|
||||
*
|
||||
* ✅ AFTER (Next.js 16):
|
||||
* app/
|
||||
* ├── @modal/
|
||||
* │ ├── login/
|
||||
* │ │ └── page.tsx
|
||||
* │ └── default.tsx ← REQUIRED! Will error without this
|
||||
* └── layout.tsx
|
||||
*
|
||||
* Why the change?
|
||||
* Next.js 16 changed how parallel routes handle soft navigation. Without
|
||||
* default.js, unmatched slots will error during client-side navigation.
|
||||
*
|
||||
* What should default.tsx return?
|
||||
* - return null (most common - no UI shown)
|
||||
* - return <Skeleton /> (loading placeholder)
|
||||
* - redirect() to another route
|
||||
* - return fallback UI
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Common Patterns for default.tsx
|
||||
// ============================================================================
|
||||
|
||||
// Pattern 1: Null (no UI)
|
||||
export function DefaultNull() {
|
||||
return null
|
||||
}
|
||||
|
||||
// Pattern 2: Loading skeleton
|
||||
export function DefaultSkeleton() {
|
||||
return (
|
||||
<div className="skeleton">
|
||||
<div className="skeleton-line" />
|
||||
<div className="skeleton-line" />
|
||||
<div className="skeleton-line" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Pattern 3: Fallback message
|
||||
export function DefaultFallback() {
|
||||
return (
|
||||
<div>
|
||||
<p>Content not available</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Pattern 4: Redirect
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export function DefaultRedirect() {
|
||||
redirect('/dashboard')
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary:
|
||||
*
|
||||
* Parallel Routes in Next.js 16:
|
||||
* 1. ✅ Use @folder convention for parallel slots
|
||||
* 2. ✅ MUST include default.tsx for each @folder
|
||||
* 3. ✅ default.tsx handles unmatched routes during navigation
|
||||
* 4. ✅ Can have loading.tsx for loading states
|
||||
* 5. ✅ Can have error.tsx for error boundaries
|
||||
*
|
||||
* Common use cases:
|
||||
* - Modals + main content
|
||||
* - Dashboard panels
|
||||
* - Product + reviews/recommendations
|
||||
* - Conditional banners
|
||||
* - Auth-gated content
|
||||
*
|
||||
* Best practices:
|
||||
* - Keep default.tsx simple (usually return null)
|
||||
* - Use loading.tsx for better UX
|
||||
* - Use error.tsx for error handling
|
||||
* - Test soft navigation (client-side routing)
|
||||
*/
|
||||
274
templates/proxy-migration.ts
Normal file
274
templates/proxy-migration.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* Next.js 16 - Proxy Migration (middleware.ts → proxy.ts)
|
||||
*
|
||||
* BREAKING CHANGE: middleware.ts is deprecated in Next.js 16.
|
||||
* Use proxy.ts instead.
|
||||
*
|
||||
* Migration: Rename file and function, keep same logic.
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { NextRequest } from 'next/server'
|
||||
|
||||
// ============================================================================
|
||||
// Example 1: Basic Proxy (Auth Check)
|
||||
// ============================================================================
|
||||
|
||||
export function proxy(request: NextRequest) {
|
||||
const token = request.cookies.get('token')
|
||||
|
||||
// Redirect to login if no token
|
||||
if (!token) {
|
||||
return NextResponse.redirect(new URL('/login', request.url))
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: '/dashboard/:path*',
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 2: Advanced Proxy (Multiple Checks)
|
||||
// ============================================================================
|
||||
|
||||
export function advancedProxy(request: NextRequest) {
|
||||
const { pathname } = request.nextUrl
|
||||
|
||||
// 1. Auth check
|
||||
const token = request.cookies.get('token')
|
||||
if (pathname.startsWith('/dashboard') && !token) {
|
||||
return NextResponse.redirect(new URL('/login', request.url))
|
||||
}
|
||||
|
||||
// 2. Role-based access
|
||||
const userRole = request.cookies.get('role')?.value
|
||||
if (pathname.startsWith('/admin') && userRole !== 'admin') {
|
||||
return NextResponse.redirect(new URL('/unauthorized', request.url))
|
||||
}
|
||||
|
||||
// 3. Add custom headers
|
||||
const response = NextResponse.next()
|
||||
response.headers.set('x-custom-header', 'value')
|
||||
response.headers.set('x-pathname', pathname)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
export const advancedConfig = {
|
||||
matcher: ['/dashboard/:path*', '/admin/:path*'],
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 3: Request Rewriting
|
||||
// ============================================================================
|
||||
|
||||
export function rewriteProxy(request: NextRequest) {
|
||||
// Rewrite /blog/* to /posts/*
|
||||
if (request.nextUrl.pathname.startsWith('/blog')) {
|
||||
const url = request.nextUrl.clone()
|
||||
url.pathname = url.pathname.replace('/blog', '/posts')
|
||||
return NextResponse.rewrite(url)
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
export const rewriteConfig = {
|
||||
matcher: '/blog/:path*',
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 4: Geolocation-Based Routing
|
||||
// ============================================================================
|
||||
|
||||
export function geoProxy(request: NextRequest) {
|
||||
const country = request.geo?.country || 'US'
|
||||
const url = request.nextUrl.clone()
|
||||
|
||||
// Redirect to country-specific page
|
||||
if (url.pathname === '/') {
|
||||
url.pathname = `/${country.toLowerCase()}`
|
||||
return NextResponse.rewrite(url)
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 5: A/B Testing
|
||||
// ============================================================================
|
||||
|
||||
export function abTestProxy(request: NextRequest) {
|
||||
const bucket = request.cookies.get('bucket')?.value
|
||||
|
||||
if (!bucket) {
|
||||
// Assign to A or B randomly
|
||||
const newBucket = Math.random() < 0.5 ? 'a' : 'b'
|
||||
const response = NextResponse.next()
|
||||
response.cookies.set('bucket', newBucket, {
|
||||
maxAge: 60 * 60 * 24 * 30, // 30 days
|
||||
})
|
||||
|
||||
// Rewrite to variant page
|
||||
if (newBucket === 'b') {
|
||||
const url = request.nextUrl.clone()
|
||||
url.pathname = `/variant-b${url.pathname}`
|
||||
return NextResponse.rewrite(url)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// Existing user
|
||||
if (bucket === 'b') {
|
||||
const url = request.nextUrl.clone()
|
||||
url.pathname = `/variant-b${url.pathname}`
|
||||
return NextResponse.rewrite(url)
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
export const abTestConfig = {
|
||||
matcher: '/',
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 6: Rate Limiting
|
||||
// ============================================================================
|
||||
|
||||
const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
|
||||
|
||||
export function rateLimitProxy(request: NextRequest) {
|
||||
const ip = request.headers.get('x-forwarded-for') || 'unknown'
|
||||
const now = Date.now()
|
||||
|
||||
// Check rate limit (100 requests per minute)
|
||||
const rateLimit = rateLimitMap.get(ip)
|
||||
|
||||
if (rateLimit) {
|
||||
if (now < rateLimit.resetAt) {
|
||||
if (rateLimit.count >= 100) {
|
||||
return new NextResponse('Too Many Requests', {
|
||||
status: 429,
|
||||
headers: {
|
||||
'Retry-After': String(Math.ceil((rateLimit.resetAt - now) / 1000)),
|
||||
},
|
||||
})
|
||||
}
|
||||
rateLimit.count++
|
||||
} else {
|
||||
rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 }) // 1 minute
|
||||
}
|
||||
} else {
|
||||
rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 })
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
}
|
||||
|
||||
export const rateLimitConfig = {
|
||||
matcher: '/api/:path*',
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 7: Response Modification
|
||||
// ============================================================================
|
||||
|
||||
export function modifyResponseProxy(request: NextRequest) {
|
||||
const response = NextResponse.next()
|
||||
|
||||
// Add security headers
|
||||
response.headers.set('X-Frame-Options', 'DENY')
|
||||
response.headers.set('X-Content-Type-Options', 'nosniff')
|
||||
response.headers.set('Referrer-Policy', 'origin-when-cross-origin')
|
||||
response.headers.set(
|
||||
'Permissions-Policy',
|
||||
'camera=(), microphone=(), geolocation=()'
|
||||
)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Migration Guide: middleware.ts → proxy.ts
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* ❌ BEFORE (Next.js 15):
|
||||
*
|
||||
* // File: middleware.ts
|
||||
* import { NextResponse } from 'next/server'
|
||||
* import type { NextRequest } from 'next/server'
|
||||
*
|
||||
* export function middleware(request: NextRequest) {
|
||||
* const token = request.cookies.get('token')
|
||||
* if (!token) {
|
||||
* return NextResponse.redirect(new URL('/login', request.url))
|
||||
* }
|
||||
* return NextResponse.next()
|
||||
* }
|
||||
*
|
||||
* export const config = {
|
||||
* matcher: '/dashboard/:path*',
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* ✅ AFTER (Next.js 16):
|
||||
*
|
||||
* // File: proxy.ts
|
||||
* import { NextResponse } from 'next/server'
|
||||
* import type { NextRequest } from 'next/server'
|
||||
*
|
||||
* export function proxy(request: NextRequest) {
|
||||
* const token = request.cookies.get('token')
|
||||
* if (!token) {
|
||||
* return NextResponse.redirect(new URL('/login', request.url))
|
||||
* }
|
||||
* return NextResponse.next()
|
||||
* }
|
||||
*
|
||||
* export const config = {
|
||||
* matcher: '/dashboard/:path*',
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* Migration Steps:
|
||||
* 1. Rename file: middleware.ts → proxy.ts
|
||||
* 2. Rename function: middleware → proxy
|
||||
* 3. Keep config object the same
|
||||
* 4. Logic remains identical
|
||||
*
|
||||
* Why the change?
|
||||
* - proxy.ts runs on Node.js runtime (full Node.js APIs)
|
||||
* - middleware.ts ran on Edge runtime (limited APIs)
|
||||
* - proxy.ts makes the network boundary explicit
|
||||
*
|
||||
* Note: middleware.ts still works in Next.js 16 but is deprecated.
|
||||
* Migrate to proxy.ts for future compatibility.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Summary:
|
||||
*
|
||||
* Proxy patterns:
|
||||
* 1. ✅ Auth checks and redirects
|
||||
* 2. ✅ Role-based access control
|
||||
* 3. ✅ Custom headers
|
||||
* 4. ✅ Request rewriting (URL rewrites)
|
||||
* 5. ✅ Geolocation-based routing
|
||||
* 6. ✅ A/B testing
|
||||
* 7. ✅ Rate limiting
|
||||
* 8. ✅ Response modification (security headers)
|
||||
*
|
||||
* Best practices:
|
||||
* - Keep proxy logic lightweight (runs on every request)
|
||||
* - Use matcher to limit scope
|
||||
* - Avoid database queries (use cookies/headers instead)
|
||||
* - Cache rate limit data in memory (or Redis for production)
|
||||
* - Return NextResponse.next() if no action needed
|
||||
*/
|
||||
393
templates/route-handler-api.ts
Normal file
393
templates/route-handler-api.ts
Normal file
@@ -0,0 +1,393 @@
|
||||
/**
|
||||
* Next.js 16 - Route Handlers (API Endpoints)
|
||||
*
|
||||
* Route Handlers replace API Routes from Pages Router.
|
||||
* File: app/api/[...]/route.ts
|
||||
*/
|
||||
|
||||
import { NextResponse } from 'next/server'
|
||||
import { cookies, headers } from 'next/headers'
|
||||
|
||||
// ============================================================================
|
||||
// Example 1: Basic CRUD API
|
||||
// ============================================================================
|
||||
|
||||
// GET /api/posts
|
||||
export async function GET() {
|
||||
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
|
||||
|
||||
return NextResponse.json(posts)
|
||||
}
|
||||
|
||||
// POST /api/posts
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json()
|
||||
|
||||
const post = await fetch('https://api.example.com/posts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
}).then(r => r.json())
|
||||
|
||||
return NextResponse.json(post, { status: 201 })
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 2: Dynamic Routes
|
||||
// ============================================================================
|
||||
|
||||
// File: app/api/posts/[id]/route.ts
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params // ✅ Await params in Next.js 16
|
||||
|
||||
const post = await fetch(`https://api.example.com/posts/${id}`)
|
||||
.then(r => r.json())
|
||||
.catch(() => null)
|
||||
|
||||
if (!post) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Post not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
return NextResponse.json(post)
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
|
||||
const updated = await fetch(`https://api.example.com/posts/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
}).then(r => r.json())
|
||||
|
||||
return NextResponse.json(updated)
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params
|
||||
|
||||
await fetch(`https://api.example.com/posts/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
return NextResponse.json({ message: 'Post deleted' }, { status: 200 })
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 3: Search with Query Parameters
|
||||
// ============================================================================
|
||||
|
||||
// GET /api/search?q=nextjs&limit=10&page=1
|
||||
export async function SEARCH(request: Request) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const query = searchParams.get('q') || ''
|
||||
const limit = parseInt(searchParams.get('limit') || '10')
|
||||
const page = parseInt(searchParams.get('page') || '1')
|
||||
const offset = (page - 1) * limit
|
||||
|
||||
const results = await fetch(
|
||||
`https://api.example.com/search?q=${query}&limit=${limit}&offset=${offset}`
|
||||
).then(r => r.json())
|
||||
|
||||
return NextResponse.json({
|
||||
results: results.items,
|
||||
total: results.total,
|
||||
page,
|
||||
limit,
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 4: Authentication with Cookies
|
||||
// ============================================================================
|
||||
|
||||
// POST /api/auth/login
|
||||
export async function LOGIN(request: Request) {
|
||||
const { email, password } = await request.json()
|
||||
|
||||
// Verify credentials
|
||||
const user = await fetch('https://api.example.com/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
}).then(r => r.json())
|
||||
|
||||
if (!user.token) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid credentials' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
// Set cookie
|
||||
const response = NextResponse.json({ success: true })
|
||||
response.cookies.set('token', user.token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// GET /api/auth/me
|
||||
export async function ME() {
|
||||
const cookieStore = await cookies() // ✅ Await cookies in Next.js 16
|
||||
const token = cookieStore.get('token')?.value
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Unauthorized' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
const user = await fetch('https://api.example.com/auth/me', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}).then(r => r.json())
|
||||
|
||||
return NextResponse.json(user)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 5: Webhook Handler
|
||||
// ============================================================================
|
||||
|
||||
// File: app/api/webhooks/stripe/route.ts
|
||||
|
||||
import { headers as getHeaders } from 'next/headers'
|
||||
|
||||
export async function WEBHOOK(request: Request) {
|
||||
const body = await request.text()
|
||||
const headersList = await getHeaders() // ✅ Await headers in Next.js 16
|
||||
const signature = headersList.get('stripe-signature')
|
||||
|
||||
if (!signature) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing signature' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Verify webhook signature (example with Stripe)
|
||||
let event
|
||||
try {
|
||||
event = JSON.parse(body)
|
||||
// In production: stripe.webhooks.constructEvent(body, signature, secret)
|
||||
} catch (err) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid payload' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// Handle event
|
||||
switch (event.type) {
|
||||
case 'payment_intent.succeeded':
|
||||
await handlePaymentSuccess(event.data.object)
|
||||
break
|
||||
case 'payment_intent.failed':
|
||||
await handlePaymentFailure(event.data.object)
|
||||
break
|
||||
default:
|
||||
console.log(`Unhandled event type: ${event.type}`)
|
||||
}
|
||||
|
||||
return NextResponse.json({ received: true })
|
||||
}
|
||||
|
||||
async function handlePaymentSuccess(paymentIntent: any) {
|
||||
console.log('Payment succeeded:', paymentIntent.id)
|
||||
// Update database, send confirmation email, etc.
|
||||
}
|
||||
|
||||
async function handlePaymentFailure(paymentIntent: any) {
|
||||
console.log('Payment failed:', paymentIntent.id)
|
||||
// Notify user, log error, etc.
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 6: Streaming Response
|
||||
// ============================================================================
|
||||
|
||||
// GET /api/stream
|
||||
export async function STREAM() {
|
||||
const encoder = new TextEncoder()
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const data = `data: ${JSON.stringify({ count: i })}\n\n`
|
||||
controller.enqueue(encoder.encode(data))
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
}
|
||||
controller.close()
|
||||
},
|
||||
})
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 7: File Upload
|
||||
// ============================================================================
|
||||
|
||||
// POST /api/upload
|
||||
import { writeFile } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
|
||||
export async function UPLOAD(request: Request) {
|
||||
const formData = await request.formData()
|
||||
const file = formData.get('file') as File
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No file provided' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const bytes = await file.arrayBuffer()
|
||||
const buffer = Buffer.from(bytes)
|
||||
const filename = `${Date.now()}-${file.name}`
|
||||
const path = join(process.cwd(), 'public', 'uploads', filename)
|
||||
|
||||
await writeFile(path, buffer)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
url: `/uploads/${filename}`,
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 8: CORS Configuration
|
||||
// ============================================================================
|
||||
|
||||
export async function OPTIONS() {
|
||||
return new NextResponse(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function CORS_GET() {
|
||||
const response = NextResponse.json({ message: 'Hello' })
|
||||
|
||||
response.headers.set('Access-Control-Allow-Origin', '*')
|
||||
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
|
||||
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 9: Error Handling
|
||||
// ============================================================================
|
||||
|
||||
export async function ERROR_HANDLING() {
|
||||
try {
|
||||
const data = await fetch('https://api.example.com/data')
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error('API request failed')
|
||||
return r.json()
|
||||
})
|
||||
|
||||
return NextResponse.json(data)
|
||||
} catch (error) {
|
||||
console.error('Error:', error)
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal Server Error' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 10: Rate Limiting
|
||||
// ============================================================================
|
||||
|
||||
const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
|
||||
|
||||
export async function RATE_LIMITED() {
|
||||
const headersList = await headers()
|
||||
const ip = headersList.get('x-forwarded-for') || 'unknown'
|
||||
const now = Date.now()
|
||||
|
||||
const rateLimit = rateLimitMap.get(ip)
|
||||
|
||||
if (rateLimit) {
|
||||
if (now < rateLimit.resetAt) {
|
||||
if (rateLimit.count >= 10) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Too many requests' },
|
||||
{ status: 429 }
|
||||
)
|
||||
}
|
||||
rateLimit.count++
|
||||
} else {
|
||||
rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 })
|
||||
}
|
||||
} else {
|
||||
rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 })
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Success' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary:
|
||||
*
|
||||
* Route Handlers (app/api/*/route.ts):
|
||||
* 1. ✅ Support all HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
|
||||
* 2. ✅ Await params in Next.js 16
|
||||
* 3. ✅ Access cookies with await cookies()
|
||||
* 4. ✅ Access headers with await headers()
|
||||
* 5. ✅ Use NextResponse.json() for JSON responses
|
||||
* 6. ✅ Return Response or NextResponse
|
||||
*
|
||||
* Common patterns:
|
||||
* - CRUD operations (GET, POST, PATCH, DELETE)
|
||||
* - Query parameters with searchParams
|
||||
* - Authentication with cookies
|
||||
* - Webhooks with signature verification
|
||||
* - Streaming responses (SSE, WebSocket)
|
||||
* - File uploads with FormData
|
||||
* - CORS configuration
|
||||
* - Error handling
|
||||
* - Rate limiting
|
||||
*
|
||||
* Best practices:
|
||||
* - Use try/catch for error handling
|
||||
* - Return appropriate HTTP status codes
|
||||
* - Validate input data
|
||||
* - Set secure cookie options in production
|
||||
* - Add rate limiting for public endpoints
|
||||
* - Use CORS headers when needed
|
||||
*/
|
||||
534
templates/server-actions-form.tsx
Normal file
534
templates/server-actions-form.tsx
Normal file
@@ -0,0 +1,534 @@
|
||||
/**
|
||||
* Next.js 16 - Server Actions for Form Handling
|
||||
*
|
||||
* This template shows comprehensive Server Actions patterns including:
|
||||
* - Basic form handling
|
||||
* - Validation with Zod
|
||||
* - Loading states
|
||||
* - Error handling
|
||||
* - Optimistic updates
|
||||
* - File uploads
|
||||
*/
|
||||
|
||||
'use server'
|
||||
|
||||
import { z } from 'zod'
|
||||
import { revalidateTag } from 'next/cache'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
// ============================================================================
|
||||
// Example 1: Basic Server Action
|
||||
// ============================================================================
|
||||
|
||||
export async function createPost(formData: FormData) {
|
||||
const title = formData.get('title') as string
|
||||
const content = formData.get('content') as string
|
||||
|
||||
// Save to database
|
||||
await fetch('https://api.example.com/posts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title, content }),
|
||||
})
|
||||
|
||||
// Revalidate cache
|
||||
revalidateTag('posts', 'max')
|
||||
|
||||
// Redirect to posts list
|
||||
redirect('/posts')
|
||||
}
|
||||
|
||||
// Basic form component
|
||||
export function CreatePostForm() {
|
||||
return (
|
||||
<form action={createPost}>
|
||||
<label>
|
||||
Title:
|
||||
<input type="text" name="title" required />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Content:
|
||||
<textarea name="content" required />
|
||||
</label>
|
||||
|
||||
<button type="submit">Create Post</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 2: Server Action with Validation
|
||||
// ============================================================================
|
||||
|
||||
const PostSchema = z.object({
|
||||
title: z.string().min(3, 'Title must be at least 3 characters'),
|
||||
content: z.string().min(10, 'Content must be at least 10 characters'),
|
||||
tags: z.array(z.string()).min(1, 'At least one tag is required'),
|
||||
})
|
||||
|
||||
type ActionState = {
|
||||
errors?: {
|
||||
title?: string[]
|
||||
content?: string[]
|
||||
tags?: string[]
|
||||
_form?: string[]
|
||||
}
|
||||
success?: boolean
|
||||
}
|
||||
|
||||
export async function createPostWithValidation(
|
||||
prevState: ActionState,
|
||||
formData: FormData
|
||||
): Promise<ActionState> {
|
||||
// Parse form data
|
||||
const rawData = {
|
||||
title: formData.get('title'),
|
||||
content: formData.get('content'),
|
||||
tags: formData.get('tags')?.toString().split(',') || [],
|
||||
}
|
||||
|
||||
// Validate
|
||||
const parsed = PostSchema.safeParse(rawData)
|
||||
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
// Save to database
|
||||
try {
|
||||
await fetch('https://api.example.com/posts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(parsed.data),
|
||||
})
|
||||
|
||||
revalidateTag('posts', 'max')
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
return {
|
||||
errors: {
|
||||
_form: ['Failed to create post. Please try again.'],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Form with validation errors
|
||||
'use client'
|
||||
|
||||
import { useFormState, useFormStatus } from 'react-dom'
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus()
|
||||
|
||||
return (
|
||||
<button type="submit" disabled={pending}>
|
||||
{pending ? 'Creating...' : 'Create Post'}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function ValidatedPostForm() {
|
||||
const [state, formAction] = useFormState(createPostWithValidation, {})
|
||||
|
||||
return (
|
||||
<form action={formAction}>
|
||||
<div>
|
||||
<label>
|
||||
Title:
|
||||
<input type="text" name="title" required />
|
||||
</label>
|
||||
{state.errors?.title && (
|
||||
<p className="error">{state.errors.title[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Content:
|
||||
<textarea name="content" required />
|
||||
</label>
|
||||
{state.errors?.content && (
|
||||
<p className="error">{state.errors.content[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
Tags (comma-separated):
|
||||
<input type="text" name="tags" placeholder="nextjs, react, tutorial" />
|
||||
</label>
|
||||
{state.errors?.tags && (
|
||||
<p className="error">{state.errors.tags[0]}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{state.errors?._form && (
|
||||
<p className="error">{state.errors._form[0]}</p>
|
||||
)}
|
||||
|
||||
{state.success && (
|
||||
<p className="success">Post created successfully!</p>
|
||||
)}
|
||||
|
||||
<SubmitButton />
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 3: Server Action with Optimistic Updates
|
||||
// ============================================================================
|
||||
|
||||
'use server'
|
||||
|
||||
import { updateTag } from 'next/cache'
|
||||
|
||||
export async function likePost(postId: string) {
|
||||
await fetch(`https://api.example.com/posts/${postId}/like`, {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
// Use updateTag for immediate refresh (read-your-writes)
|
||||
updateTag('posts')
|
||||
}
|
||||
|
||||
// Client component with optimistic updates
|
||||
'use client'
|
||||
|
||||
import { useOptimistic } from 'react'
|
||||
import { likePost } from './actions'
|
||||
|
||||
export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
|
||||
const [optimisticLikes, addOptimisticLike] = useOptimistic(
|
||||
initialLikes,
|
||||
(state, amount: number) => state + amount
|
||||
)
|
||||
|
||||
async function handleLike() {
|
||||
// Update UI immediately
|
||||
addOptimisticLike(1)
|
||||
|
||||
// Sync with server
|
||||
await likePost(postId)
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={handleLike}>
|
||||
❤️ {optimisticLikes} likes
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 4: Server Action with File Upload
|
||||
// ============================================================================
|
||||
|
||||
'use server'
|
||||
|
||||
import { writeFile } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
|
||||
export async function uploadImage(formData: FormData) {
|
||||
const file = formData.get('image') as File
|
||||
|
||||
if (!file) {
|
||||
return { error: 'No file provided' }
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
const validTypes = ['image/jpeg', 'image/png', 'image/webp']
|
||||
if (!validTypes.includes(file.type)) {
|
||||
return { error: 'Invalid file type. Only JPEG, PNG, and WebP are allowed.' }
|
||||
}
|
||||
|
||||
// Validate file size (max 5MB)
|
||||
const maxSize = 5 * 1024 * 1024 // 5MB
|
||||
if (file.size > maxSize) {
|
||||
return { error: 'File too large. Maximum size is 5MB.' }
|
||||
}
|
||||
|
||||
// Save file
|
||||
const bytes = await file.arrayBuffer()
|
||||
const buffer = Buffer.from(bytes)
|
||||
const filename = `${Date.now()}-${file.name}`
|
||||
const path = join(process.cwd(), 'public', 'uploads', filename)
|
||||
|
||||
await writeFile(path, buffer)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
url: `/uploads/${filename}`,
|
||||
}
|
||||
}
|
||||
|
||||
// File upload form
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { uploadImage } from './actions'
|
||||
|
||||
export function ImageUploadForm() {
|
||||
const [preview, setPreview] = useState<string | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
async function handleSubmit(formData: FormData) {
|
||||
const result = await uploadImage(formData)
|
||||
|
||||
if (result.error) {
|
||||
setError(result.error)
|
||||
} else if (result.url) {
|
||||
setPreview(result.url)
|
||||
setError(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form action={handleSubmit}>
|
||||
<input
|
||||
type="file"
|
||||
name="image"
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
required
|
||||
/>
|
||||
|
||||
<button type="submit">Upload Image</button>
|
||||
|
||||
{error && <p className="error">{error}</p>}
|
||||
|
||||
{preview && (
|
||||
<div>
|
||||
<h3>Uploaded Image:</h3>
|
||||
<img src={preview} alt="Uploaded" width={300} />
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 5: Server Action with Progressive Enhancement
|
||||
// ============================================================================
|
||||
|
||||
'use server'
|
||||
|
||||
export async function subscribe(formData: FormData) {
|
||||
const email = formData.get('email') as string
|
||||
|
||||
await fetch('https://api.example.com/subscribe', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email }),
|
||||
})
|
||||
|
||||
return { success: true, message: 'Subscribed successfully!' }
|
||||
}
|
||||
|
||||
// Form works without JavaScript (progressive enhancement)
|
||||
export function SubscribeForm() {
|
||||
return (
|
||||
<form action={subscribe}>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
/>
|
||||
<button type="submit">Subscribe</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
// Enhanced with JavaScript
|
||||
'use client'
|
||||
|
||||
import { useFormState } from 'react-dom'
|
||||
|
||||
export function EnhancedSubscribeForm() {
|
||||
const [state, formAction] = useFormState(subscribe, null)
|
||||
|
||||
return (
|
||||
<form action={formAction}>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
/>
|
||||
<button type="submit">Subscribe</button>
|
||||
|
||||
{state?.success && (
|
||||
<p className="success">{state.message}</p>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 6: Server Action with Multi-Step Form
|
||||
// ============================================================================
|
||||
|
||||
'use server'
|
||||
|
||||
type Step1Data = { name: string; email: string }
|
||||
type Step2Data = { address: string; city: string }
|
||||
type FormData = Step1Data & Step2Data
|
||||
|
||||
export async function submitMultiStepForm(data: FormData) {
|
||||
// Validate all data
|
||||
const schema = z.object({
|
||||
name: z.string().min(2),
|
||||
email: z.string().email(),
|
||||
address: z.string().min(5),
|
||||
city: z.string().min(2),
|
||||
})
|
||||
|
||||
const parsed = schema.safeParse(data)
|
||||
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
errors: parsed.error.flatten().fieldErrors,
|
||||
}
|
||||
}
|
||||
|
||||
// Save to database
|
||||
await fetch('https://api.example.com/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(parsed.data),
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
// Multi-step form component
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
export function MultiStepForm() {
|
||||
const [step, setStep] = useState(1)
|
||||
const [formData, setFormData] = useState<Partial<FormData>>({})
|
||||
|
||||
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault()
|
||||
|
||||
if (step === 1) {
|
||||
const form = e.currentTarget
|
||||
setFormData({
|
||||
...formData,
|
||||
name: (form.elements.namedItem('name') as HTMLInputElement).value,
|
||||
email: (form.elements.namedItem('email') as HTMLInputElement).value,
|
||||
})
|
||||
setStep(2)
|
||||
} else {
|
||||
const form = e.currentTarget
|
||||
const finalData = {
|
||||
...formData,
|
||||
address: (form.elements.namedItem('address') as HTMLInputElement).value,
|
||||
city: (form.elements.namedItem('city') as HTMLInputElement).value,
|
||||
} as FormData
|
||||
|
||||
const result = await submitMultiStepForm(finalData)
|
||||
|
||||
if (result.success) {
|
||||
alert('Form submitted successfully!')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
{step === 1 && (
|
||||
<>
|
||||
<h2>Step 1: Personal Info</h2>
|
||||
<input name="name" placeholder="Name" defaultValue={formData.name} required />
|
||||
<input name="email" type="email" placeholder="Email" defaultValue={formData.email} required />
|
||||
<button type="submit">Next</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<>
|
||||
<h2>Step 2: Address</h2>
|
||||
<input name="address" placeholder="Address" defaultValue={formData.address} required />
|
||||
<input name="city" placeholder="City" defaultValue={formData.city} required />
|
||||
<button type="button" onClick={() => setStep(1)}>Back</button>
|
||||
<button type="submit">Submit</button>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Example 7: Server Action with Rate Limiting
|
||||
// ============================================================================
|
||||
|
||||
'use server'
|
||||
|
||||
import { headers } from 'next/headers'
|
||||
|
||||
const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
|
||||
|
||||
export async function submitContactForm(formData: FormData) {
|
||||
const headersList = await headers()
|
||||
const ip = headersList.get('x-forwarded-for') || 'unknown'
|
||||
|
||||
// Check rate limit (5 requests per hour)
|
||||
const now = Date.now()
|
||||
const rateLimit = rateLimitMap.get(ip)
|
||||
|
||||
if (rateLimit) {
|
||||
if (now < rateLimit.resetAt) {
|
||||
if (rateLimit.count >= 5) {
|
||||
return { error: 'Too many requests. Please try again later.' }
|
||||
}
|
||||
rateLimit.count++
|
||||
} else {
|
||||
// Reset rate limit
|
||||
rateLimitMap.set(ip, { count: 1, resetAt: now + 3600000 }) // 1 hour
|
||||
}
|
||||
} else {
|
||||
rateLimitMap.set(ip, { count: 1, resetAt: now + 3600000 })
|
||||
}
|
||||
|
||||
// Process form
|
||||
const name = formData.get('name') as string
|
||||
const message = formData.get('message') as string
|
||||
|
||||
await fetch('https://api.example.com/contact', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, message }),
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary:
|
||||
*
|
||||
* Server Actions patterns:
|
||||
* 1. ✅ Basic form handling with formData
|
||||
* 2. ✅ Validation with Zod (safeParse)
|
||||
* 3. ✅ Loading states with useFormStatus
|
||||
* 4. ✅ Error handling with useFormState
|
||||
* 5. ✅ Optimistic updates with useOptimistic
|
||||
* 6. ✅ File uploads
|
||||
* 7. ✅ Progressive enhancement (works without JS)
|
||||
* 8. ✅ Multi-step forms
|
||||
* 9. ✅ Rate limiting
|
||||
*
|
||||
* Best practices:
|
||||
* - Always validate on server (never trust client input)
|
||||
* - Use revalidateTag() for background revalidation
|
||||
* - Use updateTag() for immediate refresh (forms, settings)
|
||||
* - Return errors instead of throwing (better UX)
|
||||
* - Use TypeScript for type safety
|
||||
* - Add rate limiting for public forms
|
||||
*/
|
||||
Reference in New Issue
Block a user