Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

Readme.md

Next.js Complete Learning Path

📚 Table of Contents

Phase 1: Foundations

  1. What is Next.js and Why Use It?
  2. Installation & Project Setup
  3. Project Structure Deep Dive
  4. File-Based Routing Fundamentals

Phase 2: Core Concepts

  1. Server Components vs Client Components
  2. Data Fetching Strategies
  3. Linking and Navigation
  4. Layouts and Templates

Phase 3: Advanced Features

  1. Caching & Cache Components (New in v16)
  2. Server Actions & Mutations
  3. API Routes & Route Handlers
  4. Middleware & Proxy.ts (New in v16)

Phase 4: Production Ready

  1. Authentication Patterns
  2. Performance Optimization
  3. Error Handling & Loading States
  4. Deployment & Best Practices

Phase 1: Foundations

Lesson 1: What is Next.js and Why Use It?

Simple Explanation: Next.js is a React framework that makes building web applications easier. Think of React as a toolbox, and Next.js as a pre-built workshop with all the tools organized and ready to use.

Key Benefits:

  • File-based Routing: Create a file, get a route automatically (no router configuration)
  • Server & Client Rendering: Choose where your code runs for better performance
  • Built-in Optimization: Images, fonts, and scripts optimized automatically
  • Full-Stack Capable: Build both frontend and backend in one project

Real-World Analogy: Building a website with React is like building a house from scratch. Next.js gives you the foundation, plumbing, and electrical already set up—you just decorate and customize.


Lesson 2: Installation & Project Setup

Step 1: Create Your First Project

# Install Node.js 18+ first, then run:
npx create-next-app@latest my-first-app

# You'll be asked questions:
✔ 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 App Router? → Yes
✔ Would you like to use Turbopack? → Yes (default in v16)

Step 2: Start Development Server

cd my-first-app
npm run dev

Open http://localhost:3000 — you'll see your app running!

🎯 Exercise 1.1: Create your first Next.js app following the steps above. Explore the default page.


Lesson 3: Project Structure Deep Dive

my-first-app/
├── app/                  # Your application code (App Router)
│   ├── layout.tsx       # Root layout (wraps all pages)
│   ├── page.tsx         # Home page (/)
│   └── favicon.ico      # Site favicon
├── public/              # Static files (images, etc.)
├── node_modules/        # Dependencies
├── package.json         # Project configuration
├── next.config.ts       # Next.js configuration
└── tsconfig.json        # TypeScript configuration

Key Folder: app/

This is where all your magic happens. Every file here has special meaning:

File Name Purpose Example
page.tsx Creates a route/page app/about/page.tsx/about
layout.tsx Wraps pages with shared UI Navigation bar, footer
loading.tsx Shows while page loads Spinner, skeleton
error.tsx Catches errors Error message
not-found.tsx 404 page "Page not found"

🎯 Exercise 1.2: Create an app/about/page.tsx file:

// app/about/page.tsx
export default function AboutPage() {
  return (
    <div>
      <h1>About Us</h1>
      <p>This is my first Next.js page!</p>
    </div>
  );
}

Visit http://localhost:3000/about — it just works! No router configuration needed.


Lesson 4: File-Based Routing Fundamentals

How Routing Works: The folder structure in app/ automatically becomes your URL structure.

app/
├── page.tsx              → /
├── about/
│   └── page.tsx          → /about
├── blog/
│   ├── page.tsx          → /blog
│   └── [slug]/
│       └── page.tsx      → /blog/any-post-name
└── dashboard/
    ├── page.tsx          → /dashboard
    └── settings/
        └── page.tsx      → /dashboard/settings

Dynamic Routes: Use [brackets] for dynamic segments:

// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
  return <h1>Reading post: {params.slug}</h1>;
}

Visit /blog/hello-world → Shows "Reading post: hello-world"

Route Groups (Organization): Use (parentheses) to organize without affecting URLs:

app/
├── (marketing)/
│   ├── about/page.tsx     → /about
│   └── pricing/page.tsx   → /pricing
└── (shop)/
    └── products/page.tsx  → /products

The (marketing) and (shop) folders don't appear in URLs—they just help you organize!

🎯 Exercise 1.3: Build a Blog Structure

Create this structure:

app/
└── blog/
    ├── page.tsx           # List all posts
    └── [slug]/
        └── page.tsx       # Individual post
