Next.js is the world's most popular React framework — used by Vercel, Netflix, TikTok, Twitch, and over 1 million websites. It adds file-based routing, server-side rendering, static generation, and full-stack capabilities on top of React. This tutorial takes you from zero to building real Next.js applications, step by step.
What you'll learn
| Topic | What you'll be able to do |
|---|---|
| Setup | Create a Next.js app and run it locally |
| App Router | Build pages with the modern file-based router |
| Server Components | Render components on the server by default |
| Client Components | Add interactivity where needed |
| Data Fetching | Fetch data in Server Components and Client Components |
| Routing | Dynamic routes, layouts, nested routes |
| API Routes | Build backend endpoints in the same project |
| Deployment | Deploy to Vercel in minutes |
Why Next.js?
React alone is a UI library — it doesn't include routing, data fetching, or server-side rendering. Next.js adds all of that:
| Feature | React alone | Next.js |
|---|---|---|
| Routing | Manual (React Router) | Built-in file-based |
| Server-side rendering | Manual setup | Built-in |
| Static generation | Manual | Built-in |
| API endpoints | Needs Express | Built-in API routes |
| Image optimization | Manual | Built-in <Image> |
| SEO | Hard | Easy (metadata API) |
| Full-stack | No | Yes |
| Deployment | Any | Optimized for Vercel |
Setup
Prerequisites
You need:
- Node.js 18.17 or later
- Basic JavaScript knowledge
- Basic React knowledge (components, props, useState)
Create your first Next.js app
npx create-next-app@latest my-app
You'll be asked:
Would you like to use TypeScript? › No
Would you like to use ESLint? › Yes
Would you like to use Tailwind CSS? › No
Would you like your code inside a `src/` directory? › No
Would you like to use App Router? › Yes ← say Yes
Would you like to use Turbopack? › No
Would you like to customize the default import alias? › No
Then:
cd my-app
npm run dev
Open http://localhost:3000 — you'll see the Next.js welcome page.
Project structure
my-app/
├── app/ # App Router (pages live here)
│ ├── layout.js # Root layout (wraps every page)
│ ├── page.js # Home page (route: /)
│ └── globals.css # Global styles
├── public/ # Static files (images, fonts)
├── next.config.mjs # Next.js configuration
└── package.json
App Router basics
Next.js 13+ uses the App Router. Every folder inside app/ maps to a URL route.
| File | Route | Purpose |
|---|---|---|
app/page.js |
/ |
Home page |
app/about/page.js |
/about |
About page |
app/blog/page.js |
/blog |
Blog index |
app/blog/[slug]/page.js |
/blog/any-slug |
Dynamic blog post |
app/layout.js |
All routes | Root layout |
app/loading.js |
All routes | Loading UI |
app/error.js |
All routes | Error UI |
app/not-found.js |
404 | Not found page |
Your first page
app/page.js:
export default function HomePage() {
return (
<main>
<h1>Hello, Next.js!</h1>
<p>Welcome to my first Next.js app.</p>
</main>
);
}
Adding a new page
Create app/about/page.js:
export default function AboutPage() {
return (
<main>
<h1>About</h1>
<p>This is the about page.</p>
</main>
);
}
Now visit /about — it works automatically.
Layouts
Layouts wrap multiple pages and preserve state between navigations (like a shared header and footer).
Root layout
app/layout.js — this wraps every page:
export const metadata = {
title: 'My App',
description: 'Built with Next.js',
};
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/blog">Blog</a>
</nav>
</header>
<main>{children}</main>
<footer>© 2025 My App</footer>
</body>
</html>
);
}
Nested layouts
Create app/blog/layout.js to add a sidebar only on blog pages:
export default function BlogLayout({ children }) {
return (
<div style={{ display: 'flex', gap: '2rem' }}>
<aside>
<h3>Categories</h3>
<ul>
<li><a href="/blog/tech">Tech</a></li>
<li><a href="/blog/life">Life</a></li>
</ul>
</aside>
<div>{children}</div>
</div>
);
}
Server Components vs Client Components
This is the most important concept in Next.js 13+.
| Server Component | Client Component | |
|---|---|---|
| Runs on | Server | Browser |
| Default | Yes | No (add 'use client') |
| Can fetch data | Yes (async/await) | Yes (useEffect) |
| Can use hooks | No | Yes (useState, useEffect) |
| Can handle events | No | Yes (onClick, onChange) |
| Bundle size | Zero JS sent | JS included in bundle |
| Access to DB, files | Yes (directly) | No |
| Best for | Static content, data fetching | Interactive UI |
Server Component (default)
// app/users/page.js — Server Component (no 'use client')
async function getUsers() {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
return res.json();
}
export default async function UsersPage() {
const users = await getUsers(); // runs on server
return (
<div>
<h1>Users</h1>
<ul>
{users.map(user => (
<li key={user.id}>{user.name} — {user.email}</li>
))}
</ul>
</div>
);
}
No loading spinner needed — data is fetched before the page is sent to the browser.
Client Component
'use client'; // ← required for hooks and interactivity
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Mixing both
You can use Client Components inside Server Components:
// app/page.js — Server Component
import Counter from '@/components/Counter'; // Client Component
export default function HomePage() {
return (
<div>
<h1>Welcome</h1>
<Counter /> {/* Client Component embedded in Server Component */}
</div>
);
}
Routing
Dynamic routes
Create app/blog/[slug]/page.js for /blog/my-post, /blog/any-slug:
export default function BlogPost({ params }) {
return (
<div>
<h1>Post: {params.slug}</h1>
</div>
);
}
Multiple dynamic segments
app/shop/[category]/[product]/page.js → /shop/electronics/laptop:
export default function ProductPage({ params }) {
return (
<div>
<p>Category: {params.category}</p>
<p>Product: {params.product}</p>
</div>
);
}
Catch-all routes
app/docs/[...slug]/page.js matches /docs/a, /docs/a/b, /docs/a/b/c:
export default function DocsPage({ params }) {
// params.slug = ['a', 'b', 'c']
return <h1>Docs: {params.slug.join('/')}</h1>;
}
Route groups
Use parentheses to group routes without affecting the URL:
app/
├── (marketing)/
│ ├── about/page.js → /about
│ └── pricing/page.js → /pricing
└── (app)/
├── dashboard/page.js → /dashboard
└── settings/page.js → /settings
Different layouts for each group without URL nesting.
Link and Navigation
<Link> component
Use Link instead of <a> for client-side navigation:
import Link from 'next/link';
export default function Navbar() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog">Blog</Link>
</nav>
);
}
Programmatic navigation
'use client';
import { useRouter } from 'next/navigation';
export default function LoginPage() {
const router = useRouter();
async function handleLogin() {
// ... login logic
router.push('/dashboard'); // redirect
// router.replace('/dashboard'); // replace history (no back button)
// router.back(); // go back
}
return <button onClick={handleLogin}>Log in</button>;
}
usePathname — get current path
'use client';
import { usePathname } from 'next/navigation';
import Link from 'next/link';
export default function NavLink({ href, children }) {
const pathname = usePathname();
const isActive = pathname === href;
return (
<Link
href={href}
style={{ fontWeight: isActive ? 'bold' : 'normal' }}
>
{children}
</Link>
);
}
Data Fetching
Fetch in Server Components (recommended)
// app/posts/page.js
async function getPosts() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!res.ok) throw new Error('Failed to fetch posts');
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<div>
<h1>Posts</h1>
{posts.slice(0, 10).map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.body.slice(0, 100)}...</p>
</article>
))}
</div>
);
}
Fetch with caching options
// Default: cached (like getStaticProps)
const res = await fetch('https://api.example.com/data');
// No cache: fresh every request (like getServerSideProps)
const res = await fetch('https://api.example.com/data', {
cache: 'no-store'
});
// Revalidate every 60 seconds (ISR)
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
});
Caching behaviour summary
| Option | Behaviour | Old equivalent |
|---|---|---|
| Default | Cached indefinitely | getStaticProps |
cache: 'no-store' |
No cache, fresh each request | getServerSideProps |
next: { revalidate: N } |
Cache, refresh every N seconds | ISR (Incremental Static Regeneration) |
Loading states
Create app/posts/loading.js — shown while the page fetches:
export default function Loading() {
return <p>Loading posts...</p>;
}
Error handling
Create app/posts/error.js:
'use client';
export default function Error({ error, reset }) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
Fetch in Client Components
When you need data on the client (e.g., after user action):
'use client';
import { useState, useEffect } from 'react';
export default function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => {
setUsers(data);
setLoading(false);
});
}, []);
if (loading) return <p>Loading...</p>;
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
API Routes
Next.js lets you build backend endpoints inside app/api/:
Simple GET endpoint
app/api/hello/route.js:
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({ message: 'Hello from Next.js API!' });
}
Visit /api/hello — you get JSON back.
Full CRUD example
app/api/posts/route.js:
import { NextResponse } from 'next/server';
// In-memory store (use a real DB in production)
let posts = [
{ id: 1, title: 'First post', body: 'Hello world' },
{ id: 2, title: 'Second post', body: 'More content' },
];
export async function GET() {
return NextResponse.json(posts);
}
export async function POST(request) {
const body = await request.json();
const newPost = { id: Date.now(), ...body };
posts.push(newPost);
return NextResponse.json(newPost, { status: 201 });
}
app/api/posts/[id]/route.js:
import { NextResponse } from 'next/server';
let posts = [/* same posts array */];
export async function GET(request, { params }) {
const post = posts.find(p => p.id === Number(params.id));
if (!post) return NextResponse.json({ error: 'Not found' }, { status: 404 });
return NextResponse.json(post);
}
export async function PUT(request, { params }) {
const body = await request.json();
const index = posts.findIndex(p => p.id === Number(params.id));
if (index === -1) return NextResponse.json({ error: 'Not found' }, { status: 404 });
posts[index] = { ...posts[index], ...body };
return NextResponse.json(posts[index]);
}
export async function DELETE(request, { params }) {
posts = posts.filter(p => p.id !== Number(params.id));
return new NextResponse(null, { status: 204 });
}
Image optimization
Next.js has a built-in <Image> component that automatically:
- Resizes images
- Converts to modern formats (WebP, AVIF)
- Lazy loads
- Prevents layout shift
import Image from 'next/image';
export default function ProfileCard() {
return (
<div>
{/* Local image */}
<Image
src="/profile.jpg"
alt="Profile picture"
width={200}
height={200}
/>
{/* Remote image (add hostname to next.config.mjs) */}
<Image
src="https://example.com/photo.jpg"
alt="Remote photo"
width={800}
height={600}
/>
{/* Fill parent container */}
<div style={{ position: 'relative', width: '100%', height: '400px' }}>
<Image
src="/hero.jpg"
alt="Hero"
fill
style={{ objectFit: 'cover' }}
/>
</div>
</div>
);
}
Configure remote hostnames in next.config.mjs:
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com',
},
],
},
};
export default nextConfig;
Metadata and SEO
Static metadata
// app/about/page.js
export const metadata = {
title: 'About Us',
description: 'Learn more about our company.',
openGraph: {
title: 'About Us',
description: 'Learn more about our company.',
images: ['/og-about.jpg'],
},
};
export default function AboutPage() {
return <h1>About</h1>;
}
Dynamic metadata
// app/blog/[slug]/page.js
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
};
}
export default async function BlogPost({ params }) {
const post = await getPost(params.slug);
return <article>{post.body}</article>;
}
Favicon and app icons
Put these in app/:
app/
├── favicon.ico # Browser tab icon
├── icon.png # App icon (iOS, Android)
└── apple-icon.png # Apple touch icon
Fonts
Next.js has built-in Google Fonts — no network requests at runtime:
// app/layout.js
import { Inter, Roboto_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
});
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
});
export default function RootLayout({ children }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
);
}
Environment variables
Create .env.local (never commit to git):
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
NEXT_PUBLIC_API_URL=https://api.example.com
SECRET_API_KEY=supersecretkey123
Rules:
| Variable | Available in | Use for |
|---|---|---|
NEXT_PUBLIC_* |
Server + browser | Public config (API URLs) |
| No prefix | Server only | Secrets (DB passwords, API keys) |
Use in code:
// Server Component or API route — both work
const dbUrl = process.env.DATABASE_URL;
// Client Component — NEXT_PUBLIC_ only
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
Project 1: Blog with Markdown
A static blog that reads Markdown files from the filesystem.
npm install gray-matter remark remark-html
File structure:
posts/
├── hello-world.md
└── my-second-post.md
app/
├── blog/
│ ├── page.js
│ └── [slug]/page.js
└── lib/
└── posts.js
posts/hello-world.md:
---
title: "Hello World"
date: "2025-01-15"
excerpt: "My first blog post"
---
# Hello World
Welcome to my blog! This post is written in Markdown.
app/lib/posts.js:
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
import { remark } from 'remark';
import html from 'remark-html';
const postsDirectory = path.join(process.cwd(), 'posts');
export function getAllPosts() {
const fileNames = fs.readdirSync(postsDirectory);
return fileNames
.filter(name => name.endsWith('.md'))
.map(fileName => {
const slug = fileName.replace(/\.md$/, '');
const fullPath = path.join(postsDirectory, fileName);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const { data } = matter(fileContents);
return { slug, ...data };
})
.sort((a, b) => new Date(b.date) - new Date(a.date));
}
export async function getPostBySlug(slug) {
const fullPath = path.join(postsDirectory, `${slug}.md`);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const { data, content } = matter(fileContents);
const processedContent = await remark().use(html).process(content);
const contentHtml = processedContent.toString();
return { slug, contentHtml, ...data };
}
app/blog/page.js:
import Link from 'next/link';
import { getAllPosts } from '@/app/lib/posts';
export const metadata = {
title: 'Blog',
description: 'All blog posts',
};
export default function BlogIndex() {
const posts = getAllPosts();
return (
<div>
<h1>Blog</h1>
{posts.map(post => (
<article key={post.slug}>
<Link href={`/blog/${post.slug}`}>
<h2>{post.title}</h2>
</Link>
<time>{post.date}</time>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
app/blog/[slug]/page.js:
import { getAllPosts, getPostBySlug } from '@/app/lib/posts';
export async function generateStaticParams() {
const posts = getAllPosts();
return posts.map(post => ({ slug: post.slug }));
}
export async function generateMetadata({ params }) {
const post = await getPostBySlug(params.slug);
return { title: post.title, description: post.excerpt };
}
export default async function BlogPost({ params }) {
const post = await getPostBySlug(params.slug);
return (
<article>
<h1>{post.title}</h1>
<time>{post.date}</time>
<div dangerouslySetInnerHTML={{ __html: post.contentHtml }} />
</article>
);
}
Project 2: Todo App with API Routes
Full-stack todo app — frontend + backend in the same Next.js project.
app/api/todos/route.js:
import { NextResponse } from 'next/server';
// Simple in-memory store
let todos = [];
let nextId = 1;
export async function GET() {
return NextResponse.json(todos);
}
export async function POST(request) {
const { text } = await request.json();
if (!text?.trim()) {
return NextResponse.json({ error: 'Text is required' }, { status: 400 });
}
const todo = { id: nextId++, text: text.trim(), done: false };
todos.push(todo);
return NextResponse.json(todo, { status: 201 });
}
app/api/todos/[id]/route.js:
import { NextResponse } from 'next/server';
// Same todos array (in a real app, use a DB)
export async function PATCH(request, { params }) {
const { done } = await request.json();
const todo = todos.find(t => t.id === Number(params.id));
if (!todo) return NextResponse.json({ error: 'Not found' }, { status: 404 });
todo.done = done;
return NextResponse.json(todo);
}
export async function DELETE(request, { params }) {
todos = todos.filter(t => t.id !== Number(params.id));
return new NextResponse(null, { status: 204 });
}
app/page.js:
'use client';
import { useState, useEffect } from 'react';
export default function TodoApp() {
const [todos, setTodos] = useState([]);
const [input, setInput] = useState('');
useEffect(() => {
fetch('/api/todos').then(r => r.json()).then(setTodos);
}, []);
async function addTodo(e) {
e.preventDefault();
if (!input.trim()) return;
const res = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: input }),
});
const todo = await res.json();
setTodos(prev => [...prev, todo]);
setInput('');
}
async function toggleTodo(id, done) {
await fetch(`/api/todos/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ done: !done }),
});
setTodos(prev =>
prev.map(t => t.id === id ? { ...t, done: !done } : t)
);
}
async function deleteTodo(id) {
await fetch(`/api/todos/${id}`, { method: 'DELETE' });
setTodos(prev => prev.filter(t => t.id !== id));
}
return (
<div style={{ maxWidth: 500, margin: '2rem auto', padding: '0 1rem' }}>
<h1>Todo App</h1>
<form onSubmit={addTodo} style={{ display: 'flex', gap: '0.5rem' }}>
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Add a todo..."
style={{ flex: 1, padding: '0.5rem' }}
/>
<button type="submit">Add</button>
</form>
<ul style={{ listStyle: 'none', padding: 0, marginTop: '1rem' }}>
{todos.map(todo => (
<li key={todo.id} style={{ display: 'flex', gap: '0.5rem', marginBottom: '0.5rem' }}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleTodo(todo.id, todo.done)}
/>
<span style={{ flex: 1, textDecoration: todo.done ? 'line-through' : 'none' }}>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
Project 3: E-Commerce Product Listing
Static product pages with ISR (Incremental Static Regeneration).
app/products/page.js:
async function getProducts() {
const res = await fetch('https://fakestoreapi.com/products', {
next: { revalidate: 3600 }, // refresh every hour
});
return res.json();
}
export const metadata = {
title: 'Products',
description: 'Browse all products',
};
export default async function ProductsPage() {
const products = await getProducts();
return (
<div>
<h1>Products</h1>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem' }}>
{products.map(product => (
<a key={product.id} href={`/products/${product.id}`} style={{ textDecoration: 'none', color: 'inherit', border: '1px solid #eee', padding: '1rem', borderRadius: 8 }}>
<img src={product.image} alt={product.title} style={{ width: '100%', height: 200, objectFit: 'contain' }} />
<h3 style={{ fontSize: '0.9rem' }}>{product.title}</h3>
<p style={{ fontWeight: 'bold' }}>${product.price}</p>
</a>
))}
</div>
</div>
);
}
app/products/[id]/page.js:
export async function generateStaticParams() {
const res = await fetch('https://fakestoreapi.com/products');
const products = await res.json();
return products.map(p => ({ id: String(p.id) }));
}
async function getProduct(id) {
const res = await fetch(`https://fakestoreapi.com/products/${id}`, {
next: { revalidate: 3600 },
});
if (!res.ok) return null;
return res.json();
}
export async function generateMetadata({ params }) {
const product = await getProduct(params.id);
return {
title: product?.title ?? 'Product',
description: product?.description,
};
}
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
if (!product) return <h1>Product not found</h1>;
return (
<div style={{ maxWidth: 600, margin: '2rem auto' }}>
<img src={product.image} alt={product.title} style={{ width: '100%', height: 300, objectFit: 'contain' }} />
<h1>{product.title}</h1>
<p style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>${product.price}</p>
<p>{product.description}</p>
<button style={{ padding: '0.75rem 1.5rem', background: '#0070f3', color: 'white', border: 'none', borderRadius: 6, cursor: 'pointer' }}>
Add to Cart
</button>
</div>
);
}
Deployment to Vercel
Vercel builds and deploys Next.js with zero configuration.
npm install -g vercel
vercel
Or connect your GitHub repo at vercel.com — every push to main auto-deploys.
Other deployment options
| Platform | Command | Notes |
|---|---|---|
| Vercel | vercel |
Best Next.js support |
| Netlify | netlify deploy |
Good support |
| AWS Amplify | Git push | Auto-deploys |
| Docker (self-hosted) | docker build |
Full control |
| Node.js server | npm start |
next build && next start |
Docker deployment
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
Add to next.config.mjs:
const nextConfig = {
output: 'standalone',
};
Learning path
| Stage | Topics | Time |
|---|---|---|
| 1. React basics | Components, props, useState, useEffect | 1-2 weeks |
| 2. Next.js setup | App Router, pages, layouts | 1 week |
| 3. Server Components | Async components, data fetching, caching | 1 week |
| 4. Routing | Dynamic routes, route groups, redirects | 3 days |
| 5. API Routes | GET/POST/PUT/DELETE, middleware | 3 days |
| 6. Database | Prisma or Drizzle with PostgreSQL | 1-2 weeks |
| 7. Auth | NextAuth.js or Clerk | 1 week |
| 8. Deployment | Vercel, environment variables | 1 day |
| 9. Advanced | Middleware, ISR, caching strategies | 2 weeks |
Common mistakes
| Mistake | Problem | Fix |
|---|---|---|
Missing 'use client' |
Hook used in Server Component throws error | Add 'use client' at the top of the file |
useRouter from next/router |
Only works in Pages Router | Import from next/navigation |
| Fetching in useEffect instead of Server Component | Slower, needs loading state | Use async Server Component instead |
No alt on <Image> |
Accessibility failure + Next.js error | Always provide meaningful alt text |
| Hardcoded secrets in code | Security vulnerability | Use .env.local, never commit secrets |
| Importing server-only code in Client Component | Build error | Move to Server Component or API route |
Not using <Link> for navigation |
Full page reload | Use <Link> for internal links |
cache: 'no-store' everywhere |
No caching = slow site | Only use where fresh data is needed |
Next.js vs related frameworks
| Next.js | Create React App | Gatsby | Remix | Astro | |
|---|---|---|---|---|---|
| Rendering | SSR/SSG/ISR/CSR | CSR only | SSG/SSR | SSR/CSR | SSG/SSR |
| Routing | File-based | React Router | File-based | File-based | File-based |
| Full-stack | Yes | No | Partial | Yes | Yes |
| Data fetching | Server Components | useEffect | GraphQL | loaders | fetch |
| Best for | General web apps | SPAs | Content sites | Web apps | Content sites |
| Bundle size | Moderate | Large | Moderate | Small | Tiny |
| Learning curve | Medium | Low | Medium | Medium | Low |
Quick reference
// New page
export default function Page() { return <h1>Hello</h1>; }
// Server Component with data
export default async function Page() {
const data = await fetch('...').then(r => r.json());
return <div>{data.name}</div>;
}
// Client Component
'use client';
import { useState } from 'react';
export default function Client() { const [n, setN] = useState(0); return <button onClick={() => setN(n+1)}>{n}</button>; }
// Dynamic route params
export default function Page({ params }) { return <h1>{params.id}</h1>; }
// Static params (SSG)
export function generateStaticParams() { return [{ id: '1' }, { id: '2' }]; }
// Metadata
export const metadata = { title: 'Page Title' };
// Dynamic metadata
export async function generateMetadata({ params }) { return { title: params.slug }; }
// API route
import { NextResponse } from 'next/server';
export async function GET() { return NextResponse.json({ ok: true }); }
export async function POST(req) { const body = await req.json(); return NextResponse.json(body, { status: 201 }); }
// Redirect
import { redirect } from 'next/navigation';
redirect('/login');
// Not found
import { notFound } from 'next/navigation';
if (!data) notFound();
FAQ
Do I need to know React before learning Next.js? Yes — Next.js builds on React. Learn components, props, useState, and useEffect first.
What's the difference between Pages Router and App Router?
Pages Router is the old approach (pages/ directory). App Router is the modern approach (app/ directory) with Server Components. Use App Router for new projects.
When should I use a Server Component vs Client Component?
Default to Server Components. Only add 'use client' when you need hooks (useState, useEffect) or browser events (onClick, onChange).
Is Next.js only for React? Yes — Next.js is specifically a React framework. For Vue, use Nuxt.js. For Svelte, use SvelteKit.
Do I need Vercel to deploy Next.js? No. Vercel is the creator of Next.js and offers the best hosting experience, but you can deploy to any platform that runs Node.js (AWS, GCP, Railway, Render, Docker, etc.).
What database should I use with Next.js? PostgreSQL with Prisma or Drizzle is the most popular choice. SQLite works great for small projects. For hosted databases, try Neon (serverless Postgres) or PlanetScale (MySQL).