Full-Stack Bootcamp

React & TypeScript
Next.js Mastery

A production-grade bootcamp covering React fundamentals, TypeScript typing, Next.js SSR/SSG, state management, database integration, testing, performance optimization, and deployment — from zero to enterprise-ready.

React 18+ TypeScript 5 Next.js 14 App Router Tailwind CSS Framer Motion 13 Chapters · 50+ Examples
01
Introduction
React, Next.js, TypeScript — what they are and why they matter
What is React.js and Next.js?

React is a JavaScript library for building user interfaces through reusable components. Built by Meta, it uses a virtual DOM to efficiently update the UI when data changes.

Next.js is a React meta-framework built by Vercel that adds server-side rendering, file-based routing, API routes, image optimization, and production-ready tooling on top of React.

React.js

UI library · Component-based · Virtual DOM · Hooks API · 200k+ stars on GitHub · Powers millions of apps

Next.js

Full-stack framework · SSR/SSG/ISR · File routing · API routes · Edge runtime · App Router (v13+)

Browser Next.js Server React Components HTML/CSS/JS
Next.js handles routing, data fetching & rendering · React handles UI component logic
Why TypeScript with React & Next.js?

TypeScript adds static type checking to JavaScript. In React projects it catches prop mismatches, API shape errors, and refactoring regressions at compile time — not in production.

JavaScriptTypeScript
Errors at runtimeErrors at compile time
No IntelliSense for propsFull autocomplete & docs
Refactoring is riskySafe, automated refactoring
API shapes are implicitAPI shapes are explicit interfaces
💡 Industry Standard
Over 85% of professional React projects use TypeScript. Every major company (Airbnb, Microsoft, Google) mandates TypeScript for their frontend code. Learning it from day one puts you ahead.
SPA vs SSR vs SSG vs ISR
SPA (Client-Side)

JS renders everything in browser. Fast navigation after load. Poor SEO. Used for dashboards, apps behind auth.

SSR (Server-Side)

HTML generated per request on server. Great SEO, fresh data. Slower TTFB. Used for e-commerce, social feeds.

SSG (Static)

HTML pre-built at deploy time. Blazing fast, CDN-cached. Used for blogs, docs, marketing pages.

ISR (Incremental)

Static pages revalidated in background. Best of SSG + SSR. Used for news sites, product pages.

02
Environment Setup
Node.js, Vite, Next.js, folder structure, ESLint & Prettier
Node.js & npm Installation
  • 1Install Node.js: Download Node.js 20 LTS from nodejs.org. This includes npm. Verify with node -v and npm -v.
  • 2Install pnpm (recommended): npm install -g pnpm — faster installs, better monorepo support than npm.
  • 3Install VS Code: Use the ESLint, Prettier, and TypeScript extensions for the best DX.
Create React App with Vite + TypeScript
terminal
SHELL
# Create a Vite + React + TypeScript project
pnpm create vite my-react-app --template react-ts
cd my-react-app
pnpm install
pnpm dev   # starts at http://localhost:5173
Create Next.js App with TypeScript
terminal
SHELL
# Create Next.js 14 with TypeScript, Tailwind, App Router
npx create-next-app@latest my-next-app \
  --typescript \
  --tailwind \
  --app \
  --eslint \
  --src-dir

cd my-next-app
pnpm dev   # starts at http://localhost:3000
Recommended Folder Structure
Next.js App Router structure
TREE
src/
├── app/                    # App Router pages
│   ├── (auth)/             # Route group (no URL segment)
│   │   ├── login/page.tsx
│   │   └── register/page.tsx
│   ├── dashboard/
│   │   ├── layout.tsx      # Nested layout
│   │   └── page.tsx
│   ├── api/                # API routes
│   │   └── students/route.ts
│   ├── globals.css
│   ├── layout.tsx          # Root layout
│   └── page.tsx            # Home page (/)
├── components/
│   ├── ui/                 # Reusable UI primitives
│   │   ├── Button.tsx
│   │   └── Input.tsx
│   └── features/           # Feature-specific components
│       └── StudentCard.tsx
├── hooks/                  # Custom hooks
│   └── useStudents.ts
├── lib/                    # Utilities, API clients
│   ├── api.ts
│   └── db.ts
├── types/                  # Shared TypeScript types
│   └── index.ts
└── store/                  # State management
    └── studentStore.ts
