How to Migrate from Create React App to Next.js

Type: Software Reference Confidence: 0.93 Sources: 8 Verified: 2026-02-23 Freshness: monthly

TL;DR

Constraints

Quick Reference

CRA PatternNext.js EquivalentExample
react-scripts startnext devnpm run dev uses Turbopack by default (Next.js 16)
react-scripts buildnext buildOutputs to .next/ (or build/ with distDir); Turbopack default in 16
public/index.htmlapp/layout.tsx (Root Layout)Metadata API replaces manual <head> tags
src/index.tsx (ReactDOM.render)app/page.tsx + 'use client' wrapperEntry point becomes a Server Component page
react-router-dom <Route>File-based routing (app/{route}/page.tsx)app/dashboard/page.tsx = /dashboard
<Link to="/path"><Link href="/path">import Link from 'next/link'
useNavigate() / useHistory()useRouter() from next/navigationconst router = useRouter(); router.push('/path')
useParams() (react-router)useParams() from next/navigationDynamic route: app/users/[id]/page.tsx
REACT_APP_* env varsNEXT_PUBLIC_* env varsNEXT_PUBLIC_API_URL exposed to browser
proxy in package.jsonrewrites() in next.config.ts{ source: '/api/:path*', destination: 'http://backend:3001/:path*' }
import img from './image.png' (string)import img from './image.png' (object with .src)Use <img src={img.src} /> or <Image src={img} />
CSS Modules (*.module.css)CSS Modules (same, built-in)No change needed
import './global.css' in index.tsximport './global.css' in app/layout.tsxGlobal CSS must be imported in layout
reportWebVitals()Built-in Next.js analytics or @next/third-partiesRemove CRA’s web vitals setup
Custom webpack in eject / cracowebpack key in next.config.ts (requires --webpack in 16)webpack: (config) => { ... return config }
homepage field in package.jsonbasePath in next.config.tsbasePath: '/my-app'
middleware.ts (Next.js 15)proxy.ts (Next.js 16)Renamed: export proxy(request) instead of middleware(request)

Decision Tree

START
├── Is the CRA app purely client-side (SPA) with no SSR needs?
│   ├── YES → Use output: 'export' in next.config.ts for static SPA
│   └── NO ↓
├── Does the app use react-router-dom?
│   ├── YES → Phase 1: Keep react-router via catch-all route, Phase 2: Migrate to file-based routing
│   └── NO ↓
├── Does the app need server-side rendering or API routes?
│   ├── YES → Remove output: 'export', create API routes in app/api/, use Server Components
│   └── NO ↓
├── Does the app use CRACO or ejected webpack config?
│   ├── YES → Use --webpack flag in Next.js 16, map custom webpack plugins to next.config.ts
│   └── NO ↓
├── Does the app rely heavily on window/document APIs?
│   ├── YES → Wrap those components with dynamic(() => import(...), { ssr: false })
│   └── NO ↓
├── Targeting Next.js 16 specifically?
│   ├── YES → Ensure Node.js >= 20.9, await all params/searchParams, rename middleware.ts to proxy.ts
│   └── NO (Next.js 15) → Synchronous params still work with deprecation warnings
└── DEFAULT → Follow the 8-step migration, start with SPA mode, adopt features incrementally

Step-by-Step Guide

1. Install Next.js and update scripts

Add Next.js alongside your existing CRA dependencies. Do not remove react-scripts yet. [src1]

npm install next@latest

Update package.json scripts:

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "dev:old": "react-scripts start"
  }
}

Add .next, .next/dev, and next-env.d.ts to .gitignore. (Next.js 16 uses .next/dev for development output.)

Verify: npx next --version shows the installed version (15.x+ or 16.x+).

2. Create next.config.ts

Start with static export mode to match CRA's SPA behavior. [src1]

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  output: 'export',   // Static SPA mode (matches CRA behavior)
  distDir: 'build',   // Match CRA's output directory
}

export default nextConfig

Note: If you have a custom webpack config and are using Next.js 16, Turbopack is the default. Add --webpack to your build script or migrate to Turbopack-compatible options. [src5]

Verify: File exists at project root alongside package.json.

3. Create the root layout

Replace public/index.html with app/layout.tsx. [src1, src4]

