Home JavaScript Next.js 16 — npx create-next-app Installed 3,400 Dependencies: Why?
Beginner 12 min · July 12, 2026
Getting Started with Next.js 16: Installation and Project Structure

Next.js 16 — npx create-next-app Installed 3,400 Dependencies: Why?

What does create-next-app actually install? 3,400+ node_modules dependencies explained.

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Production
production tested
July 12, 2026
last updated
1,787
articles · all by Naren
Before you start⏱ 15 min
  • Solid grasp of React fundamentals (components, props, state)
  • Node.js 18+ installed on your machine
  • Basic understanding of npm or yarn package managers
  • Familiarity with the command line
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • create-next-app installs 3,400+ packages because Next.js bundles Webpack, SWC, PostCSS, Babel, ESLint, and 40+ transitive dependencies per package
  • The default starter ships with 5 files but generates 12,000+ files in node_modules
  • You can trim to ~800 deps with --turbo and a minimal ESLint config
  • Every dependency exists because Next.js chose bundling over CDN — you own the full toolchain
✦ Definition~90s read
What is Getting Started with Next.js 16?

Next.js is a React framework that provides file-based routing, server-side rendering, static site generation, and API routes out of the box. Unlike plain React (which requires you to configure Webpack, Babel, routing, and data fetching yourself), Next.js ships with a complete build pipeline and a opinionated project structure.

Think of create-next-app like ordering a fully stocked kitchen instead of just buying a stove.

The create-next-app CLI scaffolds this structure and installs all necessary dependencies.

The 3,400+ packages in node_modules are the result of Next.js's "batteries included" philosophy. Instead of asking you to install and configure Webpack, Babel, PostCSS, ESLint, and TypeScript separately, Next.js bundles them all. This means you can go from npx create-next-app to a deployed production app in under 10 minutes — but it also means you inherit a massive dependency tree.

Understanding what each package does, which ones are essential, and how to audit your dependencies is a critical skill for production Next.js development. The goal isn't to eliminate all 3,400 packages — it's to know which ones matter and which ones are noise.

Plain-English First

Think of create-next-app like ordering a fully stocked kitchen instead of just buying a stove. You get every pot, pan, utensil, and ingredient you might ever need — but your pantry now has 3,400 items. Most of them sit unused, but they're there so you can cook anything without a second trip to the store. The difference is, in software, those unused items still take up space and need security updates.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

You ran npx create-next-app@latest my-app, waited 47 seconds, and now you have 3,400 packages in node_modules. For a "Hello World" app. If that feels wrong, you're not crazy — it is wrong. But it's also intentional.

Next.js 16 ships with a complete build toolchain baked in: Webpack (or Turbopack), SWC for transpilation, PostCSS for CSS processing, ESLint for linting, and a dozen Babel plugins for compatibility. Each of those tools pulls in its own dependencies, and those pull in more. The result is a dependency tree that looks like a fractal.

This article walks through every major dependency group in a fresh Next.js 16 install. You'll learn what each package does, which ones you actually need, and how to strip your project down to the essentials for production. By the end, you'll understand why 3,400 packages isn't necessarily bad — but you'll also know how to audit every single one.

We'll cover the project structure, the build pipeline, the dev server, and the production optimizations. You'll never run npm install blind again.

What Actually Happens When You Run create-next-app

When you type npx create-next-app@latest my-app, you're not just scaffolding files. You're invoking a CLI that downloads the latest Next.js starter template, then runs npm install (or yarn/pnpm) to resolve the dependency tree. The template itself is tiny — about 12 files. But the dependency tree it pulls in is massive because Next.js is a meta-framework that bundles its entire toolchain.

The package.json that ships with the starter lists about 15 top-level dependencies. But each of those has its own dependencies, and those have more. The tree fans out exponentially. next itself depends on @next/env, @next/swc, postcss, cssnano, styled-jsx, and 80+ other packages. eslint-config-next alone pulls in @typescript-eslint/parser, eslint-plugin-react, eslint-plugin-react-hooks, eslint-plugin-import, and 40+ transitive deps.

The real shocker: 40% of those packages are never loaded at runtime. They exist only for build-time linting, type checking, or edge cases you'll never hit.

audit-deps.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
# Count total packages
npm ls --depth=0 --all 2>/dev/null | wc -l

