Skip to main content
Back to Blog

Next.js 15 Migration Guide: What You Need to Know

9 min read
By Eric Mitton
Next.jsWeb DevelopmentReactMigration

Next.js 15 brings significant improvements to performance, developer experience, and capabilities. However, like any major version upgrade, it includes breaking changes that require careful migration. This guide provides a practical, tested approach to upgrading your Next.js applications.

What's New in Next.js 15

Before diving into migration, understand the key changes and their implications.

Major Features and Improvements

React 19 support: Next.js 15 supports React 19, bringing new features like Actions, improved hydration, and better error handling.

Turbopack for development: Faster development server using Turbopack instead of Webpack (opt-in, becoming default).

Enhanced caching strategies: More granular control over caching behavior with new cache configuration options.

Improved static export: Better support for fully static sites with enhanced export capabilities.

Server Actions enhancements: More powerful and easier to use server-side mutations.

Async Request APIs: Request-specific APIs (headers, cookies, params) are now async, preventing accidental runtime errors.

Breaking Changes

Async Request APIs: headers(), cookies(), and dynamic route params must now be awaited.

Route handlers default to dynamic: Previously cached by default, now dynamic by default.

fetch caching changes: fetch requests are no longer cached by default.

Minimum Node.js version: Requires Node.js 18.18 or later.

TypeScript updates: Updated TypeScript types for better type safety.

Pre-Migration Assessment

Before starting, evaluate your application's readiness.

Compatibility Check

Run this command to identify potential issues:

npx @next/codemod@latest upgrade latest

This will analyze your codebase and report compatibility issues.

Dependency Audit

Check your dependencies for Next.js 15 compatibility:

npm outdated
# or
pnpm outdated

Key dependencies to verify:

  • React (must be compatible with React 19)
  • TypeScript (4.5.2 or later)
  • Any Next.js plugins or extensions
  • UI libraries and component frameworks
  • Testing libraries (Jest, Vitest, Playwright)

Create a Migration Branch

Never migrate directly on your main branch:

git checkout -b migration/nextjs-15
git push -u origin migration/nextjs-15

Backup and Testing Strategy

Ensure you have:

  • Recent backup of your codebase
  • Comprehensive test suite
  • Documented manual testing procedures
  • Rollback plan if issues arise

Step-by-Step Migration Process

Follow this systematic approach to minimize issues.

Step 1: Update Dependencies

Update Next.js and React:

pnpm add next@latest react@latest react-dom@latest
# or
npm install next@latest react@latest react-dom@latest

Update TypeScript (if using):

pnpm add -D typescript@latest @types/react@latest @types/node@latest
# or
npm install --save-dev typescript@latest @types/react@latest @types/node@latest

Step 2: Update next.config.js

Update your configuration file to use new syntax:

Before (next.config.js):

module.exports = {
  reactStrictMode: true,
  images: {
    domains: ['example.com'],
  },
};

After (next.config.ts or next.config.mjs):

import type { NextConfig } from 'next';

const config: NextConfig = {
  reactStrictMode: true,
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'example.com',
      },
    ],
  },
};

export default config;

Key changes:

  • images.domains deprecated → use images.remotePatterns
  • Can now use TypeScript for config file
  • ESM format preferred

Step 3: Update Async Request APIs

This is the most common breaking change. Update code that uses headers(), cookies(), or route params.

Before:

import { headers } from 'next/headers';

export default function Page() {
  const headersList = headers();
  const userAgent = headersList.get('user-agent');

  return <div>User Agent: {userAgent}</div>;
}

After:

import { headers } from 'next/headers';

export default async function Page() {
  const headersList = await headers();
  const userAgent = headersList.get('user-agent');

  return <div>User Agent: {userAgent}</div>;
}

Route params before:

export default function Page({ params }: { params: { id: string } }) {
  return <div>Post {params.id}</div>;
}

Route params after:

export default async function Page({
  params
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params;
  return <div>Post {id}</div>;
}

Cookies before:

import { cookies } from 'next/headers';

export default function Page() {
  const cookieStore = cookies();
  const token = cookieStore.get('token');
  return <div>Token: {token?.value}</div>;
}

Cookies after:

import { cookies } from 'next/headers';

export default async function Page() {
  const cookieStore = await cookies();
  const token = cookieStore.get('token');
  return <div>Token: {token?.value}</div>;
}

Step 4: Update Caching Behavior

Next.js 15 changes default caching behavior. Update your code explicitly.

Route handlers - add explicit caching:

// Before: GET requests were cached by default
export async function GET() {
  const data = await fetchData();
  return Response.json(data);
}

// After: Explicitly opt into caching
export async function GET() {
  const data = await fetchData();
  return Response.json(data);
}

// Opt into caching:
export const dynamic = 'force-static';
export const revalidate = 3600; // Revalidate every hour

Fetch requests - add explicit caching:

// Before: fetch was cached by default
const data = await fetch('https://api.example.com/data');

// After: Explicitly opt into caching
const data = await fetch('https://api.example.com/data', {
  cache: 'force-cache', // or 'no-store' for no caching
  next: { revalidate: 3600 } // Revalidate after 1 hour
});

Step 5: Update Metadata API

If using metadata, update to new format:

// Before
export const metadata = {
  title: 'My Page',
  description: 'Page description',
};

// After - same, but with better TypeScript support
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'My Page',
  description: 'Page description',
};

