Initial commit

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

View File

@@ -0,0 +1,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
View 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.

1383
SKILL.md Normal file

File diff suppressed because it is too large Load Diff

85
plugin.lock.json Normal file
View 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": []
}
}

View 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, 25× 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**:
- 25× 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
View 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
View 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

View File

@@ -0,0 +1,282 @@
/**
* Next.js 16 - Async Route Parameters
*
* BREAKING CHANGE: params, searchParams, cookies(), headers(), draftMode()
* are now async and must be awaited in Next.js 16.
*
* This template shows the correct patterns for accessing route parameters,
* search parameters, cookies, and headers in Next.js 16.
*/
import { cookies, headers, draftMode } from 'next/headers'
import { notFound } from 'next/navigation'
// ============================================================================
// Example 1: Page with Async Params
// ============================================================================
interface PageProps {
params: Promise<{ slug: string }>
searchParams: Promise<{ q?: string; page?: string }>
}
export default async function BlogPostPage({ params, searchParams }: PageProps) {
// ✅ Await params and searchParams in Next.js 16
const { slug } = await params
const { q, page } = await searchParams
// Fetch post data
const post = await fetch(`https://api.example.com/posts/${slug}`)
.then(r => r.json())
.catch(() => null)
if (!post) {
notFound()
}
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
{/* Show search query if present */}
{q && <p>Search query: {q}</p>}
{/* Show page number if present */}
{page && <p>Page: {page}</p>}
</article>
)
}
// ============================================================================
// Example 2: Layout with Async Params
// ============================================================================
interface LayoutProps {
children: React.ReactNode
params: Promise<{ category: string }>
}
export async function ProductLayout({ children, params }: LayoutProps) {
// ✅ Await params in layouts too
const { category } = await params
return (
<div>
<nav>
<h2>Category: {category}</h2>
</nav>
<main>{children}</main>
</div>
)
}
// ============================================================================
// Example 3: Accessing Cookies (Async in Next.js 16)
// ============================================================================
export async function UserGreeting() {
// ✅ Await cookies() in Next.js 16
const cookieStore = await cookies()
const userId = cookieStore.get('userId')?.value
const theme = cookieStore.get('theme')?.value || 'light'
if (!userId) {
return <p>Welcome, Guest!</p>
}
const user = await fetch(`https://api.example.com/users/${userId}`)
.then(r => r.json())
return (
<div data-theme={theme}>
<p>Welcome back, {user.name}!</p>
</div>
)
}
// ============================================================================
// Example 4: Accessing Headers (Async in Next.js 16)
// ============================================================================
export async function RequestInfo() {
// ✅ Await headers() in Next.js 16
const headersList = await headers()
const userAgent = headersList.get('user-agent') || 'Unknown'
const referer = headersList.get('referer') || 'Direct'
const ip = headersList.get('x-forwarded-for') || 'Unknown'
return (
<div>
<p>User Agent: {userAgent}</p>
<p>Referrer: {referer}</p>
<p>IP: {ip}</p>
</div>
)
}
// ============================================================================
// Example 5: Draft Mode (Async in Next.js 16)
// ============================================================================
export async function DraftBanner() {
// ✅ Await draftMode() in Next.js 16
const { isEnabled } = await draftMode()
if (!isEnabled) {
return null
}
return (
<div style={{ background: 'yellow', padding: '1rem' }}>
<p>🚧 Draft Mode Enabled</p>
<a href="/api/disable-draft">Exit Draft Mode</a>
</div>
)
}
// ============================================================================
// Example 6: Generate Metadata with Async Params
// ============================================================================
import type { Metadata } from 'next'
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
// ✅ Await params in generateMetadata
const { slug } = await params
const post = await fetch(`https://api.example.com/posts/${slug}`)
.then(r => r.json())
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
}
}
// ============================================================================
// Example 7: Generate Static Params (Async)
// ============================================================================
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts')
.then(r => r.json())
return posts.map((post: { slug: string }) => ({
slug: post.slug,
}))
}
// ============================================================================
// Example 8: Route Handler with Async Params
// ============================================================================
// File: app/api/posts/[id]/route.ts
import { NextResponse } from 'next/server'
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
// ✅ Await params in route handlers
const { id } = await params
const post = await fetch(`https://api.example.com/posts/${id}`)
.then(r => r.json())
return NextResponse.json(post)
}
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
// ✅ Await params in route handlers
const { id } = await params
await fetch(`https://api.example.com/posts/${id}`, {
method: 'DELETE',
})
return NextResponse.json({ message: 'Post deleted' })
}
// ============================================================================
// Migration Guide: Next.js 15 → Next.js 16
// ============================================================================
// ❌ BEFORE (Next.js 15):
/*
export default function Page({ params, searchParams }) {
const slug = params.slug // ❌ Sync access
const query = searchParams.q // ❌ Sync access
}
export function MyComponent() {
const cookieStore = cookies() // ❌ Sync access
const headersList = headers() // ❌ Sync access
}
*/
// ✅ AFTER (Next.js 16):
/*
export default async function Page({ params, searchParams }) {
const { slug } = await params // ✅ Async access
const { q: query } = await searchParams // ✅ Async access
}
export async function MyComponent() {
const cookieStore = await cookies() // ✅ Async access
const headersList = await headers() // ✅ Async access
}
*/
// ============================================================================
// TypeScript Types
// ============================================================================
// Correct types for Next.js 16:
type Params<T = Record<string, string>> = Promise<T>
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>
// Usage:
type ProductPageProps = {
params: Params<{ id: string }>
searchParams: SearchParams
}
// ============================================================================
// Codemod (Automatic Migration)
// ============================================================================
// Run this command to automatically migrate your code:
// npx @next/codemod@canary upgrade latest
/**
* Summary:
*
* 1. ALL route parameters are now async:
* - params → await params
* - searchParams → await searchParams
*
* 2. ALL next/headers functions are now async:
* - cookies() → await cookies()
* - headers() → await headers()
* - draftMode() → await draftMode()
*
* 3. Components using these must be async:
* - export default async function Page({ params }) { ... }
* - export async function Layout({ params }) { ... }
* - export async function generateMetadata({ params }) { ... }
*
* 4. Route handlers must await params:
* - export async function GET(request, { params }) {
* const { id } = await params
* }
*/