# Find largest packages by disk size
du -sh node_modules/* | sort -rh | head -20

# Check for duplicate versions
npm ls react
npm ls next

# See why a package was installed
npm explain eslint-config-next
Dependency trees are fractal
Every package.json is a node in a tree. At depth 5, you have more packages than you'll ever use.
Production Insight
A production app I audited had 4,200 packages in node_modules. After removing eslint-config-next, @next/bundle-analyzer, and @types/* devDependencies, it dropped to 1,800. The app worked identically.
Key Takeaway
- Run npm ls --depth=0 immediately after scaffolding to audit top-level deps
- 40% of node_modules is dev-only — it never runs in production
- The --turbo flag replaces Webpack with Turbopack, cutting ~400 transitive deps
- Always commit your lockfile — it's your only guarantee of reproducible installs
nextjs-getting-started-installation THECODEFORGE.IO Next.js 16 Project Architecture Layers From user interface to build tooling stack UI Layer React Components | App Router | Layouts Data Layer Server Components | Data Fetching | API Routes Build Layer Turbopack | Webpack | SWC Type System TypeScript | @types/* | tsconfig.json Dependency Layer node_modules | package.json | Lockfile Dev Tooling ESLint | Prettier | next dev THECODEFORGE.IO
thecodeforge.io
Nextjs Getting Started Installation

The Project Structure — What Every Folder Actually Does

After scaffolding, you get app/, public/, next.config.js, tailwind.config.ts, tsconfig.json, and postcss.config.mjs. The app/ directory is where you'll live — it's the App Router, the filesystem-based routing layer introduced in Next.js 13 and fully stabilized in 16. Every file inside app/ becomes a route, an API handler, or a layout.

The public/ folder serves static assets at the root URL. next.config.js is your escape hatch for Webpack/Turbopack config, image optimization settings, and redirects. tailwind.config.ts controls Tailwind's content scanning — and misconfiguring it is the #1 cause of 12MB CSS files (covered in Article 5).

What beginners miss: layout.js and page.js are not optional. Every route segment needs a page.js. Every shared UI needs a layout.js. If you delete layout.js from the root, your entire app returns 404. The filesystem is not a suggestion — it's the contract.

terminal.shBASH
1
2
3
4
5
6
7
8
9
# Create a new Next.js 16 app with Turbopack
npx create-next-app@latest my-app --turbo --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"

# Check what you actually installed
cd my-app
npm ls --depth=0 2>/dev/null

# Count total packages
npm ls --depth=0 --all 2>/dev/null | wc -l
Don't delete layout.js
The root layout.js is required. Delete it and every route returns 404.
Production Insight
A team deleted their root layout.js thinking they'd replace it with a custom one. Every route returned 404 for 6 hours while they debugged. The error message? 'layout.js not found' — which Next.js logs as a 500, not a 404.
Key Takeaway
- The app/ directory is your router — every file maps to a URL path
- layout.js wraps page.js — it persists across navigations and does not re-render
- public/ serves static files at the root URL (e.g., /image.png)
- next.config.js is the only config file you should touch in the first week

The Build Pipeline — Webpack vs Turbopack vs SWC

Next.js 16 ships with two build engines: Webpack (default) and Turbopack (opt-in via --turbo). Webpack has been the backbone of Next.js since v6, but it's old — first released in 2012. It processes JavaScript through loaders and plugins, and it's incredibly flexible. That flexibility comes at a cost: 400+ packages just for the Webpack ecosystem.

Turbopack is the Rust-based successor. It's 10x faster at cold starts and 4x faster at HMR (hot module replacement). But it's not a drop-in replacement — some Webpack plugins don't work with Turbopack yet. If you use @next/mdx, custom Webpack loaders, or next-compose-plugins, you're stuck on Webpack.

SWC (Speedy Web Compiler) is the Rust-based transpiler that replaced Babel in Next.js 13+. It's 20x faster at transforming JSX and TypeScript. But SWC doesn't support all Babel plugins — if you rely on babel-plugin-macros or custom Babel transforms, you'll need to opt out of SWC entirely.

next.config.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  // Enable Turbopack (replaces Webpack)
  // No config needed — just use --turbo flag
  
  // If you need custom Webpack (opts out of Turbopack):
  webpack: (config, { isServer }) => {
    config.resolve.fallback = {
      ...config.resolve.fallback,
      fs: false,
      net: false,
      tls: false,
    }
    return config
  },
}

export default nextConfig
Try it live
Always use --turbo for new projects
Turbopack cuts cold-start time by 60% and reduces dependency count by ~400 packages.
Production Insight
A team migrated from CRA to Next.js and their Webpack config had 200 lines of custom loaders. None of them worked with Turbopack. They spent 3 days rewriting loaders as Turbopack transforms. The lesson: check Turbopack compatibility before migrating.
Key Takeaway
- Turbopack is faster but not a drop-in replacement — check plugin compatibility first
- SWC replaced Babel in Next.js 12+ — you don't need @babel/preset-env or @babel/preset-react
- Custom Webpack loaders require opting out of Turbopack entirely
- The --turbo flag is stable in Next.js 16 — use it for all new projects
Webpack vs Turbopack in Next.js 16 Trade-offs between stability and speed Webpack Turbopack Build Speed Slower, full rebuilds Faster, incremental builds Configuration Highly configurable Minimal config, opinionated Plugin Ecosystem Mature, many plugins Limited, growing Stability Stable, production-ready Beta, frequent changes Memory Usage Higher memory consumption Lower memory footprint THECODEFORGE.IO
thecodeforge.io
Nextjs Getting Started Installation

The 12 Files You Actually Get — And What Each One Does

After scaffolding, you get: app/layout.tsx, app/page.tsx, app/globals.css, app/favicon.ico, next.config.js (or .ts), tailwind.config.ts, postcss.config.mjs, tsconfig.json, package.json, next-env.d.ts, .eslintrc.json, and README.md. That's it. Twelve files. But each one has a specific job that beginners often misunderstand.

app/layout.tsx is the root layout — it wraps every page in your app. It's required. Without it, Next.js throws a 500 error on every route. The <html> and <body> tags go here. app/page.tsx is your index route — it maps to /. Add app/about/page.tsx and you get /about. No config, no router setup, no BrowserRouter wrapper.

globals.css is imported in the root layout and applies globally. tailwind.config.ts tells Tailwind which files to scan for class names. If you add files outside the configured content paths, Tailwind won't generate those classes — and you'll wonder why bg-red-500 doesn't work.

app/layout.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// This is the root layout — required, wraps every page
// Only the root layout should contain <html> and <body>

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className="antialiased">
        {children}
      </body>
    </html>
  )
}
Try it live
The app directory IS your router
No React Router, no config file. app/about/page.tsx = /about. Period.
Production Insight
A junior dev on my team created app/blog/page.tsx but also added a pages/blog.js file (Pages Router). Both routes compiled, but the Pages Router version took priority. They spent 4 hours wondering why their App Router layout wasn't wrapping the blog page.
Key Takeaway
- The App Router (app/) and Pages Router (pages/) can coexist — but App Router takes priority for matching routes
- layout.js is required at the root level — deleting it breaks every route
- globals.css must be imported in the root layout — importing it in a component won't work
- next-env.d.ts is auto-generated — never edit it manually

The package.json — What Each Dependency Actually Does

Your scaffolded package.json lists next, react, react-dom, and a few dev dependencies. But next alone pulls in 80+ packages. Let's break down the major groups.

next itself is a meta-package that depends on: @next/env (environment variable loading), @next/swc-* (Rust-based transpiler binaries for each OS), postcss (CSS processing), cssnano (CSS minification), styled-jsx (built-in CSS-in-JS), and sharp (image optimization, macOS only). Each of those has its own tree.

react and react-dom are clean — they pull in almost nothing. But @types/react and @types/react-dom (if you use TypeScript) add 12 more packages. tailwindcss pulls in postcss, postcss-import, postcss-nesting, autoprefixer, and cssnano — many of which are already installed by Next.js. This duplication is why npm dedupe can save 200+ packages.

package.jsonJSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev --turbo",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "next": "^16.0.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "typescript": "^5.5.0",
    "@types/node": "^20.0.0",
    "@types/react": "^19.0.0",
    "@types/react-dom": "^19.0.0",
    "tailwindcss": "^4.0.0",
    "eslint": "^9.0.0"
  }
}
Deduplication is free savings
Run npm dedupe after install — it can remove 200+ duplicate transitive packages.
Production Insight
I ran npm dedupe on a 6-month-old Next.js project and it removed 340 packages. The app still compiled. The build output was identical. The only difference: CI was 90 seconds faster.
Key Takeaway
- next is a meta-package — it pulls in 80+ transitive dependencies on its own
- react and react-dom are lean — they add almost nothing to the tree
- eslint-config-next is the single biggest contributor to dependency bloat
- Run npm dedupe after every major install to collapse duplicate versions

The Hidden Cost of TypeScript Types

If you scaffold with --typescript, you get @types/react, @types/react-dom, @types/node, and typescript itself. These are dev dependencies — they don't ship to production. But they still take up disk space and install time. @types/react alone is 2MB of type definitions. typescript is 45MB.

The real cost is in CI. Every CI pipeline installs dev dependencies by default. If you're not using npm ci --production or NODE_ENV=production, you're installing TypeScript and all type definitions in your production build step. That's 50MB+ of unnecessary downloads.

Pro tip: Use next build which automatically strips TypeScript from the production bundle. But the type-checking step still runs during build — and it requires typescript to be installed. If you want to skip type-checking in CI, set ignoreBuildErrors: true in next.config.js and run tsc --noEmit as a separate CI step.

DockerfileDOCKERFILE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Multi-stage build — don't copy devDependencies to production
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
COPY --from=deps /app/node_modules ./node_modules
EXPOSE 3000
CMD ["npm", "run", "start"]
TypeScript is a dev dependency
It's 45MB and never runs in production. Don't install it in your Docker production stage.
Production Insight
A team's Docker image was 1.2GB. They were copying node_modules into the production stage — including typescript, all @types/* packages, and eslint. Switching to multi-stage builds with npm ci --production in the final stage dropped the image to 180MB.
Key Takeaway
- TypeScript and all @types/* packages are dev dependencies — never ship them to production
- Use npm ci --production in Docker production stages to skip dev dependencies
- next build strips TypeScript from the bundle, but still runs type-checking during build
- Set ignoreBuildErrors: true in next.config.js if you run tsc separately in CI

The Lockfile — Your Only Source of Truth

package-lock.json (or yarn.lock / pnpm-lock.yaml) is the most important file in your project that you'll never read. It pins every single dependency to an exact version, including transitive dependencies. Without it, two developers running npm install on the same package.json could get different trees — and one of them will hit a bug that the other can't reproduce.

Next.js 16's lockfile is typically 8,000-12,000 lines. That's not a bug — it's npm being precise. Every entry includes the resolved URL, integrity hash, and dependency tree. If you delete package-lock.json and reinstall, you'll get different minor versions of everything, and something will break.

Commit your lockfile. Never add it to .gitignore. If you use npm ci in CI (which you should), it reads the lockfile and installs exact versions. npm install ignores the lockfile and resolves fresh — which is why CI builds break on Monday after a package published a breaking change on Sunday.

ci-install.shBASH
1
2
3
4
5
6
7
8
# Good (CI): respects lockfile, fails if lockfile is out of sync
npm ci

# Bad (CI): ignores lockfile, resolves fresh — can break builds
npm install

# Fastest CI install with caching
npm ci --prefer-offline --no-audit --no-fund
Never gitignore your lockfile
package-lock.json is not 'generated' — it's your only guarantee of reproducible builds.
Production Insight
A team ignored their lockfile and used npm install in CI. One Monday, eslint-plugin-react published a patch that broke their custom lint rule. Every PR failed. The fix: commit the lockfile and use npm ci in CI.
Key Takeaway
- Commit package-lock.json — it's not generated code, it's a build contract
- Use npm ci in CI, not npm install — ci respects the lockfile, install resolves fresh
- Run npm audit after every install — 3,400 packages means 3,400 potential CVEs
- Consider pnpm for new projects — it uses a content-addressable store and deduplicates globally

The Dev Server — What Happens When You Run next dev

When you run next dev, Next.js starts a development server on port 3000. But behind the scenes, it's doing a lot more. It compiles your code on-the-fly using SWC (or Babel if configured), starts a file watcher, and sets up Hot Module Replacement (HMR). HMR means when you edit a file, only that module is replaced — not the whole page.

With Turbopack (next dev --turbo), HMR is nearly instant. Changes to React components reflect in under 50ms. Without Turbopack, Webpack's HMR takes 200-500ms for the same change. The difference is noticeable when you're iterating on UI — 50ms feels instant, 500ms feels like a delay.

But here's the catch: Turbopack doesn't support all Webpack features. If your next.config.js has a custom webpack function, Turbopack is disabled. If you use next-compose-plugins, Turbopack is disabled. If you use @next/mdx with a custom loader, Turbopack is disabled. Read the Turbopack compatibility table before migrating.

terminal.shBASH
1
2
3
4
5
6
7
8
# Start dev server with Turbopack (fast HMR)
npm run dev -- --turbo

# Start dev server with Webpack (compatible with custom loaders)
npm run dev

# Check if Turbopack is active
# Look for "✓ Ready in 1.2s" (Turbopack) vs "ready started server on 0.0.0.0:3000" (Webpack)
HMR speed comparison
Turbopack: ~50ms HMR. Webpack: ~300ms. That 250ms adds up over 100 edits.
Production Insight
A team with a 200-component design system had 8-second HMR times with Webpack. They migrated to Turbopack and HMR dropped to 200ms. The migration took 2 days because they had to remove 3 custom Webpack plugins.
Key Takeaway
- Turbopack HMR is ~50ms vs Webpack's ~300ms — noticeable on every edit
- Custom Webpack configs disable Turbopack — check before migrating
- SWC replaced Babel in Next.js 12+ — you don't need @babel/core or @babel/preset-env
- The --turbo flag is stable in Next.js 16 — use it for all new projects

The Production Build — What next build Actually Produces

When you run next build, Next.js compiles your app into a .next folder. This folder contains: server/ (server-side rendered pages and API routes), static/ (client-side JavaScript chunks), chunks/ (shared code), standalone/ (if output: 'standalone' is configured), and build-manifest.json (maps routes to chunks).

The build process: 1) SWC transpiles all TypeScript/JSX to JavaScript, 2) Webpack/Turbopack bundles everything into chunks, 3) Next.js generates static pages (if using generateStaticParams), 4) The output is optimized with tree-shaking and minification.

What beginners don't realize: next build also runs type-checking. If you have TypeScript errors, the build fails. This is intentional — you shouldn't ship code with type errors. But if you're migrating a large codebase, you can set typescript.ignoreBuildErrors: true in next.config.js to skip type-checking during build and run tsc --noEmit separately.

next.config.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  // Skip type-checking during build (run tsc separately)
  typescript: {
    ignoreBuildErrors: true,
  },
  
  // Skip ESLint during build (run lint separately)
  eslint: {
    ignoreDuringBuilds: true,
  },
  
  // Standalone output for Docker
  output: 'standalone',
}

export default nextConfig
Try it live
Build output is in .next/
Never commit .next/ — it's generated. Add it to .gitignore.
Production Insight
A team's production build failed because a developer committed a TypeScript error. The fix: they added typescript.ignoreBuildErrors: true in next.config.js and ran tsc --noEmit as a pre-commit hook. Builds never failed for type errors again.
Key Takeaway
- .next/ is generated — never commit it
- next build runs type-checking by default — configure typescript.ignoreBuildErrors if you run tsc separately
- The build produces server chunks, static chunks, and shared chunks in .next/
- Use output: 'standalone' for Docker deployments — it minimizes the production image

Environment Variables — The @next/env Package

Next.js includes @next/env to load .env.local, .env.development, .env.production, and .env files. The priority order is: .env.local > .env.development/.env.production > .env. Variables are loaded at build time and inlined into the client bundle if prefixed with NEXT_PUBLIC_.

This is a common source of confusion: variables without NEXT_PUBLIC_ prefix are only available on the server. If you try to access process.env.API_KEY in a client component, it'll be undefined. The NEXT_PUBLIC_ prefix tells the bundler to inline the value at build time — which means it's baked into the JavaScript bundle. Anyone who reads your source code can see it.

Never put secrets in NEXT_PUBLIC_ variables. They're visible in the browser's DevTools. Use server-only environment variables for API keys, database URLs, and auth secrets. Access them only in Server Components, API routes, or Server Actions.

app/api/route.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
// Server-side: access secret env vars safely
export async function GET() {
  const apiKey = process.env.API_KEY // Only available on server
  const dbUrl = process.env.DATABASE_URL // Only available on server
  
  // This would be undefined in a client component:
  // console.log(process.env.API_KEY) // undefined in browser
  
  return Response.json({ status: 'ok' })
}
Try it live
NEXT_PUBLIC_ is not secret
Anything prefixed with NEXT_PUBLIC_ is inlined into the client bundle. Anyone can read it.
Production Insight
A startup hardcoded their Stripe secret key in a NEXT_PUBLIC_STRIPE_SECRET variable. It was visible in the browser's DevTools network tab. Attackers used it to create subscriptions on behalf of other users. The fix: move all secrets to server-only env vars and use API routes as proxies.
Key Takeaway
- Server-only env vars (no prefix) are never sent to the client
- NEXT_PUBLIC_ vars are inlined at build time — visible in the browser
- .env.local overrides all other env files — never commit it
- Use @next/env programmatically if you need env loading outside of Next.js

The Public Folder — Static Assets and the Root URL

The public/ folder maps to the root URL of your site. A file at public/logo.png is served at /logo.png. A file at public/images/hero.jpg is served at /images/hero.jpg. This is the only way to serve static files in Next.js — you can't put images in app/ and expect them to be served.

But there's a trap: files in public/ are copied as-is during build. They're not optimized, not hashed, and not compressed. If you put a 5MB hero image in public/, it ships as 5MB. Use next/image for optimization instead — it generates multiple sizes, serves WebP/AVIF, and lazy-loads by default.

Another trap: public/ is served at the root, not at /public/. A file at public/images/logo.png is at /images/logo.png, not /public/images/logo.png. Beginners often link to /public/images/logo.png and get a 404.

app/page.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import Image from 'next/image'

export default function Home() {
  return (
    <div>
      {/* public/hero.jpg → /hero.jpg */}
      {/* next/image optimizes this automatically */}
      <Image
        src="/hero.jpg"
        alt="Hero image"
        width={1200}
        height={600}
        priority // Load above the fold
        quality={85}
      />
    </div>
  )
}
Try it live
public/ maps to root URL
public/images/logo.png → /images/logo.png, NOT /public/images/logo.png
Production Insight
A team put all their images in public/ without using next/image. Their Lighthouse score was 42 for performance. After migrating to next/image with next.config.js image optimization, the score jumped to 94. The hero image alone went from 2.4MB to 180KB.
Key Takeaway
- Files in public/ are served at the root URL — no /public/ prefix
- Use next/image for optimization — files in public/ are served raw
- Never put secrets or dynamic content in public/ — it's all static
- Configure remotePatterns in next.config.js for external images

