Next.js 16 Project Setup & App Router: Your First Page
Step 1 of 31 — Next.js Tutorial Series | Source code for this step
Commands in This Step
| Command | Purpose |
|---|---|
pnpm create next-app@latest nextjs-16-crud --yes | Scaffold a new Next.js project |
What You Will Build
By the end of this step you will have a running Next.js application with a custom home page styled with Tailwind CSS. You will understand the core architecture that makes Next.js different from plain React.
Goal: See your own "Superblog" home page at http://localhost:3000.
Table of Contents
- What is Next.js? (For React Developers)
- Create the Project
- Project Structure Explained
- Core Concept: The App Router
- Core Concept: Server Components
- Understanding layout.tsx — The Root Layout
- Understanding page.tsx — Your Route Component
- Customize Your Home Page
- Update the Root Layout
- Run the App
- Summary & Key Takeaways
What is Next.js? (For React Developers)
You already know React — it is a library for building UI with components. But React alone does not give you:
- Routing — navigating between pages (you need React Router or similar)
- Server-side rendering (SSR) — generating HTML on the server for faster load and SEO
- API endpoints — handling backend logic like database queries
- File-based routing — creating pages simply by adding files, no configuration
- Built-in optimizations — image optimization, font loading, code splitting
Next.js is a full-stack framework built on top of React that provides all of these out of the box. Think of it this way:
React = the engine. Next.js = the complete car.
Next.js takes your React components and adds the infrastructure around them: a server, a router, a bundler (Turbopack), and production optimizations.
Create the Project
Open your terminal, navigate to the folder where you want the project, and run:
pnpm create next-app@latest nextjs-16-crud --yes
This command scaffolds a new Next.js project with all the recommended defaults:
- TypeScript — type safety
- Tailwind CSS v4 — utility-first styling
- App Router — the modern routing system
- Turbopack — fast dev server bundler
Now enter the project folder:
cd nextjs-16-crud
Project Structure Explained
After creation, your project looks like this:
nextjs-16-crud/
├── app/
│ ├── favicon.ico # Browser tab icon
│ ├── globals.css # Global CSS (Tailwind is imported here)
│ ├── layout.tsx # Root layout — wraps ALL pages
│ └── page.tsx # Home page — the "/" route
├── public/ # Static files (images, SVGs)
│ ├── file.svg
│ ├── globe.svg
│ ├── next.svg
│ ├── vercel.svg
│ └── window.svg
├── eslint.config.mjs # Linting rules
├── next.config.ts # Next.js configuration
├── package.json # Dependencies and scripts
├── postcss.config.mjs # PostCSS config (used by Tailwind v4)
├── tsconfig.json # TypeScript configuration
└── pnpm-lock.yaml # Locked dependency versions
The most important directory is app/ — this is where all your pages and UI live.
Key rule: Only two files inside app/ actually matter for routing:
layout.tsx— the wrapper (shell) around your pagespage.tsx— the actual page content for a given URL
Every other file (globals.css, favicon.ico) is just a supporting file that gets imported or served.
Core Concept: The App Router
Next.js has two routing systems. The modern one is called the App Router (introduced in Next.js 13). It uses the app/ directory.
The fundamental rule is simple:
Every folder inside
app/that contains apage.tsxbecomes a URL route.
| File path | URL |
|---|---|
app/page.tsx | / |
app/about/page.tsx | /about |
app/posts/page.tsx | /posts |
app/posts/new/page.tsx | /posts/new |
This is called file-based routing. You do not need React Router, createBrowserRouter, or any routing library. You just create folders and files.
Why is this better? In plain React, you would write:
// Plain React — you configure routes manually
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/posts" element={<Posts />} />
</Routes>
In Next.js, you skip all of that. The file system is the router.
Core Concept: Server Components
This is the single biggest difference between plain React and Next.js App Router.
In regular React, every component runs in the browser. The browser downloads JavaScript, executes it, and renders the UI. This is called Client-Side Rendering (CSR).
In the App Router, every component is a Server Component by default. This means:
| Feature | Server Component (default) | Client Component ("use client") |
|---|---|---|
| Where it runs | On the server | In the browser |
| Can access database | ✅ Yes | ❌ No |
Can use useState | ❌ No | ✅ Yes |
Can use useEffect | ❌ No | ✅ Yes |
Can use onClick | ❌ No | ✅ Yes |
| Sends JavaScript to browser | ❌ No (just HTML) | ✅ Yes |
Why Server Components?
- Faster page loads — the server sends ready-made HTML, not JavaScript the browser has to execute
- Direct data access — you can query a database right inside the component, no API layer needed
- Smaller bundle size — server-only code never reaches the browser
When you need interactivity (state, effects, click handlers), you add "use client" at the very top of the file:
'use client' // ← This makes it a Client Component
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>
}
The rule of thumb: Start with Server Components. Only add "use client" when you need browser interactivity. We will see this in practice in Step 3.
Understanding layout.tsx — The Root Layout
Open app/layout.tsx. This is the Root Layout — the outermost wrapper for your entire application.
import type { Metadata } from 'next'
import { Geist, Geist_Mono } from 'next/font/google'
import './globals.css'
const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin'],
})
const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin'],
})
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
)
}
Let's break down what each part does:
Fonts
const geistSans = Geist({ variable: '--font-geist-sans', subsets: ['latin'] })
Next.js has a built-in font optimization system (next/font). Instead of adding a <link> to Google Fonts (which blocks page rendering), Next.js downloads the font at build time and serves it as a local file. This is faster and avoids layout shift.
The variable option creates a CSS custom property (--font-geist-sans) so you can reference the font in Tailwind or CSS.
Metadata
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
In plain React, you would use react-helmet or manually set document.title. In Next.js, you export a metadata object from any layout.tsx or page.tsx, and Next.js automatically generates the <head> tags (<title>, <meta>, etc.).
This works because the component runs on the server — it can set the HTML head before the page reaches the browser.
💡 Tip: You can export
metadatafrom bothlayout.tsx(applies to all pages that layout wraps) and from individualpage.tsxfiles (overrides the layout's metadata for that specific page). In Step 2 we will add per-page metadata to each of our new pages and see exactly how the inheritance works.
Server Components only.
export const metadatais only valid in Server Components. If you add'use client'to a file, you cannot exportmetadatafrom it — Next.js will throw a build error. For pages that need client-side interactivity and custom metadata, keep themetadataexport in a parent layout or a separate server component wrapper.
The Layout Component
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
This is the only place in a Next.js app where you write <html> and <body> tags. The Root Layout is required — every app must have one.
The {children} prop is where your page content gets injected. When you visit /, the children will be the component from app/page.tsx. When you visit /about, it will be from app/about/page.tsx.
Key insight: The Root Layout is a Server Component. It wraps every single page in your app. This is where you put shared UI like headers, footers, and global providers.
Understanding page.tsx — Your Route Component
Open app/page.tsx. This is the component that renders when someone visits / (the home page).
The important thing to understand is the naming convention:
| File name | Purpose |
|---|---|
page.tsx | The UI for a route — this is what users see |
layout.tsx | Shared wrapper that persists across child routes |
loading.tsx | Loading UI shown while page is loading |
error.tsx | Error UI shown when something fails |
not-found.tsx | 404 page |
route.ts | API endpoint (no UI) |
For now, we only care about page.tsx and layout.tsx. We will use the others in later steps.
The rule: The default export of page.tsx is what Next.js renders for that route. It must be a default export.
Customize Your Home Page
Now let's make this project our own. Replace the entire contents of app/page.tsx with:
export default function Home() {
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm">
<div className="mx-auto max-w-4xl px-6 py-4">
<h1 className="text-2xl font-bold text-gray-900">Superblog</h1>
</div>
</header>
<main className="mx-auto max-w-4xl px-6 py-12">
<h2 className="mb-4 text-4xl font-bold text-gray-900">
Welcome to Superblog
</h2>
<p className="mb-8 text-lg text-gray-600">
A full-stack blog application built with Next.js, Prisma, and
NextAuth.js. We are building this step by step.
</p>
<div className="grid gap-6 md:grid-cols-2">
<div className="rounded-lg bg-white p-6 shadow-md">
<h3 className="mb-2 text-lg font-semibold text-gray-800">
📝 Blog Posts
</h3>
<p className="text-gray-600">
Create, read, and delete blog posts. Coming once we connect to the
database.
</p>
</div>
<div className="rounded-lg bg-white p-6 shadow-md">
<h3 className="mb-2 text-lg font-semibold text-gray-800">
🔐 Authentication
</h3>
<p className="text-gray-600">
User registration and login with NextAuth.js. Coming in a later
step.
</p>
</div>
</div>
</main>
<footer className="mx-auto max-w-4xl px-6 py-8 text-center text-sm text-gray-400">
Built with Next.js 16, React 19, Prisma & NextAuth.js
</footer>
</div>
)
}
Let's examine what is happening here and why:
-
This is a Server Component — notice there is no
"use client"at the top. This component runs entirely on the server. The server generates the HTML and sends it to the browser. Zero JavaScript is shipped to the client for this page. -
No imports needed — we are using only HTML elements and Tailwind CSS classes. No React hooks, no state, no effects. Server Components are perfect for static or data-driven pages.
-
Tailwind CSS classes —
min-h-screen,bg-gray-50,max-w-4xl,mx-auto,shadow-md, etc. These are utility classes provided by Tailwind CSS v4, which was set up automatically when we created the project. Each class maps to a single CSS property. -
The default export —
export default function Home()is what Next.js renders when the user visits/. The function name (Home) can be anything, but theexport defaultis required.
Update the Root Layout
Now update app/layout.tsx to reflect our project. Replace the entire contents with:
import type { Metadata } from 'next'
import { Geist, Geist_Mono } from 'next/font/google'
import './globals.css'
const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin'],
})
const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin'],
})
export const metadata: Metadata = {
title: 'Superblog',
description: 'A full-stack blog built with Next.js, Prisma, and NextAuth.js',
}
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
)
}
What changed and why:
- We updated
metadata.titleto"Superblog"— this sets the browser tab title. Every page in our app will inherit this title unless a specific page overrides it. - We updated
metadata.description— this sets the<meta name="description">tag, which search engines display in results. - Everything else stays the same — the font setup and
{children}rendering are already correct.
Next.js concept: Metadata inheritance. When you set
metadatainlayout.tsx, all pages that this layout wraps inherit it. Apage.tsxcan override individual fields by exporting its ownmetadataobject — Next.js merges them, with the page value taking precedence.
Run the App
Start the development server:
pnpm dev
Open your browser and visit http://localhost:3000.
You should see your custom Superblog home page with:
- A header with "Superblog" text
- A welcome message
- Two feature cards (Blog Posts and Authentication)
- A footer
Congratulations! You have a running Next.js application. Everything you see was rendered on the server as a Server Component — the browser received pure HTML with zero client-side JavaScript for this page.
Summary & Key Takeaways
Here is what you learned in this step:
| Concept | What it means |
|---|---|
| Next.js | A full-stack React framework with routing, SSR, and APIs built in |
| App Router | The modern routing system using the app/ directory |
| File-based routing | app/page.tsx = /, app/about/page.tsx = /about |
page.tsx | The component rendered for a specific URL route |
layout.tsx | A wrapper component shared across pages (provides <html> and <body>) |
| Server Components | Components that run on the server by default — no JS sent to browser |
| Client Components | Components marked with "use client" that run in the browser |
metadata export | How Next.js sets <title> and <meta> tags — no react-helmet needed |
next/font | Built-in font optimization — downloads fonts at build time |
What is Next
In Step 2, we will create new pages (/about, /posts) and learn how file-based routing works in practice. You will see that creating a new page is as simple as creating a new folder with a page.tsx file inside it.