TypeScript Bootcamp · Enterprise Fullstack

React + Next.js Mastery Bootcamp

From zero to production-ready fullstack developer. Master React 18, Next.js 14, TypeScript, Tailwind, Framer Motion, authentication, databases, and real-world deployment.

React 18 Next.js 14 TypeScript Tailwind + ShadCN
CH
01
Introduction
React, Next.js, TypeScript — what, why, and when
What is React.js and Next.js?

React.js is a JavaScript UI library built by Facebook (Meta) for building component-based user interfaces. It renders declaratively — you describe what the UI should look like, React handles how to make it happen.

Next.js is a production framework built on React that adds server-side rendering, file-based routing, API routes, image optimisation, and automatic code splitting — turning React into a full-stack platform.

React.js

UI library · Component model · Virtual DOM · Client-side rendering · Hooks API · Ecosystem (Router, Redux, etc.)

Next.js

Full-stack framework · SSR/SSG/ISR · File-based routing · API routes · Edge functions · Vercel deployment

Browser
React renders UI
components
Next.js Server
SSR / SSG / ISR
API Routes
Database / API
Prisma / REST /
GraphQL
Why TypeScript with React/Next.js?

TypeScript adds static types to JavaScript. In React projects, this means catching prop mismatches, undefined errors, and API shape mismatches at compile time — before users ever see a bug.

Catch Errors Early

Type errors appear as you type. No more "cannot read property of undefined" at runtime.

Better DX

Full IntelliSense, auto-complete for props, and instant docs inline in VS Code.

Self-Documenting

Interfaces serve as live documentation. New developers understand component contracts instantly.

SPA vs SSR vs SSG
ApproachHow It WorksBest ForSEO
SPA (React)JS downloaded, renders in browserDashboards, appsPoor (needs extras)
SSR (Next.js)HTML generated per-request on serverDynamic content, e-commerceExcellent
SSG (Next.js)HTML generated at build timeBlogs, docs, marketingExcellent
ISR (Next.js)SSG + background revalidationDynamic data, large sitesExcellent
💡 Rule of Thumb
Use SSG for content that doesn't change often. Use SSR for user-specific or real-time data. Use ISR to get the best of both worlds — fast static pages that stay fresh.
CH
02
Environment Setup
Node.js, Vite, Next.js project scaffolding, ESLint & Prettier
Node.js & npm Installation
  • 1Install Node.js LTS: Download from nodejs.org (v20+). npm comes bundled. Verify: node --version && npm --version
  • 2Install VS Code: With extensions — ESLint, Prettier, TypeScript + Tailwind CSS IntelliSense.
  • 3Optional: install pnpm for faster installs: npm install -g pnpm
React + Vite + TypeScript
terminal
bash
# Create React + TypeScript app with Vite (recommended over CRA)
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev
project structure
tree
my-app/
├── src/
│   ├── components/         # reusable components
│   │   └── Button/
│   │       ├── Button.tsx
│   │       └── Button.test.tsx
│   ├── hooks/              # custom hooks
│   ├── pages/              # page components (if using React Router)
│   ├── types/              # shared TypeScript interfaces
│   ├── utils/              # helper functions
│   ├── services/           # API calls
│   ├── store/              # global state
│   ├── App.tsx
│   └── main.tsx
├── public/
├── tsconfig.json
├── vite.config.ts
└── package.json
Next.js Project with TypeScript
terminal
bash
# Create Next.js app — TypeScript, Tailwind, App Router
npx create-next-app@latest my-next-app \
  --typescript \
  --tailwind \
  --eslint \
  --app \
  --src-dir \
  --import-alias "@/*"

cd my-next-app && npm run dev
Next.js App Router structure
tree
src/
├── app/
│   ├── layout.tsx          # root layout (html + body)
│   ├── page.tsx            # / route
│   ├── globals.css
│   ├── about/
│   │   └── page.tsx        # /about route
│   ├── blog/
│   │   ├── page.tsx        # /blog
│   │   └── [slug]/
│   │       └── page.tsx    # /blog/my-post
│   └── api/
│       └── students/
│           └── route.ts    # /api/students endpoint
├── components/
├── lib/                    # db, auth, utils
└── types/
ESLint, Prettier & tsconfig
.prettierrc
json
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 100,
  "plugins": ["prettier-plugin-tailwindcss"]
}
tsconfig.json — strict settings
json
{
  "compilerOptions": {
    "target": "ES2017",
    "strict": true,             // enables all strict checks
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "paths": { "@/*": ["./src/*"] }
  }
}
CH
03
React Fundamentals
JSX, components, props, state, hooks, events, and forms
JSX Syntax