// app/layout.tsx
import type { Metadata } from 'next'
import '../src/index.css'

export const metadata: Metadata = {
  title: 'My App',
  description: 'Migrated from CRA to Next.js',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <div id="root">{children}</div>
      </body>
    </html>
  )
}

Verify: Compare with your public/index.html — all <meta> tags accounted for via the Metadata API.

4. Create the catch-all entrypoint page

Use an optional catch-all route to handle all URLs, preserving your existing react-router. [src1, src2]

// app/[[...slug]]/page.tsx
import { ClientOnly } from './client'

export function generateStaticParams() {
  return [{ slug: [''] }]
}

export default function Page() {
  return <ClientOnly />
}

// app/[[...slug]]/client.tsx
'use client'

import dynamic from 'next/dynamic'

const App = dynamic(() => import('../../src/App'), { ssr: false })

export function ClientOnly() {
  return <App />
}

Verify: npm run dev → app loads at http://localhost:3000 with existing routing intact.

5. Rename REACT_APP_ environment variables to NEXT_PUBLIC_

Next.js requires the NEXT_PUBLIC_ prefix for client-side environment variables. [src1]

# .env
# BEFORE
REACT_APP_API_URL=https://api.example.com

# AFTER
NEXT_PUBLIC_API_URL=https://api.example.com

Verify: grep -rn 'REACT_APP_' --include='*.ts' --include='*.tsx' returns zero results.

6. Fix static image imports

CRA image imports return a string URL. Next.js imports return an object with a .src property. [src1]

// BEFORE (CRA): import returns string
import logo from './logo.png'
<img src={logo} alt="Logo" />

// AFTER (Next.js): import returns object
import logo from './logo.png'
<img src={logo.src} alt="Logo" />

// OR use next/image for optimization
import Image from 'next/image'
<Image src={logo} alt="Logo" />

Verify: All images render correctly. Check Network tab for 404s.

7. Replace CRA proxy with Next.js rewrites

Replace proxy in package.json with Next.js rewrites. [src1]

// next.config.ts
const nextConfig: NextConfig = {
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: 'http://localhost:3001/api/:path*',
      },
    ]
  },
}

Note: In Next.js 16, if you need request-level middleware logic, create a proxy.ts file (renamed from middleware.ts). [src5]

Verify: API requests proxied correctly in the Network tab.

8. Clean up CRA artifacts and uninstall react-scripts

Remove CRA-specific files and dependencies. [src1, src4]

npm uninstall react-scripts
rm public/index.html
rm src/index.tsx
rm src/react-app-env.d.ts
rm src/reportWebVitals.ts

Verify: npm run build completes without errors. npm run dev starts cleanly.

Code Examples

TypeScript/Next.js: Migrating React Router routes to file-based routing

Full script: typescript-next-js-migrating-a-react-router-app-to.tsx (46 lines)

// Input:  CRA app with react-router-dom route definitions
// Output: Equivalent Next.js App Router file structure

// BEFORE: CRA with react-router-dom (src/App.tsx)
// <BrowserRouter>
//   <Routes>
//     <Route path="/" element={<Home />} />
//     <Route path="/dashboard" element={<Dashboard />} />
//     <Route path="/users/:id" element={<UserProfile />} />
//   </Routes>
// </BrowserRouter>

// AFTER: Next.js App Router
// app/page.tsx → /
export default function Home() {
  return <h1>Home Page</h1>
}

// app/dashboard/page.tsx → /dashboard
'use client'
export default function Dashboard() {
  return <h1>Dashboard</h1>
}

