Master Next.js from the ground up. Learn why Next.js was created, understand core concepts like file-based routing, server components, API routes, data fetching, and deployment. Build a complete production-ready e-commerce platform covering all Next.js fundamentals with best practices.

Next.js revolutionizes React development by providing a complete full-stack framework for building production-ready applications. Unlike traditional React SPAs, Next.js enables server-side rendering, static generation, API routes, and seamless full-stack development. But why does Next.js exist, and what makes it fundamentally different?
In this article, we'll explore Next.js's philosophy, understand why it was created, dive deep into core concepts, and build a complete production-ready e-commerce platform that demonstrates all fundamental Next.js patterns.
Before Next.js, React developers faced significant challenges:
Vercel created Next.js in 2016 with a revolutionary approach:
Next.js automatically creates routes based on file structure in the app directory.
// File structure creates routes automatically
app/
├── page.tsx // / (home page)
├── about/
│ └── page.tsx // /about
├── products/
│ ├── page.tsx // /products
│ └── [id]/
│ └── page.tsx // /products/:id (dynamic)
└── api/
└── products/
└── route.ts // /api/products (API route)
// Dynamic route with params
// app/products/[id]/page.tsx
export default function ProductPage({ params }: { params: { id: string } }) {
return <h1>Product {params.id}</h1>;
}React Server Components enable rendering on the server by default.
// Server Component (default)
// app/products/page.tsx
import { getProducts } from '@/lib/db';
export default async function ProductsPage() {
const products = await getProducts();
return (
<div>
<h1>Products</h1>
<ul>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
);
}
// Client Component (for interactivity)
// app/components/ProductFilter.tsx
'use client';
import { useState } from 'react';
export default function ProductFilter() {
const [filter, setFilter] = useState('');
return (
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter products..."
/>
);
}Create backend API endpoints without a separate server.
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const products = [
{ id: 1, name: 'Product 1', price: 99.99 },
{ id: 2, name: 'Product 2', price: 149.99 },
];
return NextResponse.json(products);
}
export async function POST(request: NextRequest) {
const data = await request.json();
// Validate and save product
const newProduct = {
id: 3,
...data,
};
return NextResponse.json(newProduct, { status: 201 });
}
// Dynamic API route
// app/api/products/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const product = {
id: params.id,
name: 'Product',
price: 99.99,
};
return NextResponse.json(product);
}Multiple strategies for fetching data in Next.js.
// Server-side fetching (recommended)
// app/products/page.tsx
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 60 }, // ISR - revalidate every 60 seconds
});
return res.json();
}
export default async function ProductsPage() {
const products = await getProducts();
return <div>{/* render products */}</div>;
}
// Client-side fetching with SWR
// app/components/ProductList.tsx
'use client';
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then(r => r.json());
export default function ProductList() {
const { data, error, isLoading } = useSWR('/api/products', fetcher);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading products</div>;
return (
<ul>
{data?.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
// Static generation with dynamic params
// app/products/[id]/page.tsx
export async function generateStaticParams() {
const products = await fetch('https://api.example.com/products').then(r => r.json());
return products.map(product => ({
id: product.id.toString(),
}));
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await fetch(`https://api.example.com/products/${params.id}`).then(r => r.json());
return <h1>{product.name}</h1>;
}Run code before requests are processed.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
// Check authentication
const token = request.cookies.get('auth-token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Add custom headers
const response = NextResponse.next();
response.headers.set('X-Custom-Header', 'value');
return response;
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};Built-in image optimization for performance.
import Image from 'next/image';
export default function ProductImage() {
return (
<Image
src="/products/product-1.jpg"
alt="Product 1"
width={300}
height={300}
priority // Load immediately
quality={80} // Optimize quality
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
);
}Create shared layouts for consistent page structure.
// app/layout.tsx (root layout)
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'My App',
description: 'Generated by create next app',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<header>Navigation</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>Sidebar</aside>
<main>{children}</main>
</div>
);
}Optimize pages for search engines.
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Products',
description: 'Browse our products',
keywords: ['products', 'shop', 'ecommerce'],
openGraph: {
title: 'Products',
description: 'Browse our products',
url: 'https://example.com/products',
siteName: 'My Store',
images: [
{
url: 'https://example.com/og-image.jpg',
width: 1200,
height: 630,
},
],
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'Products',
description: 'Browse our products',
images: ['https://example.com/twitter-image.jpg'],
},
};
export default function ProductsPage() {
return <h1>Products</h1>;
}Handle errors gracefully with error boundaries.
// app/error.tsx
'use client';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
// app/not-found.tsx
export default function NotFound() {
return (
<div>
<h2>Not Found</h2>
<p>Could not find requested resource</p>
</div>
);
}Manage configuration across environments.
# .env.local
DATABASE_URL=postgresql://user:password@localhost/dbname
API_KEY=your-api-key
NEXT_PUBLIC_API_URL=https://api.example.com
# .env.production
DATABASE_URL=postgresql://prod-user:prod-password@prod-host/prod-db
API_KEY=prod-api-key
NEXT_PUBLIC_API_URL=https://api.production.com// app/api/products/route.ts
export async function GET() {
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
const apiKey = process.env.API_KEY;
const res = await fetch(`${apiUrl}/products`, {
headers: {
'Authorization': `Bearer ${apiKey}`,
},
});
return res;
}Let's build a complete e-commerce platform with Next.js.
ecommerce-app/
├── app/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── products/
│ │ ├── page.tsx
│ │ └── [id]/
│ │ └── page.tsx
│ ├── cart/
│ │ └── page.tsx
│ ├── checkout/
│ │ └── page.tsx
│ ├── api/
│ │ ├── products/
│ │ │ ├── route.ts
│ │ │ └── [id]/route.ts
│ │ ├── cart/
│ │ │ └── route.ts
│ │ └── orders/
│ │ └── route.ts
│ └── components/
│ ├── Header.tsx
│ ├── ProductCard.tsx
│ ├── Cart.tsx
│ └── Checkout.tsx
├── lib/
│ ├── db.ts
│ └── types.ts
├── middleware.ts
├── .env.local
└── next.config.jsexport interface Product {
id: string;
name: string;
description: string;
price: number;
image: string;
stock: number;
category: string;
}
export interface CartItem {
productId: string;
quantity: number;
price: number;
}
export interface Order {
id: string;
items: CartItem[];
total: number;
status: 'pending' | 'processing' | 'shipped' | 'delivered';
createdAt: Date;
}
export interface User {
id: string;
email: string;
name: string;
cart: CartItem[];
}import { Product, Order, CartItem } from './types';
// Mock database
const products: Product[] = [
{
id: '1',
name: 'Laptop',
description: 'High-performance laptop',
price: 999.99,
image: '/products/laptop.jpg',
stock: 10,
category: 'electronics',
},
{
id: '2',
name: 'Mouse',
description: 'Wireless mouse',
price: 29.99,
image: '/products/mouse.jpg',
stock: 50,
category: 'accessories',
},
];
export async function getProducts(): Promise<Product[]> {
// Simulate database query
return new Promise(resolve => {
setTimeout(() => resolve(products), 100);
});
}
export async function getProduct(id: string): Promise<Product | null> {
return new Promise(resolve => {
setTimeout(() => {
resolve(products.find(p => p.id === id) || null);
}, 100);
});
}
export async function createOrder(items: CartItem[]): Promise<Order> {
const total = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
return {
id: Math.random().toString(36).substr(2, 9),
items,
total,
status: 'pending',
createdAt: new Date(),
};
}import { NextRequest, NextResponse } from 'next/server';
import { getProducts } from '@/lib/db';
export async function GET(request: NextRequest) {
try {
const products = await getProducts();
return NextResponse.json(products);
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch products' },
{ status: 500 }
);
}
}import { NextRequest, NextResponse } from 'next/server';
import { getProduct } from '@/lib/db';
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const product = await getProduct(params.id);
if (!product) {
return NextResponse.json(
{ error: 'Product not found' },
{ status: 404 }
);
}
return NextResponse.json(product);
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch product' },
{ status: 500 }
);
}
}import { NextRequest, NextResponse } from 'next/server';
import { createOrder } from '@/lib/db';
export async function POST(request: NextRequest) {
try {
const { items } = await request.json();
if (!items || items.length === 0) {
return NextResponse.json(
{ error: 'Cart is empty' },
{ status: 400 }
);
}
const order = await createOrder(items);
return NextResponse.json(order, { status: 201 });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create order' },
{ status: 500 }
);
}
}'use client';
import Link from 'next/link';
import { useState } from 'react';
export default function Header() {
const [cartCount, setCartCount] = useState(0);
return (
<header className="header">
<div className="container">
<Link href="/" className="logo">
E-Commerce Store
</Link>
<nav className="nav">
<Link href="/products">Products</Link>
<Link href="/cart" className="cart-link">
Cart ({cartCount})
</Link>
</nav>
</div>
</header>
);
}import Image from 'next/image';
import Link from 'next/link';
import { Product } from '@/lib/types';
export default function ProductCard({ product }: { product: Product }) {
return (
<div className="product-card">
<Image
src={product.image}
alt={product.name}
width={300}
height={300}
className="product-image"
/>
<div className="product-info">
<h3>{product.name}</h3>
<p className="description">{product.description}</p>
<p className="price">${product.price.toFixed(2)}</p>
<div className="actions">
<Link href={`/products/${product.id}`} className="btn btn-primary">
View Details
</Link>
<button className="btn btn-secondary">Add to Cart</button>
</div>
</div>
</div>
);
}import Link from 'next/link';
export default function Home() {
return (
<main className="home">
<section className="hero">
<h1>Welcome to Our Store</h1>
<p>Discover amazing products at great prices</p>
<Link href="/products" className="btn btn-primary btn-large">
Shop Now
</Link>
</section>
<section className="features">
<div className="feature">
<h3>Fast Shipping</h3>
<p>Free shipping on orders over $50</p>
</div>
<div className="feature">
<h3>Secure Payment</h3>
<p>100% secure checkout</p>
</div>
<div className="feature">
<h3>Easy Returns</h3>
<p>30-day money-back guarantee</p>
</div>
</section>
</main>
);
}import { getProducts } from '@/lib/db';
import ProductCard from '@/app/components/ProductCard';
export const metadata = {
title: 'Products',
description: 'Browse our products',
};
export default async function ProductsPage() {
const products = await getProducts();
return (
<main className="products-page">
<h1>Products</h1>
<div className="products-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</main>
);
}import { getProduct } from '@/lib/db';
import Image from 'next/image';
import { notFound } from 'next/navigation';
export async function generateMetadata({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
if (!product) {
return {
title: 'Product Not Found',
};
}
return {
title: product.name,
description: product.description,
};
}
export default async function ProductPage({ params }: { params: { id: string } }) {
const product = await getProduct(params.id);
if (!product) {
notFound();
}
return (
<main className="product-page">
<div className="product-container">
<div className="product-image">
<Image
src={product.image}
alt={product.name}
width={500}
height={500}
/>
</div>
<div className="product-details">
<h1>{product.name}</h1>
<p className="description">{product.description}</p>
<div className="price-section">
<span className="price">${product.price.toFixed(2)}</span>
<span className="stock">
{product.stock > 0 ? `${product.stock} in stock` : 'Out of stock'}
</span>
</div>
<div className="actions">
<input type="number" min="1" max={product.stock} defaultValue="1" />
<button className="btn btn-primary btn-large">Add to Cart</button>
</div>
<div className="details-section">
<h3>Product Details</h3>
<ul>
<li>Category: {product.category}</li>
<li>SKU: {product.id}</li>
<li>Free shipping on orders over $50</li>
</ul>
</div>
</div>
</div>
</main>
);
}'use client';
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { CartItem } from '@/lib/types';
export default function CartPage() {
const [items, setItems] = useState<CartItem[]>([]);
const [total, setTotal] = useState(0);
useEffect(() => {
// Load cart from localStorage
const savedCart = localStorage.getItem('cart');
if (savedCart) {
const cartItems = JSON.parse(savedCart);
setItems(cartItems);
calculateTotal(cartItems);
}
}, []);
const calculateTotal = (cartItems: CartItem[]) => {
const sum = cartItems.reduce((acc, item) => acc + item.price * item.quantity, 0);
setTotal(sum);
};
const removeItem = (productId: string) => {
const updated = items.filter(item => item.productId !== productId);
setItems(updated);
calculateTotal(updated);
localStorage.setItem('cart', JSON.stringify(updated));
};
const updateQuantity = (productId: string, quantity: number) => {
const updated = items.map(item =>
item.productId === productId ? { ...item, quantity } : item
);
setItems(updated);
calculateTotal(updated);
localStorage.setItem('cart', JSON.stringify(updated));
};
if (items.length === 0) {
return (
<main className="cart-page">
<h1>Shopping Cart</h1>
<p>Your cart is empty</p>
<Link href="/products" className="btn btn-primary">
Continue Shopping
</Link>
</main>
);
}
return (
<main className="cart-page">
<h1>Shopping Cart</h1>
<div className="cart-container">
<div className="cart-items">
{items.map(item => (
<div key={item.productId} className="cart-item">
<div className="item-info">
<p>Product ID: {item.productId}</p>
<p>${item.price.toFixed(2)}</p>
</div>
<div className="item-quantity">
<input
type="number"
min="1"
value={item.quantity}
onChange={(e) => updateQuantity(item.productId, parseInt(e.target.value))}
/>
</div>
<div className="item-total">
${(item.price * item.quantity).toFixed(2)}
</div>
<button
onClick={() => removeItem(item.productId)}
className="btn btn-danger"
>
Remove
</button>
</div>
))}
</div>
<div className="cart-summary">
<h2>Order Summary</h2>
<div className="summary-row">
<span>Subtotal:</span>
<span>${total.toFixed(2)}</span>
</div>
<div className="summary-row">
<span>Shipping:</span>
<span>Free</span>
</div>
<div className="summary-row total">
<span>Total:</span>
<span>${total.toFixed(2)}</span>
</div>
<Link href="/checkout" className="btn btn-primary btn-large">
Proceed to Checkout
</Link>
</div>
</div>
</main>
);
}// ✅ Use Server Components by default
// ✅ Only use 'use client' when you need interactivity
// ✅ Keep client components small and focused
// ✅ Fetch data on the server when possible
// ❌ Avoid making entire pages client components
// ❌ Don't fetch data on the client if server can do it
// ❌ Avoid unnecessary 'use client' directives// ✅ Use server-side fetching for SEO
// ✅ Use ISR (Incremental Static Regeneration) for dynamic content
// ✅ Use SWR or React Query for client-side data
// ✅ Implement proper error handling
// ❌ Avoid fetching all data on the client
// ❌ Don't ignore caching strategies
// ❌ Avoid N+1 query problems// ✅ Use Image component for optimization
// ✅ Implement code splitting with dynamic imports
// ✅ Use next/font for font optimization
// ✅ Monitor Core Web Vitals
// ❌ Avoid large JavaScript bundles
// ❌ Don't use unoptimized images
// ❌ Avoid render-blocking resources// ✅ Use generateMetadata for dynamic metadata
// ✅ Include Open Graph tags
// ✅ Use structured data (JSON-LD)
// ✅ Create sitemaps and robots.txt
// ❌ Avoid duplicate meta tags
// ❌ Don't ignore mobile optimization
// ❌ Avoid keyword stuffing// ✅ Validate all user input
// ✅ Use environment variables for secrets
// ✅ Implement CSRF protection
// ✅ Use secure headers
// ❌ Don't expose sensitive data to the client
// ❌ Avoid SQL injection vulnerabilities
// ❌ Don't skip authentication checks// ❌ Wrong - fetching in client component
'use client';
export default function Products() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch('/api/products').then(r => r.json()).then(setProducts);
}, []);
return <div>{/* render products */}</div>;
}
// ✅ Correct - fetch on server
export default async function Products() {
const products = await fetch('https://api.example.com/products').then(r => r.json());
return <div>{/* render products */}</div>;
}// ❌ Wrong - unoptimized image
<img src="/products/product.jpg" alt="Product" />
// ✅ Correct - optimized image
import Image from 'next/image';
<Image
src="/products/product.jpg"
alt="Product"
width={300}
height={300}
priority
/>// ❌ Wrong - no metadata
export default function ProductPage() {
return <h1>Product</h1>;
}
// ✅ Correct - include metadata
export const metadata = {
title: 'Product',
description: 'Product description',
};
export default function ProductPage() {
return <h1>Product</h1>;
}// ❌ Wrong - entire page is client component
'use client';
export default function Page() {
// All rendering happens on client
return <div>{/* content */}</div>;
}
// ✅ Correct - only interactive parts are client components
export default function Page() {
return (
<div>
<ServerComponent />
<ClientComponent />
</div>
);
}// ❌ Wrong - no error handling
export default async function Page() {
const data = await fetch('https://api.example.com/data').then(r => r.json());
return <div>{data}</div>;
}
// ✅ Correct - handle errors
export default async function Page() {
try {
const data = await fetch('https://api.example.com/data').then(r => r.json());
return <div>{data}</div>;
} catch (error) {
return <div>Error loading data</div>;
}
}# Install Vercel CLI
npm i -g vercel
# Deploy
vercel
# Deploy to production
vercel --prod# Build for production
npm run build
# Start production server
npm start
# Docker deployment
docker build -t nextjs-app .
docker run -p 3000:3000 nextjs-appNext.js revolutionizes React development by providing a complete full-stack framework for building production-ready applications. By combining server-side rendering, static generation, API routes, and automatic optimization, Next.js enables developers to build fast, scalable, and SEO-friendly applications.
The e-commerce platform we built demonstrates all core Next.js concepts in action. Understanding file-based routing, server components, API routes, data fetching strategies, and deployment is essential for building modern Next.js applications.
Key takeaways:
Next steps:
Next.js is the future of full-stack React development. Keep learning, building, and pushing the boundaries of what's possible.