Type something to search...
Next.js Installation

Next.js Installation

Every Next.js project starts the same way: one command, a handful of prompts, and a folder full of generated files. That simplicity is deceptive. The installation step is where you make decisions that are expensive to reverse later — TypeScript or JavaScript, ESLint or Biome, a src/ directory or not, Turbopack or Webpack, App Router or Pages Router. Get these right on day one and you never think about them again. Get them wrong and you're rewriting import paths or migrating a linter config six months in.

This guide walks through everything the installation process touches: what create-next-app actually generates and why, how to build the same setup by hand if you want full control, and the configuration steps — TypeScript, linting, path aliases — that most tutorials treat as optional footnotes but that you'll want configured correctly from the very first commit.

Why the Installation Choices Matter

A lot of developers treat create-next-app as a black box: run it, accept the defaults, start coding. That's a perfectly reasonable approach for a throwaway prototype. But every prompt you answer during setup bakes an assumption into your project structure, and some of those assumptions are hard to undo:

  • App Router vs. Pages Router determines your entire routing and rendering model. Pages Router uses getServerSideProps and getStaticProps; App Router uses Server Components, fetch caching, and Server Actions. These are not two flavors of the same thing — they're different mental models, and migrating from one to the other later is a multi-day project, not a find-and-replace.
  • TypeScript vs. JavaScript is easy to bolt on later (Next.js will offer to set it up the moment it sees a .ts file), but starting without it means you write months of code with no static checking, and retrofitting types onto an existing codebase is far more tedious than writing them as you go.
  • The import alias (@/* by default) shapes every import statement in your codebase. Changing it later means touching every file that imports anything.
  • src/ directory or not affects where your app folder, components, and lib code live relative to your config files. It's cosmetic, but it's the kind of cosmetic decision that's annoying to reverse across a large codebase.

None of this means you need to agonize over the prompts. It means you should actually read them instead of mashing Enter, because you're not just installing a package — you're picking a project skeleton.

System Requirements and Supported Browsers

Before running anything, make sure your environment can actually run Next.js. The minimum Node.js version is 20.9, and Next.js officially supports macOS, Windows (including WSL), and Linux. If you're on an older LTS Node version because some other project pinned it, you'll want a version manager (nvm, fnm, or volta) so you can switch per-project rather than upgrading Node globally and breaking something else.

On the browser side, Next.js supports modern browsers out of the box with zero configuration:

  • Chrome 111+
  • Edge 111+
  • Firefox 111+
  • Safari 16.4+

If you need to support older browsers — think corporate intranets running Internet Explorer holdovers, or a client base still on ancient Safari versions — you'll need to configure polyfills and target specific browsers explicitly. That's a real, if increasingly rare, constraint worth checking before you commit to a project timeline that assumes "it just works everywhere."

The Fast Path: create-next-app

The overwhelming majority of Next.js projects start with create-next-app, and for good reason — it wires together TypeScript, linting, styling, and the App Router into a working project in under a minute.

# npm
npx create-next-app@latest my-app --yes

# pnpm
pnpm create next-app@latest my-app --yes

# yarn
yarn create next-app@latest my-app --yes

# bun
bun create next-app@latest my-app --yes

Once that finishes:

cd my-app
npm run dev

Visit http://localhost:3000 and you'll see the default Next.js starter page.

The --yes flag is worth understanding rather than treating as a magic incantation. It skips the interactive prompts entirely and applies either your saved preferences (if you've run create-next-app before and it remembered your choices) or the framework defaults: TypeScript, Tailwind CSS, ESLint, App Router, and Turbopack, with the import alias set to @/*. It also scaffolds an AGENTS.md file — and a CLAUDE.md that simply points to it — specifically so that AI coding agents working in your repo pull instructions from the version of Next.js you actually have installed, rather than confidently generating code based on stale training data. If you've ever had an AI assistant suggest getServerSideProps inside an App Router project that has no pages directory, this is the mechanism designed to stop that from happening.

Running It Interactively

Drop the --yes flag and you get prompted instead:

What is your project named? my-app
Would you like to use the recommended Next.js defaults?
    Yes, use recommended defaults - TypeScript, ESLint, Tailwind CSS, App Router, AGENTS.md
    No, reuse previous settings
    No, customize settings - Choose your own preferences

Picking "customize settings" unlocks the full list:

Would you like to use TypeScript? No / Yes
Which linter would you like to use? ESLint / Biome / None
Would you like to use React Compiler? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like your code inside a `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the import alias (`@/*` by default)? No / Yes
What import alias would you like configured? @/*
Would you like to include AGENTS.md to guide coding agents to write up-to-date Next.js code? No / Yes

A couple of these deserve more thought than a reflexive "yes":

Biome vs. ESLint — Biome is genuinely fast (it's written in Rust) and bundles linting and formatting into one tool, which means one less dependency (prettier) and one less config file to maintain. But ESLint has a vastly larger plugin ecosystem — if your team relies on a specific accessibility linter, an import-sorting rule, or a framework-specific plugin that only exists for ESLint, Biome won't have a drop-in replacement yet. Default to ESLint unless you've specifically evaluated Biome's rule coverage against what you currently use.

React Compiler — this automatically memoizes components and hooks at build time, which in principle removes the need for manual useMemo/useCallback/React.memo calls. It's stable enough to turn on for new projects, but if your team has existing muscle memory around manual memoization, mixing both approaches in the same codebase can get confusing. Pick one strategy and be consistent about it.

src/ directory — this is genuinely a coin flip that mostly comes down to taste. Without src/, your app folder sits at the project root next to next.config.js, package.json, and node_modules. With it, your app code is cleanly separated from tooling config. If you're coming from a monorepo where src/ is the convention across every package, keep it consistent here too.

What Actually Gets Generated

Understanding the generated project matters more once you start reading the file conventions reference and wondering why certain files exist. A default App Router project looks roughly like this:

my-app/
├── app/
│   ├── favicon.ico
│   ├── globals.css
│   ├── layout.tsx
│   └── page.tsx
├── public/
├── AGENTS.md
├── CLAUDE.md
├── next.config.ts
├── package.json
├── tsconfig.json
└── eslint.config.mjs

The app/layout.tsx and app/page.tsx files are the two files that matter most on day one, and they're worth understanding at the source level rather than treating as boilerplate — which is exactly what the next section walks through.

Manual Installation

If you're retrofitting Next.js into an existing project, adding it to a monorepo package, or you simply want to know what create-next-app is doing under the hood, you can install everything by hand.

Install the three required packages:

# npm
npm i next@latest react@latest react-dom@latest

# pnpm
pnpm i next@latest react@latest react-dom@latest

# yarn
yarn add next@latest react@latest react-dom@latest

# bun
bun add next@latest react@latest react-dom@latest

One detail that trips people up: the App Router runs on React canary releases internally, which include every stable React 19 feature plus newer capabilities still being validated across frameworks. You still need to declare react and react-dom explicitly in package.json — for tooling compatibility and so package managers resolve peer dependencies correctly — but the actual React runtime the App Router uses under the hood tracks ahead of what you'd get from a plain npm install react. If you're using the Pages Router instead, this doesn't apply — it uses whatever React version is pinned in your package.json, full stop.

Next, add the standard scripts to package.json:

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint",
    "lint:fix": "eslint --fix"
  }
}
  • next dev starts the development server. As of recent Next.js versions, Turbopack is the default bundler for next dev — you don't need to opt into it. If you need the old Webpack-based dev server for a plugin that hasn't caught up to Turbopack yet, run next dev --webpack.
  • next build produces the production build.
  • next start serves that production build.
  • eslint runs your linter as a standalone script — notably, next build no longer runs linting automatically as of Next.js 16, which is a meaningful change if you're used to lint errors surfacing during your build step. Wire lint into your CI pipeline explicitly rather than assuming the build will catch style violations.

Creating the app Directory

Next.js uses file-system routing: the folder structure under app/ directly determines your application's routes. There's no route config file to maintain — the filesystem is the config.

Start by creating an app folder, and inside it, a layout.tsx file. This is the root layout, and it's non-negotiable — every App Router project needs one, and it must render <html> and <body>:

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

Then add a home page at app/page.tsx:

// app/page.tsx
export default function Page() {
  return <h1>Hello, Next.js!</h1>;
}

Both files render together when a visitor hits /. The layout wraps every page beneath it in the route tree, which is why it's the one place responsible for the outer <html>/<body> shell — a child page.tsx never renders its own <html> tag.

If you forget the root layout entirely, Next.js will silently generate one for you the moment you run next dev. That's a helpful safety net during prototyping, but don't rely on it in a real project — you'll almost always want a custom root layout to set the lang attribute correctly, load a global stylesheet, or wrap the page in shared providers (theme context, auth context, and so on).

You can optionally nest your code inside a src/ folder to keep application code separate from root-level config files like next.config.js and tsconfig.json. This is purely organizational — Next.js resolves src/app exactly the same way it resolves a top-level app/.

Creating the public Folder

Static assets — images, fonts, robots.txt, favicons — live in a public folder at your project root. Anything placed there is served from the base URL of your site. Drop profile.png into public/ and it's reachable at /profile.png, no import or build step required:

// app/page.tsx
import Image from "next/image";

export default function Page() {
  return <Image src="/profile.png" alt="Profile" width={100} height={100} />;
}

A practical note the reference docs don't dwell on: because public files are served verbatim with no processing, this is not the place for anything you want optimized, bundled, or content-hashed for cache-busting. Images referenced through next/image still benefit from Next's built-in optimization pipeline even when the source file lives in public — the optimization happens at request time, not at build time for that folder. But raw assets like PDFs, downloadable files, or a manifest.json you're hand-maintaining should go here specifically because you don't want Next.js touching them.

Running the Development Server

With the app directory in place:

npm run dev

Visit http://localhost:3000, and edit app/page.tsx — Next.js's Fast Refresh will pick up the change and update the browser without a full reload, while preserving component state where it can. This is one of the most underrated developer-experience wins of the framework: you rarely lose your place in a form or a scroll position just because you tweaked some JSX.

Setting Up TypeScript

Next.js has TypeScript support built in — there's no separate plugin to install. The moment you rename a file to .ts or .tsx and run next dev, Next.js detects it, automatically installs the necessary type-checking dependencies, and generates a tsconfig.json with sensible defaults. You genuinely don't need to pre-configure anything; the framework configures itself reactively the first time it notices TypeScript in your project.

The minimum supported TypeScript version is 5.1.0, which is old enough that this is rarely a constraint in practice — but worth knowing if you're maintaining a project pinned to an ancient TypeScript release for unrelated reasons.

The IDE Plugin

Next.js ships a custom TypeScript language service plugin that gives editors deeper insight into Next.js-specific patterns — things like validating the props shape of page.tsx or catching invalid exports from a route.ts file, checks a generic TypeScript server wouldn't know to perform.

To enable it in VS Code:

  1. Open the command palette (Ctrl/⌘ + Shift + P)
  2. Search for "TypeScript: Select TypeScript Version"
  3. Select "Use Workspace Version"

Skipping this step doesn't break anything — your code still type-checks — but you lose the extra layer of Next.js-aware validation that catches framework-specific mistakes (like a page.tsx exporting the wrong prop shape) before you even run the dev server.

Setting Up Your Editor

This is a small but genuinely useful quality-of-life fix that most getting-started guides skip entirely. Because the App Router names files by convention — page.tsx, layout.tsx, route.ts, loading.tsx — your editor's tab bar fills up with identically-named tabs the moment your project has more than a couple of routes. You end up with five tabs all labeled page.tsx and no way to tell them apart without hovering over each one.

In VS Code 1.88+ or Cursor, you can fix this with custom editor labels in .vscode/settings.json:

// .vscode/settings.json
{
  "workbench.editor.customLabels.patterns": {
    "**/app/**/page.tsx": "${dirname(1)}/${dirname} - page.tsx",
    "**/app/**/layout.tsx": "${dirname(1)}/${dirname} - layout.tsx",
    "**/app/**/loading.tsx": "${dirname(1)}/${dirname} - loading.tsx",
    "**/app/**/error.tsx": "${dirname(1)}/${dirname} - error.tsx",
    "**/app/**/not-found.tsx": "${dirname(1)}/${dirname} - not-found.tsx",
    "**/app/**/template.tsx": "${dirname(1)}/${dirname} - template.tsx",
    "**/app/**/default.tsx": "${dirname(1)}/${dirname} - default.tsx",
    "**/app/**/route.ts": "${dirname(1)}/${dirname} - route.ts"
  }
}