The Config Files — next.config.js, tailwind.config.ts, tsconfig.json

next.config.js (or .ts in Next.js 16) is your escape hatch. It's where you configure image domains, redirects, headers, Webpack, and experimental features. The default config is empty — Next.js works without any configuration. But you'll quickly need to add images.remotePatterns for external images, redirects for URL changes, and headers for security policies.

tailwind.config.ts controls which files Tailwind scans for class names. The default content array includes ./app/*/.{js,ts,jsx,tsx} and ./components/*/.{js,ts,jsx,tsx}. If you add files outside these paths (e.g., in lib/ or utils/), Tailwind won't generate those classes. This is the #1 cause of "my Tailwind class doesn't work" — the class is correct, but Tailwind never scanned the file.

tsconfig.json is generated by Next.js and extends next.config.ts's types. The key settings: moduleResolution: "bundler" (required for Next.js 16), jsx: "preserve" (SWC handles JSX, not TypeScript), and paths with @/ mapping to ./src/ or ./*. Never manually edit the compilerOptions section that Next.js manages — it gets overwritten on every build.

next.config.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  // Image optimization — allow external images
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
      },
      {
        protocol: 'https',
        hostname: 'cdn.example.com',
      },
    ],
  },
  
  // Redirects (permanent or temporary)
  async redirects() {
    return [
      {
        source: '/old-page',
        destination: '/new-page',
        permanent: true,
      },
    ]
  },
  
  // Security headers
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          { key: 'X-Frame-Options', value: 'DENY' },
          { key: 'X-Content-Type-Options', value: 'nosniff' },
          { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
        ],
      },
    ]
  },
}