ESLint & Prettier Configuration
.prettierrc
JSON
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
  "printWidth": 90,
  "plugins": ["prettier-plugin-tailwindcss"]
}
03
React Fundamentals
JSX, components, props, state, events, hooks — the core of React
JSX Syntax

JSX is JavaScript XML — a syntax extension that lets you write HTML-like markup inside JavaScript. React compiles it to React.createElement() calls.

JSXBasics.tsx
TSX
// JSX rules:
// 1. Return a single root element (or Fragment)
// 2. Use className instead of class
// 3. All tags must be closed
// 4. JavaScript expressions go in { }

const name = 'Alice';
const isActive = true;

function WelcomeBanner() {
  return (
    <div className="banner">
      <h1>Hello, {name}!</h1>
      <p className={isActive ? 'active' : 'inactive'}>
        Status: {isActive ? 'Online' : 'Offline'}
      </p>
      <img src="/avatar.png" alt="User avatar" />
    </div>
  );
}
Functional Components

Components are the building blocks of React UIs. Functional components are plain functions that return JSX. They're the modern standard (class components are legacy).

StudentCard.tsx
TSX
import React from 'react';

// Type definition for props
interface StudentCardProps {
  name: string;
  grade: string;
  score: number;
  isEnrolled?: boolean;  // optional prop
}

// Functional component — destructure props directly
function StudentCard({ name, grade, score, isEnrolled = true }: StudentCardProps) {
  return (
    <div className="student-card">
      <h2>{name}</h2>
      <span>Grade: {grade} · Score: {score}/100</span>
      {isEnrolled && <span className="badge">Enrolled</span>}
    </div>
  );
}

// Usage
function App() {
  return <StudentCard name="Alice" grade="A" score={95} />;
}

export default StudentCard;
Props & State

Props flow down from parent to child — they are read-only. State is data owned by a component that can change over time, triggering a re-render.

Counter.tsx
TSX
import { useState } from 'react';

// Props: passed from parent, immutable
interface CounterProps {
  initialCount?: number;
  label: string;
}

function Counter({ initialCount = 0, label }: CounterProps) {
  // State: owned by this component, triggers re-render on change
  const [count, setCount] = useState<number>(initialCount);

  const increment = () => setCount(prev => prev + 1);
  const decrement = () => setCount(prev => prev - 1);
  const reset     = () => setCount(initialCount);

  return (
    <div>
      <p>{label}: <strong>{count}</strong></p>
      <button onClick={decrement}>−</button>
      <button onClick={reset}>Reset</button>
      <button onClick={increment}>+</button>
    </div>
  );
}
Event Handling
EventHandling.tsx
TSX
import { useState } from 'react';

function SearchBox() {
  const [query, setQuery] = useState('');

  // TypeScript knows this is a React input change event
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setQuery(e.target.value);
  };

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault(); // stop page reload
    alert(`Searching for: ${query}`);
  };

  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Escape') setQuery('');
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={query}
        onChange={handleChange}
        onKeyDown={handleKeyDown}
        placeholder="Search students..."
      />
      <button type="submit">Search</button>
    </form>
  );
}
Conditional Rendering
ConditionalRender.tsx
TSX
interface StatusBadgeProps { status: 'active' | 'inactive' | 'pending'; }

