# 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