// app/blog/page.tsx
export default function BlogIndex() {
  const posts = ['first-post', 'second-post', 'third-post'];
  
  return (
    <div>
      <h1>My Blog</h1>
      <ul>
        {posts.map(post => (
          <li key={post}>
            <a href={`/blog/${post}`}>{post}</a>
          </li>
        ))}
      </ul>
    </div>
  );
}

// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
  return (
    <article>
      <h1>{params.slug.replace('-', ' ')}</h1>
      <p>This is the content of {params.slug}</p>
    </article>
  );
}

Phase 2: Core Concepts

Lesson 5: Server Components vs Client Components

🚀 BIGGEST CONCEPT IN NEXT.JS 16

Server Components (Default):

  • Run only on the server
  • Can access databases, file systems, secrets
  • Don't send JavaScript to the browser
  • Faster, lighter pages

Client Components:

  • Run in the browser
  • Can use hooks (useState, useEffect)
  • Can handle events (onClick, onChange)
  • Interactive elements

The Golden Rule:

"Use Server Components by default. Only add 'use client' when you need interactivity."

Real-World Example:

// app/products/page.tsx (Server Component - default)
async function getProducts() {
  // This runs on the server, can access database directly
  const res = await fetch('https://api.example.com/products');
  return res.json();
}

export default async function ProductsPage() {
  const products = await getProducts(); // Direct data fetch!
  
  return (
    <div>
      <h1>Products</h1>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

// components/ProductCard.tsx (Client Component)
'use client'; // This line makes it a Client Component

import { useState } from 'react';

export default function ProductCard({ product }) {
  const [liked, setLiked] = useState(false);
  
  return (
    <div>
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <button onClick={() => setLiked(!liked)}>
        {liked ? '❤️' : '🤍'}
      </button>
    </div>
  );
}

When to Use Each:

Need Use
Fetch data from API/database Server Component
Display static content Server Component
Use useState, useEffect Client Component
Handle clicks, form inputs Client Component
Access browser APIs (localStorage) Client Component

Composition Pattern (Best Practice):

// ✅ GOOD: Server Component wraps Client Component
// app/dashboard/page.tsx (Server Component)
import ClientSidebar from './ClientSidebar';

async function getData() {
  const data = await fetch('...');
  return data;
}

export default async function Dashboard() {
  const data = await getData();
  
  return (
    <div>
      <ClientSidebar items={data.menuItems} /> {/* Pass data as props */}
      <main>{data.content}</main>
    </div>
  );
}

// app/dashboard/ClientSidebar.tsx (Client Component)
'use client';

import { useState } from 'react';

export default function ClientSidebar({ items }) {
  const [open, setOpen] = useState(false);
  
  return (
    <aside>
      <button onClick={() => setOpen(!open)}>Toggle</button>
      {open && <nav>{/* menu items */}</nav>}
    </aside>
  );
}

🎯 Exercise 2.1: Build a Product Dashboard

Create a page that fetches products on the server and displays them with interactive "Add to Cart" buttons:

// app/shop/page.tsx
async function getProducts() {
  const res = await fetch('https://fakestoreapi.com/products');
  return res.json();
}

export default async function ShopPage() {
  const products = await getProducts();
  
  return (
    <div>
      <h1>Shop</h1>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '20px' }}>
        {products.map(product => (
          <ProductCard key={product.id} product={product} />
        ))}
      </div>
    </div>
  );
}

// components/ProductCard.tsx
'use client';

import { useState } from 'react';

export default function ProductCard({ product }) {
  const [quantity, setQuantity] = useState(0);
  
  return (
    <div style={{ border: '1px solid #ddd', padding: '16px' }}>
      <img src={product.image} alt={product.title} style={{ width: '100%', height: '200px', objectFit: 'contain' }} />
      <h3>{product.title}</h3>
      <p>${product.price}</p>
      <div>
        <button onClick={() => setQuantity(q => Math.max(0, q - 1))}>-</button>
        <span style={{ margin: '0 10px' }}>{quantity}</span>
        <button onClick={() => setQuantity(q => q + 1)}>+</button>
      </div>
    </div>
  );
}

Lesson 6: Data Fetching Strategies

Next.js 16 has multiple ways to fetch data. Choose based on your needs:

1. Server Component Fetch (Recommended)

// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts');
  
  if (!res.ok) throw new Error('Failed to fetch');
  
  return res.json();
}