function StatusBadge({ status }: StatusBadgeProps) {
  // Pattern 1: ternary — for two outcomes
  const color = status === 'active' ? 'green' : 'gray';

  // Pattern 2: early return — guard clause
  if (status === 'inactive') return null; // render nothing

  // Pattern 3: object map — clean multi-case rendering
  const labels: Record<string, string> = {
    active:  '✓ Active',
    pending: '⏳ Pending',
  };

  return (
    <span style={{ color }}>
      {/* Pattern 4: && short-circuit */}
      {status === 'pending' && <span>Review needed: </span>}
      {labels[status]}
    </span>
  );
}
Lists & Keys
StudentList.tsx
TSX
interface Student {
  id: number;
  name: string;
  grade: string;
}

function StudentList({ students }: { students: Student[] }) {
  if (students.length === 0) {
    return <p>No students found.</p>;
  }

  return (
    <ul>
      {students.map(student => (
        // key must be a stable, unique value — never use array index!
        <li key={student.id}>
          <strong>{student.name}</strong> — {student.grade}
        </li>
      ))}
    </ul>
  );
}
Forms & Controlled Components
StudentForm.tsx
TSX
import { useState } from 'react';

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

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

  // Generic handler — updates any field by name
  const handleChange = (
    e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
  ) => {
    const { name, value } = e.target;
    setForm(prev => ({ ...prev, [name]: value }));
    setErrors(prev => ({ ...prev, [name]: undefined })); // clear error
  };

  const validate = (): boolean => {
    const newErrors: Partial<FormData> = {};
    if (!form.name.trim()) newErrors.name = 'Name required';
    if (!form.email.includes('@')) newErrors.email = 'Valid email required';
    setErrors(newErrors);
    return Object.keys(newErrors).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} />
      {errors.name && <span className="error">{errors.name}</span>}
      <input name="email" value={form.email} onChange={handleChange} />
      <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>
  );
}
React Hooks — useState, useEffect, useRef, Custom
Hooks.tsx
TSX
import { useState, useEffect, useRef, useCallback } from 'react';

// ── useEffect: side effects & lifecycle ──────────────
function StudentFetcher({ classId }: { classId: number }) {
  const [students, setStudents] = useState<Student[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false; // prevent state update after unmount

    async function fetchStudents() {
      try {
        setLoading(true);
        const res = await fetch(`/api/students?classId=${classId}`);
        const data = await res.json();
        if (!cancelled) setStudents(data);
      } catch (err) {
        if (!cancelled) setError('Failed to load students');
      } finally {
        if (!cancelled) setLoading(false);
      }
    }

    fetchStudents();
    return () => { cancelled = true; }; // cleanup
  }, [classId]); // re-run when classId changes

  if (loading) return <div>Loading...</div>;
  if (error)   return <div>Error: {error}</div>;
  return <StudentList students={students} />;
}

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

  useEffect(() => {
    inputRef.current?.focus(); // auto-focus on mount
  }, []);

  return <input ref={inputRef} placeholder="I'm focused automatically" />;
}

// ── Custom hook — reusable stateful logic ────────────
function useLocalStorage<T>(key: string, initial: T) {
  const [value, setValue] = useState<T>(() => {
    try {
      const stored = localStorage.getItem(key);
      return stored ? JSON.parse(stored) : initial;
    } catch { return initial; }
  });

  const setStored = useCallback((newValue: T) => {
    setValue(newValue);
    localStorage.setItem(key, JSON.stringify(newValue));
  }, [key]);

  return [value, setStored] as const;
}

// Usage of custom hook
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Current: {theme}
    </button>
  );
}
04
TypeScript in React
Typing props, state, events, generics — strong types = fewer bugs
Typing Props & State
types/index.ts
TS
// Shared types — import across your whole app
export interface Student {
  id: number;
  name: string;
  email: string;
  age: number;
  grade: 'A' | 'B' | 'C' | 'D' | 'F';
  createdAt: string;
}

export type CreateStudentDto = Omit<Student, 'id' | 'createdAt'>;
export type UpdateStudentDto = Partial<CreateStudentDto>;

// API response wrapper
export interface ApiResponse<T> {
  data: T;
  message: string;
  success: boolean;
  pagination?: {
    page: number;
    total: number;
    limit: number;
  };
}

