01
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.
UI library · Component model · Virtual DOM · Client-side rendering · Hooks API · Ecosystem (Router, Redux, etc.)
Full-stack framework · SSR/SSG/ISR · File-based routing · API routes · Edge functions · Vercel deployment
components
API Routes
GraphQL
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.
Type errors appear as you type. No more "cannot read property of undefined" at runtime.
Full IntelliSense, auto-complete for props, and instant docs inline in VS Code.
Interfaces serve as live documentation. New developers understand component contracts instantly.
| Approach | How It Works | Best For | SEO |
|---|---|---|---|
| SPA (React) | JS downloaded, renders in browser | Dashboards, apps | Poor (needs extras) |
| SSR (Next.js) | HTML generated per-request on server | Dynamic content, e-commerce | Excellent |
| SSG (Next.js) | HTML generated at build time | Blogs, docs, marketing | Excellent |
| ISR (Next.js) | SSG + background revalidation | Dynamic data, large sites | Excellent |
02
- 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
# 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
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
# 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
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/
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"plugins": ["prettier-plugin-tailwindcss"]
}
{
"compilerOptions": {
"target": "ES2017",
"strict": true, // enables all strict checks
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"paths": { "@/*": ["./src/*"] }
}
}
03
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.
// 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> </> ); }
React apps are built from components — pure functions that accept props and return JSX. Functional components are the modern standard; class components are legacy.
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} />
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> ); }
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> ); }
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
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> ); }
04
// ── 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 };
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> } // ]} />
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); };
05
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();
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>; }
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> ); }
06
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> ); }
'use client' only when you need interactivity (useState, useEffect, event handlers).
// 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> ); }
'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> ); }
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 }); }
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> ); }
07
// 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> ); }
// 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> ); }
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' });
08
// 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; } }
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;
09
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';
'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> ); }
10
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; }
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); }) );
11
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(); }); });
12
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> ); }
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}` }, }; }
# 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"]
13
Static Next.js site with Framer Motion animations, contact form, and Vercel deployment. Dark/light mode toggle.
SSG + ISR blog with MDX, Prisma + MySQL, tag filtering, pagination, RSS feed, and full SEO setup.
Full-stack store with auth, Stripe payments, cart management, order tracking, admin dashboard, and Redis caching.
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
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*'], };