Next.js 15 Migration Guide: What You Need to Know
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.domainsdeprecated → useimages.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:
- Deploy to staging: Test with staging data
- Internal testing: Team members use production
- Beta users: 5-10% of users
- Gradual increase: 25%, 50%, 100%
- 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.