The dirname(1) piece is what makes this actually useful — labeling just the immediate parent folder isn't enough once you have dynamic segments, because blog/[id]/page.tsx and products/[id]/page.tsx would both collapse down to a tab labeled [id] - page.tsx. Going two folders deep disambiguates them. If you'd rather not hand-write this, it's exactly the kind of task you can point a coding agent at directly, since the change is small, mechanical, and easy to verify.

Worth noting: JetBrains IDEs (WebStorm, IntelliJ) already show the enclosing folder for same-named files by default, so if that's your editor, there's nothing to configure here.

Setting Up Linting

Next.js supports two linting paths: ESLint, the long-standing default with the deepest plugin ecosystem, and Biome, a much faster combined linter-and-formatter written in Rust. Both are wired in through plain package.json scripts rather than a Next.js-specific CLI wrapper:

// ESLint
{
  "scripts": {
    "lint": "eslint",
    "lint:fix": "eslint --fix"
  }
}
// Biome
{
  "scripts": {
    "lint": "biome check",
    "format": "biome format --write"
  }
}

If you're maintaining an older project that still calls next lint, that command has been superseded by running ESLint's own CLI directly. There's a codemod to handle the migration mechanically instead of hand-editing scripts:

npx @next/codemod@canary next-lint-to-eslint-cli .