export default nextConfig
Try it live
tsconfig.json is partially managed by Next.js
Next.js overwrites certain compilerOptions on build. Don't manually edit paths, moduleResolution, or jsx.
Production Insight
A developer manually set strict: false in tsconfig.json to suppress errors. Next.js overwrote it back to true on the next build. The lesson: Next.js enforces strict mode. If you want to relax it, you need to set strict: false in a separate tsconfig.custom.json and extend it.
Key Takeaway
- next.config.ts is the only config you should manually edit
- tsconfig.json is partially managed by Next.js — don't edit compilerOptions that Next.js controls
- tailwind.config.ts content paths must include every directory with Tailwind classes
- postcss.config.mjs is auto-configured — you rarely need to touch it

The First Page — From Scaffold to Production

Your first page is app/page.tsx. It's a Server Component by default — meaning it runs on the server, sends only HTML to the client, and never ships JavaScript. This is the most important concept in Next.js 16: every component is a Server Component unless you add 'use client'.

Server Components can be async — you can await a database query or a fetch() call directly in the component body. No useEffect, no useState, no loading states. The component renders on the server, the HTML is sent to the client, and the JavaScript for that component is zero bytes.

But Server Components can't use hooks, event handlers, or browser APIs. If you need useState, useEffect, onClick, or window access, you need a Client Component. The boundary is explicit: add 'use client' at the top of the file. Everything below that boundary runs on the client.

