- What is Next.js and Why Use It?
- Installation & Project Setup
- Project Structure Deep Dive
- File-Based Routing Fundamentals
- Server Components vs Client Components
- Data Fetching Strategies
- Linking and Navigation
- Layouts and Templates
- Caching & Cache Components (New in v16)
- Server Actions & Mutations
- API Routes & Route Handlers
- Middleware & Proxy.ts (New in v16)
- Authentication Patterns
- Performance Optimization
- Error Handling & Loading States
- Deployment & Best Practices
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.
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 devOpen 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.
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.
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>
);
}🚀 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>
);
}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>
);
}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}`);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>;
}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();
}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>
);
}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);
}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();
}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>
);
}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>
);
}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>
);
}Deploy to Vercel (Easiest):
# Install Vercel CLI
npm i -g vercel
# Deploy
vercelEnvironment 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
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.
- Official Docs: https://nextjs.org/docs
- Next.js 16 Release Notes: https://nextjs.org/blog/next-16
- Next.js Learn Course: https://nextjs.org/learn
- Server Components by default - only use
'use client'when needed - File-based routing - folder structure = URL structure
- Co-location - keep related files together in
app/ - Fetch in Server Components - direct database access, no API needed
- Use Cache Components - explicit caching with
'use cache' - Server Actions - no separate API routes needed for mutations
- Turbopack - faster builds and hot reload by default in v16
- 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
-
https://www.reddit.com/r/nextjs/comments/1k5a1mh/mixing_clientserver_components_best_practices/ ↩
-
https://talent500.com/blog/nextjs-16-release-features-benefits/ ↩
-
https://strapi.io/blog/react-and-nextjs-in-2025-modern-best-practices ↩
-
https://nextjs.org/docs/app/getting-started/server-and-client-components ↩