View File

@@ -0,0 +1,343 @@
/**
* Next.js 16 - Cache Components with "use cache" Directive
*
* NEW in Next.js 16: Explicit opt-in caching with "use cache" directive.
* Replaces implicit caching from Next.js 15.
*
* This template shows component-level, function-level, and page-level caching.
*/
// ============================================================================
// Example 1: Component-Level Caching
// ============================================================================
'use cache'
// This entire component will be cached
export async function CachedProductList() {
const products = await fetch('https://api.example.com/products')
.then(r => r.json())
return (
<div>
<h2>Products</h2>
<ul>
{products.map((product: { id: string; name: string; price: number }) => (
<li key={product.id}>
{product.name} - ${product.price}
</li>
))}
</ul>
</div>
)
}
// ============================================================================
// Example 2: Function-Level Caching
// ============================================================================
// File: lib/data.ts
'use cache'
export async function getExpensiveData(id: string) {
console.log('Fetching expensive data...') // Only logs on cache miss
// Simulate expensive operation
await new Promise(resolve => setTimeout(resolve, 1000))
const data = await fetch(`https://api.example.com/items/${id}`)
.then(r => r.json())
return data
}
// Usage in component (not cached itself):
import { getExpensiveData } from '@/lib/data'
export async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const product = await getExpensiveData(id) // Cached by function
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
</div>
)
}
// ============================================================================
// Example 3: Page-Level Caching
// ============================================================================
// File: app/blog/[slug]/page.tsx
'use cache'
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts')
.then(r => r.json())
return posts.map((post: { slug: string }) => ({
slug: post.slug,
}))
}
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await fetch(`https://api.example.com/posts/${slug}`)
.then(r => r.json())
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
// ============================================================================
// Example 4: Partial Prerendering (PPR) - Mix Static & Dynamic
// ============================================================================
// File: app/dashboard/page.tsx
// Static component (cached)
'use cache'
async function StaticHeader() {
return (
<header>
<h1>My Dashboard</h1>
<nav>
<a href="/dashboard">Overview</a>
<a href="/dashboard/settings">Settings</a>
</nav>
</header>
)
}
// Dynamic component (NOT cached) - separate file without "use cache"
// File: components/dynamic-user-info.tsx
import { cookies } from 'next/headers'
export async function DynamicUserInfo() {
const cookieStore = await cookies()
const userId = cookieStore.get('userId')?.value
if (!userId) {
return <div>Please log in</div>
}
const user = await fetch(`https://api.example.com/users/${userId}`)
.then(r => r.json())
return (
<div>
<p>Welcome, {user.name}</p>
<p>Balance: ${user.balance}</p>
</div>
)
}
// Page combines static + dynamic (Partial Prerendering)
import { DynamicUserInfo } from '@/components/dynamic-user-info'
export default function DashboardPage() {
return (
<div>
<StaticHeader /> {/* Cached (static) */}
<DynamicUserInfo /> {/* Not cached (dynamic) */}
</div>
)
}
// ============================================================================
// Example 5: Selective Caching with Multiple Functions
// ============================================================================
// Cache expensive operations, skip cheap ones
// Cached function
'use cache'
export async function getPopularPosts() {
const posts = await fetch('https://api.example.com/posts/popular')
.then(r => r.json())
return posts
}
// NOT cached (changes frequently)
export async function getRealtimeMetrics() {
const metrics = await fetch('https://api.example.com/metrics/realtime')
.then(r => r.json())
return metrics
}
// Component uses both
export async function Dashboard() {
const popularPosts = await getPopularPosts() // Cached
const metrics = await getRealtimeMetrics() // NOT cached
return (
<div>
<div>
<h2>Popular Posts</h2>
<ul>
{popularPosts.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
<div>
<h2>Realtime Metrics</h2>
<p>Active users: {metrics.activeUsers}</p>
<p>Requests/min: {metrics.requestsPerMinute}</p>
</div>
</div>
)
}
// ============================================================================
// Example 6: Cache with Revalidation (using tags)
// ============================================================================
// File: app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
await fetch('https://api.example.com/posts', {
method: 'POST',
body: JSON.stringify({ title, content }),
})
// Revalidate cached posts
revalidateTag('posts', 'max')
}
// File: lib/posts.ts
'use cache'
export async function getPosts() {
const response = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] }, // Tag for revalidation
})
return response.json()
}
// ============================================================================
// Example 7: Conditional Caching (Cache Based on User Role)
// ============================================================================
import { cookies } from 'next/headers'
export async function getContent() {
const cookieStore = await cookies()
const userRole = cookieStore.get('role')?.value
if (userRole === 'admin') {
// Don't cache admin content (changes frequently)
return fetch('https://api.example.com/admin/content').then(r => r.json())
}
// Cache public content
return getCachedPublicContent()
}
'use cache'
async function getCachedPublicContent() {
return fetch('https://api.example.com/public/content').then(r => r.json())
}
// ============================================================================
// Example 8: Inline "use cache" (Granular Control)
// ============================================================================
export async function MixedCachingComponent() {
// This function call is cached
const cachedData = await (async function() {
'use cache'
return fetch('https://api.example.com/slow-data').then(r => r.json())
})()
// This function call is NOT cached
const freshData = await fetch('https://api.example.com/fresh-data').then(r => r.json())
return (
<div>
<div>Cached: {cachedData.value}</div>
<div>Fresh: {freshData.value}</div>
</div>
)
}
// ============================================================================
// Migration Guide: Next.js 15 → Next.js 16
// ============================================================================
// ❌ BEFORE (Next.js 15 - Implicit Caching):
/*
// All Server Components were cached by default
export async function MyComponent() {
const data = await fetch('https://api.example.com/data')
return <div>{data.value}</div>
}
// To opt-out of caching:
export const revalidate = 0 // or export const dynamic = 'force-dynamic'
*/
// ✅ AFTER (Next.js 16 - Explicit Opt-In Caching):
/*
// Components are NOT cached by default
export async function MyComponent() {
const data = await fetch('https://api.example.com/data')
return <div>{data.value}</div>
}
// To opt-IN to caching, add "use cache"
'use cache'
export async function MyCachedComponent() {
const data = await fetch('https://api.example.com/data')
return <div>{data.value}</div>
}
*/
// ============================================================================
// Cache Behavior Summary
// ============================================================================
/**
* "use cache" can be added to:
* 1. ✅ Components (entire component cached)
* 2. ✅ Functions (function output cached)
* 3. ✅ Pages (entire page cached)
* 4. ✅ Layouts (layout cached)
* 5. ✅ Inline async functions (granular caching)
*
* Default behavior (without "use cache"):
* - Server Components: NOT cached (change from Next.js 15)
* - fetch() calls: Cached by default (unchanged)
*
* Revalidation:
* - Use revalidateTag() to invalidate cache by tag
* - Use updateTag() for immediate read-your-writes
* - Use refresh() for uncached data only
*
* When to use "use cache":
* ✅ Expensive computations (database queries, API calls)
* ✅ Stable data (product catalogs, blog posts)
* ✅ Partial Prerendering (static header + dynamic user info)
*
* When NOT to use "use cache":
* ❌ Real-time data (metrics, notifications)
* ❌ User-specific data (unless using cookies/headers for cache keys)
* ❌ Frequently changing data (stock prices, live scores)
*/