export default async function PostsPage() {
  const posts = await getPosts();
  
  return (
    <div>
      <h1>Posts</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.body}</p>
        </article>
      ))}
    </div>
  );
}

2. Parallel Data Fetching

// Fetch multiple things at once
async function getUser(id) {
  const res = await fetch(`https://api.example.com/users/${id}`);
  return res.json();
}

async function getPosts(userId) {
  const res = await fetch(`https://api.example.com/posts?userId=${userId}`);
  return res.json();
}

export default async function UserProfile({ params }) {
  // Both fetch at the same time!
  const [user, posts] = await Promise.all([
    getUser(params.id),
    getPosts(params.id)
  ]);
  
  return (
    <div>
      <h1>{user.name}</h1>
      <h2>Posts by {user.name}</h2>
      {posts.map(post => <div key={post.id}>{post.title}</div>)}
    </div>
  );
}

3. Caching Control

// No caching (always fresh)
async function getData() {
  const res = await fetch('https://api.example.com/data', {
    cache: 'no-store'
  });
  return res.json();
}

// Revalidate every 60 seconds
async function getData() {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 60 }
  });
  return res.json();
}

// Cache forever (default)
async function getData() {
  const res = await fetch('https://api.example.com/data', {
    cache: 'force-cache'
  });
  return res.json();
}

🎯 Exercise 2.2: User Profile with Parallel Fetching

Create a user profile page that fetches user data and their posts simultaneously:

// app/users/[id]/page.tsx
async function getUser(id: string) {
  const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
  return res.json();
}