// app/users/[id]/page.tsx → /users/:id (Next.js 16: async params)
export default async function UserProfile({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  return <h1>User {id}</h1>
}

TypeScript/Next.js: Converting CRA navigation patterns

Full script: typescript-next-js-converting-cra-navigation-patte.tsx (38 lines)

// Input:  CRA components using react-router hooks
// Output: Next.js equivalents using next/navigation

'use client'

import { useRouter, useParams, usePathname, useSearchParams } from 'next/navigation'
import Link from 'next/link'

function MyComponent() {
  const router = useRouter()
  const params = useParams()             // { id: '123' }
  const pathname = usePathname()         // '/users/123'
  const searchParams = useSearchParams() // URLSearchParams

  return (
    <div>
      {/* Programmatic navigation */}
      <button onClick={() => router.push('/dashboard')}>Go</button>

      {/* Declarative navigation (preferred) */}
      <Link href="/dashboard">Dashboard</Link>

      {/* Replace instead of push */}
      <button onClick={() => router.replace('/login')}>Login</button>

      {/* Go back */}
      <button onClick={() => router.back()}>Back</button>
    </div>
  )
}

export default MyComponent

TypeScript/Next.js: Wrapping browser-only code for SSR safety

Full script: typescript-next-js-wrapping-browser-only-code-for-.tsx (36 lines)

// Input:  CRA component using window/document/localStorage
// Output: Next.js component with SSR-safe browser API access

'use client'

import { useEffect, useState } from 'react'
import dynamic from 'next/dynamic'

// Pattern 1: useEffect guard
function ThemeToggle() {
  const [theme, setTheme] = useState<'light' | 'dark'>('light')

  useEffect(() => {
    const saved = localStorage.getItem('theme') as 'light' | 'dark' | null
    if (saved) setTheme(saved)
  }, [])

  const toggle = () => {
    const next = theme === 'light' ? 'dark' : 'light'
    setTheme(next)
    localStorage.setItem('theme', next)
  }

  return <button onClick={toggle}>Theme: {theme}</button>
}

// Pattern 2: Dynamic import with ssr: false
const MapComponent = dynamic(
  () => import('../components/Map'),
  { ssr: false, loading: () => <p>Loading map...</p> }
)

export default function LocationPage() {
  return (
    <div>
      <ThemeToggle />
      <MapComponent />
    </div>
  )
}

TypeScript/Next.js: Async params pattern (Next.js 16)

// Input:  Next.js 15 page component with synchronous params
// Output: Next.js 16 page component with async params (required)

// BEFORE (Next.js 15 — synchronous, deprecated):
// export default function UserPage({ params }: { params: { id: string } }) {
//   return <h1>User {params.id}</h1>
// }

// AFTER (Next.js 16 — async params required):
export default async function UserPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  return <h1>User {id}</h1>
}

// With searchParams (also async in Next.js 16):
export default async function SearchPage({
  searchParams,
}: {
  searchParams: Promise<{ q?: string }>
}) {
  const { q } = await searchParams
  return <h1>Results for: {q}</h1>
}

Anti-Patterns

Wrong: Importing global CSS in a page component

// ❌ BAD — Next.js only allows global CSS imports in layout.tsx
// app/dashboard/page.tsx
import '../../styles/global.css'  // Build error

export default function Dashboard() {
  return <h1>Dashboard</h1>
}

Correct: Import global CSS in the root layout only

// ✅ GOOD — Global CSS imported in app/layout.tsx
import '../styles/global.css'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

Wrong: Using react-router Link syntax in Next.js

// ❌ BAD — react-router-dom Link uses 'to' prop
import { Link } from 'react-router-dom'

function Nav() {
  return <Link to="/about">About</Link>
}

Correct: Use next/link with href prop

// ✅ GOOD — next/link uses 'href' prop
import Link from 'next/link'

function Nav() {
  return <Link href="/about">About</Link>
}

Wrong: Accessing window directly at module level in a server component

// ❌ BAD — Server Component runs on the server, crashes
// app/analytics/page.tsx (Server Component by default)
const url = window.location.href  // ReferenceError: window is not defined

export default function Analytics() {
  return <p>Current URL: {url}</p>
}

Correct: Use 'use client' and useEffect for browser APIs

// ✅ GOOD — Client component with browser API in useEffect
'use client'

import { useState, useEffect } from 'react'

export default function Analytics() {
  const [url, setUrl] = useState('')

  useEffect(() => {
    setUrl(window.location.href)
  }, [])

  return <p>Current URL: {url}</p>
}

Wrong: Keeping CRA's REACT_APP_ prefix

// ❌ BAD — REACT_APP_ variables are undefined in Next.js
const apiKey = process.env.REACT_APP_API_KEY  // undefined

Correct: Use NEXT_PUBLIC_ prefix for client-side env vars

// ✅ GOOD — NEXT_PUBLIC_ prefix works in both server and client
const apiKey = process.env.NEXT_PUBLIC_API_KEY

