Next.js Framework
Build full-stack applications with Next.js App Router
Next.js Framework: Building Modern Full-Stack React Applications
Master the Next.js framework with free flashcards and spaced repetition to reinforce your learning. This comprehensive lesson covers server-side rendering, static site generation, API routes, file-based routing, and the App Router with Server Componentsβessential concepts for building production-ready React applications with optimal performance and SEO.
Welcome to Next.js π»
Next.js is a powerful React framework created by Vercel that transforms how we build web applications. Unlike traditional React apps that run entirely in the browser, Next.js provides a hybrid approach that combines client-side interactivity with server-side rendering capabilities. This lesson will take you from understanding the fundamentals to implementing advanced patterns with Server Components and the modern App Router.
Whether you're building a high-traffic blog, an e-commerce platform, or a complex SaaS application, Next.js provides the tools and optimizations you need. By the end of this lesson, you'll understand when and how to use different rendering strategies, implement API routes, and leverage the latest features in Next.js 13+.
Core Concepts π§
What is Next.js?
Next.js extends React by providing a full-stack framework with built-in solutions for:
- Server-side rendering (SSR): Generate HTML on the server for each request
- Static site generation (SSG): Pre-render pages at build time
- Incremental static regeneration (ISR): Update static pages without rebuilding
- API routes: Build backend endpoints within your Next.js app
- File-based routing: Automatic routing based on folder structure
- Image optimization: Automatic image resizing and lazy loading
- Code splitting: Automatic bundle optimization
π€ Did You Know?
Next.js powers some of the world's largest websites including TikTok, Twitch, Hulu, and Nike. It handles billions of page views per month with exceptional performance!Pages Router vs App Router
Next.js has evolved significantly. Understanding both routing systems is crucial:
| Feature | Pages Router (Legacy) | App Router (Next.js 13+) |
|---|---|---|
| Location | pages/ directory |
app/ directory |
| Default | Client Components | Server Components |
| Data Fetching | getServerSideProps, getStaticProps | async/await in components |
| Layouts | Manual implementation | Built-in with layout.js |
| Loading States | Custom implementation | Built-in with loading.js |
π‘ Tip: New projects should use the App Router for better performance and developer experience. The Pages Router remains fully supported for existing applications.
Server Components: The Game Changer π
React Server Components are revolutionary. They run exclusively on the server, never shipping JavaScript to the client:
// app/products/page.js - Server Component by default
async function ProductsPage() {
// Fetch data directly in the component
const products = await fetch('https://api.example.com/products')
.then(res => res.json());
return (
<div>
<h1>Our Products</h1>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
export default ProductsPage;
Benefits of Server Components:
- β Zero JavaScript sent to client for server logic
- β Direct database access - no API layer needed
- β Automatic code splitting at component level
- β Improved security - keep sensitive logic on server
- β Better performance - reduced bundle size
When to use Client Components (marked with 'use client'):
- Need React hooks (useState, useEffect, etc.)
- Event listeners (onClick, onChange, etc.)
- Browser-only APIs (localStorage, window, etc.)
- Third-party libraries that rely on client-side code
// app/components/Counter.js - Client Component
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
File-Based Routing π
Next.js uses the file system for routing. Your folder structure is your URL structure:
App Router Structure: app/ βββ page.js β / βββ about/ β βββ page.js β /about βββ blog/ β βββ page.js β /blog β βββ [slug]/ β βββ page.js β /blog/:slug βββ products/ β βββ page.js β /products β βββ [id]/ β β βββ page.js β /products/:id β βββ [...category]/ β βββ page.js β /products/* (catch-all) βββ layout.js β Root layout
Special Files in App Router:
page.js- Defines a route's UIlayout.js- Shared UI that wraps pagesloading.js- Loading UI with Suspenseerror.js- Error UI with error boundariesnot-found.js- 404 UIroute.js- API endpoint
Dynamic Routes and Params
Access dynamic route parameters through props:
// app/blog/[slug]/page.js
export default async function BlogPost({ params }) {
const { slug } = params;
const post = await fetch(`https://api.example.com/posts/${slug}`)
.then(res => res.json());
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
// Generate static paths at build time
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts')
.then(res => res.json());
return posts.map(post => ({
slug: post.slug
}));
}
Rendering Strategies: SSR, SSG, and ISR
Next.js provides multiple rendering strategies. Choose based on your data freshness requirements:
| Strategy | When to Use | Performance | Freshness |
|---|---|---|---|
| SSG (Static) | Content rarely changes | β‘ Fastest | π Build-time only |
| ISR (Incremental) | Balance between speed & freshness | β‘ Very fast | π Periodic updates |
| SSR (Dynamic) | Content changes frequently | β‘ Good | π Real-time |
| CSR (Client-side) | Personalized, user-specific | β‘ Depends on device | π Real-time |
Static Site Generation (SSG) - Default in App Router:
// app/products/page.js
// Automatically static - fetched at build time
export default async function ProductsPage() {
const products = await fetch('https://api.example.com/products');
return <ProductList products={products} />;
}
Incremental Static Regeneration (ISR) - Rebuild pages periodically:
// app/news/page.js
export const revalidate = 3600; // Revalidate every hour
export default async function NewsPage() {
const articles = await fetch('https://api.example.com/news');
return <ArticleList articles={articles} />;
}
Server-Side Rendering (SSR) - Fresh data on every request:
// app/dashboard/page.js
export const dynamic = 'force-dynamic'; // Opt out of caching
export default async function Dashboard() {
const userData = await fetch('https://api.example.com/user', {
cache: 'no-store' // Don't cache this request
});
return <UserDashboard data={userData} />;
}
π§ Memory Device: Remember "SIG" - Static for blogs, Incremental for news, Get-fresh (dynamic) for dashboards.
API Routes: Building Your Backend π§
Next.js allows you to create API endpoints alongside your frontend code. In the App Router, use route.js files:
// app/api/products/route.js
import { NextResponse } from 'next/server';
// GET /api/products
export async function GET(request) {
const products = await database.products.findMany();
return NextResponse.json(products);
}
// POST /api/products
export async function POST(request) {
const body = await request.json();
const newProduct = await database.products.create({
data: body
});
return NextResponse.json(newProduct, { status: 201 });
}
Dynamic API routes:
// app/api/products/[id]/route.js
export async function GET(request, { params }) {
const { id } = params;
const product = await database.products.findUnique({
where: { id: parseInt(id) }
});
if (!product) {
return NextResponse.json(
{ error: 'Product not found' },
{ status: 404 }
);
}
return NextResponse.json(product);
}
export async function DELETE(request, { params }) {
const { id } = params;
await database.products.delete({
where: { id: parseInt(id) }
});
return NextResponse.json({ success: true });
}
β οΈ Common Mistake: Don't fetch your own API routes from Server Components! Access the database or external API directly instead:
// β DON'T DO THIS in Server Components
const data = await fetch('http://localhost:3000/api/products');
// β
DO THIS instead
const data = await database.products.findMany();
Layouts: Shared UI Structure π¨
Layouts wrap pages and can be nested for hierarchical designs:
// app/layout.js - Root Layout (required)
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<header>
<nav>{/* Navigation */}</nav>
</header>
<main>{children}</main>
<footer>{/* Footer */}</footer>
</body>
</html>
);
}
// app/dashboard/layout.js - Nested Layout
export default function DashboardLayout({ children }) {
return (
<div className="dashboard-container">
<aside>
<Sidebar />
</aside>
<section>{children}</section>
</div>
);
}
Layout Hierarchy: βββββββββββββββββββββββββββββββββββββββ β Root Layout (app/layout.js) β β βββββββββββββββββββββββββββββββββ β β β Dashboard Layout β β β β (app/dashboard/layout.js) β β β β βββββββββββββββββββββββββββ β β β β β Page β β β β β β (app/dashboard/page.js) β β β β β βββββββββββββββββββββββββββ β β β βββββββββββββββββββββββββββββββββ β βββββββββββββββββββββββββββββββββββββββ Layouts persist across navigation, maintaining state and avoiding re-renders
Loading and Error States
Next.js provides built-in UI patterns for loading and error states:
// app/dashboard/loading.js
export default function Loading() {
return (
<div className="loading-container">
<div className="spinner" />
<p>Loading dashboard...</p>
</div>
);
}
// app/dashboard/error.js
'use client'; // Error components must be Client Components
export default function Error({ error, reset }) {
return (
<div className="error-container">
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
Request Flow with Loading/Error:
User navigates to /dashboard
β
β
ββββββββββββββββββββ
β loading.js β β Instant feedback
β (Suspense UI) β
ββββββββββ¬ββββββββββ
β
β
ββββββββββββββββββββ
β Data fetching β
ββββββββββ¬ββββββββββ
β
ββββββ΄βββββ
β β
β β
βββββββββ βββββββββ
βSuccessβ βFailureβ
βββββ¬ββββ βββββ¬ββββ
β β
β β
βββββββββ βββββββββ
βpage.jsβ βerror.jsβ
βββββββββ βββββββββ
Detailed Examples with Explanations π―
Example 1: Building a Blog with SSG and ISR
Let's create a performant blog that pre-renders posts at build time but updates periodically:
// app/blog/page.js - Blog listing page
export const revalidate = 3600; // Revalidate every hour
export default async function BlogPage() {
// Fetch all posts - happens at build time and every hour
const posts = await fetch('https://api.example.com/posts')
.then(res => res.json());
return (
<div className="blog-container">
<h1>Latest Blog Posts</h1>
<div className="posts-grid">
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
<a href={`/blog/${post.slug}`}>Read more</a>
</article>
))}
</div>
</div>
);
}
// app/blog/[slug]/page.js - Individual blog post
export const revalidate = 3600;
export default async function BlogPost({ params }) {
const { slug } = params;
const post = await fetch(`https://api.example.com/posts/${slug}`)
.then(res => res.json());
return (
<article>
<h1>{post.title}</h1>
<time>{new Date(post.date).toLocaleDateString()}</time>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// Pre-generate paths for all blog posts at build time
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts')
.then(res => res.json());
return posts.map(post => ({
slug: post.slug
}));
}
// Generate metadata for SEO
export async function generateMetadata({ params }) {
const { slug } = params;
const post = await fetch(`https://api.example.com/posts/${slug}`)
.then(res => res.json());
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage]
}
};
}
Why this works:
- Build time: All blog post pages are generated, providing instant loading
- ISR: Content updates every hour without full rebuilds
- SEO: Pre-rendered HTML with metadata ensures search engines can crawl effectively
- Performance: Users get static HTML immediately, no client-side rendering delay
Example 2: Dynamic Dashboard with Authentication
A user dashboard that requires fresh data on every request:
// app/dashboard/page.js
import { redirect } from 'next/navigation';
import { cookies } from 'next/headers';
export const dynamic = 'force-dynamic'; // Always SSR
async function getUserData() {
const cookieStore = cookies();
const token = cookieStore.get('auth-token');
if (!token) {
redirect('/login');
}
const response = await fetch('https://api.example.com/user', {
headers: {
'Authorization': `Bearer ${token.value}`
},
cache: 'no-store' // Never cache user data
});
if (!response.ok) {
redirect('/login');
}
return response.json();
}
export default async function Dashboard() {
const userData = await getUserData();
return (
<div>
<h1>Welcome, {userData.name}!</h1>
<div className="stats-grid">
<StatCard title="Orders" value={userData.orderCount} />
<StatCard title="Revenue" value={userData.revenue} />
<StatCard title="Active Projects" value={userData.projects} />
</div>
<RecentActivity activities={userData.recentActivity} />
</div>
);
}
// app/dashboard/loading.js
export default function DashboardLoading() {
return (
<div className="dashboard-skeleton">
<div className="skeleton-header" />
<div className="skeleton-stats">
<div className="skeleton-card" />
<div className="skeleton-card" />
<div className="skeleton-card" />
</div>
</div>
);
}
Key points:
- dynamic = 'force-dynamic': Opts out of static generation
- cache: 'no-store': Ensures fresh data on every request
- Server-side auth: Cookies accessed securely on server
- Automatic redirects: Unauthenticated users sent to login
- Loading UI: Shows skeleton while data fetches
Example 3: E-commerce Product Page with Parallel Data Fetching
Fetch multiple data sources in parallel for optimal performance:
// app/products/[id]/page.js
export default async function ProductPage({ params }) {
const { id } = params;
// Fetch multiple resources in parallel
const [product, reviews, relatedProducts] = await Promise.all([
fetch(`https://api.example.com/products/${id}`).then(r => r.json()),
fetch(`https://api.example.com/products/${id}/reviews`).then(r => r.json()),
fetch(`https://api.example.com/products/${id}/related`).then(r => r.json())
]);
return (
<div className="product-page">
<div className="product-main">
<ProductImages images={product.images} />
<ProductInfo
name={product.name}
price={product.price}
description={product.description}
/>
<AddToCartButton productId={product.id} />
</div>
<section className="reviews-section">
<h2>Customer Reviews</h2>
<ReviewList reviews={reviews} />
</section>
<section className="related-products">
<h2>You May Also Like</h2>
<ProductGrid products={relatedProducts} />
</section>
</div>
);
}
// app/products/[id]/AddToCartButton.js
'use client'; // Needs interactivity
import { useState } from 'react';
export default function AddToCartButton({ productId }) {
const [adding, setAdding] = useState(false);
const [added, setAdded] = useState(false);
const handleAddToCart = async () => {
setAdding(true);
await fetch('/api/cart', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId, quantity: 1 })
});
setAdding(false);
setAdded(true);
setTimeout(() => setAdded(false), 2000);
};
return (
<button
onClick={handleAddToCart}
disabled={adding}
className={added ? 'success' : ''}
>
{added ? 'β Added!' : adding ? 'Adding...' : 'Add to Cart'}
</button>
);
}
Performance optimizations:
- Promise.all: Fetches all data simultaneously, not sequentially
- Server Component: Main page rendered on server with no JS overhead
- Client Component: Only the interactive button requires client-side JS
- Optimistic UI: Immediate feedback when adding to cart
Example 4: Full-Stack API with Database Integration
Create a complete CRUD API for a todo application:
// app/api/todos/route.js
import { NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// GET /api/todos - List all todos
export async function GET(request) {
try {
const { searchParams } = new URL(request.url);
const completed = searchParams.get('completed');
const todos = await prisma.todo.findMany({
where: completed !== null ? {
completed: completed === 'true'
} : undefined,
orderBy: { createdAt: 'desc' }
});
return NextResponse.json(todos);
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch todos' },
{ status: 500 }
);
}
}
// POST /api/todos - Create a new todo
export async function POST(request) {
try {
const body = await request.json();
if (!body.title || body.title.trim() === '') {
return NextResponse.json(
{ error: 'Title is required' },
{ status: 400 }
);
}
const todo = await prisma.todo.create({
data: {
title: body.title,
completed: false
}
});
return NextResponse.json(todo, { status: 201 });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create todo' },
{ status: 500 }
);
}
}
// app/api/todos/[id]/route.js
import { NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// PATCH /api/todos/:id - Update a todo
export async function PATCH(request, { params }) {
try {
const { id } = params;
const body = await request.json();
const todo = await prisma.todo.update({
where: { id: parseInt(id) },
data: body
});
return NextResponse.json(todo);
} catch (error) {
return NextResponse.json(
{ error: 'Failed to update todo' },
{ status: 500 }
);
}
}
// DELETE /api/todos/:id - Delete a todo
export async function DELETE(request, { params }) {
try {
const { id } = params;
await prisma.todo.delete({
where: { id: parseInt(id) }
});
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to delete todo' },
{ status: 500 }
);
}
}
Architecture benefits:
- Type safety: Prisma provides TypeScript types automatically
- Database abstraction: Works with PostgreSQL, MySQL, SQLite, etc.
- Error handling: Consistent error responses with proper status codes
- RESTful design: Standard HTTP methods for CRUD operations
- Query parameters: Filter todos by completion status
Common Mistakes to Avoid β οΈ
1. Fetching Internal APIs from Server Components
β Wrong:
export default async function Page() {
// Don't fetch your own API routes from Server Components!
const data = await fetch('http://localhost:3000/api/data');
return <div>{data}</div>;
}
β Correct:
import { prisma } from '@/lib/database';
export default async function Page() {
// Access database directly in Server Components
const data = await prisma.items.findMany();
return <div>{data}</div>;
}
Why: Server Components already run on the server, so making HTTP requests to your own API adds unnecessary overhead. Access the database or external services directly.
2. Using Client-Only Hooks in Server Components
β Wrong:
// This will cause an error!
export default function Page() {
const [count, setCount] = useState(0); // useState not available
return <div>{count}</div>;
}
β Correct:
'use client'; // Mark as Client Component
import { useState } from 'react';
export default function Page() {
const [count, setCount] = useState(0);
return <div>{count}</div>;
}
3. Not Setting Revalidation for Dynamic Data
β Wrong:
// Data fetched at build time, never updates!
export default async function NewsPage() {
const news = await fetch('https://api.example.com/news');
return <NewsList articles={news} />;
}
β Correct:
export const revalidate = 60; // Revalidate every minute
export default async function NewsPage() {
const news = await fetch('https://api.example.com/news');
return <NewsList articles={news} />;
}
4. Forgetting to Export HTTP Methods in API Routes
β Wrong:
// Won't work - not exported!
function GET() {
return Response.json({ data: 'test' });
}
β Correct:
// Must export named functions
export async function GET() {
return NextResponse.json({ data: 'test' });
}
5. Mixing Pages and App Router
β Wrong:
my-app/
βββ pages/
β βββ index.js β Old Pages Router
βββ app/
β βββ page.js β New App Router
β Correct: Choose one routing system for your entire application. For new projects, use App Router exclusively.
6. Not Handling Loading and Error States
β Wrong:
// No loading or error feedback
export default async function Page() {
const data = await fetch('https://api.example.com/data');
return <div>{data}</div>;
}
β Correct:
// app/dashboard/page.js
export default async function Page() {
const data = await fetch('https://api.example.com/data');
return <div>{data}</div>;
}
// app/dashboard/loading.js
export default function Loading() {
return <Spinner />;
}
// app/dashboard/error.js
'use client';
export default function Error({ error, reset }) {
return <ErrorMessage error={error} onRetry={reset} />;
}
7. Improper Image Optimization Usage
β Wrong:
// Misses Next.js optimization
<img src="/photo.jpg" alt="Photo" />
β Correct:
import Image from 'next/image';
<Image
src="/photo.jpg"
alt="Photo"
width={800}
height={600}
priority={true} // For above-fold images
/>
Benefits: Automatic image optimization, lazy loading, responsive images, and WebP conversion.
Key Takeaways π―
β Next.js is a full-stack framework that extends React with server-side rendering, static generation, and API routes
β App Router (Next.js 13+) is the modern approach with Server Components as the default, providing better performance
β Server Components run only on the server, reducing JavaScript bundle size and improving load times
β Use 'use client' directive when you need interactivity, React hooks, or browser APIs
β File-based routing automatically creates routes from your folder structureβno configuration needed
β Choose the right rendering strategy: SSG for static content, ISR for periodic updates, SSR for dynamic data
β API routes enable full-stack development within a single codebaseβaccess databases directly
β Layouts provide shared UI that persists across page navigations without re-rendering
β loading.js and error.js provide built-in patterns for handling async states
β Never fetch your own API routes from Server Componentsβaccess data sources directly
β Use Promise.all for parallel data fetching to minimize loading times
β Set revalidate for pages with data that changes over time
π§ Try This: Build Your First Next.js App
Create a simple blog in 5 steps:
- Initialize:
npx create-next-app@latest my-blog - Create a layout:
app/layout.jswith header and footer - Add homepage:
app/page.jswith a list of blog posts - Dynamic routes:
app/blog/[slug]/page.jsfor individual posts - Add ISR: Set
export const revalidate = 3600for periodic updates
π Quick Reference Card
| Feature | Usage | Example |
|---|---|---|
| Server Component | Default in App Router | async function Page() |
| Client Component | Add 'use client' | 'use client'; useState() |
| Dynamic Route | Use [param] folder | app/blog/[slug]/page.js |
| API Route | Create route.js | app/api/users/route.js |
| SSG | Default behavior | await fetch(url) |
| ISR | Set revalidate | export const revalidate = 3600 |
| SSR | Force dynamic | export const dynamic = 'force-dynamic' |
| Layout | Create layout.js | function Layout({ children }) |
| Loading UI | Create loading.js | function Loading() |
| Error UI | Create error.js | 'use client'; function Error() |
| Metadata | Export metadata object | export const metadata = {} |
| No Cache | fetch with options | fetch(url, {cache: 'no-store'}) |
HTTP Methods in API Routes:
GET- Retrieve dataPOST- Create resourcePATCH/PUT- Update resourceDELETE- Remove resource
Special Files:
page.js- Route UIlayout.js- Shared wrapperloading.js- Loading UIerror.js- Error boundarynot-found.js- 404 pageroute.js- API endpoint
π Further Study
- Official Next.js Documentation - https://nextjs.org/docs - Comprehensive guides, API reference, and examples
- Next.js Learn Course - https://nextjs.org/learn - Interactive tutorial covering fundamentals to advanced topics
- Vercel Next.js Examples - https://github.com/vercel/next.js/tree/canary/examples - 300+ example projects showcasing different use cases
π‘ Pro Tip: Start with the App Router for new projects. The Pages Router is still supported but App Router provides better performance with Server Components and a superior developer experience. Master the fundamentals first, then explore advanced patterns like streaming, partial pre-rendering, and middleware!