You are viewing a preview of this lesson. Sign in to start learning
Back to React

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 UI
  • layout.js - Shared UI that wraps pages
  • loading.js - Loading UI with Suspense
  • error.js - Error UI with error boundaries
  • not-found.js - 404 UI
  • route.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:

  1. Initialize: npx create-next-app@latest my-blog
  2. Create a layout: app/layout.js with header and footer
  3. Add homepage: app/page.js with a list of blog posts
  4. Dynamic routes: app/blog/[slug]/page.js for individual posts
  5. Add ISR: Set export const revalidate = 3600 for 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 data
  • POST - Create resource
  • PATCH/PUT - Update resource
  • DELETE - Remove resource

Special Files:

  • page.js - Route UI
  • layout.js - Shared wrapper
  • loading.js - Loading UI
  • error.js - Error boundary
  • not-found.js - 404 page
  • route.js - API endpoint

πŸ“š Further Study

  1. Official Next.js Documentation - https://nextjs.org/docs - Comprehensive guides, API reference, and examples
  2. Next.js Learn Course - https://nextjs.org/learn - Interactive tutorial covering fundamentals to advanced topics
  3. 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!