Step 6: Update Image Component

Update any deprecated image configuration:

// Before
<Image
  src="/image.png"
  width={500}
  height={300}
  layout="responsive"
/>

// After
<Image
  src="/image.png"
  width={500}
  height={300}
  style={{ width: '100%', height: 'auto' }}
/>

Step 7: Update Server Actions

Server Actions now have better TypeScript support:

// actions.ts
'use server';

import { z } from 'zod';

const FormSchema = z.object({
  email: z.string().email(),
  message: z.string().min(10),
});

export async function submitForm(formData: FormData) {
  const validatedFields = FormSchema.safeParse({
    email: formData.get('email'),
    message: formData.get('message'),
  });

  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    };
  }

  // Process form...
  return { success: true };
}

Testing Your Migration

Comprehensive testing is essential after migration.

Automated Testing

Update test configurations:

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    setupFiles: ['./vitest.setup.ts'],
  },
  resolve: {
    alias: {
      '@': '/src',
    },
  },
});

Run full test suite:

npm run test
# or
pnpm test

Manual Testing Checklist

  • Home page loads correctly
  • All dynamic routes work
  • Authentication flow functions
  • Forms submit successfully
  • Images load and display properly
  • API routes respond correctly
  • Search functionality works
  • Navigation between pages
  • Error pages display appropriately
  • Mobile responsive behavior
  • Performance is acceptable

Build and Production Testing

Test production build:

pnpm build
pnpm start

Check for:

  • Build completes without errors
  • No warnings about deprecated features
  • Static pages generate correctly
  • Production server starts successfully
  • Application runs in production mode

Performance Testing

Compare before and after:

# Lighthouse audit
npm install -g lighthouse

lighthouse http://localhost:3000 --only-categories=performance

Check:

  • Load times
  • First Contentful Paint
  • Largest Contentful Paint
  • Time to Interactive
  • Bundle sizes

Common Migration Issues and Solutions

Issue: Build Fails with Type Errors

Solution: Update TypeScript types and ensure all async APIs are properly awaited.

pnpm add -D @types/react@latest @types/node@latest

Issue: Image Optimization Errors

Solution: Update next.config.ts with correct remotePatterns:

images: {
  remotePatterns: [
    {
      protocol: 'https',
      hostname: '**.example.com',
      pathname: '/images/**',
    },
  ],
},

Issue: API Routes Return Unexpected Results

Solution: Add explicit caching configuration:

export const dynamic = 'force-dynamic'; // or 'force-static'
export const revalidate = 0; // or your desired revalidation time

Issue: Environment Variables Not Working

Solution: Ensure variables are prefixed with NEXT_PUBLIC_ for client-side access:

# .env.local
NEXT_PUBLIC_API_URL=https://api.example.com
DATABASE_URL=postgresql://... # server-side only

Issue: Hydration Errors

Solution: Ensure server and client render the same content. Use useEffect for client-only logic:

import { useEffect, useState } from 'react';

export default function ClientComponent() {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) return null;

  return <div>{/* client-only content */}</div>;
}

Optimizing for Next.js 15

Take advantage of new features to improve your application.

Enable Turbopack for Development

Update package.json:

{
  "scripts": {
    "dev": "next dev --turbo",
    "build": "next build",
    "start": "next start"
  }
}

Implement Partial Prerendering (Experimental)

// next.config.ts
const config: NextConfig = {
  experimental: {
    ppr: true, // Partial Prerendering
  },
};

Optimize Caching Strategy

// Fine-grained caching control
export const revalidate = 3600; // Revalidate every hour
export const dynamic = 'force-static'; // Force static generation
export const fetchCache = 'default-cache'; // Default fetch cache behavior

Post-Migration Checklist

After completing migration:

  • All tests pass
  • Production build succeeds
  • Manual testing completed
  • Performance metrics acceptable
  • No console errors or warnings
  • Documentation updated
  • Team trained on new features
  • Monitoring and alerts configured
  • Rollback plan documented
  • Deployed to staging environment
  • Stakeholder approval received

Gradual Rollout Strategy

Don't deploy to 100% of users immediately:

  1. Deploy to staging: Test with staging data
  2. Internal testing: Team members use production
  3. Beta users: 5-10% of users
  4. Gradual increase: 25%, 50%, 100%
  5. Monitor closely: Watch error rates, performance

Rollback Procedure

If critical issues arise:

# Revert to previous version
git revert HEAD
git push

# Or rollback to specific commit
git reset --hard <commit-hash>
git push --force

For platforms like Vercel:

  • Use built-in rollback feature
  • Instant revert to previous deployment

Conclusion

Migrating to Next.js 15 requires careful planning and systematic execution, but the improvements in performance, developer experience, and capabilities make it worthwhile. The async Request APIs are the most significant breaking change, but once addressed, most applications migrate smoothly.

Key takeaways:

  • Test thoroughly before deploying
  • Use codemods to automate where possible
  • Update dependencies incrementally
  • Monitor production closely after deployment
  • Document changes for your team

The enhanced performance and new features of Next.js 15 provide a solid foundation for modern web applications. Take your time with the migration, test comprehensively, and you'll be well-positioned to take advantage of everything Next.js 15 offers.


Need help migrating your Next.js application or want expert assistance? Lifestream Dynamics specializes in Next.js development and can ensure your migration is smooth and successful. Contact us to discuss your migration needs.