// Component prop patterns
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary' | 'danger';
  loading?: boolean;
  children: React.ReactNode;
}

// Callback props
export interface StudentListProps {
  students: Student[];
  onSelect: (student: Student) => void;
  onDelete: (id: number) => Promise<void>;
  renderEmpty?: () => React.ReactNode; // render prop
}
Interfaces vs Types
InterfaceVsType.ts
TS
// INTERFACE — best for object shapes (can extend, implement)
interface User {
  id: number;
  name: string;
}
// Declaration merging — can add fields later
interface User { email: string; }

// TYPE — best for unions, intersections, primitives
type Role = 'admin' | 'teacher' | 'student';
type ID = number | string;

// Intersection type (like extends)
type AdminUser = User & { role: 'admin'; permissions: string[] };

// Utility types — use these constantly
type PartialUser     = Partial<User>;    // all fields optional
type RequiredUser    = Required<User>;   // all fields required
type ReadonlyUser    = Readonly<User>;   // no mutations
type UserWithoutId   = Omit<User, 'id'>; // remove field
type OnlyName        = Pick<User, 'name'>;// keep field

// Record type — typed dictionary
type GradeMap = Record<string, 'A' | 'B' | 'C'>;
Generics in Components
GenericList.tsx
TSX
// Generic component — works with ANY data type
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string | number;
  emptyMessage?: string;
}

function List<T>({ items, renderItem, keyExtractor, emptyMessage }: ListProps<T>) {
  if (items.length === 0) {
    return <p>{emptyMessage ?? 'No items'}</p>;
  }
  return (
    <ul>
      {items.map((item, i) => (
        <li key={keyExtractor(item)}>{renderItem(item, i)}</li>
      ))}
    </ul>
  );
}

// TypeScript infers T from usage — fully type-safe
<List
  items={students}
  keyExtractor={s => s.id}
  renderItem={s => <span>{s.name}</span>}
/>
Typing Event Handlers
TypedEvents.tsx
TSX
// Full reference of typed React event handlers
const handlers = {
  onChange:    (e: React.ChangeEvent<HTMLInputElement>) => {},
  onSubmit:    (e: React.FormEvent<HTMLFormElement>)   => {},
  onClick:     (e: React.MouseEvent<HTMLButtonElement>) => {},
  onKeyDown:   (e: React.KeyboardEvent<HTMLInputElement>) => {},
  onFocus:     (e: React.FocusEvent<HTMLInputElement>)  => {},
  onDragStart: (e: React.DragEvent<HTMLDivElement>)    => {},
};

// Typed ref example
const inputRef = useRef<HTMLInputElement>(null);
const divRef   = useRef<HTMLDivElement>(null);
const timerRef = useRef<ReturnType<typeof setTimeout>>(null);
05
Advanced React Concepts
Context API, Router, useReducer, performance optimization
Context API

Context provides a way to pass data through the component tree without prop drilling. Use it for global state like auth, theme, or language.

context/AuthContext.tsx
TSX
import { createContext, useContext, useState, useEffect } from 'react';

interface AuthContextType {
  user: User | null;
  isLoading: boolean;
  login:  (email: string, password: string) => Promise<void>;
  logout: () => void;
}

// 1. Create the context
const AuthContext = createContext<AuthContextType | null>(null);

