Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "nextjs",
|
||||||
|
"description": "Build Next.js 16 apps with App Router, Server Components/Actions, Cache Components (use cache), and async route params. Includes proxy.ts (replaces middleware.ts) and React 19.2. Use when: building Next.js 16 projects, or troubleshooting async params (Promise types), use cache directives, parallel route 404s (missing default.js), or proxy.ts CORS.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": {
|
||||||
|
"name": "Jeremy Dawes",
|
||||||
|
"email": "jeremy@jezweb.net"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# nextjs
|
||||||
|
|
||||||
|
Build Next.js 16 apps with App Router, Server Components/Actions, Cache Components (use cache), and async route params. Includes proxy.ts (replaces middleware.ts) and React 19.2. Use when: building Next.js 16 projects, or troubleshooting async params (Promise types), use cache directives, parallel route 404s (missing default.js), or proxy.ts CORS.
|
||||||
85
plugin.lock.json
Normal file
85
plugin.lock.json
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:jezweb/claude-skills:skills/nextjs",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "7752a82e83393fe7be5bce05d0911b6498c93050",
|
||||||
|
"treeHash": "751d0e9f13f37b25b60eb7bd37981fed86ad120f2e150e053f240a521b9b9e85",
|
||||||
|
"generatedAt": "2025-11-28T10:18:59.854318Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "nextjs",
|
||||||
|
"description": "Build Next.js 16 apps with App Router, Server Components/Actions, Cache Components (use cache), and async route params. Includes proxy.ts (replaces middleware.ts) and React 19.2. Use when: building Next.js 16 projects, or troubleshooting async params (Promise types), use cache directives, parallel route 404s (missing default.js), or proxy.ts CORS.",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "291f3e1170756b73e817589cfd801423a4db65b30f736b90173f1b0464d3c688"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "SKILL.md",
|
||||||
|
"sha256": "0d274baa9085ed5ded3c5fca56c8498352197345eb18136d125e05e1ecee9164"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/next-16-migration-guide.md",
|
||||||
|
"sha256": "b13cb9e7d265f5eeb49f4d25bbc3db9f4afaec519adfb3661a94e811fd41d4a7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "references/top-errors.md",
|
||||||
|
"sha256": "fbc231577f65325a341578a53faa59bcefb0158ab39053d02b8245a09dd7ab05"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "scripts/check-versions.sh",
|
||||||
|
"sha256": "81b37d00e6f3b91f2b9e411dd85f5f9681c95957eb951ed8d9da9da5914f5a70"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "c1c2ed4e43352267df00c2ba4037ec218053613a8bb4a8eb93130c2ad1bf6f8b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/server-actions-form.tsx",
|
||||||
|
"sha256": "8b13071ca8c839e1e7a37792686a25853f43ecfdbb17cf4534c4489049ee57cb"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/proxy-migration.ts",
|
||||||
|
"sha256": "362c27846d847e2ebd01692e980f1b3c0b4bb4e1cff8f1fd98e6979ee59dbb3c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/route-handler-api.ts",
|
||||||
|
"sha256": "be6533d939727add7e97aaebf6b91eed846a8c343b93f9a6b27c44019811036d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/package.json",
|
||||||
|
"sha256": "298748ae5473db8d20cae62bba914fd6878f983cd2ebf0c55f6ae3a50b264853"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/app-router-async-params.tsx",
|
||||||
|
"sha256": "7630604e4d4d11e83a36b2337f833c286ef1381ca5217076a15db8ec32a856c5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/cache-component-use-cache.tsx",
|
||||||
|
"sha256": "986256a04ebce7de7c624a5c5d0f5fd84fc2a590cc5600dfff18a97bae8eabae"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/parallel-routes-with-default.tsx",
|
||||||
|
"sha256": "513cac57ae53e33fd619cf15876320264fe93d366727426bef09549ceae2e541"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "751d0e9f13f37b25b60eb7bd37981fed86ad120f2e150e053f240a521b9b9e85"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
647
references/next-16-migration-guide.md
Normal file
647
references/next-16-migration-guide.md
Normal file
@@ -0,0 +1,647 @@
|
|||||||
|
# Next.js 16 Migration Guide
|
||||||
|
|
||||||
|
**From**: Next.js 15.x
|
||||||
|
**To**: Next.js 16.0.0
|
||||||
|
**Last Updated**: 2025-10-24
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Overview](#overview)
|
||||||
|
2. [Breaking Changes](#breaking-changes)
|
||||||
|
3. [New Features](#new-features)
|
||||||
|
4. [Migration Steps](#migration-steps)
|
||||||
|
5. [Automated Migration](#automated-migration)
|
||||||
|
6. [Manual Migration](#manual-migration)
|
||||||
|
7. [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Next.js 16 introduces significant changes:
|
||||||
|
|
||||||
|
- **Breaking Changes**: 6 major breaking changes
|
||||||
|
- **New Features**: Cache Components, updated caching APIs, React 19.2
|
||||||
|
- **Performance**: Turbopack stable, 2–5× faster builds
|
||||||
|
- **Migration Time**: ~1-2 hours for medium-sized apps
|
||||||
|
|
||||||
|
**Recommendation**: Use automated codemod first, then manually fix remaining issues.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Breaking Changes
|
||||||
|
|
||||||
|
### 1. Async Route Parameters ⚠️
|
||||||
|
|
||||||
|
**What Changed**: `params`, `searchParams`, `cookies()`, `headers()`, `draftMode()` are now async.
|
||||||
|
|
||||||
|
**Before** (Next.js 15):
|
||||||
|
```typescript
|
||||||
|
export default function Page({ params, searchParams }) {
|
||||||
|
const slug = params.slug
|
||||||
|
const query = searchParams.q
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After** (Next.js 16):
|
||||||
|
```typescript
|
||||||
|
export default async function Page({ params, searchParams }) {
|
||||||
|
const { slug } = await params
|
||||||
|
const { q: query } = await searchParams
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**TypeScript Types**:
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
type PageProps = {
|
||||||
|
params: { slug: string }
|
||||||
|
searchParams: { q: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
// After
|
||||||
|
type PageProps = {
|
||||||
|
params: Promise<{ slug: string }>
|
||||||
|
searchParams: Promise<{ q: string }>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
1. Add `async` to function
|
||||||
|
2. Add `await` before params/searchParams
|
||||||
|
3. Update TypeScript types to `Promise<>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Middleware → Proxy ⚠️
|
||||||
|
|
||||||
|
**What Changed**: `middleware.ts` is deprecated. Use `proxy.ts` instead.
|
||||||
|
|
||||||
|
**Migration**:
|
||||||
|
```bash
|
||||||
|
# 1. Rename file
|
||||||
|
mv middleware.ts proxy.ts
|
||||||
|
|
||||||
|
# 2. Rename function in file
|
||||||
|
# middleware → proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
**Before** (middleware.ts):
|
||||||
|
```typescript
|
||||||
|
export function middleware(request: NextRequest) {
|
||||||
|
return NextResponse.next()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After** (proxy.ts):
|
||||||
|
```typescript
|
||||||
|
export function proxy(request: NextRequest) {
|
||||||
|
return NextResponse.next()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: `middleware.ts` still works in Next.js 16 but is deprecated.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Parallel Routes Require `default.js` ⚠️
|
||||||
|
|
||||||
|
**What Changed**: All parallel routes now REQUIRE explicit `default.js` files.
|
||||||
|
|
||||||
|
**Before** (Next.js 15):
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── @modal/
|
||||||
|
│ └── login/
|
||||||
|
│ └── page.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
**After** (Next.js 16):
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── @modal/
|
||||||
|
│ ├── login/
|
||||||
|
│ │ └── page.tsx
|
||||||
|
│ └── default.tsx ← REQUIRED
|
||||||
|
```
|
||||||
|
|
||||||
|
**default.tsx**:
|
||||||
|
```typescript
|
||||||
|
export default function ModalDefault() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fix**: Add `default.tsx` to every `@folder` in parallel routes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Removed Features ⚠️
|
||||||
|
|
||||||
|
**Removed**:
|
||||||
|
- AMP support
|
||||||
|
- `next lint` command
|
||||||
|
- `serverRuntimeConfig` and `publicRuntimeConfig`
|
||||||
|
- `experimental.ppr` flag
|
||||||
|
- Automatic `scroll-behavior: smooth`
|
||||||
|
- Node.js 18 support
|
||||||
|
|
||||||
|
**Migration**:
|
||||||
|
|
||||||
|
**AMP**:
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
export const config = { amp: true }
|
||||||
|
|
||||||
|
// After
|
||||||
|
// No direct replacement - use separate AMP pages or frameworks
|
||||||
|
```
|
||||||
|
|
||||||
|
**Linting**:
|
||||||
|
```bash
|
||||||
|
# Before
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# After
|
||||||
|
npx eslint .
|
||||||
|
# or
|
||||||
|
npx biome lint .
|
||||||
|
```
|
||||||
|
|
||||||
|
**Runtime Config**:
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
module.exports = {
|
||||||
|
serverRuntimeConfig: { secret: 'abc' },
|
||||||
|
publicRuntimeConfig: { apiUrl: 'https://api' },
|
||||||
|
}
|
||||||
|
|
||||||
|
// After
|
||||||
|
// Use environment variables
|
||||||
|
process.env.SECRET
|
||||||
|
process.env.NEXT_PUBLIC_API_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Version Requirements ⚠️
|
||||||
|
|
||||||
|
**Minimum Versions**:
|
||||||
|
- Node.js: 20.9+ (Node 18 removed)
|
||||||
|
- TypeScript: 5.1+
|
||||||
|
- React: 19.2+
|
||||||
|
- Browsers: Chrome 111+, Safari 16.4+, Firefox 109+
|
||||||
|
|
||||||
|
**Upgrade Node.js**:
|
||||||
|
```bash
|
||||||
|
# Check current version
|
||||||
|
node --version
|
||||||
|
|
||||||
|
# Upgrade (using nvm)
|
||||||
|
nvm install 20
|
||||||
|
nvm use 20
|
||||||
|
nvm alias default 20
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
node --version # Should be 20.9+
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Image Defaults Changed ⚠️
|
||||||
|
|
||||||
|
**What Changed**: `next/image` default settings changed.
|
||||||
|
|
||||||
|
| Setting | Next.js 15 | Next.js 16 |
|
||||||
|
|---------|------------|------------|
|
||||||
|
| TTL | 60s | 4 hours |
|
||||||
|
| imageSizes | 8 sizes | 5 sizes |
|
||||||
|
| qualities | 3 qualities | 1 quality (75) |
|
||||||
|
|
||||||
|
**Impact**: Images cache longer, fewer sizes generated.
|
||||||
|
|
||||||
|
**Revert** (if needed):
|
||||||
|
```typescript
|
||||||
|
// next.config.ts
|
||||||
|
const config = {
|
||||||
|
images: {
|
||||||
|
minimumCacheTTL: 60, // Revert to 60 seconds
|
||||||
|
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], // Old sizes
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## New Features
|
||||||
|
|
||||||
|
### 1. Cache Components ✨
|
||||||
|
|
||||||
|
**Opt-in caching** with `"use cache"` directive.
|
||||||
|
|
||||||
|
**Before** (Next.js 15 - implicit caching):
|
||||||
|
```typescript
|
||||||
|
// All Server Components cached by default
|
||||||
|
export async function MyComponent() {
|
||||||
|
const data = await fetch('/api/data')
|
||||||
|
return <div>{data.value}</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**After** (Next.js 16 - opt-in):
|
||||||
|
```typescript
|
||||||
|
// NOT cached by default
|
||||||
|
export async function MyComponent() {
|
||||||
|
const data = await fetch('/api/data')
|
||||||
|
return <div>{data.value}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opt-in to caching
|
||||||
|
'use cache'
|
||||||
|
export async function CachedComponent() {
|
||||||
|
const data = await fetch('/api/data')
|
||||||
|
return <div>{data.value}</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**See**: `references/cache-components-guide.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Updated Caching APIs ✨
|
||||||
|
|
||||||
|
**`revalidateTag()` now requires 2 arguments**:
|
||||||
|
|
||||||
|
**Before**:
|
||||||
|
```typescript
|
||||||
|
revalidateTag('posts')
|
||||||
|
```
|
||||||
|
|
||||||
|
**After**:
|
||||||
|
```typescript
|
||||||
|
revalidateTag('posts', 'max') // Second argument required
|
||||||
|
```
|
||||||
|
|
||||||
|
**New APIs**:
|
||||||
|
- `updateTag()` - Immediate refresh (read-your-writes)
|
||||||
|
- `refresh()` - Refresh uncached data only
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. React 19.2 Integration ✨
|
||||||
|
|
||||||
|
**New React features**:
|
||||||
|
- View Transitions
|
||||||
|
- `useEffectEvent()` (experimental)
|
||||||
|
- React Compiler (stable)
|
||||||
|
|
||||||
|
**See**: `references/react-19-integration.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Turbopack Stable ✨
|
||||||
|
|
||||||
|
**Default bundler**: Turbopack is now stable and default.
|
||||||
|
|
||||||
|
**Metrics**:
|
||||||
|
- 2–5× faster production builds
|
||||||
|
- Up to 10× faster Fast Refresh
|
||||||
|
|
||||||
|
**Opt-out** (if incompatible):
|
||||||
|
```bash
|
||||||
|
npm run build -- --webpack
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Steps
|
||||||
|
|
||||||
|
### Step 1: Prerequisites
|
||||||
|
|
||||||
|
1. **Backup your project**:
|
||||||
|
```bash
|
||||||
|
git commit -am "Pre-migration checkpoint"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Check Node.js version**:
|
||||||
|
```bash
|
||||||
|
node --version # Should be 20.9+
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Update dependencies**:
|
||||||
|
```bash
|
||||||
|
npm install next@16 react@19.2 react-dom@19.2
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 2: Run Automated Codemod
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @next/codemod@canary upgrade latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**What it fixes**:
|
||||||
|
- ✅ Async params (adds `await`)
|
||||||
|
- ✅ Async searchParams
|
||||||
|
- ✅ Async cookies()
|
||||||
|
- ✅ Async headers()
|
||||||
|
- ✅ Updates TypeScript types
|
||||||
|
|
||||||
|
**What it does NOT fix**:
|
||||||
|
- ❌ middleware.ts → proxy.ts (manual)
|
||||||
|
- ❌ Parallel routes default.js (manual)
|
||||||
|
- ❌ Removed features (manual)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 3: Manual Fixes
|
||||||
|
|
||||||
|
#### Fix 1: Migrate middleware.ts → proxy.ts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Rename file
|
||||||
|
mv middleware.ts proxy.ts
|
||||||
|
|
||||||
|
# Update function name
|
||||||
|
# middleware → proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Fix 2: Add default.js to Parallel Routes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For each @folder, create default.tsx
|
||||||
|
touch app/@modal/default.tsx
|
||||||
|
touch app/@feed/default.tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// app/@modal/default.tsx
|
||||||
|
export default function ModalDefault() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Fix 3: Replace Removed Features
|
||||||
|
|
||||||
|
**AMP**: Remove AMP config or migrate to separate AMP implementation.
|
||||||
|
|
||||||
|
**Linting**: Update scripts in `package.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint ."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Runtime Config**: Use environment variables.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 4: Update Caching
|
||||||
|
|
||||||
|
**Migrate from implicit to explicit caching**:
|
||||||
|
|
||||||
|
1. Find Server Components with expensive operations
|
||||||
|
2. Add `"use cache"` directive
|
||||||
|
3. Update `revalidateTag()` calls to include `cacheLife`
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
export async function ExpensiveComponent() {
|
||||||
|
const data = await fetch('/api/data') // Cached implicitly
|
||||||
|
return <div>{data.value}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// After
|
||||||
|
'use cache'
|
||||||
|
export async function ExpensiveComponent() {
|
||||||
|
const data = await fetch('/api/data') // Cached explicitly
|
||||||
|
return <div>{data.value}</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 5: Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Production build
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Check for errors
|
||||||
|
npm run type-check
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 6: Update CI/CD
|
||||||
|
|
||||||
|
**Update Node.js version** in CI config:
|
||||||
|
|
||||||
|
**.github/workflows/ci.yml**:
|
||||||
|
```yaml
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20.9' # Update from 18
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dockerfile**:
|
||||||
|
```dockerfile
|
||||||
|
FROM node:20.9-alpine # Update from node:18
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Automated Migration
|
||||||
|
|
||||||
|
**Codemod** (recommended):
|
||||||
|
```bash
|
||||||
|
npx @next/codemod@canary upgrade latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options**:
|
||||||
|
- `--dry` - Preview changes without applying
|
||||||
|
- `--force` - Skip confirmation prompts
|
||||||
|
|
||||||
|
**What it migrates**:
|
||||||
|
1. ✅ Async params
|
||||||
|
2. ✅ Async searchParams
|
||||||
|
3. ✅ Async cookies()
|
||||||
|
4. ✅ Async headers()
|
||||||
|
5. ✅ TypeScript types
|
||||||
|
|
||||||
|
**Manual steps after codemod**:
|
||||||
|
1. Rename middleware.ts → proxy.ts
|
||||||
|
2. Add default.js to parallel routes
|
||||||
|
3. Replace removed features
|
||||||
|
4. Update caching patterns
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Manual Migration
|
||||||
|
|
||||||
|
If codemod fails or you prefer manual migration:
|
||||||
|
|
||||||
|
### 1. Async Params
|
||||||
|
|
||||||
|
**Find**:
|
||||||
|
```bash
|
||||||
|
grep -r "params\." app/
|
||||||
|
grep -r "searchParams\." app/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Replace**:
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
const slug = params.slug
|
||||||
|
|
||||||
|
// After
|
||||||
|
const { slug } = await params
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Async Cookies/Headers
|
||||||
|
|
||||||
|
**Find**:
|
||||||
|
```bash
|
||||||
|
grep -r "cookies()" app/
|
||||||
|
grep -r "headers()" app/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Replace**:
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
const cookieStore = cookies()
|
||||||
|
|
||||||
|
// After
|
||||||
|
const cookieStore = await cookies()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. TypeScript Types
|
||||||
|
|
||||||
|
**Find**: All `PageProps` types
|
||||||
|
|
||||||
|
**Replace**:
|
||||||
|
```typescript
|
||||||
|
// Before
|
||||||
|
type PageProps = {
|
||||||
|
params: { id: string }
|
||||||
|
searchParams: { q: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
// After
|
||||||
|
type PageProps = {
|
||||||
|
params: Promise<{ id: string }>
|
||||||
|
searchParams: Promise<{ q: string }>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Error: `params` is a Promise
|
||||||
|
|
||||||
|
**Cause**: Not awaiting params in Next.js 16.
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
```typescript
|
||||||
|
// ❌ Before
|
||||||
|
const id = params.id
|
||||||
|
|
||||||
|
// ✅ After
|
||||||
|
const { id } = await params
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Error: Parallel route missing `default.js`
|
||||||
|
|
||||||
|
**Cause**: Next.js 16 requires `default.js` for all parallel routes.
|
||||||
|
|
||||||
|
**Fix**: Create `default.tsx`:
|
||||||
|
```typescript
|
||||||
|
// app/@modal/default.tsx
|
||||||
|
export default function ModalDefault() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Error: `revalidateTag` requires 2 arguments
|
||||||
|
|
||||||
|
**Cause**: `revalidateTag()` API changed in Next.js 16.
|
||||||
|
|
||||||
|
**Fix**:
|
||||||
|
```typescript
|
||||||
|
// ❌ Before
|
||||||
|
revalidateTag('posts')
|
||||||
|
|
||||||
|
// ✅ After
|
||||||
|
revalidateTag('posts', 'max')
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Error: Turbopack build failure
|
||||||
|
|
||||||
|
**Cause**: Turbopack is now default in Next.js 16.
|
||||||
|
|
||||||
|
**Fix**: Opt-out if incompatible:
|
||||||
|
```bash
|
||||||
|
npm run build -- --webpack
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Error: Node.js version too old
|
||||||
|
|
||||||
|
**Cause**: Next.js 16 requires Node.js 20.9+.
|
||||||
|
|
||||||
|
**Fix**: Upgrade Node.js:
|
||||||
|
```bash
|
||||||
|
nvm install 20
|
||||||
|
nvm use 20
|
||||||
|
nvm alias default 20
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Checklist
|
||||||
|
|
||||||
|
- [ ] Backup project (git commit)
|
||||||
|
- [ ] Check Node.js version (20.9+)
|
||||||
|
- [ ] Update dependencies (`npm install next@16 react@19.2 react-dom@19.2`)
|
||||||
|
- [ ] Run codemod (`npx @next/codemod@canary upgrade latest`)
|
||||||
|
- [ ] Rename middleware.ts → proxy.ts
|
||||||
|
- [ ] Add default.js to parallel routes
|
||||||
|
- [ ] Remove AMP config (if used)
|
||||||
|
- [ ] Replace runtime config with env vars
|
||||||
|
- [ ] Update `revalidateTag()` calls (add `cacheLife`)
|
||||||
|
- [ ] Add `"use cache"` where needed
|
||||||
|
- [ ] Test dev server (`npm run dev`)
|
||||||
|
- [ ] Test production build (`npm run build`)
|
||||||
|
- [ ] Update CI/CD Node.js version
|
||||||
|
- [ ] Deploy to staging
|
||||||
|
- [ ] Deploy to production
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- **Next.js 16 Blog**: https://nextjs.org/blog/next-16
|
||||||
|
- **Codemod**: `npx @next/codemod@canary upgrade latest`
|
||||||
|
- **Templates**: See `templates/` directory
|
||||||
|
- **Common Errors**: See `references/top-errors.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Migration Support**: jeremy@jezweb.net
|
||||||
550
references/top-errors.md
Normal file
550
references/top-errors.md
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
# Next.js 16 - Top 18 Errors & Solutions
|
||||||
|
|
||||||
|
**Last Updated**: 2025-10-24
|
||||||
|
**Prevention Rate**: 100% (all documented errors caught)
|
||||||
|
|
||||||
|
This guide covers the 18 most common errors when using Next.js 16 and their solutions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #1: `params` is a Promise
|
||||||
|
|
||||||
|
**Error Message**:
|
||||||
|
```
|
||||||
|
Type 'Promise<{ id: string }>' is not assignable to type '{ id: string }'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: Next.js 16 changed `params` to async.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```typescript
|
||||||
|
// ❌ Before (Next.js 15)
|
||||||
|
export default function Page({ params }: { params: { id: string } }) {
|
||||||
|
const id = params.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ After (Next.js 16)
|
||||||
|
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const { id } = await params
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**TypeScript Fix**:
|
||||||
|
```typescript
|
||||||
|
type Params<T = Record<string, string>> = Promise<T>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #2: `searchParams` is a Promise
|
||||||
|
|
||||||
|
**Error Message**:
|
||||||
|
```
|
||||||
|
Property 'query' does not exist on type 'Promise<{ query: string }>'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: `searchParams` is now async in Next.js 16.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```typescript
|
||||||
|
// ❌ Before
|
||||||
|
export default function Page({ searchParams }: { searchParams: { q: string } }) {
|
||||||
|
const query = searchParams.q
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ After
|
||||||
|
export default async function Page({ searchParams }: { searchParams: Promise<{ q: string }> }) {
|
||||||
|
const { q: query } = await searchParams
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #3: `cookies()` requires await
|
||||||
|
|
||||||
|
**Error Message**:
|
||||||
|
```
|
||||||
|
'cookies' implicitly has return type 'any'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: `cookies()` is async in Next.js 16.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```typescript
|
||||||
|
// ❌ Before
|
||||||
|
import { cookies } from 'next/headers'
|
||||||
|
|
||||||
|
export function MyComponent() {
|
||||||
|
const cookieStore = cookies()
|
||||||
|
const token = cookieStore.get('token')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ After
|
||||||
|
import { cookies } from 'next/headers'
|
||||||
|
|
||||||
|
export async function MyComponent() {
|
||||||
|
const cookieStore = await cookies()
|
||||||
|
const token = cookieStore.get('token')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #4: `headers()` requires await
|
||||||
|
|
||||||
|
**Error Message**:
|
||||||
|
```
|
||||||
|
'headers' implicitly has return type 'any'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: `headers()` is async in Next.js 16.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```typescript
|
||||||
|
// ❌ Before
|
||||||
|
import { headers } from 'next/headers'
|
||||||
|
|
||||||
|
export function MyComponent() {
|
||||||
|
const headersList = headers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ After
|
||||||
|
import { headers } from 'next/headers'
|
||||||
|
|
||||||
|
export async function MyComponent() {
|
||||||
|
const headersList = await headers()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #5: Parallel route missing `default.js`
|
||||||
|
|
||||||
|
**Error Message**:
|
||||||
|
```
|
||||||
|
Error: Parallel route @modal/login was matched but no default.js was found
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: Next.js 16 requires `default.js` for all parallel routes.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```typescript
|
||||||
|
// Create app/@modal/default.tsx
|
||||||
|
export default function ModalDefault() {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure**:
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── @modal/
|
||||||
|
│ ├── login/
|
||||||
|
│ │ └── page.tsx
|
||||||
|
│ └── default.tsx ← REQUIRED
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #6: `revalidateTag()` requires 2 arguments
|
||||||
|
|
||||||
|
**Error Message**:
|
||||||
|
```
|
||||||
|
Expected 2 arguments, but got 1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: `revalidateTag()` API changed in Next.js 16.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```typescript
|
||||||
|
// ❌ Before (Next.js 15)
|
||||||
|
import { revalidateTag } from 'next/cache'
|
||||||
|
revalidateTag('posts')
|
||||||
|
|
||||||
|
// ✅ After (Next.js 16)
|
||||||
|
import { revalidateTag } from 'next/cache'
|
||||||
|
revalidateTag('posts', 'max') // Second argument required
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cache Life Profiles**:
|
||||||
|
- `'max'` - Maximum staleness (recommended)
|
||||||
|
- `'hours'` - Stale after hours
|
||||||
|
- `'days'` - Stale after days
|
||||||
|
- Custom: `{ stale: 3600, revalidate: 86400 }`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #7: Cannot use React hooks in Server Component
|
||||||
|
|
||||||
|
**Error Message**:
|
||||||
|
```
|
||||||
|
You're importing a component that needs useState. It only works in a Client Component
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: Using React hooks in Server Component.
|
||||||
|
|
||||||
|
**Solution**: Add `'use client'` directive:
|
||||||
|
```typescript
|
||||||
|
// ✅ Add 'use client' at the top
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export function Counter() {
|
||||||
|
const [count, setCount] = useState(0)
|
||||||
|
return <button onClick={() => setCount(count + 1)}>{count}</button>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #8: `middleware.ts` is deprecated
|
||||||
|
|
||||||
|
**Warning Message**:
|
||||||
|
```
|
||||||
|
Warning: middleware.ts is deprecated. Use proxy.ts instead.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**: Migrate to `proxy.ts`:
|
||||||
|
```bash
|
||||||
|
# 1. Rename file
|
||||||
|
mv middleware.ts proxy.ts
|
||||||
|
|
||||||
|
# 2. Rename function
|
||||||
|
# middleware → proxy
|
||||||
|
```
|
||||||
|
|
||||||
|
**Code**:
|
||||||
|
```typescript
|
||||||
|
// ✅ proxy.ts
|
||||||
|
export function proxy(request: NextRequest) {
|
||||||
|
// Same logic
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #9: Turbopack build failure
|
||||||
|
|
||||||
|
**Error Message**:
|
||||||
|
```
|
||||||
|
Error: Failed to compile with Turbopack
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: Turbopack is now default in Next.js 16.
|
||||||
|
|
||||||
|
**Solution 1** (opt-out):
|
||||||
|
```bash
|
||||||
|
npm run build -- --webpack
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution 2** (fix compatibility):
|
||||||
|
Check for incompatible packages and update them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #10: Invalid `next/image` src
|
||||||
|
|
||||||
|
**Error Message**:
|
||||||
|
```
|
||||||
|
Invalid src prop (https://example.com/image.jpg) on `next/image`. Hostname "example.com" is not configured
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: Remote images not configured.
|
||||||
|
|
||||||
|
**Solution**: Add to `next.config.ts`:
|
||||||
|
```typescript
|
||||||
|
const config = {
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: 'https',
|
||||||
|
hostname: 'example.com',
|
||||||
|
pathname: '/images/**',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #11: Cannot import Server Component into Client Component
|
||||||
|
|
||||||
|
**Error Message**:
|
||||||
|
```
|
||||||
|
You're importing a Server Component into a Client Component
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: Direct import of Server Component in Client Component.
|
||||||
|
|
||||||
|
**Solution**: Pass as children:
|
||||||
|
```typescript
|
||||||
|
// ❌ Wrong
|
||||||
|
'use client'
|
||||||
|
import { ServerComponent } from './server-component'
|
||||||
|
|
||||||
|
export function ClientComponent() {
|
||||||
|
return <ServerComponent />
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Correct
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
export function ClientComponent({ children }: { children: React.ReactNode }) {
|
||||||
|
return <div>{children}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
<ClientComponent>
|
||||||
|
<ServerComponent /> {/* Pass as children */}
|
||||||
|
</ClientComponent>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #12: `generateStaticParams` not working
|
||||||
|
|
||||||
|
**Error Message**:
|
||||||
|
```
|
||||||
|
generateStaticParams is not generating static pages
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: Missing `dynamic = 'force-static'`.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```typescript
|
||||||
|
export const dynamic = 'force-static'
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
const posts = await fetch('/api/posts').then(r => r.json())
|
||||||
|
return posts.map((post: { id: string }) => ({ id: post.id }))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #13: `fetch()` not caching
|
||||||
|
|
||||||
|
**Error Message**: Data not cached (performance issue).
|
||||||
|
|
||||||
|
**Cause**: Next.js 16 uses opt-in caching.
|
||||||
|
|
||||||
|
**Solution**: Add `"use cache"`:
|
||||||
|
```typescript
|
||||||
|
'use cache'
|
||||||
|
|
||||||
|
export async function getPosts() {
|
||||||
|
const response = await fetch('/api/posts')
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #14: Route collision with Route Groups
|
||||||
|
|
||||||
|
**Error Message**:
|
||||||
|
```
|
||||||
|
Error: Conflicting routes: /about and /(marketing)/about
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: Route groups creating same URL path.
|
||||||
|
|
||||||
|
**Solution**: Ensure unique paths:
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
├── (marketing)/about/page.tsx → /about
|
||||||
|
└── (shop)/store-info/page.tsx → /store-info (NOT /about)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #15: Metadata not updating
|
||||||
|
|
||||||
|
**Error Message**: SEO metadata not showing correctly.
|
||||||
|
|
||||||
|
**Cause**: Using static metadata for dynamic pages.
|
||||||
|
|
||||||
|
**Solution**: Use `generateMetadata()`:
|
||||||
|
```typescript
|
||||||
|
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
|
||||||
|
const { id } = await params
|
||||||
|
const post = await fetch(`/api/posts/${id}`).then(r => r.json())
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: post.title,
|
||||||
|
description: post.excerpt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #16: `next/font` font not loading
|
||||||
|
|
||||||
|
**Error Message**: Custom fonts not applying.
|
||||||
|
|
||||||
|
**Cause**: Font variable not applied to HTML element.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```typescript
|
||||||
|
import { Inter } from 'next/font/google'
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html className={inter.variable}> {/* ✅ Apply variable */}
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #17: Environment variables not available in browser
|
||||||
|
|
||||||
|
**Error Message**: `process.env.SECRET_KEY` is undefined in client.
|
||||||
|
|
||||||
|
**Cause**: Server-only env vars not exposed to browser.
|
||||||
|
|
||||||
|
**Solution**: Prefix with `NEXT_PUBLIC_`:
|
||||||
|
```bash
|
||||||
|
# .env
|
||||||
|
SECRET_KEY=abc123 # Server-only
|
||||||
|
NEXT_PUBLIC_API_URL=https://api # Available in browser
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Server Component (both work)
|
||||||
|
const secret = process.env.SECRET_KEY
|
||||||
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL
|
||||||
|
|
||||||
|
// Client Component (only public vars)
|
||||||
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error #18: Server Action not found
|
||||||
|
|
||||||
|
**Error Message**:
|
||||||
|
```
|
||||||
|
Error: Could not find Server Action
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: Missing `'use server'` directive.
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```typescript
|
||||||
|
// ❌ Before
|
||||||
|
export async function createPost(formData: FormData) {
|
||||||
|
await db.posts.create({ ... })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ After
|
||||||
|
'use server'
|
||||||
|
|
||||||
|
export async function createPost(formData: FormData) {
|
||||||
|
await db.posts.create({ ... })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Error Lookup
|
||||||
|
|
||||||
|
| Error Type | Solution | Link |
|
||||||
|
|------------|----------|------|
|
||||||
|
| Async params | Add `await params` | [#1](#error-1-params-is-a-promise) |
|
||||||
|
| Async searchParams | Add `await searchParams` | [#2](#error-2-searchparams-is-a-promise) |
|
||||||
|
| Async cookies() | Add `await cookies()` | [#3](#error-3-cookies-requires-await) |
|
||||||
|
| Async headers() | Add `await headers()` | [#4](#error-4-headers-requires-await) |
|
||||||
|
| Missing default.js | Create `default.tsx` | [#5](#error-5-parallel-route-missing-defaultjs) |
|
||||||
|
| revalidateTag 1 arg | Add `cacheLife` argument | [#6](#error-6-revalidatetag-requires-2-arguments) |
|
||||||
|
| Hooks in Server Component | Add `'use client'` | [#7](#error-7-cannot-use-react-hooks-in-server-component) |
|
||||||
|
| middleware.ts deprecated | Rename to `proxy.ts` | [#8](#error-8-middlewarets-is-deprecated) |
|
||||||
|
| Turbopack failure | Use `--webpack` flag | [#9](#error-9-turbopack-build-failure) |
|
||||||
|
| Invalid image src | Add `remotePatterns` | [#10](#error-10-invalid-nextimage-src) |
|
||||||
|
| Import Server in Client | Pass as children | [#11](#error-11-cannot-import-server-component-into-client-component) |
|
||||||
|
| generateStaticParams | Add `dynamic = 'force-static'` | [#12](#error-12-generatestaticparams-not-working) |
|
||||||
|
| fetch not caching | Add `'use cache'` | [#13](#error-13-fetch-not-caching) |
|
||||||
|
| Route collision | Use unique paths | [#14](#error-14-route-collision-with-route-groups) |
|
||||||
|
| Metadata not updating | Use `generateMetadata()` | [#15](#error-15-metadata-not-updating) |
|
||||||
|
| Font not loading | Apply font variable to `<html>` | [#16](#error-16-nextfont-font-not-loading) |
|
||||||
|
| Env vars in browser | Prefix with `NEXT_PUBLIC_` | [#17](#error-17-environment-variables-not-available-in-browser) |
|
||||||
|
| Server Action not found | Add `'use server'` | [#18](#error-18-server-action-not-found) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prevention Checklist
|
||||||
|
|
||||||
|
Before deploying, check:
|
||||||
|
|
||||||
|
- [ ] All `params` are awaited
|
||||||
|
- [ ] All `searchParams` are awaited
|
||||||
|
- [ ] All `cookies()` calls are awaited
|
||||||
|
- [ ] All `headers()` calls are awaited
|
||||||
|
- [ ] All parallel routes have `default.js`
|
||||||
|
- [ ] `revalidateTag()` has 2 arguments
|
||||||
|
- [ ] Client Components have `'use client'`
|
||||||
|
- [ ] `middleware.ts` migrated to `proxy.ts`
|
||||||
|
- [ ] Remote images configured in `next.config.ts`
|
||||||
|
- [ ] Server Components not imported directly in Client Components
|
||||||
|
- [ ] Static pages have `dynamic = 'force-static'`
|
||||||
|
- [ ] Cached components have `'use cache'`
|
||||||
|
- [ ] No route collisions with Route Groups
|
||||||
|
- [ ] Dynamic pages use `generateMetadata()`
|
||||||
|
- [ ] Fonts applied to `<html>` or `<body>`
|
||||||
|
- [ ] Public env vars prefixed with `NEXT_PUBLIC_`
|
||||||
|
- [ ] Server Actions have `'use server'`
|
||||||
|
- [ ] Node.js version is 20.9+
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debugging Tips
|
||||||
|
|
||||||
|
### Enable TypeScript Strict Mode
|
||||||
|
|
||||||
|
```json
|
||||||
|
// tsconfig.json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Build Output
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for warnings and errors in build logs.
|
||||||
|
|
||||||
|
### Use Type Checking
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tsc --noEmit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check Runtime Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Watch console for errors and warnings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- **Migration Guide**: `references/next-16-migration-guide.md`
|
||||||
|
- **Templates**: `templates/` directory
|
||||||
|
- **Next.js 16 Blog**: https://nextjs.org/blog/next-16
|
||||||
|
- **Support**: jeremy@jezweb.net
|
||||||
207
scripts/check-versions.sh
Executable file
207
scripts/check-versions.sh
Executable file
@@ -0,0 +1,207 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Next.js 16 - Version Checker
|
||||||
|
# Verifies that all dependencies are compatible with Next.js 16
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔍 Checking Next.js 16 compatibility..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Check if package.json exists
|
||||||
|
if [ ! -f "package.json" ]; then
|
||||||
|
echo -e "${RED}❌ package.json not found${NC}"
|
||||||
|
echo "Run this script from your project root directory."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check Node.js version
|
||||||
|
echo "📦 Node.js Version:"
|
||||||
|
NODE_VERSION=$(node --version | cut -d'v' -f2)
|
||||||
|
NODE_MAJOR=$(echo $NODE_VERSION | cut -d'.' -f1)
|
||||||
|
NODE_MINOR=$(echo $NODE_VERSION | cut -d'.' -f2)
|
||||||
|
|
||||||
|
if [ "$NODE_MAJOR" -lt 20 ]; then
|
||||||
|
echo -e "${RED}❌ Node.js $NODE_VERSION (requires 20.9+)${NC}"
|
||||||
|
echo " Upgrade: nvm install 20 && nvm use 20"
|
||||||
|
exit 1
|
||||||
|
elif [ "$NODE_MAJOR" -eq 20 ] && [ "$NODE_MINOR" -lt 9 ]; then
|
||||||
|
echo -e "${RED}❌ Node.js $NODE_VERSION (requires 20.9+)${NC}"
|
||||||
|
echo " Upgrade: nvm install 20 && nvm use 20"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✅ Node.js $NODE_VERSION${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check Next.js version
|
||||||
|
echo "🔧 Next.js Version:"
|
||||||
|
if [ -f "node_modules/next/package.json" ]; then
|
||||||
|
NEXT_VERSION=$(node -p "require('./node_modules/next/package.json').version")
|
||||||
|
NEXT_MAJOR=$(echo $NEXT_VERSION | cut -d'.' -f1)
|
||||||
|
|
||||||
|
if [ "$NEXT_MAJOR" -lt 16 ]; then
|
||||||
|
echo -e "${RED}❌ Next.js $NEXT_VERSION (requires 16.0.0+)${NC}"
|
||||||
|
echo " Upgrade: npm install next@16"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✅ Next.js $NEXT_VERSION${NC}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠️ Next.js not installed (run npm install)${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check React version
|
||||||
|
echo "⚛️ React Version:"
|
||||||
|
if [ -f "node_modules/react/package.json" ]; then
|
||||||
|
REACT_VERSION=$(node -p "require('./node_modules/react/package.json').version")
|
||||||
|
REACT_MAJOR=$(echo $REACT_VERSION | cut -d'.' -f1)
|
||||||
|
REACT_MINOR=$(echo $REACT_VERSION | cut -d'.' -f2)
|
||||||
|
|
||||||
|
if [ "$REACT_MAJOR" -lt 19 ]; then
|
||||||
|
echo -e "${RED}❌ React $REACT_VERSION (requires 19.2+)${NC}"
|
||||||
|
echo " Upgrade: npm install react@19.2 react-dom@19.2"
|
||||||
|
exit 1
|
||||||
|
elif [ "$REACT_MAJOR" -eq 19 ] && [ "$REACT_MINOR" -lt 2 ]; then
|
||||||
|
echo -e "${YELLOW}⚠️ React $REACT_VERSION (recommends 19.2+)${NC}"
|
||||||
|
echo " Upgrade: npm install react@19.2 react-dom@19.2"
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✅ React $REACT_VERSION${NC}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠️ React not installed (run npm install)${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check TypeScript version (if using TypeScript)
|
||||||
|
if [ -f "tsconfig.json" ]; then
|
||||||
|
echo "📘 TypeScript Version:"
|
||||||
|
if [ -f "node_modules/typescript/package.json" ]; then
|
||||||
|
TS_VERSION=$(node -p "require('./node_modules/typescript/package.json').version")
|
||||||
|
TS_MAJOR=$(echo $TS_VERSION | cut -d'.' -f1)
|
||||||
|
TS_MINOR=$(echo $TS_VERSION | cut -d'.' -f2)
|
||||||
|
|
||||||
|
if [ "$TS_MAJOR" -lt 5 ]; then
|
||||||
|
echo -e "${RED}❌ TypeScript $TS_VERSION (requires 5.1+)${NC}"
|
||||||
|
echo " Upgrade: npm install -D typescript@latest"
|
||||||
|
exit 1
|
||||||
|
elif [ "$TS_MAJOR" -eq 5 ] && [ "$TS_MINOR" -lt 1 ]; then
|
||||||
|
echo -e "${RED}❌ TypeScript $TS_VERSION (requires 5.1+)${NC}"
|
||||||
|
echo " Upgrade: npm install -D typescript@latest"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✅ TypeScript $TS_VERSION${NC}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${YELLOW}⚠️ TypeScript not installed (run npm install)${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for deprecated files
|
||||||
|
echo "🔎 Checking for deprecated patterns..."
|
||||||
|
|
||||||
|
DEPRECATED_FOUND=0
|
||||||
|
|
||||||
|
if [ -f "middleware.ts" ]; then
|
||||||
|
echo -e "${YELLOW}⚠️ middleware.ts found (deprecated in Next.js 16)${NC}"
|
||||||
|
echo " Migrate: Rename to proxy.ts and update function name"
|
||||||
|
DEPRECATED_FOUND=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "middleware.js" ]; then
|
||||||
|
echo -e "${YELLOW}⚠️ middleware.js found (deprecated in Next.js 16)${NC}"
|
||||||
|
echo " Migrate: Rename to proxy.js and update function name"
|
||||||
|
DEPRECATED_FOUND=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for parallel routes missing default.js
|
||||||
|
if [ -d "app" ]; then
|
||||||
|
PARALLEL_ROUTES=$(find app -type d -name '@*' 2>/dev/null)
|
||||||
|
|
||||||
|
if [ ! -z "$PARALLEL_ROUTES" ]; then
|
||||||
|
for route in $PARALLEL_ROUTES; do
|
||||||
|
if [ ! -f "$route/default.tsx" ] && [ ! -f "$route/default.jsx" ] && [ ! -f "$route/default.js" ]; then
|
||||||
|
echo -e "${YELLOW}⚠️ $route missing default.tsx (required in Next.js 16)${NC}"
|
||||||
|
echo " Create: touch $route/default.tsx"
|
||||||
|
DEPRECATED_FOUND=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $DEPRECATED_FOUND -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}✅ No deprecated patterns found${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "📊 Summary:"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
ALL_GOOD=1
|
||||||
|
|
||||||
|
# Node.js check
|
||||||
|
if [ "$NODE_MAJOR" -ge 20 ] && [ "$NODE_MINOR" -ge 9 ]; then
|
||||||
|
echo -e "${GREEN}✅ Node.js compatible${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Node.js incompatible${NC}"
|
||||||
|
ALL_GOOD=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Next.js check
|
||||||
|
if [ -f "node_modules/next/package.json" ]; then
|
||||||
|
if [ "$NEXT_MAJOR" -ge 16 ]; then
|
||||||
|
echo -e "${GREEN}✅ Next.js compatible${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Next.js incompatible${NC}"
|
||||||
|
ALL_GOOD=0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# React check
|
||||||
|
if [ -f "node_modules/react/package.json" ]; then
|
||||||
|
if [ "$REACT_MAJOR" -ge 19 ]; then
|
||||||
|
echo -e "${GREEN}✅ React compatible${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ React incompatible${NC}"
|
||||||
|
ALL_GOOD=0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# TypeScript check (if applicable)
|
||||||
|
if [ -f "tsconfig.json" ] && [ -f "node_modules/typescript/package.json" ]; then
|
||||||
|
if [ "$TS_MAJOR" -ge 5 ] && [ "$TS_MINOR" -ge 1 ]; then
|
||||||
|
echo -e "${GREEN}✅ TypeScript compatible${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ TypeScript incompatible${NC}"
|
||||||
|
ALL_GOOD=0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ $ALL_GOOD -eq 1 ] && [ $DEPRECATED_FOUND -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}🎉 All checks passed! Your project is ready for Next.js 16.${NC}"
|
||||||
|
exit 0
|
||||||
|
elif [ $ALL_GOOD -eq 1 ]; then
|
||||||
|
echo -e "${YELLOW}⚠️ Dependencies compatible, but deprecated patterns found.${NC}"
|
||||||
|
echo "Fix deprecation warnings before migrating to Next.js 16."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo -e "${RED}❌ Compatibility issues found. Fix errors above before continuing.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
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