What is Next.js?
Next.js is a React framework that gives you building blocks to create fast, full-stack web applications. It handles the complex configuration that React apps need for production, including routing, server-side rendering, and optimization.
Created by Vercel, Next.js is used by companies like Netflix, TikTok, Twitch, Nike, and Notion to build their web applications.
React vs Next.js:
REACT (Library) NEXT.JS (Framework)
─────────────────────────────────────────────────────────
Client-side rendering only Multiple rendering options:
• Server-Side Rendering (SSR)
• Static Site Generation (SSG)
• Client-Side Rendering (CSR)
Manual routing setup File-based routing built-in
(React Router) (automatic from file structure)
No built-in optimization Automatic optimization:
• Code splitting
• Image optimization
• Font optimization
Need separate API server API routes built-in
(Express, etc.) (full-stack in one project)
Complex build configuration Zero configuration
(webpack, babel) (just start coding)
SEO challenges SEO-friendly by default
(client-rendered content) (server-rendered HTML)
Getting Started
# Create a new Next.js project
npx create-next-app@latest my-app
# Options you'll be asked:
# ✔ 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 to use `src/` directory? Yes
# ✔ Would you like to use App Router? Yes
# ✔ Would you like to customize import alias? No
cd my-app
npm run dev
# Project structure (App Router):
my-app/
├── src/
│ └── app/
│ ├── layout.tsx # Root layout (shared UI)
│ ├── page.tsx # Home page (/)
│ ├── globals.css # Global styles
│ ├── about/
│ │ └── page.tsx # About page (/about)
│ └── blog/
│ ├── page.tsx # Blog list (/blog)
│ └── [slug]/
│ └── page.tsx # Blog post (/blog/my-post)
├── public/ # Static files
├── next.config.js # Next.js configuration
└── package.json
File-Based Routing
In Next.js, the file system IS your router. Create a file, get a route. It's that simple.
// File structure → Routes
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── blog/
│ ├── page.tsx → /blog
│ └── [slug]/
│ └── page.tsx → /blog/:slug (dynamic)
├── products/
│ ├── page.tsx → /products
│ └── [category]/
│ └── [id]/
│ └── page.tsx → /products/:category/:id
└── (marketing)/ → Route group (no URL impact)
├── pricing/
│ └── page.tsx → /pricing
└── features/
└── page.tsx → /features
// Basic page component
// app/about/page.tsx
export default function AboutPage() {
return (
<main>
<h1>About Us</h1>
<p>Welcome to our company.</p>
</main>
);
}
// Dynamic route with params
// app/blog/[slug]/page.tsx
interface Props {
params: { slug: string };
}
export default function BlogPost({ params }: Props) {
return (
<article>
<h1>Blog Post: {params.slug}</h1>
</article>
);
}
// Multiple dynamic segments
// app/products/[category]/[id]/page.tsx
interface Props {
params: { category: string; id: string };
}
export default function Product({ params }: Props) {
const { category, id } = params;
return <div>Product {id} in {category}</div>;
}
Layouts and Templates
Layouts wrap pages and preserve state across navigation. They're perfect for shared UI like headers and sidebars.
// app/layout.tsx - Root layout (required)
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata = {
title: 'My App',
description: 'Built with Next.js',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<header>
<nav>{/* Navigation */}</nav>
</header>
<main>{children}</main>
<footer>{/* Footer */}</footer>
</body>
</html>
);
}
// app/dashboard/layout.tsx - Nested layout
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard">
<aside className="sidebar">
{/* Dashboard navigation */}
</aside>
<section className="content">
{children}
</section>
</div>
);
}
// Layout nesting visualization:
//
// RootLayout (header, footer)
// └── DashboardLayout (sidebar)
// └── page.tsx (content)
// app/loading.tsx - Loading UI
export default function Loading() {
return (
<div className="loading-spinner">
Loading...
</div>
);
}
// app/error.tsx - Error boundary
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
// app/not-found.tsx - 404 page
export default function NotFound() {
return (
<div>
<h2>Page Not Found</h2>
<p>Could not find the requested page.</p>
</div>
);
}
Server Components vs Client Components
Next.js 13+ uses React Server Components by default. Understanding when to use each is crucial.
SERVER COMPONENTS (Default) CLIENT COMPONENTS
─────────────────────────────────────────────────────────
✓ Render on server ✓ Render on client
✓ Direct database access ✓ Use React hooks
✓ Access backend resources ✓ Event handlers
✓ Keep secrets on server ✓ Browser APIs
✓ Smaller client bundle ✓ Interactivity
Use for: Use for:
• Data fetching • onClick, onChange
• Database queries • useState, useEffect
• API calls • Forms
• Markdown rendering • Animations
• Static content • Third-party libraries
// Server Component (default)
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts');
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts(); // Fetch on server!
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
// Client Component (add 'use client' directive)
// components/Counter.tsx
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}
// Mixing Server and Client Components
// app/page.tsx (Server)
import Counter from '@/components/Counter';
async function getData() {
const res = await fetch('https://api.example.com/data');
return res.json();
}
export default async function Page() {
const data = await getData();
return (
<div>
<h1>{data.title}</h1> {/* Server rendered */}
<Counter /> {/* Client component */}
</div>
);
}
Data Fetching
Next.js provides multiple ways to fetch data depending on your needs.
// 1. Server Components - Direct fetch (recommended)
// Automatically cached and deduplicated
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`);
if (!res.ok) throw new Error('Failed to fetch');
return res.json();
}
export default async function ProductPage({
params
}: {
params: { id: string }
}) {
const product = await getProduct(params.id);
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
</div>
);
}
// 2. Caching options
// Static (default) - cached indefinitely
fetch('https://api.example.com/data');
// Revalidate - refresh every 60 seconds
fetch('https://api.example.com/data', {
next: { revalidate: 60 }
});
// Dynamic - no caching
fetch('https://api.example.com/data', {
cache: 'no-store'
});
// 3. Page-level caching
// Static page (default)
export default async function Page() {
const data = await getData();
return <div>{data.content}</div>;
}
// Revalidate page every 60 seconds
export const revalidate = 60;
export default async function Page() {
const data = await getData();
return <div>{data.content}</div>;
}
// Force dynamic rendering
export const dynamic = 'force-dynamic';
// 4. Parallel data fetching
async function Page() {
// Sequential (slow)
const user = await getUser();
const posts = await getPosts(); // Waits for user
// Parallel (fast)
const [user, posts] = await Promise.all([
getUser(),
getPosts()
]);
return (/* ... */);
}
// 5. Streaming with Suspense
import { Suspense } from 'react';
async function SlowComponent() {
const data = await fetchSlowData();
return <div>{data}</div>;
}
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<Loading />}>
<SlowComponent />
</Suspense>
</div>
);
}
API Routes
Build your backend API directly in your Next.js project using Route Handlers.
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
// GET /api/users
export async function GET(request: NextRequest) {
const users = await db.user.findMany();
return NextResponse.json(users);
}
// POST /api/users
export async function POST(request: NextRequest) {
const body = await request.json();
const user = await db.user.create({
data: {
name: body.name,
email: body.email
}
});
return NextResponse.json(user, { status: 201 });
}
// app/api/users/[id]/route.ts
// GET /api/users/:id
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const user = await db.user.findUnique({
where: { id: params.id }
});
if (!user) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}
return NextResponse.json(user);
}
// PUT /api/users/:id
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const body = await request.json();
const user = await db.user.update({
where: { id: params.id },
data: body
});
return NextResponse.json(user);
}
// DELETE /api/users/:id
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
await db.user.delete({
where: { id: params.id }
});
return new NextResponse(null, { status: 204 });
}
// Working with query parameters
// GET /api/search?q=hello&page=2
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const query = searchParams.get('q');
const page = searchParams.get('page') || '1';
const results = await search(query, parseInt(page));
return NextResponse.json(results);
}
// Setting headers and cookies
export async function GET() {
const response = NextResponse.json({ message: 'Hello' });
response.headers.set('X-Custom-Header', 'value');
response.cookies.set('token', 'abc123', {
httpOnly: true,
secure: true
});
return response;
}
Server Actions
Server Actions allow you to run server code directly from client components - no API routes needed for simple mutations.
// Inline server action
// app/posts/page.tsx
export default function PostsPage() {
async function createPost(formData: FormData) {
'use server'; // This runs on the server!
const title = formData.get('title');
const content = formData.get('content');
await db.post.create({
data: { title, content }
});
revalidatePath('/posts');
}
return (
<form action={createPost}>
<input name="title" placeholder="Title" />
<textarea name="content" placeholder="Content" />
<button type="submit">Create Post</button>
</form>
);
}
// Separate file for server actions
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
// Validation
if (!title || !content) {
return { error: 'Title and content required' };
}
await db.post.create({
data: { title, content }
});
revalidatePath('/posts');
redirect('/posts');
}
export async function deletePost(id: string) {
await db.post.delete({ where: { id } });
revalidatePath('/posts');
}
// Using with client components
// components/DeleteButton.tsx
'use client';
import { deletePost } from '@/app/actions';
import { useTransition } from 'react';
export function DeleteButton({ postId }: { postId: string }) {
const [isPending, startTransition] = useTransition();
const handleDelete = () => {
startTransition(async () => {
await deletePost(postId);
});
};
return (
<button onClick={handleDelete} disabled={isPending}>
{isPending ? 'Deleting...' : 'Delete'}
</button>
);
}
Navigation
// Link component for client-side navigation
import Link from 'next/link';
function Navigation() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/blog">Blog</Link>
{/* Dynamic routes */}
<Link href={`/products/${productId}`}>
View Product
</Link>
{/* With query parameters */}
<Link href="/search?q=next.js">
Search
</Link>
{/* Prefetching (default: true) */}
<Link href="/dashboard" prefetch={false}>
Dashboard
</Link>
</nav>
);
}
// Programmatic navigation
'use client';
import { useRouter } from 'next/navigation';
function SearchForm() {
const router = useRouter();
const handleSubmit = (e) => {
e.preventDefault();
const query = e.target.search.value;
// Navigate programmatically
router.push(`/search?q=${query}`);
// Other methods
router.replace('/login'); // Replace history
router.back(); // Go back
router.forward(); // Go forward
router.refresh(); // Refresh current route
};
return (
<form onSubmit={handleSubmit}>
<input name="search" />
<button>Search</button>
</form>
);
}
// usePathname and useSearchParams
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
function ActiveLink({ href, children }) {
const pathname = usePathname();
const isActive = pathname === href;
return (
<Link
href={href}
className={isActive ? 'active' : ''}
>
{children}
</Link>
);
}
function SearchResults() {
const searchParams = useSearchParams();
const query = searchParams.get('q');
return <h1>Results for: {query}</h1>;
}
Image Optimization
import Image from 'next/image';
// Basic usage
function Avatar() {
return (
<Image
src="/avatar.jpg"
alt="User avatar"
width={100}
height={100}
/>
);
}
// Fill container
function Hero() {
return (
<div className="hero" style={{ position: 'relative', height: '400px' }}>
<Image
src="/hero.jpg"
alt="Hero image"
fill
style={{ objectFit: 'cover' }}
priority // Load immediately (above the fold)
/>
</div>
);
}
// External images (configure in next.config.js)
function ExternalImage() {
return (
<Image
src="https://example.com/image.jpg"
alt="External image"
width={300}
height={200}
/>
);
}
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com',
},
],
},
};
// Responsive images
function ResponsiveImage() {
return (
<Image
src="/photo.jpg"
alt="Responsive photo"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
fill
/>
);
}
// Benefits of next/image:
// ✓ Automatic WebP/AVIF format conversion
// ✓ Lazy loading by default
// ✓ Prevents layout shift (CLS)
// ✓ Responsive sizing
// ✓ On-demand optimization
Metadata and SEO
// Static metadata
// app/layout.tsx or app/page.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'My App',
description: 'Welcome to my app',
keywords: ['next.js', 'react', 'javascript'],
authors: [{ name: 'Your Name' }],
openGraph: {
title: 'My App',
description: 'Welcome to my app',
url: 'https://example.com',
siteName: 'My App',
images: [
{
url: 'https://example.com/og.jpg',
width: 1200,
height: 630,
},
],
locale: 'en_US',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'My App',
description: 'Welcome to my app',
images: ['https://example.com/og.jpg'],
},
};
// Dynamic metadata
// app/products/[id]/page.tsx
export async function generateMetadata({
params
}: {
params: { id: string }
}): Promise<Metadata> {
const product = await getProduct(params.id);
return {
title: product.name,
description: product.description,
openGraph: {
images: [product.image],
},
};
}
// Title template
// app/layout.tsx
export const metadata: Metadata = {
title: {
default: 'My App',
template: '%s | My App', // Product Name | My App
},
};
// app/about/page.tsx
export const metadata: Metadata = {
title: 'About', // Becomes "About | My App"
};
Middleware
Middleware runs before a request is completed, allowing you to modify the response, redirect, or add headers.
// middleware.ts (in project root)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Get the pathname
const { pathname } = request.nextUrl;
// Authentication check
const token = request.cookies.get('token')?.value;
if (pathname.startsWith('/dashboard') && !token) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Add headers
const response = NextResponse.next();
response.headers.set('x-custom-header', 'my-value');
return response;
}
// Configure which paths middleware runs on
export const config = {
matcher: [
'/dashboard/:path*',
'/api/:path*',
'/((?!_next/static|favicon.ico).*)',
],
};
// Geo-based redirect
export function middleware(request: NextRequest) {
const country = request.geo?.country || 'US';
if (country === 'UK' && !request.nextUrl.pathname.startsWith('/uk')) {
return NextResponse.redirect(new URL('/uk', request.url));
}
return NextResponse.next();
}
// Rate limiting example
const rateLimit = new Map();
export function middleware(request: NextRequest) {
const ip = request.ip || 'anonymous';
const now = Date.now();
const windowMs = 60000; // 1 minute
const maxRequests = 100;
const requests = rateLimit.get(ip) || [];
const recentRequests = requests.filter(time => now - time < windowMs);
if (recentRequests.length >= maxRequests) {
return new NextResponse('Too Many Requests', { status: 429 });
}
recentRequests.push(now);
rateLimit.set(ip, recentRequests);
return NextResponse.next();
}
Deployment
# Build for production
npm run build
# Start production server
npm start
# Deploy to Vercel (recommended)
npm i -g vercel
vercel
# Vercel automatically:
# ✓ Detects Next.js
# ✓ Configures build settings
# ✓ Enables edge functions
# ✓ Sets up CDN
# ✓ Provides preview deployments
# Deploy to other platforms:
# Docker
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
# Static export (no server features)
// next.config.js
module.exports = {
output: 'export',
};
// Then: npm run build
// Output in 'out' directory
# Environment variables
# .env.local (for local development)
DATABASE_URL=postgres://...
API_KEY=secret
# Access in code
const dbUrl = process.env.DATABASE_URL;
# Client-side env vars (prefix with NEXT_PUBLIC_)
NEXT_PUBLIC_API_URL=https://api.example.com
Best Practices
- Use Server Components by default: Only add 'use client' when you need interactivity
- Leverage caching: Use appropriate fetch caching strategies for your data
- Optimize images: Always use next/image for automatic optimization
- Add metadata: Every page should have proper SEO metadata
- Use Server Actions: For simple mutations, prefer Server Actions over API routes
- Stream large responses: Use Suspense to show loading states while streaming
- Handle errors gracefully: Add error.tsx and loading.tsx to route segments
- Use middleware wisely: Keep middleware lightweight - it runs on every request
Build Production Apps with Next.js
Our Full Stack JavaScript program covers Next.js in depth. Build fast, SEO-friendly applications with expert guidance.
Explore JavaScript Program