If you go with ESLint, use an explicit config file — eslint.config.mjs is the recommended modern format, though ESLint still supports the legacy .eslintrc.* style if you're integrating with an existing setup that depends on it.

The detail worth repeating because it catches people off guard: starting with Next.js 16, next build no longer runs the linter as part of the build. If your CI pipeline was relying on a bad build to catch lint errors, it won't anymore — you need an explicit lint step in your pipeline. This is a deliberate change (linting and building are different concerns with different failure semantics), but it's a silent behavior change if you're upgrading from an older major version and haven't updated your CI config to match.

Absolute Imports and Module Path Aliases

Next.js has built-in support for the paths and baseUrl options in tsconfig.json (or jsconfig.json for plain JavaScript projects), which lets you replace long relative import chains with clean absolute ones:

// Before
import { Button } from "../../../components/button";

// After
import { Button } from "@/components/button";

To enable this, set baseUrl:

// tsconfig.json or jsconfig.json
{
  "compilerOptions": {
    "baseUrl": "src/"
  }
}

And optionally define explicit aliases with paths:

// tsconfig.json or jsconfig.json
{
  "compilerOptions": {
    "baseUrl": "src/",
    "paths": {
      "@/styles/*": ["styles/*"],
      "@/components/*": ["components/*"]
    }
  }
}