JSX is a syntax extension that lets you write HTML-like markup inside JavaScript. Babel compiles it to React.createElement() calls. With TypeScript, it's .tsx files.

JsxBasics.tsx
tsx
// JSX Rules:
// 1. Return a single root element (or Fragment <>)
// 2. Self-close empty tags: 
// 3. class → className, for → htmlFor // 4. Expressions in curly braces {}
const name = 'Alice'; const isLoggedIn = true; function WelcomeBanner() { return ( <> {/* Fragment — no extra DOM node */} <h1 className="text-2xl font-bold"> Hello, {name}! {/* JavaScript expression */} </h1> {/* Conditional rendering */} {isLoggedIn ? ( <p>Welcome back</p> ) : ( <p>Please log in</p> )} {/* Inline style is an object */} <div style={{ color: 'cyan', fontSize: 16 }}>Styled</div> </> ); }
Functional Components

React apps are built from components — pure functions that accept props and return JSX. Functional components are the modern standard; class components are legacy.

StudentCard.tsx
tsx
import { FC } from 'react';

// TypeScript interface for props
interface StudentCardProps {
  name:    string;
  grade:   string;
  age:     number;
  active?: boolean; // optional prop
}

// FC = FunctionComponent — typed return + children
const StudentCard: FC<StudentCardProps> = ({
  name, grade, age, active = true  // default value
}) => {
  return (
    <div className=`card ${active ? 'active' : 'inactive'}`>
      <h2>{name}</h2>
      <p>Grade: {grade} · Age: {age}</p>
    </div>
  );
};

export default StudentCard;

// Usage:
// <StudentCard name="Alice" grade="A" age={20} />
// <StudentCard name="Bob" grade="C" age={22} active={false} />
Props & State with useState
Counter.tsx
tsx
import { useState } from 'react';

interface CounterProps {
  initialCount?: number;
  step?:         number;
}

export function Counter({ initialCount = 0, step = 1 }: CounterProps) {
  // useState infers type from initial value: number
  const [count, setCount] = useState<number>(initialCount);
  const [history, setHistory] = useState<number[]>([initialCount]);

  const increment = () => {
    const next = count + step;
    setCount(next);
    // Functional update — safe for async batching
    setHistory(prev => [...prev, next]);
  };

  const reset = () => {
    setCount(initialCount);
    setHistory([initialCount]);
  };

  return (
    <div>
      <p>Count: <strong>{count}</strong></p>
      <button onClick={increment}>+{step}</button>
      <button onClick={reset}>Reset</button>
      <p>History: {history.join(' → ')}</p>
    </div>
  );
}
Event Handling & Lists
StudentList.tsx
tsx
import { useState } from 'react';

interface Student {
  id:    number;
  name:  string;
  grade: string;
}

export function StudentList() {
  const [students, setStudents] = useState<Student[]>([
    { id: 1, name: 'Alice', grade: 'A' },
    { id: 2, name: 'Bob',   grade: 'C' },
  ]);

  // Typed event handler
  const handleDelete = (id: number) => {
    setStudents(prev => prev.filter(s => s.id !== id));
  };

  return (
    <ul>
      {students.map(student => (
        {/* key must be stable and unique */}
        <li key={student.id}>
          {student.name} — {student.grade}
          <button onClick={() => handleDelete(student.id)}>
            Remove
          </button>
        </li>
      ))}
    </ul>
  );
}
Core Hooks — useEffect, useRef & Custom Hooks
hooks/useFetch.ts — Custom Hook
tsx
import { useState, useEffect, useRef } from 'react';

// ── useEffect: side effects (fetch, subscriptions, timers) ──
function UserProfile({ userId }: { userId: number }) {
  const [user, setUser] = useState<User | null>(null);

  useEffect(() => {
    // Runs after render, and when userId changes
    let cancelled = false;  // cleanup race condition

    fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .then(data => { if (!cancelled) setUser(data); });

    return () => { cancelled = true; };  // cleanup
  }, [userId]);  // dependency array

  return <div>{user?.name}</div>;
}

// ── useRef: DOM access + mutable values ──────────────────
function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    inputRef.current?.focus();  // optional chaining for safety
  }, []);

  return <input ref={inputRef} placeholder="Auto-focused!" />;
}

// ── CUSTOM HOOK: encapsulate reusable logic ───────────────
function useFetch<T>(url: string) {
  const [data,    setData   ] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error,   setError  ] = useState<string | null>(null);

  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then(r => { if (!r.ok) throw new Error('Fetch failed'); return r.json(); })
      .then(d => setData(d as T))
      .catch(e => setError(e.message))
      .finally(() => setLoading(false));
  }, [url]);

  return { data, loading, error };
}

// Usage:
// const { data, loading, error } = useFetch<Student[]>('/api/students');
// Fully typed — data is Student[] | null
Forms & Controlled Components
AddStudentForm.tsx
tsx
import { useState } from 'react';

interface FormData {
  name:  string;
  email: string;
  grade: 'A' | 'B' | 'C' | 'D' | 'F';
}

interface Props {
  onSubmit: (data: FormData) => void;
}

export function AddStudentForm({ onSubmit }: Props) {
  const [form, setForm] = useState<FormData>({
    name: '', email: '', grade: 'A'
  });
  const [errors, setErrors] = useState<Partial<FormData>>({});

  // Generic change handler — handles all inputs
  const handleChange = (
    e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
  ) => {
    const { name, value } = e.target;
    setForm(prev => ({ ...prev, [name]: value }));
    setErrors(prev => ({ ...prev, [name]: undefined }));
  };

  const validate = (): boolean => {
    const e: Partial<FormData> = {};
    if (!form.name.trim())  e.name  = 'Name is required';
    if (!form.email.trim()) e.email = 'Email is required';
    setErrors(e);
    return Object.keys(e).length === 0;
  };

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (validate()) onSubmit(form);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="name" value={form.name}
        onChange={handleChange}
        placeholder="Student name"
      />
      {errors.name && <span className="error">{errors.name}</span>}

      <select name="grade" value={form.grade} onChange={handleChange}>
        {['A', 'B', 'C', 'D', 'F'].map(g => (
          <option key={g} value={g}>{g}</option>
        ))}
      </select>

      <button type="submit">Add Student</button>
    </form>
  );
}
CH
04
TypeScript in React
Typing props, state, events, generics, and utility types
Typing Props, State & Utility Types
types/index.ts — shared interfaces
ts
// ── Domain models ──────────────────────────────────────
export interface Student {
  id:        number;
  name:      string;
  email:     string;
  grade:     'A' | 'B' | 'C' | 'D' | 'F';
  createdAt: string;    // ISO date string from API
  active?:   boolean;
}

// ── API response wrapper ───────────────────────────────
export interface ApiResponse<T> {
  data:    T;
  message: string;
  success: boolean;
}

// ── Utility type usage ────────────────────────────────
type CreateStudentInput = Omit<Student, 'id' | 'createdAt'>;
type UpdateStudentInput = Partial<CreateStudentInput>;
type StudentPreview    = Pick<Student, 'id' | 'name' | 'grade'>;
type GradeType         = Student['grade'];  // 'A' | 'B' | 'C' | 'D' | 'F'

// ── Component prop patterns ───────────────────────────
interface WithChildren {
  children: React.ReactNode;
}

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant: 'primary' | 'secondary' | 'danger';
  loading?: boolean;
}

// ── Discriminated unions ──────────────────────────────
type LoadingState<T> =
  | { status: 'idle'    }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error';   error: string };
Generics in Components
components/DataTable.tsx — Generic Component
tsx
import { ReactNode } from 'react';

// Generic component — works with any data shape
interface Column<T> {
  key:      keyof T;
  header:   string;
  render?:  (value: T[keyof T], row: T) => ReactNode;
}

interface DataTableProps<T extends { id: number | string }> {
  data:    T[];
  columns: Column<T>[];
  onRowClick?: (row: T) => void;
}

function DataTable<T extends { id: number | string }>({
  data, columns, onRowClick
}: DataTableProps<T>) {
  return (
    <table>
      <thead><tr>
        {columns.map(col => (
          <th key={String(col.key)}>{col.header}</th>
        ))}
      </tr></thead>
      <tbody>
        {data.map(row => (
          <tr key={row.id} onClick={() => onRowClick?.(row)}>
            {columns.map(col => (
              <td key={String(col.key)}>
                {col.render
                  ? col.render(row[col.key], row)
                  : String(row[col.key])}
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

// Usage — fully typed, no any!
// <DataTable<Student> data={students} columns={[
//   { key: 'name', header: 'Name' },
//   { key: 'grade', header: 'Grade', render: (v) => <Badge>{v}</Badge> }
// ]} />
Typing Event Handlers
EventTypes.tsx
tsx
import type {
  ChangeEvent, FormEvent, MouseEvent, KeyboardEvent
} from 'react';

// Event types follow: React.{EventType}<{HTMLElement}>
const onInput  = (e: ChangeEvent<HTMLInputElement>)   => e.target.value;
const onSelect = (e: ChangeEvent<HTMLSelectElement>) => e.target.value;
const onSubmit = (e: FormEvent<HTMLFormElement>)      => e.preventDefault();
const onClick  = (e: MouseEvent<HTMLButtonElement>)   => e.target;
const onKey    = (e: KeyboardEvent<HTMLInputElement>) => e.key;

// File input
const onFileChange = (e: ChangeEvent<HTMLInputElement>) => {
  const file = e.target.files?.[0];  // File | undefined
  if (file) console.log(file.name);
};
CH
05
Advanced React
Context API, React Router, useReducer, memoization, and lazy loading
Context API — Global State Without Redux
context/AuthContext.tsx
tsx
import { createContext, useContext, useState, ReactNode } from 'react';

interface User { id: number; name: string; role: 'admin' | 'student'; }

interface AuthContextType {
  user:     User | null;
  login:    (user: User) => void;
  logout:   () => void;
  isAdmin:  boolean;
}

// Pattern: initialise with null, guard with custom hook
const AuthContext = createContext<AuthContextType | null>(null);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const login  = (u: User) => setUser(u);
  const logout = () => setUser(null);

  return (
    <AuthContext.Provider value={{ user, login, logout, isAdmin: user?.role === 'admin' }}>
      {children}
    </AuthContext.Provider>
  );
}

// Custom hook with null guard — clean usage everywhere
export function useAuth(): AuthContextType {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be inside AuthProvider');
  return ctx;
}

// Usage:
// const { user, login, logout, isAdmin } = useAuth();
React Router v6
App.tsx — routing setup
tsx
import { BrowserRouter, Routes, Route, Navigate, useParams, Link } from 'react-router-dom';
import { useAuth } from './context/AuthContext';

// Protected route component
function PrivateRoute({ children }: { children: JSX.Element }) {
  const { user } = useAuth();
  return user ? children : <Navigate to="/login" replace />;
}

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/"         element={<Home />} />
        <Route path="/login"    element={<Login />} />
        <Route path="/students/:id" element={<StudentDetail />} />
        <Route
          path="/dashboard"
          element={<PrivateRoute><Dashboard /></PrivateRoute>}
        />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

// useParams — typed URL params
function StudentDetail() {
  const { id } = useParams<{ id: string }>();
  const { data } = useFetch<Student>(`/api/students/${id}`);
  return <div>{data?.name}</div>;
}
Performance — memo, useCallback, useMemo, Suspense
Performance.tsx
tsx
import { memo, useCallback, useMemo, lazy, Suspense } from 'react';

// React.memo — skip re-render if props haven't changed
const StudentRow = memo(({ student, onDelete }:
  { student: Student; onDelete: (id: number) => void }) => {
  console.log('render', student.id);
  return <tr><td>{student.name}</td></tr>;
});

function StudentTable({ students }: { students: Student[] }) {
  // useCallback — stable function reference (prevents child re-renders)
  const handleDelete = useCallback((id: number) => {
    console.log('delete', id);
  }, []);  // empty deps = never recreated

  // useMemo — expensive computation, only re-runs when students changes
  const stats = useMemo(() => ({
    total:   students.length,
    passing: students.filter(s => !['D','F'].includes(s.grade)).length,
    topGrade: students.filter(s => s.grade === 'A').length,
  }), [students]);

  return (
    <>
      <p>{stats.passing}/{stats.total} passing</p>
      {students.map(s => <StudentRow key={s.id} student={s} onDelete={handleDelete} />)}
    </>
  );
}

// Lazy loading — bundle split at component boundary
const HeavyChart = lazy(() => import('./HeavyChart'));

function Dashboard() {
  return (
    <Suspense fallback={<div>Loading chart...</div>}>
      <HeavyChart />  {/* loads only when Dashboard renders */}
    </Suspense>
  );
}
CH
06
Next.js Fundamentals
App Router, file-based routing, SSR/SSG/ISR, API routes, layouts
File-Based Routing & Layouts
app/layout.tsx — Root Layout
tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';

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

// Metadata API — auto sets <head> tags
export const metadata: Metadata = {
  title:       { default: 'School App', template: '%s | School App' },
  description: 'Student management system',
  openGraph:   { type: 'website', siteName: 'School App' },
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <Navbar />
        <main>{children}</main>
        <Footer />
      </body>
    </html>
  );
}
Server Components vs Client Components
🔥 Next.js App Router Default
All components are Server Components by default in the App Router. Add 'use client' only when you need interactivity (useState, useEffect, event handlers).
app/students/page.tsx — Server Component
tsx
// No 'use client' — runs on the SERVER
// Can: directly query DB, read env vars, access filesystem
// Cannot: useState, useEffect, event handlers

import { db } from '@/lib/db';  // direct DB access!
import { StudentCard } from '@/components/StudentCard';

async function getStudents() {
  // This runs on the server — never exposed to client
  const students = await db.query('SELECT * FROM students ORDER BY name');
  return students;
}

export default async function StudentsPage() {
  const students = await getStudents();  // await in component!

  return (
    <div>
      <h1>All Students ({students.length})</h1>
      {students.map(s => <StudentCard key={s.id} {...s} />)}
    </div>
  );
}
components/LikeButton.tsx — Client Component
tsx
'use client';  // only needed for interactive components

import { useState } from 'react';

export function LikeButton({ initialLikes }: { initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes);
  return (
    <button onClick={() => setLikes(l => l + 1)}>
      ❤️ {likes}
    </button>
  );
}
API Routes (Route Handlers)
app/api/students/route.ts — REST endpoint
ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { z } from 'zod';

const StudentSchema = z.object({
  name:  z.string().min(2),
  email: z.string().email(),
  grade: z.enum(['A', 'B', 'C', 'D', 'F']),
});

// GET /api/students
export async function GET(request: NextRequest) {
  try {
    const { searchParams } = new URL(request.url);
    const grade = searchParams.get('grade');

    const students = grade
      ? await db.query('SELECT * FROM students WHERE grade = ?', [grade])
      : await db.query('SELECT * FROM students');

    return NextResponse.json({ data: students, success: true });
  } catch {
    return NextResponse.json({ error: 'Server error' }, { status: 500 });
  }
}

// POST /api/students
export async function POST(request: NextRequest) {
  const body = await request.json();
  const parsed = StudentSchema.safeParse(body);

  if (!parsed.success) {
    return NextResponse.json(
      { error: parsed.error.flatten() }, { status: 400 }
    );
  }

  const student = await db.query(
    'INSERT INTO students (name, email, grade) VALUES (?, ?, ?)',
    [parsed.data.name, parsed.data.email, parsed.data.grade]
  );
  return NextResponse.json({ data: student, success: true }, { status: 201 });
}
Dynamic Routes & generateStaticParams
app/students/[id]/page.tsx
tsx
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';

interface Props { params: { id: string } }

// Dynamic metadata per page
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const student = await getStudent(params.id);
  return { title: student?.name ?? 'Student Not Found' };
}

// Pre-generate static pages at build time
export async function generateStaticParams() {
  const students = await getAllStudents();
  return students.map(s => ({ id: String(s.id) }));
}

export default async function StudentPage({ params }: Props) {
  const student = await getStudent(params.id);

  if (!student) notFound();  // renders not-found.tsx

  return (
    <article>
      <h1>{student.name}</h1>
      <p>Grade: {student.grade}</p>
    </article>
  );
}
CH
07
Data Fetching
Server fetch, SWR, React Query, caching and revalidation
Static Data Fetching with Caching
app/blog/page.tsx — Static Generation
tsx
// Next.js 14 App Router — fetch() is extended with caching

async function getPosts() {
  const res = await fetch('https://api.blog.com/posts', {
    next: { revalidate: 3600 }  // ISR: revalidate every 1 hour
    // cache: 'force-cache'      // SSG: cache forever (default)
    // cache: 'no-store'         // SSR: never cache
  });
  if (!res.ok) throw new Error('Failed to fetch posts');
  return res.json() as Promise<Post[]>;
}

export default async function BlogPage() {
  const posts = await getPosts();
  return (
    <div>
      {posts.map(post => (
        <article key={post.slug}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    </div>
  );
}
Server-Side & Parallel Fetching
app/dashboard/page.tsx — Parallel SSR
tsx
// Parallel data fetching — both requests fire simultaneously
export default async function DashboardPage() {
  const [students, courses, stats] = await Promise.all([
    getStudents(),   // fires immediately
    getCourses(),    // fires immediately
    getStats(),      // fires immediately
  ]);

  return (
    <div className="grid grid-cols-3 gap-4">
      <StatCard data={stats} />
      <StudentList students={students} />
      <CourseList  courses={courses} />
    </div>
  );
}
Client-Side Fetching with SWR & React Query
hooks/useStudents.ts — React Query
tsx
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/axios';

// Query key factory — prevents typos
const studentsKeys = {
  all:    ['students'] as const,
  detail: (id: number) => ['students', id] as const,
};

export function useStudents() {
  return useQuery({
    queryKey: studentsKeys.all,
    queryFn:  () => api.get<Student[]>('/students').then(r => r.data),
    staleTime: 60_000,  // 1 minute
  });
}

export function useCreateStudent() {
  const qc = useQueryClient();

  return useMutation({
    mutationFn: (data: CreateStudentInput) =>
      api.post<Student>('/students', data).then(r => r.data),

    onSuccess: () => {
      // Invalidate and refetch students list
      qc.invalidateQueries({ queryKey: studentsKeys.all });
    },
  });
}

// Usage in component:
// const { data: students, isLoading, error } = useStudents();
// const { mutate: create, isPending } = useCreateStudent();
// create({ name: 'Alice', email: '...', grade: 'A' });
CH
08
State Management
Context + useReducer, Redux Toolkit, and server state patterns
Context + useReducer — Scalable Local State
store/studentsReducer.ts
ts
// Discriminated union for type-safe actions
type Action =
  | { type: 'ADD_STUDENT';    payload: Student      }
  | { type: 'DELETE_STUDENT'; payload: number       }
  | { type: 'UPDATE_GRADE';   payload: { id: number; grade: Student['grade'] } }
  | { type: 'SET_LOADING';    payload: boolean     };

interface State {
  students: Student[];
  loading:  boolean;
}

const initialState: State = { students: [], loading: false };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'ADD_STUDENT':
      return { ...state, students: [...state.students, action.payload] };

    case 'DELETE_STUDENT':
      return { ...state, students: state.students.filter(s => s.id !== action.payload) };

    case 'UPDATE_GRADE':
      return {
        ...state,
        students: state.students.map(s =>
          s.id === action.payload.id ? { ...s, grade: action.payload.grade } : s
        )
      };

    case 'SET_LOADING':
      return { ...state, loading: action.payload };

    default:
      return state;
  }
}
Redux Toolkit — For Large-Scale Apps
store/studentSlice.ts — Redux Toolkit
ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { api } from '@/lib/axios';

// Async thunk — handles loading/success/error automatically
export const fetchStudents = createAsyncThunk(
  'students/fetchAll',
  async (_, { rejectWithValue }) => {
    try {
      const { data } = await api.get<Student[]>('/students');
      return data;
    } catch (err) {
      return rejectWithValue('Failed to fetch');
    }
  }
);

const studentsSlice = createSlice({
  name: 'students',
  initialState: { list: [] as Student[], status: 'idle' as string },
  reducers: {
    addStudent: (state, action: PayloadAction<Student>) => {
      state.list.push(action.payload);   // Immer allows mutation!
    },
    removeStudent: (state, action: PayloadAction<number>) => {
      state.list = state.list.filter(s => s.id !== action.payload);
    },
  },
  extraReducers: builder => {
    builder
      .addCase(fetchStudents.pending,   state => { state.status = 'loading'; })
      .addCase(fetchStudents.fulfilled, (state, { payload }) => {
        state.status = 'success';
        state.list   = payload;
      })
      .addCase(fetchStudents.rejected, state => { state.status = 'error'; });
  },
});

export const { addStudent, removeStudent } = studentsSlice.actions;
export default studentsSlice.reducer;
CH
09
Styling — Tailwind, ShadCN & Framer Motion
Modern CSS, utility-first workflow, design system components, animations
Tailwind CSS + ShadCN/UI
components/ui/Button.tsx — ShadCN-style
tsx
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';  // clsx + tailwind-merge
import { ButtonHTMLAttributes, forwardRef } from 'react';

// cva — class variance authority for typed variant props
const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none disabled:opacity-50',
  {
    variants: {
      variant: {
        primary:   'bg-blue-600 text-white hover:bg-blue-700',
        secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
        danger:    'bg-red-600  text-white hover:bg-red-700',
        ghost:     'hover:bg-gray-100',
      },
      size: {
        sm: 'h-8 px-3 text-xs',
        md: 'h-10 px-4',
        lg: 'h-12 px-6 text-base',
      },
    },
    defaultVariants: { variant: 'primary', size: 'md' },
  }
);

interface ButtonProps
  extends ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  loading?: boolean;
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, loading, children, ...props }, ref) => (
    <button
      ref={ref}
      className={cn(buttonVariants({ variant, size }), className)}
      disabled={loading || props.disabled}
      {...props}
    >
      {loading ? <Spinner /> : children}
    </button>
  )
);
Button.displayName = 'Button';
Framer Motion Animations
components/AnimatedList.tsx
tsx
'use client';
import { motion, AnimatePresence } from 'framer-motion';

// Page transition variants
const pageVariants = {
  initial:  { opacity: 0, y: 20 },
  animate:  { opacity: 1, y: 0  },
  exit:     { opacity: 0, y: -20 },
};

// Staggered list animation
const containerVariants = {
  animate: { transition: { staggerChildren: 0.08 } }
};
const itemVariants = {
  initial: { opacity: 0, x: -20 },
  animate: { opacity: 1, x: 0   },
  exit:    { opacity: 0, x: 20  },
};

export function AnimatedStudentList({ students }: { students: Student[] }) {
  return (
    <motion.div
      variants={pageVariants}
      initial="initial"
      animate="animate"
      exit="exit"
      transition={{ duration: 0.3, ease: 'easeOut' }}
    >
      <motion.ul variants={containerVariants} animate="animate">
        <AnimatePresence mode="popLayout">
          {students.map(student => (
            <motion.li
              key={student.id}
              variants={itemVariants}
              layout             {/* animate position changes */}
              whileHover={{ scale: 1.02, x: 4 }}
              whileTap={{ scale: 0.98 }}
            >
              {student.name}
            </motion.li>
          ))}
        </AnimatePresence>
      </motion.ul>
    </motion.div>
  );
}
CH
10
Auth & API Integration
JWT authentication, Axios interceptors, protected routes, cookies
JWT Authentication with Next.js
app/api/auth/login/route.ts
ts
import { NextRequest, NextResponse } from 'next/server';
import { SignJWT } from 'jose';
import bcrypt from 'bcryptjs';

const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET!);

export async function POST(req: NextRequest) {
  const { email, password } = await req.json();

  const user = await db.users.findUnique({ where: { email } });
  if (!user) return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });

  const valid = await bcrypt.compare(password, user.passwordHash);
  if (!valid) return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });

  const token = await new SignJWT({ id: user.id, role: user.role })
    .setProtectedHeader({ alg: 'HS256' })
    .setExpirationTime('7d')
    .sign(JWT_SECRET);

  const response = NextResponse.json({ user: { id: user.id, role: user.role } });

  // HttpOnly cookie — not accessible via JS (XSS safe)
  response.cookies.set('auth-token', token, {
    httpOnly: true, secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax', maxAge: 60 * 60 * 24 * 7,
    path: '/',
  });

  return response;
}
Axios Instance with Interceptors
lib/axios.ts
ts
import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios';

export const api = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL ?? '/api',
  timeout: 10_000,
  headers: { 'Content-Type': 'application/json' },
});

// Request interceptor — attach auth token
api.interceptors.request.use((config: InternalAxiosRequestConfig) => {
  const token = typeof window !== 'undefined'
    ? localStorage.getItem('token') : null;

  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// Response interceptor — handle 401 globally
api.interceptors.response.use(
  response => response,
  (async (error: AxiosError) => {
    if (error.response?.status === 401) {
      localStorage.removeItem('token');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  })
);
CH
11
Testing
Jest, React Testing Library, and type-safe test patterns
Jest + React Testing Library
StudentCard.test.tsx
tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { StudentCard } from './StudentCard';
import { vi } from 'vitest';

const mockStudent: Student = {
  id: 1, name: 'Alice Mugisha',
  email: 'alice@school.rw', grade: 'A', createdAt: '2024-01-01'
};

describe('StudentCard', () => {
  it('renders student name and grade', () => {
    render(<StudentCard student={mockStudent} />);

    expect(screen.getByText('Alice Mugisha')).toBeInTheDocument();
    expect(screen.getByText('A')).toBeInTheDocument();
  });

  it('calls onDelete when delete button clicked', () => {
    const onDelete = vi.fn();
    render(<StudentCard student={mockStudent} onDelete={onDelete} />);

    fireEvent.click(screen.getByRole('button', { name: /delete/i }));

    expect(onDelete).toHaveBeenCalledWith(mockStudent.id);
    expect(onDelete).toHaveBeenCalledTimes(1);
  });

  it('does not render delete button when onDelete not provided', () => {
    render(<StudentCard student={mockStudent} />);
    expect(screen.queryByRole('button', { name: /delete/i })).not.toBeInTheDocument();
  });

  it('shows loading state', async () => {
    render(<StudentCard student={mockStudent} loading />);
    expect(screen.getByTestId('loading-skeleton')).toBeInTheDocument();
  });
});
CH
12
Performance & Deployment
Image optimization, code splitting, SEO, Vercel, Docker
Image Optimization & Code Splitting
components/OptimizedImage.tsx
tsx
import Image from 'next/image';

// next/image: lazy loading, WebP conversion, responsive sizes
function StudentAvatar({ name, src }: { name: string; src: string }) {
  return (
    <div className="relative w-16 h-16">
      <Image
        src={src}
        alt={name}
        fill               {/* fills container */}
        className="object-cover rounded-full"
        sizes="64px"        {/* prevents layout shift */}
        priority={false}   {/* lazy load (default) */}
        placeholder="blur" {/* show blurred placeholder */}
      />
    </div>
  );
}
SEO, Meta Tags & Structured Data
app/blog/[slug]/page.tsx — Full SEO
tsx
import type { Metadata } from 'next';

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const post = await getPost(params.slug);

  return {
    title:       post.title,
    description: post.excerpt,
    authors:     [{ name: post.author }],
    openGraph: {
      type:        'article',
      title:       post.title,
      description: post.excerpt,
      images:      [{ url: post.coverImage, width: 1200, height: 630 }],
      publishedTime: post.publishedAt,
    },
    twitter: {
      card:        'summary_large_image',
      title:       post.title,
      description: post.excerpt,
      images:      [post.coverImage],
    },
    alternates: { canonical: `https://myblog.com/blog/${post.slug}` },
  };
}
Deployment — Vercel & Docker
Dockerfile — production build
docker
# Stage 1: Install dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Stage 2: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# Stage 3: Production image
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

EXPOSE 3000
CMD ["node", "server.js"]
🚀 Vercel Deployment
Push to GitHub → connect repo on vercel.com → done. Vercel auto-deploys every push, handles CDN, edge functions, and analytics out of the box. Add environment variables in the dashboard.
CH
13
Projects — Hands-On Builds
Three projects from beginner to enterprise fullstack
Project Overview
Beginner
Portfolio Website

Static Next.js site with Framer Motion animations, contact form, and Vercel deployment. Dark/light mode toggle.

Intermediate
Blog Platform

SSG + ISR blog with MDX, Prisma + MySQL, tag filtering, pagination, RSS feed, and full SEO setup.

Advanced
E-Commerce App

Full-stack store with auth, Stripe payments, cart management, order tracking, admin dashboard, and Redis caching.

Advanced Project — E-Commerce Structure
E-commerce project structure
tree
ecommerce/
├── src/
│   ├── app/
│   │   ├── (shop)/                 # Route group — no layout prefix
│   │   │   ├── page.tsx            # / homepage
│   │   │   ├── products/
│   │   │   │   ├── page.tsx        # /products — SSG list
│   │   │   │   └── [slug]/
│   │   │   │       └── page.tsx    # /products/[slug] — ISR detail
│   │   │   └── cart/page.tsx
│   │   ├── (auth)/                 # Route group with auth layout
│   │   │   ├── login/page.tsx
│   │   │   └── register/page.tsx
│   │   ├── dashboard/              # Admin — protected
│   │   │   ├── layout.tsx          # Admin sidebar layout
│   │   │   └── orders/page.tsx
│   │   └── api/
│   │       ├── auth/[...nextauth]/route.ts   # NextAuth.js
│   │       ├── products/route.ts
│   │       ├── orders/route.ts
│   │       └── webhooks/stripe/route.ts
│   ├── components/
│   │   ├── ui/           # Button, Input, Card, Badge — ShadCN
│   │   ├── shop/         # ProductCard, CartItem, Checkout
│   │   └── layout/       # Navbar, Footer, Sidebar
│   ├── lib/
│   │   ├── db.ts         # Prisma client
│   │   ├── stripe.ts     # Stripe SDK
│   │   ├── redis.ts      # Upstash Redis for caching
│   │   └── validations/  # Zod schemas
│   ├── store/
│   │   ├── cartStore.ts  # Zustand for cart
│   │   └── wishlist.ts
│   └── types/
│       └── index.ts
├── prisma/
│   ├── schema.prisma
│   └── seed.ts
└── middleware.ts           # Auth middleware for protected routes
middleware.ts — Route Protection
ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { jwtVerify } from 'jose';

const PROTECTED = ['/dashboard', '/orders', '/profile'];
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET!);

export async function middleware(req: NextRequest) {
  const isProtected = PROTECTED.some(p => req.nextUrl.pathname.startsWith(p));
  if (!isProtected) return NextResponse.next();

  const token = req.cookies.get('auth-token')?.value;

  if (!token) {
    const url = new URL('/login', req.url);
    url.searchParams.set('from', req.nextUrl.pathname);
    return NextResponse.redirect(url);
  }

  try {
    await jwtVerify(token, JWT_SECRET);
    return NextResponse.next();
  } catch {
    return NextResponse.redirect(new URL('/login', req.url));
  }
}

export const config = {
  matcher: ['/dashboard/:path*', '/orders/:path*', '/profile/:path*'],
};
✅ You're Production-Ready When You Can:
Build a Next.js app router project · Write fully typed components · Implement JWT auth with HttpOnly cookies · Fetch data with React Query · Protect routes with middleware · Deploy to Vercel with environment variables · Write unit tests for components · Optimise images and Core Web Vitals