Basic Next.js Setup
The most minimal Next.js 15 setup — bare create-next-app with TypeScript and the App Router, nothing else. No Tailwind, no ESLint, no src/ directory. Just the essentials for a Server Component–first project on React 19.
Recipe
Quick-reference recipe card — copy-paste ready.
# Create the app with only the flags we want
npx create-next-app@latest my-app \
--typescript \
--app \
--no-tailwind \
--no-eslint \
--no-src-dir \
--import-alias "@/*"
# Enter the project and start the dev server
cd my-app
npm run devWhen to reach for this: Learning Next.js, writing a reproducible minimal bug repro, or starting a project where you plan to layer in your own tooling (Biome, Vanilla Extract, your own linter) rather than accept the defaults.
Working Example
After the recipe above finishes, you will have this directory layout:
my-app/
├── app/
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── public/
│ ├── next.svg
│ └── vercel.svg
├── next.config.ts
├── next-env.d.ts
├── package.json
├── tsconfig.json
└── README.mdThe root layout is a Server Component and wraps every route:
// app/layout.tsx
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "My App",
description: "Minimal Next.js 15 + React 19 setup",
};
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}A Server Component page that fetches data at request time — no useEffect, no client bundle cost:
// app/page.tsx
type Post = { id: number; title: string };
async function getPosts(): Promise<Post[]> {
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=5", {
// Next 15 defaults to no automatic caching for fetch; opt in explicitly
next: { revalidate: 60 },
});
if (!res.ok) throw new Error("Failed to fetch posts");
return res.json();
}
export default async function HomePage() {
const posts = await getPosts();
return (
<main>
<h1>Latest posts</h1>
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
</main>
);
}The generated tsconfig.json turns on strict mode and wires up the @/* alias:
// tsconfig.json (abridged)
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"jsx": "preserve",
"incremental": true,
"plugins": [\{ "name": "next" \}],
"paths": \{ "@/*": ["./*"] \}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}And the config file is now TypeScript by default:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// Add config here as you need it.
};
export default nextConfig;Deep Dive
How It Works
create-next-app is a small scaffolder distributed as its own npm package. When you run npx create-next-app@latest, npx downloads the latest published version into a temporary cache, runs it, and passes the remaining arguments to it. The scaffolder copies a template directory from inside the package, runs your chosen package manager's install, and (unless --disable-git is set) runs git init with an initial commit.
Every flag in the recipe maps 1:1 to an answer to an interactive prompt, so --typescript --app --no-tailwind --no-eslint --no-src-dir is the same as answering those five prompts manually. Passing all five means create-next-app runs non-interactively and your setup is reproducible in CI.
Once installed, npm run dev starts the Next.js dev server. In Next 15, Turbopack is the default dev bundler — you do not need a separate flag.
Variations
- Turbopack dev (explicit): it is on by default, but you can force it in
package.jsonwith"dev": "next dev --turbopack". - Custom port:
npm run dev -- -p 4000or setPORT=4000 npm run dev. - Custom directory: pass any path as the project name, e.g.
npx create-next-app@latest ./apps/web .... - Without git init: add
--disable-gitto the command. - Manual setup:
mkdir my-app && cd my-app && npm init -y && npm install next@latest react@latest react-dom@latest && npm install -D typescript @types/react @types/node, then createapp/layout.tsxandapp/page.tsxby hand; Next generatesnext-env.d.tson first run. - pnpm:
pnpm create next-app my-app --typescript --app --no-tailwind --no-eslint --no-src-dir. - yarn:
yarn create next-app my-app --typescript --app --no-tailwind --no-eslint --no-src-dir. - bun:
bun create next-app my-app --typescript --app --no-tailwind --no-eslint --no-src-dir.
TypeScript Notes
The --typescript flag writes a tsconfig.json with strict: true, moduleResolution: "bundler", and the Next.js TypeScript plugin registered under compilerOptions.plugins. That plugin is what powers autocomplete for metadata, route params, and the generated route types in .next/types/.
The paths entry "@/*": ["./*"] is relative to the project root, not to src/, because you opted out of --src-dir. If you later move files into src/, you must update the alias to "@/*": ["./src/*"].
next-env.d.ts is auto-generated on every dev/build. Do not edit it; do not delete it. The standard .gitignore commits it.
Gotchas
- App Router vs Pages Router. The
app/directory is the App Router. If you also create apages/directory, both routers will run side by side and you will get collisions on shared routes. Deletepages/unless you are intentionally migrating. src/vs rootapp/placement. With--no-src-dir, the App Router lives at./app. If any tutorial tells you to put files in./src/app, that is the--src-dirlayout; mixing the two silently breaks imports.- Defaults change between Next.js versions. In Next 15 the default prompts include Turbopack and
AGENTS.md— flags that did not exist in Next 13 or 14. Always pin to@latestor an exact version in your docs. - Node.js 18.18 or newer is required. Next 15 refuses to start on Node 18.17 and below. Run
node -vbeforenpm run dev; use nvm to switch if you are on an older LTS. - Port 3000 already in use.
npm run devwill bail withEADDRINUSEif another process holds 3000. Either kill it (lsof -iTCP:3000 -sTCP:LISTEN) or run with-p 3001. fetchis not cached by default in Next 15. If you expect the old "fetch is cached forever" behavior from Next 13/14, you must opt in explicitly withnext: \{ revalidate: N \}orcache: "force-cache".next.config.tsrequires a TypeScript-aware Node runtime. It works out of the box in Next 15, but some older tooling that shells out tonext buildfrom a plain Node script may complain — fall back tonext.config.mjsif you hit this.
Alternatives
| Tool | When to use it | Trade-off vs Next.js 15 |
|---|---|---|
| Vite + React Router | SPAs, dashboards behind auth, no SEO needs | No Server Components, no built-in SSR, no file-system routing |
| Remix | Form-heavy apps, nested routing, progressive enhancement | Smaller ecosystem; different data loading model (loader/action) |
| Astro | Content sites, marketing pages, blogs | React is a guest; no full React 19 Server Components story |
| Next.js Pages Router | Legacy codebases, libraries still tied to getServerSideProps | Being superseded by App Router; no Server Components |
FAQs
Quick review of this page — click to expand.
Which flag selects the App Router?
--app. Without it, create-next-app may default to App Router in Next 15 but the flag makes the choice explicit and non-interactive.
How do I opt out of Tailwind and ESLint at the same time?
Pass --no-tailwind --no-eslint to create-next-app. Both flags are required because each defaults to on in Next 15.
What does --import-alias "@/*" actually set?
It writes a paths entry in tsconfig.json so @/lib/x resolves to ./lib/x (or ./src/lib/x if you used --src-dir).
Why is my app/page.tsx a Server Component by default?
Everything in app/ is a Server Component unless the file starts with "use client". That is why you can await inside the component body.
Do I need a separate --turbopack flag in Next 15?
No. Turbopack is the default dev bundler. The flag is only useful if you want to pin it in package.json for clarity.
What is the minimum Node.js version?
18.18.0. Anything older fails at startup. Gotcha: several popular CI images still ship 18.17 by default.
What happens if port 3000 is busy?
next dev exits with EADDRINUSE. Run npm run dev -- -p 3001 or kill the process listening on 3000. Gotcha: some older Next versions auto-bumped the port; Next 15 does not.
Is next-env.d.ts safe to edit?
No. It is regenerated on every build and any edits will be lost. Leave it committed and untouched.
How do I enable strict TypeScript?
It is already on. create-next-app writes "strict": true in tsconfig.json by default when you pass --typescript. TS note: that also enables strictNullChecks, noImplicitAny, and friends.
Why does my fetch call hit the network every request?
Next 15 changed the default: fetch is no longer cached unless you opt in. Add next: \{ revalidate: 60 \} or cache: "force-cache" to the fetch options.
Can I type next.config.ts with full IntelliSense?
Yes. Import NextConfig from next and annotate the exported constant. TS note: NextConfig is the canonical type in Next 15; the older /** @type \{import('next').NextConfig\} */ JSDoc comment is no longer needed.
What is the difference between app/ and pages/?
app/ is the App Router (React 19, Server Components, layouts, streaming). pages/ is the legacy Pages Router. You can run both, but start new projects on app/ only.