// 2. Create the provider
export function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // Restore session on mount
    const token = localStorage.getItem('token');
    if (token) fetchCurrentUser(token).then(setUser).finally(() => setIsLoading(false));
    else setIsLoading(false);
  }, []);

  const login = async (email: string, password: string) => {
    const { user, token } = await loginApi(email, password);
    localStorage.setItem('token', token);
    setUser(user);
  };

  const logout = () => {
    localStorage.removeItem('token');
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, isLoading, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

// 3. Custom hook for consuming context
export function useAuth() {
  const ctx = useContext(AuthContext);
  if (!ctx) throw new Error('useAuth must be inside AuthProvider');
  return ctx;
}

// Usage in any component
function NavBar() {
  const { user, logout } = useAuth();
  return (
    <nav>
      {user ? `Hello, ${user.name}` : 'Not logged in'}
      {user && <button onClick={logout}>Logout</button>}
    </nav>
  );
}
React Router v6
App.tsx — routing setup
TSX
import { BrowserRouter, Routes, Route, Navigate, Outlet } from 'react-router-dom';
import { useAuth } from './context/AuthContext';

// Protected route wrapper
function ProtectedRoute() {
  const { user, isLoading } = useAuth();
  if (isLoading) return <div>Loading...</div>;
  if (!user) return <Navigate to="/login" replace />;
  return <Outlet />; // render child route
}

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <{/* Public routes */}
        <Route path="/"      element={<Home />} />
        <Route path="/login" element={<Login />} />

        <{/* Protected routes */}
        <Route element={<ProtectedRoute />}>
          <Route path="/dashboard"        element={<Dashboard />} />
          <Route path="/students"          element={<Students />} />
          <Route path="/students/:id"      element={<StudentDetail />} />
        </Route>

        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}
useReducer for Complex State
studentReducer.ts
TS
// Define all possible actions as a discriminated union
type StudentAction =
  | { type: 'FETCH_START' }
  | { type: 'FETCH_SUCCESS'; payload: Student[] }
  | { type: 'FETCH_ERROR';   payload: string }
  | { type: 'ADD_STUDENT';   payload: Student }
  | { type: 'UPDATE_STUDENT';payload: Student }
  | { type: 'DELETE_STUDENT';payload: number };

interface StudentState {
  students: Student[];
  loading:  boolean;
  error:    string | null;
}

const initialState: StudentState = {
  students: [], loading: false, error: null
};

function studentReducer(state: StudentState, action: StudentAction): StudentState {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null };
    case 'FETCH_SUCCESS':
      return { ...state, loading: false, students: action.payload };
    case 'FETCH_ERROR':
      return { ...state, loading: false, error: action.payload };
    case 'ADD_STUDENT':
      return { ...state, students: [...state.students, action.payload] };
    case 'UPDATE_STUDENT':
      return { ...state, students: state.students.map(
        s => s.id === action.payload.id ? action.payload : s
      )};
    case 'DELETE_STUDENT':
      return { ...state, students: state.students.filter(s => s.id !== action.payload) };
    default: return state;
  }
}

// Usage in component
const [state, dispatch] = useReducer(studentReducer, initialState);
dispatch({ type: 'ADD_STUDENT', payload: newStudent });
Performance: memo, useCallback, useMemo, Suspense
Performance.tsx
TSX
import { memo, useCallback, useMemo, lazy, Suspense } from 'react';

// React.memo — skip re-render if props unchanged
const ExpensiveCard = memo(function ExpensiveCard({ student }: { student: Student }) {
  return <div>{student.name}</div>;
});

function StudentDashboard() {
  const [students, setStudents] = useState<Student[]>([]);
  const [filter, setFilter] = useState('A');

  // useMemo — recalculate only when students or filter changes
  const filteredStudents = useMemo(
    () => students.filter(s => s.grade === filter),
    [students, filter]
  );

  // useCallback — stable function reference for child components
  const handleDelete = useCallback(async (id: number) => {
    await deleteStudent(id);
    setStudents(prev => prev.filter(s => s.id !== id));
  }, []); // empty dep array = stable forever

  return (
    <div>
      {filteredStudents.map(s => (
        <ExpensiveCard key={s.id} student={s} />
      ))}
    </div>
  );
}

// Lazy loading — code split by route/component
const Analytics = lazy(() => import('./Analytics'));

function App() {
  return (
    <Suspense fallback={<div>Loading analytics...</div>}>
      <Analytics />
    </Suspense>
  );
}
06
Next.js Fundamentals
App Router, file routing, render modes, API routes, layouts
File-Based Routing (App Router)