Every entry under paths is resolved relative to baseUrl, not relative to the file doing the importing — which is exactly the point, since the whole reason to use an alias is to stop caring how many directories deep the importing file happens to be.

If you accepted the create-next-app default, you already have @/* mapped to your project root (or src/, if you chose that layout), and in practice that single alias covers most projects without needing anything more granular. Reach for per-folder aliases like @/components/* and @/styles/* mainly in larger codebases where you want import paths to signal intent (@/components/Button versus @/lib/api/Button, say) rather than just shortening them.

Keeping Next.js Up to Date

Installation isn't a one-time event — it's the start of an ongoing relationship with a framework that ships frequently. Each release bundles security patches, bug fixes, and performance work alongside new features, and staying reasonably current keeps each individual upgrade small and low-risk instead of facing one enormous, breaking jump two years later.

Run the built-in upgrade command:

# npm
npx next upgrade

# pnpm
pnpm next upgrade

# yarn
yarn next upgrade

# bun
bunx next upgrade

Here's a detail that's easy to overlook: upgrading also updates the documentation bundled directly inside the next package at node_modules/next/dist/docs/. That's not incidental — it means the guidance available to you (and to any AI coding agent working in your repo) tracks the exact version of Next.js you have installed, not a generic "current as of some training cutoff" understanding. New features ship with their own docs, and existing pages accumulate corrections and pitfalls discovered after release. After upgrading, it's worth explicitly prompting an AI assistant working in your codebase to re-read what changed, rather than assuming it already knows.

Common Mistakes Worth Avoiding

A few practical notes from actually doing this repeatedly, none of which show up in the official installation page:

Mixing package manager lockfiles. If you scaffold with npx create-next-app but a teammate later runs yarn install, you'll end up with both a package-lock.json and a yarn.lock in the repo, and dependency resolution will silently diverge between machines. Pick one package manager for the project and add the others' lockfile patterns to .gitignore immediately.

Committing node_modules accidentally. Obvious in theory, still happens in practice, especially when someone initializes a repo before running create-next-app rather than after. Double check your .gitignore includes node_modules, .next, and .env*.local before your first commit.

Treating AGENTS.md as noise and deleting it. It's easy to assume a file you didn't ask for is scaffolding cruft, but it exists specifically to keep AI coding agents from generating outdated patterns against your installed Next.js version. If your team uses AI tooling at all, keep it, and update it if your conventions diverge from the defaults.

Assuming App Router and Pages Router are interchangeable. They're not two skins on the same engine — they have different data-fetching models, different file conventions, and different rendering defaults. If you're not sure which to pick for a new project, pick App Router; it's where new Next.js features land first, and Pages Router is increasingly the legacy path for existing projects rather than the recommended starting point for new ones.

Skipping the linter setup because "I'll add it later." Retrofitting lint rules onto an already-large codebase means either fixing hundreds of violations in one PR or disabling half the ruleset just to get CI green. Configuring ESLint or Biome on day one, when your codebase is three files, costs nothing.

Key Takeaways

DecisionRecommendation
CLI vs. manual installUse create-next-app unless you have a specific reason (monorepo integration, custom scaffolding) to do it by hand
App Router vs. Pages RouterDefault to App Router for new projects
TypeScriptEnable it from the start — retrofitting types later is far more work than writing them as you go
ESLint vs. BiomeESLint unless you've verified Biome covers your specific plugin needs
Import aliasKeep the default @/* unless your codebase is large enough to benefit from per-folder aliases
Linting in CIAdd an explicit lint script — next build no longer lints automatically as of Next.js 16
Root layoutAlways define one explicitly rather than relying on Next.js's auto-generated fallback
Staying currentRun next upgrade regularly rather than deferring major-version jumps

Installation looks like a five-minute formality, but it's really the point where you commit to a project's shape. Spend the extra ten minutes reading the prompts instead of accepting every default blindly, and you'll spend a lot less time later untangling decisions you didn't realize you were making.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper