Next.js Complete Beginner's Guide
Free Next.js guide: App Router, Server Components, SEO, data fetching, auth, deployment, and interview prep — build LearnHub step by step.
On this page
Next.js Tutorial (LearnHub) · Start Here
Next.js Complete Beginner's Guide
App Router · Server Components · SEO · Data fetching · Production patterns · Interview prep · ~50 min read
Welcome to LearnHub: Your Next.js Journey Starts Here
Picture this: Toolliyo Academy just hired you to build LearnHub, an online learning platform where students browse courses, watch lessons, take quizzes, and earn certificates. The product team wants pages that load in under two seconds on a budget Android phone, show up on Google when someone searches "React course online," and feel as smooth as Netflix when switching between lessons.
You could wire up React, React Router, Webpack, a Node server, SEO helpers, and image pipelines yourself. Or you could reach for Next.js 15 — a React framework that ships routing, server rendering, caching, and deployment as first-class citizens.
This guide is your single roadmap. We will build LearnHub step by step, from your first app/page.tsx to enterprise-grade enrollment flows with authentication and Server Actions. Every example uses TypeScript and the App Router — the modern way to write Next.js in 2025 and beyond.
Learning Objectives
By the end of this tutorial, you will be able to:
- Explain why Next.js exists and how it differs from a plain React SPA
- Scaffold a Next.js 15 project and navigate the App Router folder structure
- Build Server Components, Client Components, layouts, and dynamic routes for an LMS
- Fetch data using SSR, SSG, ISR, and Server Actions with clear trade-offs
- Implement authentication, protected routes, and form handling for course enrollment
- Optimize performance, SEO, and security for production LearnHub deployments
- Avoid common mistakes that break hydration, caching, or auth flows
- Answer interview questions from fresher to architect level with confidence
Figuring out why each piece exists matters more than memorizing syntax. When you understand the problem, the API names start to make sense on their own.
Prerequisites
Before you start, you should be comfortable with:
| Skill | Why LearnHub Needs It |
|---|---|
| HTML & CSS | Course cards, lesson layouts, responsive dashboards |
| JavaScript (ES6+) | Async/await, destructuring, modules |
| React basics | Components, props, state, useEffect, useState |
| TypeScript fundamentals | Types for course data, user roles, API responses |
| Git | Branching while building features |
| Node.js 20 LTS | Running create-next-app, npm scripts, local dev server |
You do not need prior Next.js experience. You do not need to know backend frameworks — we introduce Server Actions and API routes gradually.
Install Node.js from nodejs.org and verify:
node --version # v20.x or v22.x recommended
npm --version # 10.x or higher
The Problem with React SPAs
A classic React Single Page Application (SPA) ships one HTML shell and loads JavaScript in the browser. React then fetches JSON from APIs and paints the UI. This works — until LearnHub scales.
Pain points for an LMS like LearnHub
1. Slow first paint — Students on 3G see a blank screen while a large JS bundle downloads. Course catalog pages feel broken before React even starts. 2. Poor SEO — Search engines struggle with empty HTML shells. "Best TypeScript course" never ranks if Google sees <div id="root"></div> and a script tag. 3. Waterfall data loading — Browser loads JS → JS mounts → JS calls /api/courses → finally shows cards. Three round trips where one would suffice. 4. Auth complexity — Tokens in localStorage, refresh logic, and protected routes are all DIY. One mistake exposes instructor dashboards. 5. No sensible defaults — Routing, code splitting, image optimization, and caching require manual configuration and ongoing maintenance. 6. Bundle bloat — Importing a date library on the catalog page can accidentally ship it to the lesson player unless you split carefully.
Next.js does not replace React. It organizes React so the server can do heavy work before the browser ever asks a question.
Real-World Analogy: A Restaurant, Not a Vending Machine
Think of a React SPA as a vending machine in an empty lobby. You walk in, wait for the machine to boot, press buttons, and hope the snack arrives. If the machine is broken, you stare at a blank panel.
Think of Next.js as a restaurant with a kitchen and a dining room:
- The kitchen (Node.js server) prepares dishes (HTML + data) before guests arrive.
- The menu (routes) is organized by section — appetizers, mains, desserts map to
/courses,/lessons,/profile. - Some dishes are pre-made (static pages cached at build time).
- Some are cooked to order (SSR per request).
- The waiter (hydration) brings interactivity — you can still customize your order at the table (Client Components with state).
LearnHub's course catalog is the daily special board: mostly the same for everyone, refreshed on a schedule. The student dashboard is cooked to order: unique progress, unique certificates. Next.js lets you pick the right kitchen workflow per page.
Alternatively, imagine a library:
- SSG = books printed and shelved ahead of time (course landing pages).
- SSR = librarian fetches your requested book when you ask (personalized dashboard).
- ISR = popular books reprinted every night with updated chapters (catalog with nightly refresh).
- Client Components = the reading lamp at your desk — only you control it (video player controls, quiz timer).
What Is Next.js?
Simple definition
Next.js is a React framework for building full-stack web applications. You write React components in TypeScript. Next.js decides which components run on the server, which run in the browser, how URLs map to files, and how HTML reaches the user.
Technical definition
Next.js 15 extends React 19 with:
- App Router — File-system routing under
app/with nested layouts, loading UI, and error boundaries - React Server Components (RSC) — Components that execute on the server, never shipping their logic to the client bundle
- Server Actions — Type-safe server functions invoked from forms and client code without hand-written REST endpoints
- Built-in optimizations — Image, font, and script optimization; automatic code splitting per route
- Flexible rendering — Static generation, server rendering, incremental static regeneration, and streaming
- Route Handlers — API endpoints as
route.tsfiles alongside pages
LearnHub uses all of these: static marketing pages, dynamic course pages, server-fetched catalogs, and enrollment forms that mutate the database on the server.
Why Next.js Is Needed for LearnHub
| Requirement | Without Next.js | With Next.js |
|---|---|---|
| Fast catalog load | Manual SSR setup | Server Components + caching |
| SEO for course pages | react-helmet hacks | generateMetadata, static HTML |
| Protected instructor area | Custom auth middleware | Middleware + session libraries |
| Enrollment forms | Separate Express API | Server Actions |
| Image thumbnails | Manual <img> tags | next/image with lazy load |
| Deploy to production | Configure Webpack, Node, CDN | next build + Vercel or Docker |
Toolliyo Academy chose Next.js because one codebase serves students, instructors, and admins — with less glue code and fewer moving parts in production.
Real-World Applications of Next.js
| Industry | Example Use | Next.js Feature Used |
|---|---|---|
| EdTech (LearnHub) | Course catalog, lesson player, certificates | SSR, ISR, Server Actions |
| E-commerce | Product pages, checkout | SSG + dynamic cart (Client Components) |
| SaaS dashboards | Analytics, billing | Server Components, Route Handlers |
| Marketing sites | Landing pages, blogs | SSG, Metadata API |
| News & media | Article pages with ads | ISR, streaming |
| Internal tools | HR portals, admin panels | Auth middleware, API routes |
| Documentation | Developer docs (MDX) | Static generation, route groups |
If your app is mostly public content with occasional personalization — like LearnHub — Next.js is a strong default.
How Next.js Works Internally
When a student visits learnhub.toolliyo.com/courses/react-fundamentals, this is the journey:
Step 1: Browser sends request
The browser asks for a URL. DNS resolves to your hosting provider (Vercel, AWS, or a VPS running Node).
Step 2: Next.js server receives request
The Node.js process matches the URL to a route in app/courses/[slug]/page.tsx. Middleware may run first (auth checks, redirects, locale detection).
Step 3: React Server Components render
Server Components fetch course data from PostgreSQL or a CMS. They render to a special RSC payload — a compact stream describing UI structure and serialized data. Sensitive logic (database queries, API keys) never leaves the server.
Step 4: HTML streams to the browser
Next.js sends HTML immediately for the static shell, then streams dynamic sections as they resolve. The student sees the course title and syllabus before the "Related courses" sidebar finishes loading.
Step 5: Hydration attaches interactivity
Client Components — marked with "use client" — download their JavaScript bundles. React hydrates them: attaching event listeners to server-rendered HTML. The "Enroll now" button becomes clickable; the video player initializes.
Step 6: Client-side navigation
When the student clicks another course, Next.js fetches only the RSC payload for that route — not a full page reload. LearnHub feels like an SPA while keeping server rendering benefits.
Rendering Pipeline (Mermaid Diagram)
flowchart LR
A[Browser Request] --> B[Next.js Server]
B --> C{Middleware}
C -->|Pass| D[Match Route in app/]
C -->|Fail| R[Redirect / 401]
D --> E[Server Components]
E --> F[Data Layer DB / API]
F --> E
E --> G[RSC Payload + HTML Stream]
G --> H[Browser]
H --> I[Hydrate Client Components]
I --> J[Interactive LearnHub UI]
J -->|Client Navigation| A
Reading the diagram: Requests enter from the left. Middleware gates protected routes. Server Components talk to your data layer. The response combines HTML and RSC data. Client Components hydrate in the browser. Subsequent navigations loop back without full reloads.
Installation: Creating LearnHub
Open a terminal and scaffold the project:
npx create-next-app@latest learnhub
Recommended prompts for this tutorial:
| Prompt | Choice | Reason |
|---|---|---|
| TypeScript | Yes | Type-safe course models and auth |
| ESLint | Yes | Catch mistakes early |
| Tailwind CSS | Yes | Fast LMS styling |
src/ directory | No | Keeps paths simple for beginners |
| App Router | Yes | Required for Next.js 15 patterns |
| Turbopack | Yes (optional) | Faster local dev |
Import alias @/* | Yes | Clean imports like @/components/CourseCard |
Start the dev server:
cd learnhub
npm run dev
Visit http://localhost:3000. You should see the default Next.js welcome page — soon replaced by LearnHub branding.
Project Structure Explained
After scaffolding, your LearnHub folder looks like this:
learnhub/
├── app/ # App Router — URLs live here
│ ├── layout.tsx # Root layout (navbar, fonts, global CSS)
│ ├── page.tsx # Home page (/)
│ ├── courses/
│ │ ├── page.tsx # /courses
│ │ └── [slug]/
│ │ └── page.tsx # /courses/react-fundamentals
│ ├── dashboard/
│ │ └── page.tsx # /dashboard (protected later)
│ ├── api/ # Optional Route Handlers
│ │ └── health/
│ │ └── route.ts
│ ├── globals.css
│ └── favicon.ico
├── components/ # Reusable UI (CourseCard, Navbar)
├── lib/ # Utilities (db client, auth helpers)
├── public/ # Static files (logo.svg, certificates)
├── types/ # Shared TypeScript interfaces
├── next.config.ts # Next.js configuration
├── package.json
├── tsconfig.json
└── .env.local # Secrets (never commit)
Key rules:
app/page.tsx= route for/app/courses/page.tsx= route for/courses- Folders define URL segments;
page.tsxmakes a route publicly accessible layout.tsxwraps child routes — perfect for LearnHub's persistent sidebarloading.tsxanderror.tsxare optional UX files per route segment
Beginner Example: LearnHub Home Page
Replace app/page.tsx with LearnHub's landing page:
// app/page.tsx
import Link from "next/link";
export default function HomePage() {
return (
<main className="mx-auto max-w-3xl px-4 py-16">
<h1 className="text-4xl font-bold text-slate-900">
Welcome to LearnHub
</h1>
<p className="mt-4 text-lg text-slate-600">
Toolliyo Academy's learning platform — master web development
with hands-on courses.
</p>
<Link
href="/courses"
className="mt-8 inline-block rounded-lg bg-indigo-600 px-6 py-3 text-white"
>
Browse courses
</Link>
</main>
);
}
Line-by-line explanation
Line 1 — import Link from "next/link" Next.js provides a Link component that prefetches routes on hover. Plain <a> tags cause full page reloads; Link enables client-side navigation while keeping SEO-friendly anchor semantics.
Line 3 — export default function HomePage() Every page exports a default React component. The function name is for your readability — Next.js cares about the file location, not the name. This component is a Server Component by default (no "use client" directive).
Lines 5–8 — <main> and heading Semantic HTML helps screen readers and SEO. Tailwind classes style the layout. Because this renders on the server, students see the heading in the first HTML chunk — no waiting for JavaScript.
Lines 9–12 — Description paragraph ' is the HTML entity for an apostrophe in JSX text. The copy is static; no database call needed. Perfect for SSG at build time.
Lines 13–18 — Navigation link href="/courses" maps to app/courses/page.tsx (which you will create next). The button styling is pure presentation. When clicked, Next.js swaps route content without reloading the entire document.
Why this matters: In under 20 lines you have a fast, SEO-friendly home page with client-side navigation ready. No router configuration file. No BrowserRouter wrapper.
Intermediate Example: Course Listing Page
Create app/courses/page.tsx to show LearnHub's catalog with server-fetched data:
// app/courses/page.tsx
import Link from "next/link";
type Course = {
id: string;
slug: string;
title: string;
instructor: string;
durationHours: number;
level: "Beginner" | "Intermediate" | "Advanced";
};
async function getCourses(): Promise<Course[]> {
// In production: await db.course.findMany()
// Simulated delay teaches async Server Component patterns
await new Promise((resolve) => setTimeout(resolve, 100));
return [
{
id: "1",
slug: "react-fundamentals",
title: "React Fundamentals",
instructor: "Priya Sharma",
durationHours: 12,
level: "Beginner",
},
{
id: "2",
slug: "nextjs-mastery",
title: "Next.js Mastery",
instructor: "Arjun Mehta",
durationHours: 18,
level: "Intermediate",
},
{
id: "3",
slug: "typescript-deep-dive",
title: "TypeScript Deep Dive",
instructor: "Sneha Patel",
durationHours: 10,
level: "Advanced",
},
];
}
export default async function CoursesPage() {
const courses = await getCourses();
return (
<main className="mx-auto max-w-4xl px-4 py-12">
<h1 className="text-3xl font-bold">Course Catalog</h1>
<p className="mt-2 text-slate-600">
{courses.length} courses available on LearnHub
</p>
<ul className="mt-8 grid gap-4 sm:grid-cols-2">
{courses.map((course) => (
<li
key={course.id}
className="rounded-xl border border-slate-200 p-6 shadow-sm"
>
<span className="text-xs font-medium uppercase text-indigo-600">
{course.level}
</span>
<h2 className="mt-2 text-xl font-semibold">{course.title}</h2>
<p className="mt-1 text-sm text-slate-500">
{course.instructor} · {course.durationHours}h
</p>
<Link
href={`/courses/${course.slug}`}
className="mt-4 inline-block text-indigo-600 hover:underline"
>
View syllabus →
</Link>
</li>
))}
</ul>
</main>
);
}
Concepts introduced
asyncpage components — Server Components canawaitdata directly. NouseEffectfetch on the client.- TypeScript
Coursetype — Documents shape for API/database migration later. - Dynamic links — `
/courses/${course.slug}maps toapp/courses/[slug]/page.tsx`. - Grid layout — Responsive catalog without a client-side state library.
Add app/courses/[slug]/page.tsx for individual course pages using params:
// app/courses/[slug]/page.tsx
type Props = {
params: Promise<{ slug: string }>;
};
export default async function CourseDetailPage({ params }: Props) {
const { slug } = await params;
// Fetch single course by slug from database
return (
<main className="mx-auto max-w-3xl px-4 py-12">
<h1 className="text-3xl font-bold capitalize">
{slug.replace(/-/g, " ")}
</h1>
<p className="mt-4 text-slate-600">Full syllabus coming from CMS...</p>
</main>
);
}
In Next.js 15, params is a Promise — always await it in async components.
Enterprise Example: Auth + Server Action Enrollment
LearnHub must ensure only logged-in students enroll, and enrollment must write to the database securely. Here is a production-shaped pattern.
Database schema (Prisma example)
// prisma/schema.prisma (conceptual)
model User {
id String @id @default(cuid())
email String @unique
enrollments Enrollment[]
}
model Course {
id String @id @default(cuid())
slug String @unique
title String
enrollments Enrollment[]
}
model Enrollment {
id String @id @default(cuid())
userId String
courseId String
enrolledAt DateTime @default(now())
user User @relation(fields: [userId], references: [id])
course Course @relation(fields: [courseId], references: [id])
@@unique([userId, courseId])
}
Auth helper
// lib/auth.ts
import { cookies } from "next/headers";
export type SessionUser = {
id: string;
email: string;
};
export async function getSessionUser(): Promise<SessionUser | null> {
const cookieStore = await cookies();
const sessionId = cookieStore.get("learnhub_session")?.value;
if (!sessionId) return null;
// Validate session against database or auth provider
// Return null if invalid — never trust cookie value alone
return { id: "user_abc", email: "student@toolliyo.com" };
}
Server Action for enrollment
// app/courses/[slug]/actions.ts
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { getSessionUser } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
export type EnrollState = {
error?: string;
success?: boolean;
};
export async function enrollInCourse(
_prevState: EnrollState,
formData: FormData
): Promise<EnrollState> {
const user = await getSessionUser();
if (!user) {
return { error: "Please sign in to enroll." };
}
const courseId = formData.get("courseId");
if (typeof courseId !== "string" || !courseId) {
return { error: "Invalid course." };
}
try {
await prisma.enrollment.create({
data: { userId: user.id, courseId },
});
} catch {
return { error: "You may already be enrolled." };
}
revalidatePath("/dashboard");
redirect("/dashboard");
}
Client form with useActionState
// components/EnrollButton.tsx
"use client";
import { useActionState } from "react";
import { enrollInCourse, type EnrollState } from "@/app/courses/[slug]/actions";
const initialState: EnrollState = {};
type Props = {
courseId: string;
courseTitle: string;
};
export function EnrollButton({ courseId, courseTitle }: Props) {
const [state, formAction, isPending] = useActionState(
enrollInCourse,
initialState
);
return (
<form action={formAction} className="mt-6">
<input type="hidden" name="courseId" value={courseId} />
<button
type="submit"
disabled={isPending}
className="rounded-lg bg-indigo-600 px-6 py-3 text-white disabled:opacity-50"
>
{isPending ? "Enrolling..." : `Enroll in ${courseTitle}`}
</button>
{state.error && (
<p className="mt-2 text-sm text-red-600" role="alert">
{state.error}
</p>
)}
</form>
);
}
Middleware for protected routes
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const session = request.cookies.get("learnhub_session");
const isProtected = request.nextUrl.pathname.startsWith("/dashboard");
if (isProtected && !session) {
const loginUrl = new URL("/login", request.url);
loginUrl.searchParams.set("callbackUrl", request.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*"],
};
Enterprise takeaways:
"use server"marks functions that run only on the server — safe for database writes- Never trust
courseIdfrom the client without validation revalidatePathrefreshes cached dashboard data after enrollment- Middleware enforces auth before the page renders
- Client Components handle pending UI; Server Actions handle mutations
Folder Organization for a Growing LearnHub
As LearnHub grows, adopt this structure:
learnhub/
├── app/
│ ├── (marketing)/ # Route group — no URL segment
│ │ ├── page.tsx # Landing
│ │ └── about/page.tsx
│ ├── (platform)/ # Authenticated app shell
│ │ ├── layout.tsx # Shared sidebar
│ │ ├── dashboard/
│ │ └── courses/
│ ├── login/
│ └── layout.tsx
├── components/
│ ├── ui/ # Buttons, inputs
│ ├── courses/ # CourseCard, EnrollButton
│ └── layout/ # Navbar, Footer
├── lib/
│ ├── prisma.ts
│ ├── auth.ts
│ └── validators/
├── types/
│ └── course.ts
└── actions/ # Shared Server Actions (optional)
Route groups (marketing) and (platform) organize files without changing URLs. Colocate components near features, but promote to components/ when reused twice.
Data Fetching: SSR, SSG, ISR, and Server Actions
| Strategy | When to Use in LearnHub | How in App Router |
|---|---|---|
| SSG | About page, static syllabus PDFs | Default fetch cached at build |
| SSR | Personalized dashboard | fetch(url, { cache: 'no-store' }) or dynamic page |
| ISR | Course catalog (updates nightly) | revalidate: 3600 in fetch or export const revalidate = 3600 |
| Client fetch | Live quiz leaderboard | Client Component + SWR/React Query |
| Server Actions | Enroll, submit assignment | "use server" functions |
Example ISR catalog fetch:
async function getCourses() {
const res = await fetch("https://api.learnhub.internal/courses", {
next: { revalidate: 3600 }, // refresh every hour
});
if (!res.ok) throw new Error("Failed to load courses");
return res.json();
}
Rule of thumb: Fetch on the server by default. Move to the client only when you need browser APIs, real-time updates, or heavy interactivity.
Routing Deep Dive
| File | Purpose | LearnHub Example |
|---|---|---|
page.tsx | UI for a route | Course detail page |
layout.tsx | Shared wrapper | Dashboard sidebar |
loading.tsx | Suspense fallback | Skeleton while courses load |
error.tsx | Error boundary | "Failed to load lesson" |
not-found.tsx | 404 UI | Unknown course slug |
route.ts | API endpoint | Webhook from payment gateway |
Dynamic segments: [slug], [...catchAll] Parallel routes: @modal for enrollment modal over catalog Intercepting routes: Show lesson preview as overlay from catalog
Navigation:
import Link from "next/link";
import { useRouter } from "next/navigation"; // Client Component only
// Declarative
<Link href="/courses/nextjs-mastery">Next.js Mastery</Link>
// Programmatic (client)
const router = useRouter();
router.push("/dashboard");
State Management Overview
LearnHub rarely needs Redux on day one. Use the right tool per layer:
| State Type | Tool | Example |
|---|---|---|
| Server data | Server Components + cache | Course list |
| Form state | Server Actions + useActionState | Enrollment |
| UI toggles | useState in Client Components | Sidebar open/close |
| Global client state | React Context or Zustand | Video player settings |
| Remote sync | TanStack Query | Live discussion threads |
Principle: If data can live on the server, keep it on the server. Client state is for interaction, not duplication of server truth.
Performance Optimization
1. Server Components by default — Ship less JavaScript to students on mobile. 2. next/image — Automatic WebP/AVIF, lazy loading, size hints for course thumbnails. 3. Dynamic imports — const Chart = dynamic(() => import('./Chart'), { ssr: false }) for heavy admin charts. 4. Streaming with Suspense — Show lesson header while comments load. 5. Font optimization — next/font eliminates layout shift for LearnHub branding. 6. Analyze bundles — @next/bundle-analyzer finds accidental imports. 7. Edge vs Node runtime — Edge for geo redirects; Node for Prisma/database.
import Image from "next/image";
<Image
src="/courses/react-fundamentals.jpg"
alt="React Fundamentals course cover"
width={640}
height={360}
priority={false}
/>
SEO for LearnHub
Search traffic drives enrollments. Next.js Metadata API makes SEO declarative:
// app/courses/[slug]/page.tsx
import type { Metadata } from "next";
type Props = { params: Promise<{ slug: string }> };
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const title = slug.replace(/-/g, " ");
return {
title: `${title} | LearnHub`,
description: `Enroll in ${title} on Toolliyo Academy's LearnHub platform.`,
openGraph: {
title: `${title} | LearnHub`,
type: "website",
},
};
}
Additional SEO practices:
- Semantic HTML (
<main>,<article>,<nav>) - Canonical URLs for duplicate content
sitemap.tsandrobots.tsinapp/- JSON-LD structured data for courses (schema.org
Coursetype) - Fast LCP — hero text in Server Components, optimized images
Authentication Patterns
LearnHub supports multiple auth strategies:
| Approach | Best For | Notes |
|---|---|---|
| Auth.js (NextAuth v5) | Email + OAuth, sessions | @auth/nextjs, database sessions |
| Clerk / Auth0 | Fast MVP, hosted UI | Less backend code |
| Custom JWT + cookies | Full control | Higher security burden |
Core patterns regardless of provider:
- Store sessions in httpOnly cookies, not localStorage
- Validate session on server for every protected Server Action
- Use middleware for route-level guards
- Pass role claims (student, instructor, admin) for authorization inside Server Components
// Example: role check in Server Component
const user = await getSessionUser();
if (!user || user.role !== "instructor") {
notFound(); // or redirect
}
API Routes (Route Handlers)
When you need REST endpoints — webhooks, mobile app backends, third-party integrations — use Route Handlers:
// app/api/courses/route.ts
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
export async function GET() {
const courses = await prisma.course.findMany({
select: { id: true, slug: true, title: true, level: true },
});
return NextResponse.json(courses);
}
export async function POST(request: Request) {
const body = await request.json();
// Validate with Zod, create course, return 201
return NextResponse.json({ ok: true }, { status: 201 });
}
Server Actions vs Route Handlers:
- Server Actions — Forms and mutations from your Next.js UI
- Route Handlers — External clients, webhooks, public APIs
LearnHub uses Server Actions for enrollment and Route Handlers for Stripe payment webhooks.
Database Integration
Typical LearnHub stack:
- PostgreSQL — Relational data (users, courses, enrollments, progress)
- Prisma ORM — Type-safe queries, migrations
- Connection pooling — PgBouncer or Prisma Accelerate in serverless deploys
// lib/prisma.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === "development" ? ["query"] : [],
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
Singleton pattern prevents too many connections during hot reload in development.
Environment variables in .env.local:
DATABASE_URL="postgresql://user:pass@localhost:5432/learnhub"
AUTH_SECRET="generate-a-long-random-string"
Never expose DATABASE_URL to the client. Only import prisma in Server Components, Server Actions, and Route Handlers.
Best Practices
1. Colocate by feature — Keep course components near app/courses/. 2. Validate all inputs — Use Zod on Server Actions and Route Handlers. 3. Prefer composition — Small Server Components wrapping Client Component islands. 4. Explicit caching — Know whether each fetch is static, dynamic, or revalidated. 5. Type everything — Shared types in types/ prevent API drift. 6. Use loading.tsx — Perceived performance matters for LMS retention. 7. Test critical flows — Enrollment, payment, certificate generation. 8. Keep secrets server-side — process.env without NEXT_PUBLIC_ prefix stays private. 9. Progressive enhancement — Forms should work before hydration where possible. 10. Monitor production — Vercel Analytics, Sentry, or OpenTelemetry for LearnHub ops.
Common Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
Adding "use client" to every file | Huge JS bundle, slow TTI | Default to Server Components |
Using useEffect to fetch on mount | Waterfall, SEO gaps | Fetch in Server Component |
| Passing non-serializable props to Client Components | Runtime errors | Pass plain JSON, not functions/classes |
Forgetting await params in Next.js 15 | Stale or undefined slug | const { slug } = await params |
| Mutating data without revalidation | Stale dashboard after enroll | revalidatePath or revalidateTag |
| Storing JWT in localStorage | XSS token theft | httpOnly cookies |
Importing prisma in Client Components | Bundle leak / crash | Server-only imports |
Overusing cache: 'no-store' | Slow every request | ISR where data allows |
Missing key in lists | React reconciliation bugs | Stable key={course.id} |
| Ignoring middleware matcher | Protected routes exposed | Test /dashboard without cookie |
Security Essentials
- CSRF — Server Actions include built-in origin checks; verify for Route Handlers
- XSS — React escapes by default; avoid
dangerouslySetInnerHTMLunless sanitized - SQL injection — Use Prisma parameterized queries, never string-concat SQL
- Authorization — Auth ≠ authorization; check roles on every sensitive action
- Rate limiting — Protect login and enrollment endpoints (Upstash, middleware)
- Security headers — Configure CSP, HSTS in
next.config.ts - Dependency audits —
npm auditin CI pipeline
// next.config.ts — example headers
const nextConfig = {
async headers() {
return [
{
source: "/(.*)",
headers: [
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
],
},
];
},
};
export default nextConfig;
Advantages of Next.js
- Full-stack in one repo — UI, API, and server logic together
- Excellent DX — Fast Refresh, TypeScript, sensible defaults
- SEO and performance — Server rendering without custom infrastructure
- Ecosystem — Vercel deploy, Auth.js, Prisma, Tailwind integrations
- Incremental adoption — Migrate page by page from legacy React
- RSC innovation — Industry direction aligned with React core team
Limitations and When to Choose Something Else
Next.js is not universal. Consider alternatives when:
| Scenario | Better Choice | Why |
|---|---|---|
| Mostly static docs/blog, minimal JS | Astro | Ships zero JS by default |
| Web-standard forms, nested routing focus | Remix | Loaders/actions model, progressive enhancement |
| Mobile app only | React Native | Next.js targets web |
| Real-time collaborative editor | Plain React + WebSocket backend | RSC less suited to CRDT sync |
| Simple internal widget embedded in legacy PHP | Plain React bundle | Full framework overhead unnecessary |
| Edge-only API with no UI | Hono / Express | No React needed |
LearnHub stays on Next.js because it blends marketing pages, authenticated dashboards, and API webhooks in one TypeScript codebase — the framework's sweet spot.
Frequently Asked Questions
1. Do I need to learn React before Next.js?
Yes. Next.js is React with conventions. Understand components, props, and basic hooks first. Toolliyo's React tutorial pairs well with this guide.
2. App Router or Pages Router?
Use App Router for all new projects. Pages Router (pages/) is maintenance mode for legacy apps. LearnHub uses app/ exclusively.
3. Can I deploy Next.js without Vercel?
Absolutely. AWS (Amplify, ECS), Docker, Netlify, Railway, and self-hosted Node all work. Run next build then next start or use standalone output for containers.
4. Are Server Components the same as SSR?
Related but different. SSR is *when* HTML is generated (per request). Server Components are *what* runs on the server (by default, most components). You can statically generate Server Components at build time too.
5. When should I use "use client"?
When you need useState, useEffect, browser APIs (window, localStorage), or event handlers (onClick). Keep the boundary as low in the tree as possible — wrap only the interactive leaf.
6. How does LearnHub handle file uploads (assignment PDFs)?
Use Server Actions with FormData and stream to S3/R2, or presigned URL uploads from a Client Component. Never process large files synchronously without streaming.
7. Is Next.js good for large teams?
Yes, with discipline: feature folders, shared ESLint rules, typed Server Actions, and documented caching strategy. Monorepo tools (Turborepo) help when LearnHub splits into @learnhub/ui packages.
8. Does Next.js replace my backend?
Partially. Server Actions and Route Handlers cover many backend needs. Complex microservices (video transcoding, ML grading) may still live in separate services that LearnHub calls via API.
9. What Node version for Next.js 15?
Node.js 20.9+ minimum. LTS 20 or 22 recommended for LearnHub production.
10. How do I debug Server Components?
Use console.log on the server — output appears in the terminal, not the browser DevTools. React DevTools shows Client Components; server logs are your friend for RSC.
Interview Questions and Answers
Fresher Level
Q: What is Next.js? A: A React framework for production web apps. It adds file-based routing, server rendering, API routes, and optimizations like image handling on top of React.
Q: What is the difference between page.tsx and layout.tsx? A: page.tsx defines unique UI for a URL segment. layout.tsx wraps pages and persists across navigations — ideal for navbars and sidebars that should not remount.
Q: What does "use client" do? A: Marks a component and its imports as Client Components that bundle JavaScript to the browser for interactivity.
Intermediate Level
Q: Explain SSR, SSG, and ISR. A: SSR generates HTML per request. SSG generates at build time. ISR regenerates static pages on a timer after first request — combining static speed with periodic freshness.
Q: How do Server Actions differ from REST API calls? A: Server Actions are typed functions invoked directly from forms or client code. Next.js handles serialization and POST wiring. REST Route Handlers expose HTTP endpoints for any client.
Q: Why must you await params in Next.js 15? A: Dynamic route params are asynchronous to support streaming and partial rendering. Pages receive params: Promise<{ slug: string }>.
Senior Level
Q: How would you design caching for LearnHub's course catalog? A: SSG or ISR for public catalog with revalidate aligned to CMS publish schedule. Tag-based revalidation (revalidateTag) when a course updates. Personalization (enrolled badge) in a small Client Component or separate Server Component fetch with no-store only for user-specific fragments.
Q: How do you prevent unauthorized enrollment in Server Actions? A: Re-authenticate session inside the action, validate input, check authorization server-side, use database constraints (@@unique([userId, courseId])), and never rely on hidden form fields alone.
Q: When would you split LearnHub into micro-frontends? A: When independent teams need separate deploy cycles at scale — e.g., video player team vs billing team. Until then, monolithic Next.js with route groups and modular packages is simpler.
Architect Level
Q: How do RSC and hydration affect your system design? A: RSC shifts data fetching and heavy computation to the server, reducing client bundle size. Hydration cost remains for Client Component islands — architect so interactivity is localized. Caching boundaries (CDN, data cache, full route cache) must be explicit in design docs.
Q: Next.js on serverless vs long-running Node — trade-offs? A: Serverless scales to zero and handles spikes (enrollment day) but cold starts and connection limits hurt database-heavy pages. Long-running Node suits WebSockets and persistent connections. LearnHub might hybrid: static/ISR on CDN, dynamic dashboard on Node with connection pooling.
Q: How would you migrate LearnHub from Pages Router to App Router? A: Incremental — enable app/ alongside pages/, migrate leaf routes first, port data fetching to Server Components, replace getServerSideProps with async pages, move API routes to route.ts, update auth middleware, and run parallel QA before cutover.
Summary
You started with a real problem — building LearnHub for Toolliyo Academy — and saw why plain React SPAs struggle with speed, SEO, and security. Next.js 15 with the App Router gives you Server Components, flexible rendering strategies, and Server Actions in one TypeScript codebase.
You learned the request path from browser to server to RSC payload to hydration. You scaffolded a project, built a home page, a course catalog, and an enterprise enrollment flow with auth middleware. You explored routing, caching, performance, SEO, databases, and security — plus when Remix, Astro, or plain React might be better fits.
The pattern repeats throughout your career: why before how, server first, client only when necessary, and validate every mutation on the server.
What's Next on Your LearnHub Journey
1. Complete the Toolliyo Next.js 100-article track — Each lesson deepens one concept from this master guide. 2. Build the lesson player — Client Component video controls + Server Component syllabus sidebar. 3. Add Auth.js — Google login for students, role-based instructor dashboard. 4. Connect Prisma + PostgreSQL — Replace mock getCourses with real data. 5. Deploy to Vercel — Preview URLs for every pull request. 6. Add E2E tests — Playwright flow: browse → enroll → dashboard. 7. Study caching deeply — Read Next.js docs on fetch cache, unstable_cache, and tags. 8. Explore parallel routes — Enrollment modal without losing catalog scroll position.
Open your terminal, run npx create-next-app@latest learnhub, and replace the default page with LearnHub's welcome screen. The catalog, dashboard, and certificates are waiting — and now you have the map to build them.
*Toolliyo Academy — LearnHub Project Thread · Next.js 15 · App Router · TypeScript*
Ready for hands-on practice?
Start Lesson 1 and build LearnHub step by step — one concept per lesson with TypeScript you run locally.
Start Lesson 1 — Introduction to Next.js View full course syllabus