32
templates/package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "nextjs-16-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"type-check": "tsc --noEmit"
},
"dependencies": {
"next": "^16.0.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.7.0"
},
"optionalDependencies": {
"zod": "^3.24.0",
"@tailwindcss/vite": "^4.1.0",
"tailwindcss": "^4.1.0"
},
"engines": {
"node": ">=20.9.0",
"npm": ">=10.0.0"
}
}

View File

@@ -0,0 +1,437 @@
/**
* Next.js 16 - Parallel Routes with Required default.js
*
* BREAKING CHANGE: Parallel routes now REQUIRE explicit default.js files.
* Without them, routes will fail during soft navigation.
*
* Directory structure:
* app/
* ├── @modal/
* │ ├── login/
* │ │ └── page.tsx
* │ └── default.tsx ← REQUIRED in Next.js 16
* ├── @feed/
* │ ├── trending/
* │ │ └── page.tsx
* │ └── default.tsx ← REQUIRED in Next.js 16
* └── layout.tsx
*/
// ============================================================================
// Example 1: Modal + Main Content (Common Pattern)
// ============================================================================
// File: app/layout.tsx
export default function RootLayout({
children,
modal,
}: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<html>
<body>
{modal}
<main>{children}</main>
</body>
</html>
)
}
// File: app/@modal/login/page.tsx
export default function LoginModal() {
return (
<div className="modal-overlay">
<div className="modal">
<h2>Login</h2>
<form>
<input type="email" placeholder="Email" />
<input type="password" placeholder="Password" />
<button type="submit">Login</button>
</form>
</div>
</div>
)
}
// File: app/@modal/default.tsx (REQUIRED)
export default function ModalDefault() {
return null // No modal shown by default
}
// File: app/page.tsx
export default function HomePage() {
return (
<div>
<h1>Home Page</h1>
<a href="/login">Open Login Modal</a>
</div>
)
}
// ============================================================================
// Example 2: Dashboard with Multiple Panels
// ============================================================================
// File: app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
notifications,
activity,
}: {
children: React.ReactNode
analytics: React.ReactNode
notifications: React.ReactNode
activity: React.ReactNode
}) {
return (
<div className="dashboard-layout">
<aside className="sidebar">
{notifications}
</aside>
<main className="main-content">
{children}
{analytics}
</main>
<aside className="activity-sidebar">
{activity}
</aside>
</div>
)
}
// File: app/dashboard/@analytics/overview/page.tsx
export default async function AnalyticsOverview() {
const stats = await fetch('https://api.example.com/stats').then(r => r.json())
return (
<div className="analytics-panel">
<h2>Analytics</h2>
<div>
<p>Page Views: {stats.pageViews}</p>
<p>Unique Visitors: {stats.uniqueVisitors}</p>
</div>
</div>
)
}
// File: app/dashboard/@analytics/default.tsx (REQUIRED)
export default function AnalyticsDefault() {
return (
<div className="analytics-panel">
<h2>Analytics</h2>
<p>No analytics data available</p>
</div>
)
}
// File: app/dashboard/@notifications/default.tsx (REQUIRED)
export default function NotificationsDefault() {
return (
<div className="notifications-panel">
<h3>Notifications</h3>
<p>No new notifications</p>
</div>
)
}
// File: app/dashboard/@activity/default.tsx (REQUIRED)
export default function ActivityDefault() {
return (
<div className="activity-panel">
<h3>Recent Activity</h3>
<p>No recent activity</p>
</div>
)
}
// ============================================================================
// Example 3: E-commerce with Product + Reviews
// ============================================================================
// File: app/products/[id]/layout.tsx
export default function ProductLayout({
children,
reviews,
recommendations,
}: {
children: React.ReactNode
reviews: React.ReactNode
recommendations: React.ReactNode
}) {
return (
<div className="product-layout">
<div className="product-main">
{children}
</div>
<div className="product-sidebar">
{reviews}
{recommendations}
</div>
</div>
)
}
// File: app/products/[id]/@reviews/page.tsx
export default async function ProductReviews({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const reviews = await fetch(`https://api.example.com/products/${id}/reviews`)
.then(r => r.json())
return (
<div className="reviews">
<h3>Reviews</h3>
<ul>
{reviews.map((review: { id: string; rating: number; comment: string }) => (
<li key={review.id}>
<p> {review.rating}/5</p>
<p>{review.comment}</p>
</li>
))}
</ul>
</div>
)
}
// File: app/products/[id]/@reviews/default.tsx (REQUIRED)
export default function ReviewsDefault() {
return (
<div className="reviews">
<h3>Reviews</h3>
<p>No reviews yet</p>
</div>
)
}
// File: app/products/[id]/@recommendations/default.tsx (REQUIRED)
export default function RecommendationsDefault() {
return (
<div className="recommendations">
<h3>Recommendations</h3>
<p>Loading recommendations...</p>
</div>
)
}
// ============================================================================
// Example 4: Auth-Gated Content
// ============================================================================
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
// File: app/@auth/default.tsx (REQUIRED)
export default async function AuthDefault() {
const cookieStore = await cookies()
const isAuthenticated = cookieStore.get('auth')?.value
if (!isAuthenticated) {
redirect('/login')
}
return null
}
// File: app/@auth/profile/page.tsx
export default async function ProfilePage() {
const cookieStore = await cookies()
const userId = cookieStore.get('userId')?.value
const user = await fetch(`https://api.example.com/users/${userId}`)
.then(r => r.json())
return (
<div>
<h2>Profile</h2>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
</div>
)
}
// ============================================================================
// Example 5: Conditional Rendering Based on Slot
// ============================================================================
// File: app/layout.tsx
export default function Layout({
children,
banner,
}: {
children: React.ReactNode
banner: React.ReactNode
}) {
// Only show banner on specific pages
const showBanner = true // Determine based on route
return (
<html>
<body>
{showBanner && banner}
<main>{children}</main>
</body>
</html>
)
}
// File: app/@banner/sale/page.tsx
export default function SaleBanner() {
return (
<div className="banner sale-banner">
🎉 50% OFF SALE! Use code SALE50
</div>
)
}
// File: app/@banner/default.tsx (REQUIRED)
export default function BannerDefault() {
return null // No banner by default
}
// ============================================================================
// Example 6: Loading States with Parallel Routes
// ============================================================================
// File: app/dashboard/@analytics/loading.tsx
export default function AnalyticsLoading() {
return (
<div className="analytics-panel">
<h2>Analytics</h2>
<p>Loading analytics...</p>
<div className="skeleton-loader" />
</div>
)
}
// File: app/dashboard/@notifications/loading.tsx
export default function NotificationsLoading() {
return (
<div className="notifications-panel">
<h3>Notifications</h3>
<div className="skeleton-loader" />
</div>
)
}
// ============================================================================
// Example 7: Error Boundaries with Parallel Routes
// ============================================================================
// File: app/dashboard/@analytics/error.tsx
'use client'
export default function AnalyticsError({
error,
reset,
}: {
error: Error
reset: () => void
}) {
return (
<div className="analytics-panel error">
<h2>Analytics</h2>
<p>Failed to load analytics</p>
<button onClick={reset}>Try Again</button>
</div>
)
}
// ============================================================================
// Migration Guide: Next.js 15 → Next.js 16
// ============================================================================
/**
* BREAKING CHANGE: default.js is now REQUIRED for all parallel routes
*
* ❌ BEFORE (Next.js 15):
* app/
* ├── @modal/
* │ └── login/
* │ └── page.tsx
* └── layout.tsx
*
* This worked in Next.js 15. If no matching route, Next.js rendered nothing.
*
* ✅ AFTER (Next.js 16):
* app/
* ├── @modal/
* │ ├── login/
* │ │ └── page.tsx
* │ └── default.tsx ← REQUIRED! Will error without this
* └── layout.tsx
*
* Why the change?
* Next.js 16 changed how parallel routes handle soft navigation. Without
* default.js, unmatched slots will error during client-side navigation.
*
* What should default.tsx return?
* - return null (most common - no UI shown)
* - return <Skeleton /> (loading placeholder)
* - redirect() to another route
* - return fallback UI
*/
// ============================================================================
// Common Patterns for default.tsx
// ============================================================================
// Pattern 1: Null (no UI)
export function DefaultNull() {
return null
}
// Pattern 2: Loading skeleton
export function DefaultSkeleton() {
return (
<div className="skeleton">
<div className="skeleton-line" />
<div className="skeleton-line" />
<div className="skeleton-line" />
</div>
)
}
// Pattern 3: Fallback message
export function DefaultFallback() {
return (
<div>
<p>Content not available</p>
</div>
)
}
// Pattern 4: Redirect
import { redirect } from 'next/navigation'
export function DefaultRedirect() {
redirect('/dashboard')
}
/**
* Summary:
*
* Parallel Routes in Next.js 16:
* 1. ✅ Use @folder convention for parallel slots
* 2. ✅ MUST include default.tsx for each @folder
* 3. ✅ default.tsx handles unmatched routes during navigation
* 4. ✅ Can have loading.tsx for loading states
* 5. ✅ Can have error.tsx for error boundaries
*
* Common use cases:
* - Modals + main content
* - Dashboard panels
* - Product + reviews/recommendations
* - Conditional banners
* - Auth-gated content
*
* Best practices:
* - Keep default.tsx simple (usually return null)
* - Use loading.tsx for better UX
* - Use error.tsx for error handling
* - Test soft navigation (client-side routing)
*/

View File

@@ -0,0 +1,274 @@
/**
* Next.js 16 - Proxy Migration (middleware.ts → proxy.ts)
*
* BREAKING CHANGE: middleware.ts is deprecated in Next.js 16.
* Use proxy.ts instead.
*
* Migration: Rename file and function, keep same logic.
*/
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// ============================================================================
// Example 1: Basic Proxy (Auth Check)
// ============================================================================
export function proxy(request: NextRequest) {
const token = request.cookies.get('token')
// Redirect to login if no token
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: '/dashboard/:path*',
}
// ============================================================================
// Example 2: Advanced Proxy (Multiple Checks)
// ============================================================================
export function advancedProxy(request: NextRequest) {
const { pathname } = request.nextUrl
// 1. Auth check
const token = request.cookies.get('token')
if (pathname.startsWith('/dashboard') && !token) {
return NextResponse.redirect(new URL('/login', request.url))
}
// 2. Role-based access
const userRole = request.cookies.get('role')?.value
if (pathname.startsWith('/admin') && userRole !== 'admin') {
return NextResponse.redirect(new URL('/unauthorized', request.url))
}
// 3. Add custom headers
const response = NextResponse.next()
response.headers.set('x-custom-header', 'value')
response.headers.set('x-pathname', pathname)
return response
}
export const advancedConfig = {
matcher: ['/dashboard/:path*', '/admin/:path*'],
}
// ============================================================================
// Example 3: Request Rewriting
// ============================================================================
export function rewriteProxy(request: NextRequest) {
// Rewrite /blog/* to /posts/*
if (request.nextUrl.pathname.startsWith('/blog')) {
const url = request.nextUrl.clone()
url.pathname = url.pathname.replace('/blog', '/posts')
return NextResponse.rewrite(url)
}
return NextResponse.next()
}
export const rewriteConfig = {
matcher: '/blog/:path*',
}
// ============================================================================
// Example 4: Geolocation-Based Routing
// ============================================================================
export function geoProxy(request: NextRequest) {
const country = request.geo?.country || 'US'
const url = request.nextUrl.clone()
// Redirect to country-specific page
if (url.pathname === '/') {
url.pathname = `/${country.toLowerCase()}`
return NextResponse.rewrite(url)
}
return NextResponse.next()
}
// ============================================================================
// Example 5: A/B Testing
// ============================================================================
export function abTestProxy(request: NextRequest) {
const bucket = request.cookies.get('bucket')?.value
if (!bucket) {
// Assign to A or B randomly
const newBucket = Math.random() < 0.5 ? 'a' : 'b'
const response = NextResponse.next()
response.cookies.set('bucket', newBucket, {
maxAge: 60 * 60 * 24 * 30, // 30 days
})
// Rewrite to variant page
if (newBucket === 'b') {
const url = request.nextUrl.clone()
url.pathname = `/variant-b${url.pathname}`
return NextResponse.rewrite(url)
}
return response
}
// Existing user
if (bucket === 'b') {
const url = request.nextUrl.clone()
url.pathname = `/variant-b${url.pathname}`
return NextResponse.rewrite(url)
}
return NextResponse.next()
}
export const abTestConfig = {
matcher: '/',
}
// ============================================================================
// Example 6: Rate Limiting
// ============================================================================
const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
export function rateLimitProxy(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for') || 'unknown'
const now = Date.now()
// Check rate limit (100 requests per minute)
const rateLimit = rateLimitMap.get(ip)
if (rateLimit) {
if (now < rateLimit.resetAt) {
if (rateLimit.count >= 100) {
return new NextResponse('Too Many Requests', {
status: 429,
headers: {
'Retry-After': String(Math.ceil((rateLimit.resetAt - now) / 1000)),
},
})
}
rateLimit.count++
} else {
rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 }) // 1 minute
}
} else {
rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 })
}
return NextResponse.next()
}
export const rateLimitConfig = {
matcher: '/api/:path*',
}
// ============================================================================
// Example 7: Response Modification
// ============================================================================
export function modifyResponseProxy(request: NextRequest) {
const response = NextResponse.next()
// Add security headers
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('Referrer-Policy', 'origin-when-cross-origin')
response.headers.set(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=()'
)
return response
}
// ============================================================================
// Migration Guide: middleware.ts → proxy.ts
// ============================================================================
/**
* ❌ BEFORE (Next.js 15):
*
* // File: middleware.ts
* import { NextResponse } from 'next/server'
* import type { NextRequest } from 'next/server'
*
* export function middleware(request: NextRequest) {
* const token = request.cookies.get('token')
* if (!token) {
* return NextResponse.redirect(new URL('/login', request.url))
* }
* return NextResponse.next()
* }
*
* export const config = {
* matcher: '/dashboard/:path*',
* }
*/
/**
* ✅ AFTER (Next.js 16):
*
* // File: proxy.ts
* import { NextResponse } from 'next/server'
* import type { NextRequest } from 'next/server'
*
* export function proxy(request: NextRequest) {
* const token = request.cookies.get('token')
* if (!token) {
* return NextResponse.redirect(new URL('/login', request.url))
* }
* return NextResponse.next()
* }
*
* export const config = {
* matcher: '/dashboard/:path*',
* }
*/
/**
* Migration Steps:
* 1. Rename file: middleware.ts → proxy.ts
* 2. Rename function: middleware → proxy
* 3. Keep config object the same
* 4. Logic remains identical
*
* Why the change?
* - proxy.ts runs on Node.js runtime (full Node.js APIs)
* - middleware.ts ran on Edge runtime (limited APIs)
* - proxy.ts makes the network boundary explicit
*
* Note: middleware.ts still works in Next.js 16 but is deprecated.
* Migrate to proxy.ts for future compatibility.
*/
/**
* Summary:
*
* Proxy patterns:
* 1. ✅ Auth checks and redirects
* 2. ✅ Role-based access control
* 3. ✅ Custom headers
* 4. ✅ Request rewriting (URL rewrites)
* 5. ✅ Geolocation-based routing
* 6. ✅ A/B testing
* 7. ✅ Rate limiting
* 8. ✅ Response modification (security headers)
*
* Best practices:
* - Keep proxy logic lightweight (runs on every request)
* - Use matcher to limit scope
* - Avoid database queries (use cookies/headers instead)
* - Cache rate limit data in memory (or Redis for production)
* - Return NextResponse.next() if no action needed
*/