async function getUserPosts(id: string) {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts?userId=${id}`);
  return res.json();
}

export default async function UserPage({ params }: { params: { id: string } }) {
  const [user, posts] = await Promise.all([
    getUser(params.id),
    getUserPosts(params.id)
  ]);
  
  return (
    <div>
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
      <p>Website: {user.website}</p>
      
      <h2>Posts</h2>
      {posts.map(post => (
        <article key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.body}</p>
        </article>
      ))}
    </div>
  );
}

Lesson 7: Linking and Navigation

Using the Link Component:

import Link from 'next/link';

export default function Navigation() {
  return (
    <nav>
      <Link href="/">Home</Link>
      <Link href="/about">About</Link>
      <Link href="/blog">Blog</Link>
    </nav>
  );
}

Why Use Link Instead of <a>?

  • Prefetches pages in the background
  • Client-side navigation (no full page reload)
  • Faster navigation

Programmatic Navigation:

'use client';

import { useRouter } from 'next/navigation';

export default function LoginForm() {
  const router = useRouter();
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    // ... login logic
    router.push('/dashboard'); // Navigate after login
  };
  
  return <form onSubmit={handleSubmit}>{/* form fields */}</form>;
}

Navigation with Parameters:

<Link href={`/blog/${post.slug}`}>Read More</Link>

// Or programmatically:
router.push(`/blog/${post.slug}`);

Lesson 8: Layouts and Templates

Root Layout (Required):

// app/layout.tsx - Wraps EVERY page
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <header>
          <nav>{/* Navigation */}</nav>
        </header>
        <main>{children}</main> {/* Your pages render here */}
        <footer>{/* Footer */}</footer>
      </body>
    </html>
  );
}

Nested Layouts:

// app/dashboard/layout.tsx - Only for /dashboard routes
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div style={{ display: 'flex' }}>
      <aside>{/* Sidebar */}</aside>
      <main>{children}</main>
    </div>
  );
}

// app/dashboard/page.tsx - This gets wrapped by DashboardLayout
export default function DashboardPage() {
  return <h1>Dashboard Home</h1>;
}

Templates vs Layouts:

// app/dashboard/template.tsx
// Re-renders on every navigation (good for animations)
export default function DashboardTemplate({ children }: { children: React.ReactNode }) {
  return <div className="fade-in">{children}</div>;
}

// Layouts preserve state, templates don't

🎯 Exercise 2.3: Multi-Layout App

Create an app with different layouts for marketing pages and dashboard:

// app/layout.tsx (Root)
export default function RootLayout({ children }) {
  return (
    <html>
      <body>{children}</body>
    </html>
  );
}

// app/(marketing)/layout.tsx
export default function MarketingLayout({ children }) {
  return (
    <div>
      <header style={{ background: 'blue', padding: '20px', color: 'white' }}>
        <nav>
          <a href="/">Home</a> | <a href="/about">About</a> | <a href="/pricing">Pricing</a>
        </nav>
      </header>
      {children}
    </div>
  );
}

// app/(marketing)/page.tsx
export default function HomePage() {
  return <h1>Welcome to our Marketing Site!</h1>;
}

// app/(marketing)/about/page.tsx
export default function AboutPage() {
  return <h1>About Us</h1>;
}

// app/(marketing)/pricing/page.tsx
export default function PricingPage() {
  return <h1>Pricing Plans</h1>;
}

// app/dashboard/layout.tsx
export default function DashboardLayout({ children }) {
  return (
    <div style={{ display: 'flex' }}>
      <aside style={{ width: '200px', background: '#f0f0f0', padding: '20px' }}>
        <h3>Dashboard Menu</h3>
        <ul>
          <li><a href="/dashboard">Overview</a></li>
          <li><a href="/dashboard/settings">Settings</a></li>
        </ul>
      </aside>
      <main style={{ flex: 1, padding: '20px' }}>{children}</main>
    </div>
  );
}

// app/dashboard/page.tsx
export default function DashboardPage() {
  return <h1>Dashboard Overview</h1>;
}

// app/dashboard/settings/page.tsx
export default function SettingsPage() {
  return <h1>Dashboard Settings</h1>;
}

Phase 3: Advanced Features

Lesson 9: Caching & Cache Components (NEW in v16)

Next.js 16's Big Change: Previously, Next.js decided what to cache. Now YOU control it explicitly with "use cache".

Basic Cache Component:

// app/products/page.tsx
'use cache'; // Cache this entire page!

async function getProducts() {
  const res = await fetch('https://api.example.com/products');
  return res.json();
}

export default async function ProductsPage() {
  const products = await getProducts();
  return <div>{/* Render products */}</div>;
}

Caching Individual Components:

// components/FeaturedProducts.tsx
'use cache';

async function getFeaturedProducts() {
  const res = await fetch('https://api.example.com/featured');
  return res.json();
}

export default async function FeaturedProducts() {
  const products = await getFeaturedProducts();
  
  return (
    <section>
      <h2>Featured Products</h2>
      {products.map(p => <div key={p.id}>{p.name}</div>)}
    </section>
  );
}

Cache with Revalidation:

'use cache';

export const revalidate = 3600; // Revalidate every hour

async function getData() {
  // This is cached for 1 hour
  const res = await fetch('https://api.example.com/data');
  return res.json();
}

Manual Cache Invalidation:

// app/actions.ts
'use server';

import { revalidateTag } from 'next/cache';

export async function updateProduct(id: string) {
  // Update product in database
  await db.products.update(id, { /* data */ });
  
  // Invalidate the cache
  revalidateTag('products');
}

// In your cached component:
'use cache';

async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { tags: ['products'] } // Tag this request
  });
  return res.json();
}

Lesson 10: Server Actions & Mutations

Server Actions let you run server-side code directly from client components. Think of them as built-in API routes.

Basic Form with Server Action:

// app/contact/page.tsx
import { redirect } from 'next/navigation';

async function submitContact(formData: FormData) {
  'use server'; // This function runs on the server!
  
  const name = formData.get('name');
  const email = formData.get('email');
  const message = formData.get('message');
  
  // Save to database
  await db.contacts.create({ name, email, message });
  
  redirect('/thank-you');
}

export default function ContactPage() {
  return (
    <form action={submitContact}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message" required />
      <button type="submit">Submit</button>
    </form>
  );
}

Progressive Enhancement: This works even if JavaScript is disabled! The form submits normally.

With Client Component (for validation):

// app/contact/ContactForm.tsx
'use client';

import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();
  
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Submitting...' : 'Submit'}
    </button>
  );
}

export default function ContactForm({ submitAction }) {
  return (
    <form action={submitAction}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message" required />
      <SubmitButton />
    </form>
  );
}

// app/contact/page.tsx
import ContactForm from './ContactForm';

async function submitContact(formData: FormData) {
  'use server';
  // ... save logic
}

export default function ContactPage() {
  return <ContactForm submitAction={submitContact} />;
}

🎯 Exercise 3.1: Todo App with Server Actions

// app/todos/page.tsx
import { revalidatePath } from 'next/cache';

const todos = []; // In real app, use database

async function addTodo(formData: FormData) {
  'use server';
  
  const text = formData.get('todo');
  todos.push({ id: Date.now(), text, completed: false });
  
  revalidatePath('/todos'); // Refresh the page
}

async function toggleTodo(id: number) {
  'use server';
  
  const todo = todos.find(t => t.id === id);
  if (todo) todo.completed = !todo.completed;
  
  revalidatePath('/todos');
}

export default function TodosPage() {
  return (
    <div>
      <h1>Todos</h1>
      
      <form action={addTodo}>
        <input name="todo" placeholder="Add a todo" required />
        <button type="submit">Add</button>
      </form>
      
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <form action={toggleTodo.bind(null, todo.id)} style={{ display: 'inline' }}>
              <button type="submit">
                {todo.completed ? '✅' : '⬜️'}
              </button>
            </form>
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.text}
            </span>
          </li>
        ))}
      </ul>
    </div>
  );
}

Lesson 11: API Routes & Route Handlers

Creating API Endpoints:

// app/api/hello/route.ts
export async function GET(request: Request) {
  return Response.json({ message: 'Hello from API!' });
}

export async function POST(request: Request) {
  const data = await request.json();
  
  return Response.json({ 
    received: data,
    timestamp: Date.now() 
  });
}

Access at: http://localhost:3000/api/hello

Dynamic API Routes:

// app/api/users/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const user = await db.users.findById(params.id);
  
  if (!user) {
    return Response.json({ error: 'User not found' }, { status: 404 });
  }
  
  return Response.json(user);
}

export async function PUT(
  request: Request,
  { params }: { params: { id: string } }
) {
  const data = await request.json();
  const user = await db.users.update(params.id, data);
  
  return Response.json(user);
}

export async function DELETE(
  request: Request,
  { params }: { params: { id: string } }
) {
  await db.users.delete(params.id);
  
  return Response.json({ success: true });
}

🎯 Exercise 3.2: Build a RESTful API

Create a simple products API:

// app/api/products/route.ts
const products = [
  { id: 1, name: 'Laptop', price: 999 },
  { id: 2, name: 'Mouse', price: 29 },
  { id: 3, name: 'Keyboard', price: 79 },
];

export async function GET() {
  return Response.json(products);
}

export async function POST(request: Request) {
  const newProduct = await request.json();
  newProduct.id = products.length + 1;
  products.push(newProduct);
  
  return Response.json(newProduct, { status: 201 });
}

// app/api/products/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const product = products.find(p => p.id === parseInt(params.id));
  
  if (!product) {
    return Response.json({ error: 'Product not found' }, { status: 404 });
  }
  
  return Response.json(product);
}

Lesson 12: Middleware & Proxy.ts (NEW in v16)

Important Change: middleware.ts is now proxy.ts in Next.js 16!

Basic Proxy:

// proxy.ts (at root of project)
import { NextResponse, type NextRequest } from 'next/server';

export default function proxy(request: NextRequest) {
  const url = request.nextUrl;
  
  // Redirect /old-blog to /blog
  if (url.pathname === '/old-blog') {
    return NextResponse.redirect(new URL('/blog', request.url));
  }
  
  // Continue to the requested page
  return NextResponse.next();
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
};

Authentication Middleware:

// proxy.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export default function proxy(request: NextRequest) {
  const token = request.cookies.get('auth-token');
  const isAuthPage = request.nextUrl.pathname.startsWith('/login');
  const isProtected = request.nextUrl.pathname.startsWith('/dashboard');
  
  // Redirect to login if accessing protected route without token
  if (isProtected && !token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  
  // Redirect to dashboard if already logged in and trying to access login
  if (isAuthPage && token) {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }
  
  return NextResponse.next();
}

Phase 4: Production Ready

Lesson 13: Authentication Patterns

Using NextAuth.js (now Auth.js):

npm install next-auth
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import GithubProvider from 'next-auth/providers/github';

const handler = NextAuth({
  providers: [
    GithubProvider({
      clientId: process.env.GITHUB_ID!,
      clientSecret: process.env.GITHUB_SECRET!,
    }),
  ],
});

export { handler as GET, handler as POST };

// app/dashboard/page.tsx
import { getServerSession } from 'next-auth';

export default async function Dashboard() {
  const session = await getServerSession();
  
  if (!session) {
    return <div>You must be logged in</div>;
  }
  
  return (
    <div>
      <h1>Welcome {session.user?.name}</h1>
    </div>
  );
}

Lesson 14: Performance Optimization

1. Image Optimization:

import Image from 'next/image';

export default function Gallery() {
  return (
    <Image
      src="/photo.jpg"
      alt="Description"
      width={800}
      height={600}
      priority // Load immediately for above-fold images
    />
  );
}

2. Font Optimization:

// app/layout.tsx
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'] });

export default function RootLayout({ children }) {
  return (
    <html className={inter.className}>
      <body>{children}</body>
    </html>
  );
}

3. Dynamic Imports:

'use client';

import dynamic from 'next/dynamic';

// Load component only when needed
const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => <p>Loading chart...</p>,
  ssr: false // Don't render on server
});

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <HeavyChart />
    </div>
  );
}

Lesson 15: Error Handling & Loading States

Error Boundaries:

// app/dashboard/error.tsx
'use client';

export default function ErrorPage({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <p>{error.message}</p>
      <button onClick={reset}>Try again</button>
    </div>
  );
}

Loading States:

// app/dashboard/loading.tsx
export default function Loading() {
  return (
    <div>
      <p>Loading dashboard...</p>
      {/* Add skeleton or spinner */}
    </div>
  );
}

Streaming with Suspense:

import { Suspense } from 'react';

async function SlowComponent() {
  await new Promise(resolve => setTimeout(resolve, 3000));
  return <div>Loaded!</div>;
}

export default function Page() {
  return (
    <div>
      <h1>Page Title</h1>
      <Suspense fallback={<div>Loading slow part...</div>}>
        <SlowComponent />
      </Suspense>
    </div>
  );
}

Lesson 16: Deployment & Best Practices

Deploy to Vercel (Easiest):

# Install Vercel CLI
npm i -g vercel

# Deploy
vercel

Environment Variables:

# .env.local
DATABASE_URL=postgresql://...
API_KEY=secret123
NEXT_PUBLIC_API_URL=https://api.example.com
// Use in Server Components
const dbUrl = process.env.DATABASE_URL;

// Use in Client Components (must start with NEXT_PUBLIC_)
const apiUrl = process.env.NEXT_PUBLIC_API_URL;

Production Checklist:

✅ Add error tracking (Sentry) ✅ Enable analytics ✅ Configure environment variables ✅ Set up CI/CD ✅ Add meta tags for SEO ✅ Test performance with Lighthouse ✅ Configure caching strategies ✅ Add security headers


🚀 Final Project: Full-Stack Blog Platform

Build this to cement everything you've learned:

Features:

  • Homepage with list of blog posts (Server Component)
  • Individual post pages with dynamic routes
  • Admin dashboard to create/edit posts (Client Components with Server Actions)
  • Authentication with NextAuth
  • Image uploads with Next.js Image optimization
  • API routes for CRUD operations
  • Loading states and error handling
  • Deployed to production

This project combines all concepts: routing, server/client components, data fetching, caching, authentication, and deployment.


📚 Additional Resources


Key Takeaways

  1. Server Components by default - only use 'use client' when needed
  2. File-based routing - folder structure = URL structure
  3. Co-location - keep related files together in app/
  4. Fetch in Server Components - direct database access, no API needed
  5. Use Cache Components - explicit caching with 'use cache'
  6. Server Actions - no separate API routes needed for mutations
  7. Turbopack - faster builds and hot reload by default in v16
  8. Progressive enhancement - works without JavaScript when possible

You now have a complete roadmap from beginner to production-ready Next.js 16 developer! Start with Phase 1, complete the exercises, and gradually move through each phase. Build small projects along the way, and you'll be writing clean, scalable Next.js code in no time.

Would you like me to dive deeper into any specific topic or create additional practice exercises for a particular concept? 12345678910

Footnotes

  1. https://nextjs.org/blog/next-16

  2. https://appwrite.io/blog/post/everything-new-in-nextjs16

  3. https://www.reddit.com/r/nextjs/comments/1k5a1mh/mixing_clientserver_components_best_practices/

  4. https://nextjs.org/docs/app/getting-started

  5. https://talent500.com/blog/nextjs-16-release-features-benefits/

  6. https://strapi.io/blog/react-and-nextjs-in-2025-modern-best-practices

  7. https://nextjs.org/docs/app/guides

  8. https://nextjs.org/blog

  9. https://nextjs.org/docs/app/getting-started/server-and-client-components

  10. https://nextjs.org/docs/app