Next.js 13+ App Router uses the filesystem as the router. Every folder inside app/ is a route segment; a page.tsx file makes it accessible.

app/students/[id]/page.tsx
TSX
// URL: /students/42
// This file is a Server Component by default

interface PageProps {
  params: { id: string };        // dynamic segment
  searchParams: Record<string, string>; // ?query=value
}

// Server Component — runs on server, can fetch directly
export default async function StudentPage({ params }: PageProps) {
  // Direct DB/API call — no useEffect needed!
  const student = await fetchStudent(parseInt(params.id));

  if (!student) notFound(); // renders 404 page

  return (
    <main>
      <h1>{student.name}</h1>
      <p>Grade: {student.grade}</p>
    </main>
  );
}

// Generate page metadata (SEO)
export async function generateMetadata({ params }: PageProps) {
  const student = await fetchStudent(parseInt(params.id));
  return { title: student?.name ?? 'Student' };
}
Render Modes — Server vs Client Components
app/students/page.tsx (Server) + ClientFilter.tsx
TSX
// ── SERVER COMPONENT (default) ───────────────────────
// No "use client" directive
// Can: async/await, access DB, no useState/useEffect
export default async function StudentsPage() {
  // Runs on server at request time
  const students = await db.student.findMany({ orderBy: { name: 'asc' } });

  return (
    <div>
      <h1>Students ({students.length})</h1>
      {/* Client component for interactivity */}
      <ClientFilter students={students} />
    </div>
  );
}

// ── CLIENT COMPONENT ─────────────────────────────────
'use client'; // marks as client component

import { useState } from 'react';

export function ClientFilter({ students }: { students: Student[] }) {
  const [query, setQuery] = useState('');
  const filtered = students.filter(s => s.name.toLowerCase().includes(query));

  return (
    <>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <StudentList students={filtered} />
    </>
  );
}
🧭 When to use each?
Server Component: Data fetching, DB access, sensitive secrets, heavy computation, no interactivity needed.
Client Component: useState, useEffect, event listeners, browser APIs, animations.
API Routes (Route Handlers)
app/api/students/route.ts
TS
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';

// GET /api/students?grade=A
export async function GET(req: NextRequest) {
  try {
    const grade = req.nextUrl.searchParams.get('grade');
    const students = await db.student.findMany({
      where: grade ? { grade } : undefined
    });
    return NextResponse.json({ success: true, data: students });
  } catch (error) {
    return NextResponse.json({ success: false, error: 'Server error' }, { status: 500 });
  }
}

// POST /api/students
export async function POST(req: NextRequest) {
  try {
    const body: CreateStudentDto = await req.json();

    // Validate with Zod
    const validated = createStudentSchema.parse(body);
    const student = await db.student.create({ data: validated });

    return NextResponse.json({ success: true, data: student }, { status: 201 });
  } catch (error) {
    if (error instanceof ZodError) {
      return NextResponse.json({ success: false, errors: error.errors }, { status: 400 });
    }
    return NextResponse.json({ error: 'Server error' }, { status: 500 });
  }
}

// app/api/students/[id]/route.ts
export async function DELETE(
  req: NextRequest,
  { params }: { params: { id: string } }
) {
  await db.student.delete({ where: { id: parseInt(params.id) } });
  return NextResponse.json({ success: true });
}
Layouts & Nested Routing
app/dashboard/layout.tsx
TSX
import { redirect } from 'next/navigation';
import { getServerSession } from 'next-auth';

export default async function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  // Server-side auth check — runs before any page renders
  const session = await getServerSession();
  if (!session) redirect('/login');

  return (
    <div className="flex h-screen">
      <Sidebar />
      <main className="flex-1 overflow-auto">
        <TopNav user={session.user} />
        <div className="p-6">
          {children} {/* page.tsx renders here */}
        </div>
      </main>
    </div>
  );
}