Getting Started with Next.js
With Git and GitHub in place, you're ready to build something. This chapter walks you through creating your first Next.js application — from installing the tools to deploying a live website. By the end, you'll have a working web app running in the cloud and a solid understanding of how Next.js organizes a project.
What Is React, and How Does Next.js Build on It?#
Before diving into Next.js, it helps to understand the tool it builds on.
React is a JavaScript library created by Meta for building user interfaces. It lets you break your UI into reusable pieces called components — functions that return HTML-like syntax called JSX. For example, a Button component, a Header component, and a UserCard component can each be written once and reused across many pages.
React handles one job very well: keeping the UI in sync with your data. When your data changes, React automatically updates the relevant parts of the page — without you having to manually find and edit the HTML.
What React doesn't handle is everything else a real web app needs: routing between pages, fetching data from a server, writing backend logic, or optimizing images. For those things, you'd need to wire up additional libraries yourself.
Next.js is a full-stack framework built on top of React. It adds everything React leaves out:
| Feature | Plain React | Next.js |
|---|---|---|
| Routing | Manual — requires a separate library like React Router | Built-in, file-based — create a file, get a route |
| Server-side rendering | Manual setup required | Built-in |
| Backend API endpoints | Requires a separate server | Built-in Route Handlers |
| Image optimization | Manual | Built-in <Image> component |
| Code splitting | Manual configuration | Automatic |
| TypeScript | Manual configuration | Built-in support |
The short version: React is the engine, Next.js is the car. React gives you the core capability to build UIs; Next.js gives you everything around it — routing, data fetching, a backend, and deployment tooling — so you can build a complete application without stitching together a dozen separate tools.
Why These Advantages Matter#
Server-side rendering (SSR) means your pages are built on the server before being sent to the browser. The user sees content immediately, even on a slow connection, and search engines can index your pages properly. With plain React, pages are often blank until JavaScript loads and runs — a real problem for performance and SEO.
File-based routing means the URL structure of your app is determined by how you organize your files. Want a page at /about? Create a file at app/about/page.tsx. No router configuration, no imported libraries — the file is the route.
Built-in API routes mean you can write backend logic in the same project as your frontend. You don't need a separate Express server or a separate deployment.
Step 1: Install Node.js#
Next.js requires Node.js — a runtime that lets you execute JavaScript outside the browser. Installing Node.js also gives you npm (Node Package Manager), which you use to install third-party packages and run project scripts.
Download and Install#
Go to nodejs.org and download the LTS (Long-Term Support) version. LTS releases are stable and receive security updates for years — they're the right choice for most development work. Avoid the "Current" release unless you have a specific reason to use it.
- Mac: Use the
.pkginstaller, or install via Homebrew:brew install node - Windows: Use the
.msiinstaller. Keep all default options during installation. - Linux: Use your package manager (e.g.,
sudo apt install nodejs npmon Ubuntu)
Tip for managing multiple Node.js versions: If you ever need to switch between different Node.js versions for different projects, use nvm (Node Version Manager). It lets you install multiple versions side by side and switch between them with a single command. Find it at github.com/nvm-sh/nvm (Mac/Linux) or github.com/coreybutler/nvm-windows (Windows).
Verify the Installation#
After installing, open a new terminal window and run:
node -v
npm -v
You should see version numbers printed for both. If you see a "command not found" error, try closing and reopening your terminal — the installer may need a fresh session to take effect.
Step 2: Create Your First Next.js Project#
You don't need to install Next.js separately. The npx command (included with npm) downloads and runs the project creator for you:
npx create-next-app@latest
The tool will ask you a series of questions. Here's what you'll see and what to choose:
What is your project named? my-app
Would you like to use TypeScript? Yes
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like your code inside a `src/` directory? No
Would you like to use App Router? (recommended) Yes
Would you like to use Turbopack for `next dev`? Yes
Would you like to customize the import alias (`@/*` by default)? No
What these mean:
- TypeScript — adds type checking to JavaScript, catching bugs before they reach the browser. Recommended for any project you take seriously.
- ESLint — automatically checks your code for common mistakes and style issues. Recommended.
- Tailwind CSS — a utility-first CSS library. Rather than writing separate CSS files, you style elements directly in your markup using short class names like
text-blue-500andp-4. Very widely used in the Next.js ecosystem. src/directory — some developers prefer to put their code inside asrc/subfolder to separate it from config files. For a new learner, choosingNokeeps the structure simpler.- App Router — the modern, recommended routing system in Next.js. Always choose
Yes. - Turbopack — a faster development bundler that replaces the older Webpack. Choose
Yesfor faster page reload times during development. - Import alias — the
@/*alias lets you import files from the project root using@/instead of long relative paths like../../components/Button. Keep the default.
After you answer the prompts, create-next-app creates your project folder and installs all dependencies automatically. This may take a minute or two.
Step 3: Run the Development Server#
Navigate into your new project folder and start the development server:
cd my-app
npm run dev
Your terminal will show:
▲ Next.js 15.x.x (Turbopack)
- Local: http://localhost:3000
- Network: http://192.168.x.x:3000
Open http://localhost:3000 in your browser. You'll see the default Next.js welcome page.
Make Your First Change#
Open app/page.tsx in VS Code and replace its contents with something simple:
export default function Page() {
return (
<main>
<h1>Hello, Next.js!</h1>
<p>My first Next.js app.</p>
</main>
)
}
Save the file. The browser updates immediately — no manual page reload needed. This is called Fast Refresh. The development server watches your files and applies changes on the fly, which makes iteration very fast.
To stop the server, go back to the terminal and press Ctrl + C.
Project Structure#
After running create-next-app, your project looks like this:
my-app/
├── app/ # Pages, layouts, and API routes
│ ├── layout.tsx # Root layout (wraps every page)
│ ├── page.tsx # Home page — renders at /
│ └── globals.css # Global CSS styles
├── public/ # Static files (images, fonts, etc.)
├── node_modules/ # Installed packages — do not edit
├── next.config.ts # Next.js configuration
├── package.json # Project scripts and dependencies
├── tsconfig.json # TypeScript configuration
├── eslint.config.mjs # ESLint configuration
└── .gitignore # Files excluded from Git
What Each Part Does#
app/ is where you'll spend most of your time. Every page, layout, and API endpoint lives here. Next.js reads this folder to determine your app's routes.
public/ holds static files that are served directly. A file at public/logo.png is accessible at /logo.png in your app. Use this for images and other assets that don't need processing.
node_modules/ contains all the installed packages your project depends on. Never edit files here, and never commit this folder to Git — it can be regenerated at any time by running npm install.
next.config.ts is where you configure Next.js behavior — things like redirects, allowed image domains, and environment variable exposure. Most beginners won't need to touch this at first.
package.json is the project's manifest. It lists your dependencies and defines the scripts you can run:
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
}
dev— starts the development server atlocalhost:3000build— compiles an optimized production buildstart— runs the production build locally (useful for testing before deploy)lint— checks your code for style and correctness issues
tsconfig.json configures the TypeScript compiler — how strict the type checking should be, which files to include, and how module imports resolve. You rarely need to edit this directly; create-next-app sets it up correctly for you.
App Router: File-Based Routing#
The most important concept in Next.js is how routing works. Instead of configuring a router in JavaScript, you create files and folders — and Next.js infers the routes from them.
The rules are simple:
- A folder inside
app/creates a URL segment - A
page.tsxfile inside that folder makes the route publicly accessible - A
layout.tsxfile wraps all pages at that level and below
| File path | URL | Notes |
|---|---|---|
app/page.tsx | / | Home page |
app/about/page.tsx | /about | About page |
app/blog/page.tsx | /blog | Blog index |
app/blog/[slug]/page.tsx | /blog/anything | Dynamic route — slug changes per post |
app/dashboard/settings/page.tsx | /dashboard/settings | Nested route |
page.tsx — A Basic Page#
Every page is a React component that you export default. The file must be named exactly page.tsx:
// app/about/page.tsx
export default function AboutPage() {
return (
<div>
<h1>About Us</h1>
<p>We build things with Next.js.</p>
</div>
)
}
Create this file, and /about becomes a working page. No router configuration needed.
layout.tsx — Shared UI That Wraps Pages#
A layout wraps all pages at its level and below. During client-side navigation, the layout renders once and stays mounted as the user moves between pages — it does not re-render on every page visit. This makes it ideal for navigation bars, sidebars, and site-wide structure that should persist across page transitions.
The root layout at app/layout.tsx is required. It must include the <html> and <body> tags because it wraps your entire application:
// app/layout.tsx — Required root layout
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<nav>My Site</nav>
<main>{children}</main>
</body>
</html>
)
}
{children} is where Next.js inserts the current page. Every page you create will appear inside this layout automatically.
Nested Layouts#
You can add a layout.tsx inside any subfolder to add extra wrapping for just those routes:
// app/blog/layout.tsx — Only wraps pages under /blog
export default function BlogLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<section>
<aside>Blog sidebar</aside>
<div>{children}</div>
</section>
)
}
Dynamic Routes#
Wrap a folder name in square brackets to create a dynamic segment — a part of the URL that varies per request:
// app/blog/[slug]/page.tsx
// Visiting /blog/hello-world makes slug = "hello-world"
// Visiting /blog/my-recipe makes slug = "my-recipe"
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
return <h1>Post: {slug}</h1>
}
Why
await params? In Next.js 15,paramsis asynchronous — it's returned as a Promise rather than a plain object. You need toawaitit before accessing its values. This is a change from older versions of Next.js and is worth remembering if you encounter examples written for version 14 or earlier.
Linking Between Pages#
Use the built-in <Link> component instead of a plain <a> tag. It enables fast client-side navigation and prefetches linked pages in the background, so they load almost instantly:
import Link from 'next/link'
export default function Nav() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog">Blog</Link>
</nav>
)
}
Using <Link> instead of <a> means that clicking a link doesn't trigger a full page reload — the transition is seamless.
API Routes (Route Handlers)#
Next.js lets you write backend API endpoints directly in your project. Create a file named route.ts (not page.tsx) inside any folder under app/api/.
A Simple GET Endpoint#
// app/api/hello/route.ts
export async function GET() {
return Response.json({ message: 'Hello from the server!' })
}
This creates a GET endpoint at /api/hello. Visit it in your browser or call it from your frontend with fetch.
Handling GET and POST Together#
// app/api/messages/route.ts
export async function GET() {
const messages = ['Hello', 'World']
return Response.json(messages)
}
export async function POST(request: Request) {
const body = await request.json()
const { text } = body
// In a real app: save to a database here
console.log('Received:', text)
return Response.json({ received: text }, { status: 201 })
}
Your frontend can now call /api/messages with a GET to retrieve messages, or a POST to send one.
When to Use API Routes#
API routes are useful when you need to:
- Keep secrets on the server (API keys, database credentials) without ever exposing them to the browser
- Accept form submissions or webhooks from external services
- Build a backend that both your web app and a mobile app can call
If you only need to fetch and display data, Server Components (covered next) are often simpler and don't require a separate API route.
| HTTP Method | Typical Use |
|---|---|
GET | Retrieve data (list of users, a blog post) |
POST | Create something new (submit a form, add a record) |
PUT / PATCH | Update an existing record |
DELETE | Remove a record |
Server vs. Client Components#
This is one of the most important concepts in the App Router, and one that trips up many beginners. Understanding the distinction makes the rest of Next.js much easier to reason about.
Server Components: The Default#
In the App Router, every component is a Server Component by default. Server Components run on the server, and their code is never sent to the user's browser. This means they can:
- Directly
awaitdatabase queries or API calls — no need foruseEffect+fetch - Safely read secret environment variables
- Reduce the amount of JavaScript the browser needs to download
// app/posts/page.tsx — Server Component (default — no special directive needed)
async function getPosts() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5')
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts() // Runs on the SERVER, not in the browser
return (
<ul>
{posts.map((post: { id: number; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
No loading states, no useEffect, no client-side fetch. The data is ready before the page is sent to the browser.
Client Components: Add 'use client'#
When you need interactivity — click handlers, form inputs, component state, or browser APIs — add 'use client' as the very first line of the file:
// app/ui/counter.tsx — Client Component
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Add one</button>
</div>
)
}
The 'use client' directive tells Next.js: "This component needs to run in the browser." Everything it uses — useState, event handlers, useEffect — only works in a client context.
How to Decide#
| Use a Server Component when... | Use a Client Component when... |
|---|---|
| Fetching data from a database or API | The component has buttons, forms, or click handlers |
| Reading secret environment variables | You use useState or useReducer |
| Rendering content that doesn't change based on user interaction | You use useEffect or other lifecycle hooks |
| You want to reduce JavaScript sent to the browser | You access browser-only APIs like localStorage or window |
Combining Both: The Common Pattern#
A very common pattern is to fetch data in a Server Component and pass it down to a Client Component that handles interactivity:
// app/post/[id]/page.tsx — SERVER Component: fetches data
import LikeButton from '@/app/ui/like-button'
async function getPost(id: string) {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`)
return res.json()
}
export default async function PostPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const post = await getPost(id)
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
<LikeButton /> {/* Client Component handles the click */}
</article>
)
}
// app/ui/like-button.tsx — CLIENT Component: handles the click
'use client'
import { useState } from 'react'
export default function LikeButton() {
const [liked, setLiked] = useState(false)
return (
<button onClick={() => setLiked(!liked)}>
{liked ? 'Liked!' : 'Like this post'}
</button>
)
}
The page loads fast because data is fetched on the server. The LikeButton is small and interactive because it runs in the browser.
Key rule: Apply
'use client'to as few components as possible — only the ones that genuinely need interactivity. The more you keep on the server, the less JavaScript the browser downloads, and the faster your app loads.
Environment Variables#
Most real apps connect to external services: a database, an authentication provider, an AI API. These connections require secret keys — and those keys must never appear in your Git repository or in the JavaScript sent to the user's browser.
Next.js handles this with environment variables loaded from a .env.local file.
The .env.local File#
Create a file named .env.local in your project root. It is already listed in .gitignore, so it will never be committed to GitHub:
# .env.local
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
OPENAI_API_KEY=sk-...
NEXT_PUBLIC_SITE_URL=https://mysite.com
Next.js loads this file automatically. You access variables with process.env.VARIABLE_NAME.
The NEXT_PUBLIC_ Rule#
This is the most important rule for keeping secrets safe:
| Variable name | Available where? | Use for |
|---|---|---|
DATABASE_URL | Server only | Database connection strings, never expose to clients |
OPENAI_API_KEY | Server only | API keys and secrets — keep on the server |
NEXT_PUBLIC_SITE_URL | Server AND browser | Public values safe for anyone to see |
Without the NEXT_PUBLIC_ prefix: The variable is only accessible in server-side code — Server Components, API routes, and server-side data fetching functions. It will be undefined in the browser. This is how Next.js protects your secrets.
With the NEXT_PUBLIC_ prefix: Next.js inlines the value directly into the JavaScript bundle at build time. The value is permanently baked into the client code and visible to anyone who inspects the browser's network traffic or JavaScript files. Never put a secret here.
// app/api/chat/route.ts — Safe: this code runs on the SERVER
export async function POST(request: Request) {
const body = await request.json()
const response = await fetch('https://api.openai.com/v1/chat/completions', {
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, // Safe: server only
},
method: 'POST',
body: JSON.stringify(body),
})
return response
}
// app/ui/analytics.tsx — NEXT_PUBLIC_ variables are safe here
'use client'
export function Analytics() {
// This is fine: the value is intentionally public
const siteId = process.env.NEXT_PUBLIC_ANALYTICS_SITE_ID
return <div data-site={siteId} />
}
Never Commit .env.local#
Your .env.local is already in .gitignore — but it's worth understanding why. If you accidentally push API keys or database passwords to a public GitHub repository, you must treat them as compromised immediately and rotate them. Many services scan public repositories for leaked credentials and exploit them within seconds of a push.
When working with a team, share environment variables through a secure channel (a password manager, your deployment platform's secrets store, etc.) — never through Git.
Building and Deploying to Vercel#
Vercel is the company that created Next.js. Their hosting platform is the easiest way to deploy Next.js apps — it requires zero configuration and handles everything from SSL certificates to global distribution automatically.
Step 1: Verify the Build Locally#
Before deploying, make sure your app builds without errors:
npm run build
This creates an optimized production build in the .next/ folder. Fix any TypeScript or ESLint errors that appear — they will block the build on Vercel too.
Step 2: Push Your Code to GitHub#
Vercel deploys directly from GitHub. Make sure your project is in a GitHub repository (from the GitHub chapter) with the latest changes pushed:
git add .
git commit -m "Initial Next.js app"
git push
Step 3: Create a Vercel Account and Import Your Project#
- Go to vercel.com and click Sign Up
- Choose Continue with GitHub — this links your GitHub account to Vercel and makes it easy to import repositories
- Click Add New Project
- Find your repository in the list and click Import
- Vercel automatically detects that it's a Next.js project — no build configuration is needed
- Before clicking Deploy, go to the Environment Variables section and add any variables from your
.env.localfile. Vercel will inject them during the build process. - Click Deploy
Your app is live within about a minute at a URL like https://my-app-username.vercel.app.
Step 4: Automatic Deployments#
After the initial setup, the workflow becomes seamless:
- Every
git pushto yourmainbranch automatically triggers a production deployment - Every push to any other branch creates a preview deployment with its own unique URL
- Vercel posts a comment on your GitHub pull requests with a link to the preview — you can share it with teammates before merging
This means your deployment workflow is simply: write code, push to GitHub, done.
Setting Environment Variables After Deployment#
Your .env.local is never committed to Git, so Vercel doesn't see it automatically. Add your environment variables to Vercel separately:
- Go to your project on vercel.com
- Click Settings → Environment Variables
- Add each variable, choose which environments it applies to (Production, Preview, Development), and click Save
- Trigger a new deployment — the next build will have access to the variables
Important for
NEXT_PUBLIC_variables: Because they are baked in at build time, changing them in Vercel's dashboard requires a new deployment to take effect in the browser. Simply saving the variable is not enough — you need to redeploy.
Summary#
| Topic | Key Point |
|---|---|
| React vs. Next.js | React builds UIs; Next.js adds routing, a backend, SSR, and deployment tooling on top |
| Node.js | Install the LTS version from nodejs.org; verify with node -v and npm -v |
| Create a project | npx create-next-app@latest — choose TypeScript, ESLint, Tailwind, App Router, Turbopack |
| Dev server | npm run dev → open http://localhost:3000; edits appear instantly (Fast Refresh) |
| File-based routing | A folder + page.tsx = a route; layout.tsx wraps pages; [slug] = dynamic segment |
| API routes | Create app/api/route.ts; export GET, POST, etc. — runs on the server |
| Server Components | Default — run on the server; can await data directly; no 'use client' needed |
| Client Components | Add 'use client'; needed for state, event handlers, and browser APIs |
| Environment variables | .env.local for secrets; NEXT_PUBLIC_ prefix for values safe to expose to the browser |
| Deploy to Vercel | Push to GitHub → import on vercel.com → automatic deploys on every push |
Quick Command Reference#
npx create-next-app@latest # Create a new project
npm run dev # Start the dev server (localhost:3000)
npm run build # Build for production
npm run start # Run the production build locally
npm run lint # Check for code issues
File Naming Rules#
| File name | What it creates |
|---|---|
page.tsx | A publicly accessible page at that URL |
layout.tsx | Shared UI that wraps pages at this level and below |
route.ts | An API endpoint (GET, POST, etc.) |
loading.tsx | A loading skeleton shown while a page is loading |
error.tsx | An error page for when something goes wrong |
not-found.tsx | A custom 404 page |
You now have a working Next.js application deployed to the web. In the next chapter, you'll add Claude Code to your workflow and learn how to use AI assistance to build faster — with the foundation you now have to evaluate and guide what it produces.