View File

@@ -0,0 +1,393 @@
/**
* Next.js 16 - Route Handlers (API Endpoints)
*
* Route Handlers replace API Routes from Pages Router.
* File: app/api/[...]/route.ts
*/
import { NextResponse } from 'next/server'
import { cookies, headers } from 'next/headers'
// ============================================================================
// Example 1: Basic CRUD API
// ============================================================================
// GET /api/posts
export async function GET() {
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return NextResponse.json(posts)
}
// POST /api/posts
export async function POST(request: Request) {
const body = await request.json()
const post = await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(r => r.json())
return NextResponse.json(post, { status: 201 })
}
// ============================================================================
// Example 2: Dynamic Routes
// ============================================================================
// File: app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params // ✅ Await params in Next.js 16
const post = await fetch(`https://api.example.com/posts/${id}`)
.then(r => r.json())
.catch(() => null)
if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
)
}
return NextResponse.json(post)
}
export async function PATCH(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const body = await request.json()
const updated = await fetch(`https://api.example.com/posts/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).then(r => r.json())
return NextResponse.json(updated)
}
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
await fetch(`https://api.example.com/posts/${id}`, {
method: 'DELETE',
})
return NextResponse.json({ message: 'Post deleted' }, { status: 200 })
}
// ============================================================================
// Example 3: Search with Query Parameters
// ============================================================================
// GET /api/search?q=nextjs&limit=10&page=1
export async function SEARCH(request: Request) {
const { searchParams } = new URL(request.url)
const query = searchParams.get('q') || ''
const limit = parseInt(searchParams.get('limit') || '10')
const page = parseInt(searchParams.get('page') || '1')
const offset = (page - 1) * limit
const results = await fetch(
`https://api.example.com/search?q=${query}&limit=${limit}&offset=${offset}`
).then(r => r.json())
return NextResponse.json({
results: results.items,
total: results.total,
page,
limit,
})
}
// ============================================================================
// Example 4: Authentication with Cookies
// ============================================================================
// POST /api/auth/login
export async function LOGIN(request: Request) {
const { email, password } = await request.json()
// Verify credentials
const user = await fetch('https://api.example.com/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
}).then(r => r.json())
if (!user.token) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
)
}
// Set cookie
const response = NextResponse.json({ success: true })
response.cookies.set('token', user.token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7, // 7 days
})
return response
}
// GET /api/auth/me
export async function ME() {
const cookieStore = await cookies() // ✅ Await cookies in Next.js 16
const token = cookieStore.get('token')?.value
if (!token) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
const user = await fetch('https://api.example.com/auth/me', {
headers: { Authorization: `Bearer ${token}` },
}).then(r => r.json())
return NextResponse.json(user)
}
// ============================================================================
// Example 5: Webhook Handler
// ============================================================================
// File: app/api/webhooks/stripe/route.ts
import { headers as getHeaders } from 'next/headers'
export async function WEBHOOK(request: Request) {
const body = await request.text()
const headersList = await getHeaders() // ✅ Await headers in Next.js 16
const signature = headersList.get('stripe-signature')
if (!signature) {
return NextResponse.json(
{ error: 'Missing signature' },
{ status: 400 }
)
}
// Verify webhook signature (example with Stripe)
let event
try {
event = JSON.parse(body)
// In production: stripe.webhooks.constructEvent(body, signature, secret)
} catch (err) {
return NextResponse.json(
{ error: 'Invalid payload' },
{ status: 400 }
)
}
// Handle event
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object)
break
case 'payment_intent.failed':
await handlePaymentFailure(event.data.object)
break
default:
console.log(`Unhandled event type: ${event.type}`)
}
return NextResponse.json({ received: true })
}
async function handlePaymentSuccess(paymentIntent: any) {
console.log('Payment succeeded:', paymentIntent.id)
// Update database, send confirmation email, etc.
}
async function handlePaymentFailure(paymentIntent: any) {
console.log('Payment failed:', paymentIntent.id)
// Notify user, log error, etc.
}
// ============================================================================
// Example 6: Streaming Response
// ============================================================================
// GET /api/stream
export async function STREAM() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
const data = `data: ${JSON.stringify({ count: i })}\n\n`
controller.enqueue(encoder.encode(data))
await new Promise(resolve => setTimeout(resolve, 1000))
}
controller.close()
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
})
}
// ============================================================================
// Example 7: File Upload
// ============================================================================
// POST /api/upload
import { writeFile } from 'fs/promises'
import { join } from 'path'
export async function UPLOAD(request: Request) {
const formData = await request.formData()
const file = formData.get('file') as File
if (!file) {
return NextResponse.json(
{ error: 'No file provided' },
{ status: 400 }
)
}
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
const filename = `${Date.now()}-${file.name}`
const path = join(process.cwd(), 'public', 'uploads', filename)
await writeFile(path, buffer)
return NextResponse.json({
success: true,
url: `/uploads/${filename}`,
})
}
// ============================================================================
// Example 8: CORS Configuration
// ============================================================================
export async function OPTIONS() {
return new NextResponse(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}
export async function CORS_GET() {
const response = NextResponse.json({ message: 'Hello' })
response.headers.set('Access-Control-Allow-Origin', '*')
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
return response
}
// ============================================================================
// Example 9: Error Handling
// ============================================================================
export async function ERROR_HANDLING() {
try {
const data = await fetch('https://api.example.com/data')
.then(r => {
if (!r.ok) throw new Error('API request failed')
return r.json()
})
return NextResponse.json(data)
} catch (error) {
console.error('Error:', error)
return NextResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
)
}
}
// ============================================================================
// Example 10: Rate Limiting
// ============================================================================
const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
export async function RATE_LIMITED() {
const headersList = await headers()
const ip = headersList.get('x-forwarded-for') || 'unknown'
const now = Date.now()
const rateLimit = rateLimitMap.get(ip)
if (rateLimit) {
if (now < rateLimit.resetAt) {
if (rateLimit.count >= 10) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429 }
)
}
rateLimit.count++
} else {
rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 })
}
} else {
rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 })
}
return NextResponse.json({ message: 'Success' })
}
/**
* Summary:
*
* Route Handlers (app/api/*/route.ts):
* 1. Support all HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS)
* 2. Await params in Next.js 16
* 3. Access cookies with await cookies()
* 4. Access headers with await headers()
* 5. Use NextResponse.json() for JSON responses
* 6. Return Response or NextResponse
*
* Common patterns:
* - CRUD operations (GET, POST, PATCH, DELETE)
* - Query parameters with searchParams
* - Authentication with cookies
* - Webhooks with signature verification
* - Streaming responses (SSE, WebSocket)
* - File uploads with FormData
* - CORS configuration
* - Error handling
* - Rate limiting
*
* Best practices:
* - Use try/catch for error handling
* - Return appropriate HTTP status codes
* - Validate input data
* - Set secure cookie options in production
* - Add rate limiting for public endpoints
* - Use CORS headers when needed
*/