Wrong: Using CRA image import as string directly

// ❌ BAD — Next.js image imports return an object, not a string
import logo from './logo.png'
<img src={logo} />  // Renders as [object Object]

Correct: Access the .src property or use next/image

// ✅ GOOD — Use .src for <img> or the Image component
import logo from './logo.png'
<img src={logo.src} alt="Logo" />

// Even better: Next.js Image with auto optimization
import Image from 'next/image'
<Image src={logo} alt="Logo" width={200} height={50} />

Wrong: Big-bang migration of all routes at once

// ❌ BAD — Removing react-router and converting 50 routes in one PR
// This causes weeks of merge conflicts and blocks feature development
npm uninstall react-router-dom  // Too early!

Correct: Incremental route migration with catch-all fallback

// ✅ GOOD — Catch-all route handles unmigrated pages
// app/[[...slug]]/page.tsx keeps react-router working
// Migrate one route at a time:
// 1. Create app/dashboard/page.tsx
// 2. Remove /dashboard from react-router
// 3. Test, ship, repeat

Wrong: Synchronous params access in Next.js 16

// ❌ BAD — Synchronous params access removed in Next.js 16
export default function Page({ params }: { params: { id: string } }) {
  return <h1>{params.id}</h1>  // TypeError in Next.js 16
}

Correct: Await params as a Promise in Next.js 16

// ✅ GOOD — Async params access required in Next.js 16
export default async function Page({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  return <h1>{id}</h1>
}

Common Pitfalls

Diagnostic Commands

# Verify Next.js is installed and working
npx next --version

# Check Node.js version (must be >= 20.9 for Next.js 16)
node --version

# Check for remaining CRA references
grep -rn 'react-scripts' --include='*.json' --include='*.js' --include='*.ts'

# Find remaining REACT_APP_ environment variable usage
grep -rn 'REACT_APP_' --include='*.ts' --include='*.tsx' --include='*.js' --include='*.env*'

# Count remaining react-router-dom imports (track migration progress)
grep -rn "from 'react-router" --include='*.tsx' --include='*.ts' | wc -l

# Analyze Next.js build output
npx next build 2>&1 | tail -20

# Check for SSR-unsafe window/document access in server components
grep -rn 'window\.\|document\.' --include='*.tsx' --include='*.ts' | grep -v 'node_modules' | grep -v "'use client'"

# Check for synchronous params access (needs async in Next.js 16)
grep -rn 'params\.' --include='page.tsx' --include='layout.tsx' | grep -v 'await'

# Run Next.js codemod for automated migration
npx @next/codemod@canary upgrade latest

Version History & Compatibility

VersionStatusBreaking ChangesMigration Notes
Next.js 16 (2026)CurrentAsync request APIs enforced; Turbopack default; middleware → proxy; Node.js 18 dropped; next/legacy/image deprecated; next lint removednpx @next/codemod@canary upgrade latest for automated migration
Next.js 15 (2025)LTSparams is a Promise (sync still works with warnings); Turbopack stable for dev; fetch/GET no longer cached by defaultconst { id } = await params in page components; recommended target for stability
Next.js 14 (2023)MaintenanceApp Router stable, Server Actions stableSolid target for CRA migrations
Next.js 13 (2022)EOLApp Router introduced (beta)Pages Router still supported alongside App Router
CRA 5.x (2021)Deprecated (Feb 2025)Last release, no active maintainersMigrate to Next.js, Vite, or React Router 7

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
App needs SSR, SSG, or ISR for SEO/performanceApp is a pure client-side dashboard with no SEO needsVite + React Router
Team wants built-in routing, API routes, and proxy (middleware)App is tiny (<10 components) and CRA still worksStay on CRA or migrate to Vite
Need image/font optimization and streaming SSRBackend team owns API and app is purely a SPA consumerVite (faster builds, simpler config)
Deploying to Vercel or need edge runtime supportNeed full control over server (Express/Fastify)Remix or custom Vite + Express
Enterprise app with SEO requirements and complex routingBuilding a React Native appExpo
Want React Server Components and React CompilerComplex custom webpack config with no time to migrateVite with manual React setup

Important Caveats

Related Units