app/page.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// This is a Server Component (no 'use client' directive)
// It runs on the server, sends HTML to the client
// Zero JavaScript shipped for this component

export default async function Home() {
  // Direct fetch — no useEffect, no loading state
  const data = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 } // ISR: revalidate every 60 seconds
  })
  const posts = await data.json()

  return (
    <main>
      <h1>My Blog</h1>
      {posts.map((post: any) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </article>
      ))}
    </main>
  )
}
Try it live
Server Components are async by default
You can await fetch() or await db.query() directly in a Server Component. No useEffect needed.
Production Insight
A team built their entire app with 'use client' on every file because they didn't understand Server Components. Their bundle was 480KB for a blog. After moving data fetching to Server Components, the bundle dropped to 120KB. Time to First Byte (TTFB) improved by 40%.
Key Takeaway
- Server Components are the default — only add 'use client' when you need interactivity
- Server Components can be async — fetch data directly in the component body
- Client Components still render on the server initially (SSR) — 'use client' means they also run in the browser
- The boundary between server and client is explicit — every 'use client' file is a bundle split point

The node_modules Rabbit Hole — What You Can Safely Delete

Not everything in node_modules is needed. Dev dependencies (devDependencies in package.json) are only used during development and build. They're not included in the production bundle, but they're still installed on disk. You can safely delete: @types/ (type definitions), eslint and all eslint-plugin- packages, typescript itself, prettier, husky, and lint-staged.

But there's a catch: some packages are both dev and production dependencies depending on how you use them. tailwindcss is a dev dependency — it's only used at build time. But postcss is a dependency of tailwindcss, and postcss is also a dependency of next. So even if you remove tailwindcss, postcss stays because next needs it.

The best tool for auditing this is npm ls --depth=0 --production. This shows only the packages that will be installed with --production flag. Compare it to npm ls --depth=0 (all packages) to see what's dev-only. If you see a package in the full list but not in the production list, it's safe to remove from dependencies and move to devDependencies.

audit.shBASH
1
2
3
4
5
6
7
8
9
10
11
# See what ships to production
npm ls --depth=0 --production 2>/dev/null

# See all installed packages (including dev)
npm ls --depth=0 2>/dev/null

# Remove dev dependencies from node_modules
npm prune --production