View File

@@ -0,0 +1,534 @@
/**
* Next.js 16 - Server Actions for Form Handling
*
* This template shows comprehensive Server Actions patterns including:
* - Basic form handling
* - Validation with Zod
* - Loading states
* - Error handling
* - Optimistic updates
* - File uploads
*/
'use server'
import { z } from 'zod'
import { revalidateTag } from 'next/cache'
import { redirect } from 'next/navigation'
// ============================================================================
// Example 1: Basic Server Action
// ============================================================================
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
// Save to database
await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content }),
})
// Revalidate cache
revalidateTag('posts', 'max')
// Redirect to posts list
redirect('/posts')
}
// Basic form component
export function CreatePostForm() {
return (
<form action={createPost}>
<label>
Title:
<input type="text" name="title" required />
</label>
<label>
Content:
<textarea name="content" required />
</label>
<button type="submit">Create Post</button>
</form>
)
}
// ============================================================================
// Example 2: Server Action with Validation
// ============================================================================
const PostSchema = z.object({
title: z.string().min(3, 'Title must be at least 3 characters'),
content: z.string().min(10, 'Content must be at least 10 characters'),
tags: z.array(z.string()).min(1, 'At least one tag is required'),
})
type ActionState = {
errors?: {
title?: string[]
content?: string[]
tags?: string[]
_form?: string[]
}
success?: boolean
}
export async function createPostWithValidation(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
// Parse form data
const rawData = {
title: formData.get('title'),
content: formData.get('content'),
tags: formData.get('tags')?.toString().split(',') || [],
}
// Validate
const parsed = PostSchema.safeParse(rawData)
if (!parsed.success) {
return {
errors: parsed.error.flatten().fieldErrors,
}
}
// Save to database
try {
await fetch('https://api.example.com/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(parsed.data),
})
revalidateTag('posts', 'max')
return { success: true }
} catch (error) {
return {
errors: {
_form: ['Failed to create post. Please try again.'],
},
}
}
}
// Form with validation errors
'use client'
import { useFormState, useFormStatus } from 'react-dom'
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? 'Creating...' : 'Create Post'}
</button>
)
}
export function ValidatedPostForm() {
const [state, formAction] = useFormState(createPostWithValidation, {})
return (
<form action={formAction}>
<div>
<label>
Title:
<input type="text" name="title" required />
</label>
{state.errors?.title && (
<p className="error">{state.errors.title[0]}</p>
)}
</div>
<div>
<label>
Content:
<textarea name="content" required />
</label>
{state.errors?.content && (
<p className="error">{state.errors.content[0]}</p>
)}
</div>
<div>
<label>
Tags (comma-separated):
<input type="text" name="tags" placeholder="nextjs, react, tutorial" />
</label>
{state.errors?.tags && (
<p className="error">{state.errors.tags[0]}</p>
)}
</div>
{state.errors?._form && (
<p className="error">{state.errors._form[0]}</p>
)}
{state.success && (
<p className="success">Post created successfully!</p>
)}
<SubmitButton />
</form>
)
}
// ============================================================================
// Example 3: Server Action with Optimistic Updates
// ============================================================================
'use server'
import { updateTag } from 'next/cache'
export async function likePost(postId: string) {
await fetch(`https://api.example.com/posts/${postId}/like`, {
method: 'POST',
})
// Use updateTag for immediate refresh (read-your-writes)
updateTag('posts')
}
// Client component with optimistic updates
'use client'
import { useOptimistic } from 'react'
import { likePost } from './actions'
export function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
const [optimisticLikes, addOptimisticLike] = useOptimistic(
initialLikes,
(state, amount: number) => state + amount
)
async function handleLike() {
// Update UI immediately
addOptimisticLike(1)
// Sync with server
await likePost(postId)
}
return (
<button onClick={handleLike}>
{optimisticLikes} likes
</button>
)
}
// ============================================================================
// Example 4: Server Action with File Upload
// ============================================================================
'use server'
import { writeFile } from 'fs/promises'
import { join } from 'path'
export async function uploadImage(formData: FormData) {
const file = formData.get('image') as File
if (!file) {
return { error: 'No file provided' }
}
// Validate file type
const validTypes = ['image/jpeg', 'image/png', 'image/webp']
if (!validTypes.includes(file.type)) {
return { error: 'Invalid file type. Only JPEG, PNG, and WebP are allowed.' }
}
// Validate file size (max 5MB)
const maxSize = 5 * 1024 * 1024 // 5MB
if (file.size > maxSize) {
return { error: 'File too large. Maximum size is 5MB.' }
}
// Save file
const bytes = await file.arrayBuffer()
const buffer = Buffer.from(bytes)
const filename = `${Date.now()}-${file.name}`
const path = join(process.cwd(), 'public', 'uploads', filename)
await writeFile(path, buffer)
return {
success: true,
url: `/uploads/${filename}`,
}
}
// File upload form
'use client'
import { useState } from 'react'
import { uploadImage } from './actions'
export function ImageUploadForm() {
const [preview, setPreview] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
async function handleSubmit(formData: FormData) {
const result = await uploadImage(formData)
if (result.error) {
setError(result.error)
} else if (result.url) {
setPreview(result.url)
setError(null)
}
}
return (
<form action={handleSubmit}>
<input
type="file"
name="image"
accept="image/jpeg,image/png,image/webp"
required
/>
<button type="submit">Upload Image</button>
{error && <p className="error">{error}</p>}
{preview && (
<div>
<h3>Uploaded Image:</h3>
<img src={preview} alt="Uploaded" width={300} />
</div>
)}
</form>
)
}
// ============================================================================
// Example 5: Server Action with Progressive Enhancement
// ============================================================================
'use server'
export async function subscribe(formData: FormData) {
const email = formData.get('email') as string
await fetch('https://api.example.com/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
})
return { success: true, message: 'Subscribed successfully!' }
}
// Form works without JavaScript (progressive enhancement)
export function SubscribeForm() {
return (
<form action={subscribe}>
<input
type="email"
name="email"
placeholder="Enter your email"
required
/>
<button type="submit">Subscribe</button>
</form>
)
}
// Enhanced with JavaScript
'use client'
import { useFormState } from 'react-dom'
export function EnhancedSubscribeForm() {
const [state, formAction] = useFormState(subscribe, null)
return (
<form action={formAction}>
<input
type="email"
name="email"
placeholder="Enter your email"
required
/>
<button type="submit">Subscribe</button>
{state?.success && (
<p className="success">{state.message}</p>
)}
</form>
)
}
// ============================================================================
// Example 6: Server Action with Multi-Step Form
// ============================================================================
'use server'
type Step1Data = { name: string; email: string }
type Step2Data = { address: string; city: string }
type FormData = Step1Data & Step2Data
export async function submitMultiStepForm(data: FormData) {
// Validate all data
const schema = z.object({
name: z.string().min(2),
email: z.string().email(),
address: z.string().min(5),
city: z.string().min(2),
})
const parsed = schema.safeParse(data)
if (!parsed.success) {
return {
errors: parsed.error.flatten().fieldErrors,
}
}
// Save to database
await fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(parsed.data),
})
return { success: true }
}
// Multi-step form component
'use client'
import { useState } from 'react'
export function MultiStepForm() {
const [step, setStep] = useState(1)
const [formData, setFormData] = useState<Partial<FormData>>({})
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
if (step === 1) {
const form = e.currentTarget
setFormData({
...formData,
name: (form.elements.namedItem('name') as HTMLInputElement).value,
email: (form.elements.namedItem('email') as HTMLInputElement).value,
})
setStep(2)
} else {
const form = e.currentTarget
const finalData = {
...formData,
address: (form.elements.namedItem('address') as HTMLInputElement).value,
city: (form.elements.namedItem('city') as HTMLInputElement).value,
} as FormData
const result = await submitMultiStepForm(finalData)
if (result.success) {
alert('Form submitted successfully!')
}
}
}
return (
<form onSubmit={handleSubmit}>
{step === 1 && (
<>
<h2>Step 1: Personal Info</h2>
<input name="name" placeholder="Name" defaultValue={formData.name} required />
<input name="email" type="email" placeholder="Email" defaultValue={formData.email} required />
<button type="submit">Next</button>
</>
)}
{step === 2 && (
<>
<h2>Step 2: Address</h2>
<input name="address" placeholder="Address" defaultValue={formData.address} required />
<input name="city" placeholder="City" defaultValue={formData.city} required />
<button type="button" onClick={() => setStep(1)}>Back</button>
<button type="submit">Submit</button>
</>
)}
</form>
)
}
// ============================================================================
// Example 7: Server Action with Rate Limiting
// ============================================================================
'use server'
import { headers } from 'next/headers'
const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
export async function submitContactForm(formData: FormData) {
const headersList = await headers()
const ip = headersList.get('x-forwarded-for') || 'unknown'
// Check rate limit (5 requests per hour)
const now = Date.now()
const rateLimit = rateLimitMap.get(ip)
if (rateLimit) {
if (now < rateLimit.resetAt) {
if (rateLimit.count >= 5) {
return { error: 'Too many requests. Please try again later.' }
}
rateLimit.count++
} else {
// Reset rate limit
rateLimitMap.set(ip, { count: 1, resetAt: now + 3600000 }) // 1 hour
}
} else {
rateLimitMap.set(ip, { count: 1, resetAt: now + 3600000 })
}
// Process form
const name = formData.get('name') as string
const message = formData.get('message') as string
await fetch('https://api.example.com/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, message }),
})
return { success: true }
}
/**
* Summary:
*
* Server Actions patterns:
* 1. ✅ Basic form handling with formData
* 2. ✅ Validation with Zod (safeParse)
* 3. ✅ Loading states with useFormStatus
* 4. ✅ Error handling with useFormState
* 5. ✅ Optimistic updates with useOptimistic
* 6. ✅ File uploads
* 7. ✅ Progressive enhancement (works without JS)
* 8. ✅ Multi-step forms
* 9. ✅ Rate limiting
*
* Best practices:
* - Always validate on server (never trust client input)
* - Use revalidateTag() for background revalidation
* - Use updateTag() for immediate refresh (forms, settings)
* - Return errors instead of throwing (better UX)
* - Use TypeScript for type safety
* - Add rate limiting for public forms
*/