Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:25:04 +08:00
commit 331f882756
14 changed files with 5182 additions and 0 deletions

View 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
* }
*/

View 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
View 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"
}
}

View 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)
*/

View 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
*/

View 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
*/

View 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
*/