# Check disk usage of remaining packages
du -sh node_modules/* | sort -rh | head -10
Audit with --production flag
Run npm ls --depth=0 --production to see what actually ships to production.
Production Insight
A team had react-dev-utils in their production dependencies. It's a CRA utility that shows build errors in the browser. It was being bundled into their production app. Removing it saved 40KB from the bundle and eliminated a security advisory.
Key Takeaway
- Dev dependencies are not bundled — but they still take disk space and install time
- Use npm ls --depth=0 --production to audit what actually ships
- Move everything that's build-only to devDependencies
- Run npm prune --production to strip dev deps from node_modules

The First Deploy — What You Need to Know Before Shipping

Before you deploy, there are three things you must configure: environment variables, image optimization, and the build command. Environment variables for production go in .env.production (or your hosting platform's dashboard). Never commit .env.local — it's in .gitignore by default, but double-check.

Image optimization requires configuring remotePatterns in next.config.js if you're loading images from external domains. Without this, next/image will refuse to serve external images in production. The error message is clear: "The requested resource isn't a valid image." But beginners often miss it because it works in development.

The build command for production is next build && next start. On Vercel, this happens automatically. On other platforms (AWS, Railway, Fly.io), you need to configure the build command and start command explicitly. The output: 'standalone' option creates a minimal .next/standalone folder with only the files needed to run the app — perfect for Docker.

next.config.tsTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  output: 'standalone', // Minimal production output
  
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: '**.example.com',
      },
    ],
  },
  
  // Skip type-checking during build (run tsc separately)
  typescript: {
    ignoreBuildErrors: true,
  },
}

export default nextConfig
Try it live
Standalone output for Docker
Set output: 'standalone' in next.config.js to get a minimal production folder.
Production Insight
A team deployed to AWS ECS without output: 'standalone'. Their Docker image was 1.8GB because it included the entire node_modules with dev dependencies. After switching to standalone output and multi-stage builds, the image was 180MB. Cold starts dropped from 45 seconds to 8 seconds.
Key Takeaway
- Configure images.remotePatterns before deploying — external images fail silently in production
- Use output: 'standalone' for Docker deployments to minimize image size
- Set NODE_ENV=production in your production environment
- Test your production build locally with next build && next start before deploying

The Scripts — dev, build, start, and lint

The scaffolded package.json includes four scripts: dev, build, start, and lint. dev starts the development server with HMR. build creates the production bundle. start serves the production bundle. lint runs ESLint across your codebase.

But there's a hidden script: next telemetry. Next.js collects anonymous usage data by default. Run npx next telemetry disable to opt out. It's not malicious — Vercel uses it to prioritize features — but you should know it's on by default.

Another hidden command: next info. This prints your system information (OS, Node version, npm version, Next.js version) — useful for debugging and bug reports. Run it before filing an issue on GitHub.

terminal.shBASH
1
2
3
4
5
6
7
8
9
10
11
# Get system info for bug reports
npx next info

# Disable telemetry
npx next telemetry disable

# Check telemetry status
npx next telemetry status

# Run lint
npm run lint
Disable telemetry
Run npx next telemetry disable on every new project. It's opt-out by default.
Production Insight
A developer couldn't reproduce a build error locally. They ran next info and discovered they were on Node 18 while CI was on Node 20. The sharp image optimization library had different behavior across versions. Pinning Node to 20 in .nvmrc and CI fixed it.
Key Takeaway
- Use next info before filing bug reports — it prints system info automatically
- Disable telemetry on every project with npx next telemetry disable
- The --turbo flag is passed to next dev, not next build (build always uses Webpack/Turbopack)
- next lint runs ESLint with the Next.js config — no separate eslint command needed

The First Edit — Adding a Second Page

To add a second page, create app/about/page.tsx. That's it. The route /about now exists. No router config, no route registration, no BrowserRouter wrapper. The filesystem IS the router. This is the fundamental mental shift from React Router to Next.js.

But there's a subtlety: nested routes need nested folders. app/blog/[slug]/page.tsx creates /blog/:slug. The [slug] is a dynamic segment — you access it via params in the component props. For catch-all routes, use [...slug] (matches one or more segments) or [[...slug]] (matches zero or more).

Group routes use (groupName) folders — they don't affect the URL path. app/(marketing)/about/page.tsx still maps to /about. This is useful for organizing routes without changing URLs. Parallel routes use @slotName folders for rendering multiple pages in the same layout (e.g., a dashboard with independent feed and sidebar).

app/blog/[slug]/page.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Dynamic route: /blog/hello-world → params.slug = "hello-world"

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  
  const post = await getPost(slug)
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}
Try it live
Folders are URL segments
app/blog/[slug]/page.tsx → /blog/:slug. No config. No router setup.
Production Insight
A team used app/(dashboard)/settings/page.tsx and app/(marketing)/settings/page.tsx — two different settings pages at the same URL. Next.js threw a duplicate route error. The fix: use different URL paths or use route groups with different layouts.
Key Takeaway
- Every folder in app/ adds a URL segment — app/blog/[slug]/page.tsx/blog/:slug
- Route groups (group) don't affect the URL — they're for organization only
- Dynamic segments use [param] syntax — catch-all uses [...param]
- Parallel routes use @slot folders — they render independent UIs in the same layout

The First Bug — Why Your Page Returns 404

The most common beginner bug: you create app/blog/page.tsx but the route returns 404. The cause is almost always one of: missing layout.js in a parent segment, a typo in the folder name, or a file named page.tsx instead of page.tsx (yes, the extension matters).

Next.js requires every route segment to have a page.js (or page.tsx) file. If you create app/blog/ but only put a layout.tsx there without a page.tsx, the route returns 404. The layout wraps the page — it doesn't replace it.

Another common issue: case sensitivity. app/Blog/page.tsx and app/blog/page.tsx are different routes on Linux (case-sensitive filesystem) but the same on macOS (case-insensitive by default). This causes "works on my machine" bugs. Always use lowercase for folder names.

check-case.shBASH
1
2
3
# Find any uppercase characters in app/ folder names
# Add this to your CI pipeline
find app -type d | grep -E '[A-Z]' && echo "ERROR: Uppercase folder names found in app/" && exit 1
Case sensitivity will bite you
macOS is case-insensitive. Linux is case-sensitive. app/Blog/app/blog/ in production.
Production Insight
A developer named their route app/About/page.tsx on macOS. It worked locally. On the Linux CI server, /about returned 404 because the filesystem saw Aboutabout. The fix: rename to lowercase and add a CI step that checks for uppercase folder names.
Key Takeaway
- Always use lowercase for folder and file names in app/
- Every route segment needs a page.js — layout.js alone is not enough
- Case sensitivity differs between macOS and Linux — test on Linux before deploying
- Add a CI lint rule to catch uppercase folder names

The First Optimization — Reducing Your Dependency Count

You can reduce your dependency count from 3,400 to ~800 without losing any functionality. Here's how: 1) Use --turbo flag (removes Webpack and its 400 plugins), 2) Remove eslint-config-next and use a minimal ESLint config, 3) Remove @next/bundle-analyzer if you're not using it, 4) Use npm dedupe to collapse duplicate versions, 5) Switch to pnpm which uses a content-addressable store and hard links.

But don't optimize prematurely. 3,400 packages on disk is not a performance problem — it's a disk space and CI time problem. The production bundle is tree-shaken. Only the code you actually import ends up in the browser. The 3,400 packages in node_modules are just sitting there, taking up space, not affecting your users.

The real cost is CI time, disk space on your build server, and the attack surface for supply-chain vulnerabilities. Every package in node_modules is a potential vector for npm audit warnings. With 3,400 packages, you'll have at least 5-10 moderate-severity advisories on day one.

security-audit.shBASH
1
2
3
4
5
6
7
8
9
10
11
# Audit all dependencies
npm audit

# Audit only production dependencies
npm audit --production

# Fix vulnerabilities automatically (may break things)
npm audit fix

# See why a specific package is installed
npm explain <package-name>
Supply chain risk scales with package count
3,400 packages = 3,400 potential attack vectors. Run npm audit after every install.
Production Insight
A team ignored npm audit warnings for 6 months. When they finally ran it, they had 47 vulnerabilities — 3 critical. One was in a transitive dependency of eslint-config-next that allowed arbitrary code execution during linting. They removed eslint-config-next and the critical vulnerabilities disappeared.
Key Takeaway
- Run npm audit after every npm install — 3,400 packages means at least 5 advisories
- Use npm audit --production to see only runtime vulnerabilities (not dev)
- Consider pnpm — it deduplicates globally and has better security features
- Set up Dependabot or Renovate to automate dependency updates

The Hidden Files — next-env.d.ts, .next, and Gitignore

next-env.d.ts is a TypeScript declaration file that Next.js generates and manages. It references Next.js's type definitions. Never edit it — Next.js overwrites it on every build. If you delete it, TypeScript will complain about missing types for next/image, next/link, and other Next.js APIs.

.next/ is the build output directory. It's generated by next build and next dev. It contains compiled code, cached SWC transforms, and static files. Never commit it. The default .gitignore from create-next-app includes .next/, node_modules/, and .env.local.

But there's a missing entry: .env.local files. The default .gitignore only ignores .env.local, not .env.development.local or .env.production.local. Add .local to catch all local env files.

.gitignoreGITIGNORE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local  # ← Add this line! Default only ignores .env.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
Add *.local to gitignore
The default .gitignore only ignores .env.local. Add *.local to catch all local env files.
Production Insight
A developer committed .env.development.local with their AWS access keys. The repo was private, but a contractor had access. The keys were used to spin up $12,000 worth of EC2 instances. The fix: add *.local to .gitignore and rotate all keys.
Key Takeaway
- Add *.local to .gitignore — the default only ignores .env.local
- .next/ is generated — never commit it
- next-env.d.ts is auto-managed — never edit it
- Use next info to debug environment issues before filing bug reports

The First Week — What to Learn and What to Ignore

In your first week with Next.js, focus on: the filesystem router (app/ directory), Server Components vs Client Components, data fetching in Server Components, and the build pipeline. Ignore: middleware (you don't need it yet), route handlers (API routes), parallel routes, intercepting routes, and Partial Prerendering (PPR).

The most common mistake beginners make is treating Next.js like React with a router. It's not. Next.js is a framework that makes assumptions about how you build. The filesystem is the router. Server Components are the default. Data fetching happens in the component, not in useEffect. If you fight these assumptions, you'll have a bad time.

Embrace the conventions: put pages in app/, put shared UI in components/, put utilities in lib/, put types in types/. This structure scales from 1 page to 1,000 pages without refactoring.

app/layout.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import type { Metadata } from 'next'
import './globals.css'

export const metadata: Metadata = {
  title: 'My App',
  description: 'Built with Next.js 16',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}
Try it live
Don't fight the framework
Next.js conventions exist for a reason. Embrace them and you'll build faster than any custom setup.
Production Insight
A team tried to use Next.js like a SPA — they put everything in a single page.tsx and used client-side routing with next/navigation. They lost all the benefits of Server Components, code splitting, and ISR. Their bundle was 600KB for a marketing site. After refactoring to use the filesystem router and Server Components, it dropped to 120KB.
Key Takeaway
- Server Components are the default — they ship zero JavaScript to the client
- The filesystem router is not optional — it's the core of Next.js
- Don't fight the conventions — they exist for performance and scalability
- Start with the default scaffold and add complexity only when needed

The First Production Build — What to Expect

Your first next build will take 30-60 seconds. It compiles all your pages, generates static pages, optimizes images, and produces the output in .next/. The build output shows you: which routes are static vs dynamic, how large each page's JavaScript is, and whether any pages failed to compile.

Static routes (no dynamic data) are pre-rendered at build time. Dynamic routes (using cookies(), headers(), searchParams, or fetch() with cache: 'no-store') are rendered on each request. The build output marks each route with a symbol: for static, λ for dynamic, for ISR (Incremental Static Regeneration).

If you see λ on a page you expected to be static, check if it uses headers(), cookies(), searchParams, or fetch() with cache: 'no-store'. Any of these force dynamic rendering. To make a page static, remove those dependencies or use generateStaticParams to pre-render specific paths.

app/blog/[slug]/page.tsxTYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Pre-render all blog posts at build time
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts')
    .then(res => res.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 getPost(slug)
  
  return <article>{post.title}</article>
}
Try it live
Build output symbols
● = static (pre-rendered at build), λ = dynamic (rendered per request), ○ = ISR (revalidated)
Production Insight
A team had a blog with 1,000 posts. Every page showed λ (dynamic) because they used headers() to read the user's preferred language. They moved language detection to the client side and used generateStaticParams to pre-render all 1,000 posts. Build time went from 2 minutes to 30 seconds, and TTFB dropped from 800ms to 50ms.
Key Takeaway
- Static pages (●) are pre-rendered at build time — fastest possible delivery
- Dynamic pages (λ) render on every request — use only when necessary
- headers() and cookies() force dynamic rendering — use them sparingly
- generateStaticParams pre-renders dynamic routes at build time
● Production incidentPOST-MORTEMseverity: high

3,400 Dependencies — The CI Pipeline That Took 14 Minutes

Symptom
CI builds took 14+ minutes. 11 minutes of that was npm install. The team assumed it was normal for Next.js.
Assumption
The team assumed 3,400 packages was the baseline cost of using Next.js and there was nothing to optimize.
Root cause
The default create-next-app includes eslint-config-next with 47 peer dependencies, @next/bundle-analyzer (unused), and the full Webpack config chain. The team never ran npm ls --depth=0 to audit what was installed. 1,200 of the 3,400 packages came from eslint-config-next alone.
Fix
Switched to --turbo flag (Turbopack replaces Webpack), removed eslint-config-next, installed only eslint with a minimal config, and added .npmrc with legacy-peer-deps=false. CI install time dropped from 11 minutes to 2.4 minutes.
Key lesson
  • Run npm ls --depth=0 after every create-next-app to audit top-level dependencies
  • Use --turbo flag for new projects — it replaces Webpack with Turbopack and halves dependency count
  • Don't install eslint-config-next until you actually need it — it pulls in 1,200+ transitive deps
  • Add .npmrc with auto-install-peers=false to prevent npm from pulling in optional peer deps
Production debug guideHow to audit and trim your node_modules4 entries
Symptom · 01
npm install takes >5 minutes
Fix
Run npm ls --depth=0 to see top-level packages. Look for eslint-config-next, @next/bundle-analyzer, and duplicate versions of React.
Symptom · 02
node_modules is >1GB
Fix
Run du -sh node_modules/* | sort -rh | head -20 to find the largest packages.
Symptom · 03
CI pipeline is slow on install step
Fix
Check if npm ci is being used instead of npm install. npm ci is faster and respects lockfile exactly.
Symptom · 04
Duplicate React versions in bundle
Fix
Run npm ls react to see if multiple React versions are installed. This happens when a package pins an older React version.
★ Quick Debug ReferenceFast commands for diagnosing dependency bloat
Too many dependencies
Immediate action
List top-level packages
Commands
npm ls --depth=0
npm ls --depth=0 --all | wc -l
Fix now
Remove eslint-config-next and @next/bundle-analyzer if unused
node_modules too large on disk+
Immediate action
Find largest packages
Commands
du -sh node_modules/* | sort -rh | head -20
npx cost-of-modules
Fix now
Run npm dedupe and check for duplicate versions of React and Next.js
CI install is slow+
Immediate action
Check if npm ci is being used
Commands
npm ci --prefer-offline
npm cache clean --force && npm ci
Fix now
Add --prefer-offline flag and ensure lockfile is committed
⚙ Quick Reference
19 commands from this guide
FileCommand / CodePurpose
audit-deps.shnpm ls --depth=0 --all 2>/dev/null | wc -lWhat Actually Happens When You Run create-next-app
terminal.shnpx create-next-app@latest my-app --turbo --typescript --tailwind --eslint --app...The Project Structure
next.config.tsconst nextConfig: NextConfig = {The Build Pipeline
applayout.tsxexport default function RootLayout({The 12 Files You Actually Get
package.json{The package.json
DockerfileFROM node:20-alpine AS depsThe Hidden Cost of TypeScript Types
ci-install.shnpm ciThe Lockfile
terminal.shnpm run dev -- --turboThe Dev Server
appapiroute.tsexport async function GET() {Environment Variables
apppage.tsxexport default function Home() {The Public Folder
apppage.tsxexport default async function Home() {The First Page
audit.shnpm ls --depth=0 --production 2>/dev/nullThe node_modules Rabbit Hole
terminal.shnpx next infoThe Scripts
appblog[slug]page.tsxexport default async function BlogPost({The First Edit
check-case.shfind app -type d | grep -E '[A-Z]' && echo "ERROR: Uppercase folder names found ...The First Bug
security-audit.shnpm auditThe First Optimization
.gitignore/node_modulesThe Hidden Files
applayout.tsxexport const metadata: Metadata = {The First Week
appblog[slug]page.tsxexport async function generateStaticParams() {The First Production Build

Key takeaways

1
create-next-app installs 3,400+ packages because Next.js bundles its entire toolchain
Webpack, SWC, PostCSS, ESLint, and all their transitive dependencies
2
Use --turbo flag for new projects to replace Webpack with Turbopack, cutting ~400 packages
3
eslint-config-next is the single biggest contributor to dependency bloat
skip it until you need it
4
Commit your lockfile and use npm ci in CI for reproducible, fast installs
5
Run npm dedupe after every install to collapse duplicate transitive dependencies
6
Multi-stage Docker builds with npm ci --production in the final stage reduce image size by 70%
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What happens when you run `npx create-next-app`? Walk through the entire...
Q02JUNIOR
What's the difference between next dev and next build + next start?
Q03SENIOR
How does Next.js decide whether a page is static or dynamic?
Q04JUNIOR
What's the difference between npm, npx, yarn, and pnpm? Which should I u...
Q05JUNIOR
Explain the difference between dependencies and devDependencies in packa...
Q06SENIOR
What is SWC and why did Next.js replace Babel with it?
Q01 of 06JUNIOR

What happens when you run `npx create-next-app`? Walk through the entire process.

ANSWER
First, npx downloads and executes the create-next-app package from npm. It prompts for configuration (TypeScript, ESLint, Tailwind, src/ directory, import alias). Then it scaffolds the project files: app/layout.tsx, app/page.tsx, app/globals.css, next.config.js, tailwind.config.ts, tsconfig.json, postcss.config.mjs, and package.json. Finally, it runs npm install which resolves the dependency tree — next pulls in 80+ packages, eslint-config-next pulls in 1,200+, and the total reaches 3,400+. The lockfile is generated with exact versions for every transitive dependency.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Why does create-next-app install 3,400 packages for a Hello World app?
02
Can I delete node_modules and reinstall faster?
03
What's the difference between npm install and npm ci?
04
Do I need all those @types packages?
05
Why does my production build fail with TypeScript errors when my local build works?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
July 12, 2026
last updated
1,787
articles · all by Naren
🔥

That's Next.js. Mark it forged?

12 min read · try the examples if you haven't

Previous
Building Multi-Agent AI Systems with Next.js and LangGraph
20 / 56 · Next.js
Next
Pages and Layouts in Next.js: File-Based Routing Explained