// Full Stack Web Development

Notes &
Reference

A comprehensive study guide covering JavaScript fundamentals, async patterns, Node.js backend, MongoDB, security, and React — with code examples and alternatives for every concept.

JavaScript Node.js MongoDB React Security CSS

Primitives vs Reference Types

Understanding how JavaScript stores data in memory is fundamental. The difference determines how copying, comparison, and mutation work.

Primitive Types

Types: String, Number, Boolean, null, undefined, Symbol, BigInt

Storage: Stack — fast, fixed-size memory

Behavior: Immutable. Assignment copies the actual value. Modifying creates a new value, never alters the original.

Reference Types (Non-Primitives)

Types: Objects, Arrays, Functions

Storage: Heap (with a reference pointer in the Stack)

Behavior: Mutable. = only copies the reference pointer. Modifying the "copy" mutates the original!

Copying Objects & Arrays

Because let obj2 = obj1 only copies the reference, specific methods are needed to actually copy data.

Shallow Copy

Copies top-level elements, but nested objects/arrays still share the same reference. Changes to nested data will affect both copies.

Deep Copy

Creates a completely independent copy including all nested objects and arrays. Changes to any level do not affect the original.

JavaScript — Shallow Copy
let d = { a: 10, nested: { b: 20 } };

// Alt 1: Spread Operator (Modern & Preferred)
let cd1 = { ...d };

// Alt 2: Object.assign() (Older method)
let cd2 = Object.assign({}, d);
JavaScript — Deep Copy
let order = { id: "101", address: { city: "Hyd" } };

// Alt 1: structuredClone() — Modern Native (PREFERRED)
// Handles Dates, Maps, Sets, and nested objects perfectly
let copyOrder = structuredClone(order);
copyOrder.address.city = "uppal"; // Original stays "Hyd" ✓

// Alt 2: JSON.parse(JSON.stringify()) — Classic Hack
// ⚠ Loses: functions, undefined, Dates become strings
let copyOrder2 = JSON.parse(JSON.stringify(order));
// TIP: Always prefer structuredClone() for deep copies in modern JS. Use JSON.parse(JSON.stringify()) only for simple plain-data objects in older codebases.

Special JavaScript Operators

Operator Syntax What it does Returns
Optional Chaining ?. Safely accesses deeply nested properties without throwing if a reference is null/undefined Value if exists, otherwise undefined
Nullish Coalescing ?? Checks if left side is exactly null or undefined, provides fallback if so Left value if it exists, otherwise right-side fallback
Spread ... Expands an iterable (array or object) into its individual elements Unpacked elements — used for shallow copies and merging
Destructuring {a,b}=obj Extracts values from arrays or properties from objects into distinct variables Extracted values assigned to your variables
JavaScript — Operator Examples
// Optional Chaining — won't crash if city is undefined
console.log(student.city?.length);

// Nullish Coalescing — fallback if city is null/undefined
console.log(student.city?.length ?? "Not exist");

// Spread — add new course to array immutably
let addCourse = (newCourse) => [...courses, newCourse];

// Destructuring — extract from req.params
let { uid, pid } = req.params;

Array Methods

JavaScript provides powerful higher-order functions for manipulating arrays and arrays of objects. These methods are the backbone of modern JS development.

.filter()
Returns a NEW array containing only items that satisfy the condition. Does not modify the original.
→ Returns: New Array
.map()
Returns a NEW array populated with the transformed results of a callback applied to each element.
→ Returns: New Array (same length)
.some()
Checks if at least one element in the array satisfies the condition. Short-circuits on first match.
→ Returns: Boolean
.every()
Checks if ALL elements in the array satisfy the condition. Short-circuits on first failure.
→ Returns: Boolean
.find()
Returns the first element that matches the condition. Stops iterating after the first match is found.
→ Returns: Element | undefined
.reduce()
Accumulates array values into a single output. The accumulator carries value across iterations.
→ Returns: Single Accumulated Value
.sort()
Sorts elements IN PLACE. Use spread (...) to create a copy first to avoid mutating the original array.
→ Mutates original — copy first!
JavaScript — Array Methods
const users = [
  { id: 1, name: "Ravi",  role: "student", active: true },
  { id: 2, name: "Anil",  role: "admin",   active: true },
];
const courses = [{ id: 101, title: "JS", price: 999 }];

// filter: Returns new array of items matching condition
let activeUsers = users.filter(user => user.active);

// map: Chain after filter to transform results
let activeUserNames = users
  .filter(user => user.active)
  .map(user => user.name); // ["Ravi", "Anil"]

// some: true if AT LEAST ONE matches
const adminExists = users.some(user => user.role === "admin");

// every: true only if ALL match
let allPaid = courses.every(c => c.price > 0);

// find: Returns first match
let findUserById = (id) => users.find(u => u.id === id);

// reduce: Accumulate to single value
let total = courses.reduce((sum, c) => sum + c.price, 0);

// sort: Spread first to avoid mutating original
let sortPrices = [...courses].sort((a, b) => b.price - a.price);

Rest Parameters

In most other languages, passing more arguments than a function expects causes an error. In JS it silently ignores the extras. Rest parameters let you capture all those extra arguments into a single array so nothing is lost.

Without Rest — Extras Ignored

function sum(a, b) called with sum(10, 20, 30, 40) — JS takes only a=10, b=20 and silently discards 30 and 40. No error, just unexpected results.

With Rest — Pack Into Array

function sum(...a) — the ... packs ALL passed arguments into an array. Now you can loop over them, reduce them, or pass them elsewhere. Works with any number of arguments.

⚠ Rule: Rest Must Be Last

The rest parameter must always be the last parameter. You can have normal params before it: function fn(a, b, ...rest). Putting anything after the rest param is a syntax error.

JavaScript — Rest Parameters
// Without rest — extras are silently ignored
function sum(a, b) {
  return a + b;
}
sum(10, 20, 30, 40); // returns 30 — 30 and 40 are ignored

// With rest — all arguments packed into array 'a'
function sum(...a) {
  console.log(a); // [10, 20, 30, 40]
  return a.reduce((x, y) => x + y);
}
sum(10, 20, 30, 40); // returns 100 ✓

// Mix of fixed + rest params — rest must be LAST
function log(label, ...values) {
  console.log(label, values);
}
log("scores:", 90, 85, 78); // "scores:" [90, 85, 78]

// ✗ Syntax Error — rest param must be last
// function bad(...a, b) { } 

Dates in JavaScript

Dates internally store time as a UTC timestamp in milliseconds since Jan 1, 1970 (Unix Epoch). The Date constructor accepts multiple formats.

JavaScript — Dates
let date = new Date();              // Current date & time
let d2   = new Date('2022-01-12');   // ISO string format
let d3   = new Date(2022, 0, 12);    // Year, Month (0=Jan!), Day

console.log(date.getFullYear()); // 2024
console.log(date.getMonth());    // 0-11 (⚠ 0 = January!)
console.log(date.getDate());     // 1-31 (day of month)
console.log(date.getDay());      // 0-6 (0 = Sunday)
console.log(date.getTime());     // Milliseconds since epoch
⚠ Gotcha: getMonth() returns 0–11, not 1–12. January = 0. Always add 1 when displaying month to users.

Modules & Exports

Default Export

One per module. The importer can name it whatever they want. Best for a module's primary export.

Export: export default a;

Import: import myVar from './file.js'

Named Export

Multiple per module. The importer MUST use the exact name in curly braces. Best for utility functions.

Export: export { b, marks };

Import: import { b, marks } from './file.js'

First-Class Functions & Closure

In JavaScript, functions are first-class objects — they can be stored in variables, passed as arguments, and returned from other functions. This is why JS supports both OOP and functional programming styles.

Stored in a Variable

Called a function expression. The function has no name of its own — it's called an anonymous function. The variable holds a reference to it.

const greet = function() { ... }

Passed as an Argument

Called a callback function. You pass the function itself (not its result) to another function, which calls it later. This is the backbone of async JS and array methods like .map(), .filter().

Returned from a Function

A function that returns another function. Less common but powerful — used in factory functions, memoization, and closures. This is where closure kicks in.

How Closure Works

A closure is a function bundled together with its surrounding scope. This is the default behaviour in JS — every function "remembers" the variables from the scope it was defined in, even after that outer function has finished executing.

Normal Execution (no closure)

When a function runs, its local variables are created in the Call Stack. When the function finishes, those variables are removed from the stack. Gone.

With Closure (inner function)

If the function returns another function, the inner function still holds a reference to the outer variables. The JS engine sees this and moves those variables to the Heap (where reference types live) instead of deleting them — keeping them alive as long as the inner function exists.

JavaScript — Closure Example
function increment() {
  let counter = 0;          // local to increment()
  return function() {       // inner function "closes over" counter
    counter++;
    return counter;
  };
}

let x = increment(); // increment() runs and finishes...
                      // but counter is NOT deleted — x holds a reference to the inner fn

console.log(x()); // 1 — counter is remembered
console.log(x()); // 2 — same counter, still alive in memory
console.log(x()); // 3

// Each call to increment() creates a SEPARATE closure
let y = increment();
console.log(y()); // 1 — y has its own private counter, separate from x
// Memory model: Stack = fast, temporary storage for function calls and primitives. Heap = slower, long-term storage for objects and references. When a closure is formed, the closed-over variables are moved from the stack to the heap so they survive after the outer function returns.

Promises & Async/Await

JavaScript is single-threaded — one task at a time. The runtime (Browser or Node.js) handles async tasks via Web APIs + Event Loop, pushing callbacks back to the engine when ready.

PENDING
Initial State
FULFILLED
resolve() called
|
REJECTED
reject() called
JavaScript — Creating a Promise
let futureAvailability = true;

let promiseObj = new Promise((fulfill, reject) => {
  setTimeout(() => {
    futureAvailability === true
      ? fulfill("hello kiran")
      : reject("Sorry call later");
  }, 5000);
});
JavaScript — Alt 1: .then() / .catch()
// Classic chaining — still valid, can get "callback-hell"-y
promiseObj
  .then((msg) => console.log("message: ", msg))
  .catch((msg) => console.log("error: ", msg));
JavaScript — Alt 2: async / await (Modern)
// Reads like synchronous code — much cleaner!
async function consumePromise() {
  try {
    let res = await promiseObj; // Pauses until resolved
    console.log(res);
  } catch (err) {
    console.log(err); // Handles rejection
  }
}
// All async functions implicitly return a Promise

JS Engine & Event Loop

JavaScript is single-threaded — but async operations don't block because the runtime (Browser or Node.js) offloads them to native APIs, then uses the Event Loop to bring results back when the JS engine is free.

Call Stack

Where JS executes code. LIFO (Last In, First Out). Functions are pushed when called, popped when they return. If a function blocks here, everything freezes.

Web APIs / Node APIs

Browser/Node handles async tasks outside the JS engine: setTimeout, HTTP requests, DOM events, file I/O. JS hands off the task and continues executing.

Callback Queue (Task Queue)

When a Web API finishes (e.g. timer fires), the callback is placed in this queue. It waits here until the Call Stack is empty.

Event Loop

Continuously checks: "Is the Call Stack empty?" If yes, it takes the next callback from the queue and pushes it onto the stack. This is how async callbacks run.

JavaScript — Event Loop Order of Execution
console.log("1 — synchronous, runs immediately");

setTimeout(() => {
  console.log("3 — async, runs AFTER call stack clears");
}, 0); // Even 0ms delay goes to callback queue!

console.log("2 — synchronous, runs before setTimeout callback");

// Output order: 1 → 2 → 3
// The Event Loop only pushes the setTimeout callback
// after lines 1 and 2 have finished and the stack is empty.
// Browser Runtime = JS Engine + Web APIs + Event Loop
// Node.js Runtime = JS Engine + Node APIs (libuv) + Event Loop

REST API Concepts

// Endpoints Rule: Use nouns, plurals, lowercase, and hyphens → GET /user-profiles
200
OK
201
Created
400
Bad Request
401
Unauthorized
404
Not Found
500
Server Error

Express & Middlewares

Middlewares run between receiving a request and sending a response. They form a pipeline — each middleware calls next() to pass control forward.

Shell — Installation
# Initialize a Node project first (creates package.json)
npm init -y

# Install Express + cookie-parser
npm install express cookie-parser

# Add "type": "module" to package.json to use ES module imports
# (needed for: import exp from 'express')
JavaScript — Express Setup
import exp from 'express';
import cookieParser from 'cookie-parser';

const app = exp();

// Built-in Middlewares
app.use(exp.json());       // Parses JSON body → req.body
app.use(cookieParser());  // Parses cookies → req.cookies

// Custom Router
import { userApp } from "../apis/userApi.js";
app.use('/apis/user-api', userApp);

// Global Error Handler — MUST be last, needs 4 arguments
function errorHandler(err, req, res, next) {
  res.json({ message: "error occured!", reason: err.message });
}
app.use(errorHandler); // ⚠ Keep at the very end

app.listen(4000, () => console.log("listening to port 4000"));

Controllers & Route Files

A Controller is just a request handler function stored in a variable instead of being written inline on the route. It keeps route files clean — the route just says what path + method, and the controller says what to do.

JavaScript — Inline vs Controller Pattern
// ✗ Before — handler written inline, gets messy fast
app.get('/users', (req, res) => {
  // all the logic crammed here
});

// ✓ After — extract to a controller variable
const getUsers = (req, res) => {
  // logic lives here, named and reusable
};
app.get('/users', getUsers); // route is now clean and readable

// In larger apps, controllers go in separate files:
// userController.js  →  exports { getUsers, createUser, deleteUser }
// userRoutes.js      →  imports controllers, defines app.get/post/delete
// app.js             →  imports routes, mounts them with app.use()
// File naming convention: In many Express projects you'll see a file called app.routes.js — the name tells you the file (app), what it does (routes), and the extension (.js). Controllers typically live in a separate controllers/ folder.

CORS — Cross-Origin Resource Sharing

By default, browsers block frontend JS from making requests to a different origin (different domain, port, or protocol). This is the browser's Same-Origin Policy. CORS is the mechanism that lets a server say "I allow requests from this origin" — without it, your React frontend on localhost:5173 can't talk to your Express backend on localhost:4000.

What Triggers CORS Error

Your React app (origin A) makes a fetch/axios request to your Express server (origin B). Browser sees different origins → blocks the response and throws a CORS error in the console. The request DID reach the server — the browser just refuses to hand the response to JS.

The Fix

Install the cors npm package and use it as Express middleware. It adds the right headers (Access-Control-Allow-Origin) to every response so the browser allows them through.

origin: true vs "*"

origin: "*" allows every domain — fine for public APIs. origin: true mirrors the request origin back, allowing any origin with cookies. For production, pass a specific URL: origin: "https://yoursite.com".

Shell + JavaScript — CORS Setup
# Install cors package
npm install cors

// app.js — import and use BEFORE your routes
import cors from 'cors';
import exp from 'express';

const app = exp();

app.use(cors({
  origin: true,        // allow the requesting origin (any)
  credentials: true,  // allow cookies to be sent (needed for httpOnly JWT cookies)
}));

// For specific origin only (recommended in production):
// app.use(cors({ origin: "http://localhost:5173", credentials: true }));

app.use(exp.json());
// ... rest of your middlewares and routes
⚠ credentials: true is required when your frontend sends cookies (like your JWT httpOnly cookie). Without it, the browser strips cookies from cross-origin requests even if CORS is otherwise set up correctly. Always pair it with a specific origincredentials: true with origin: "*" is not allowed by browsers.

MongoDB & Mongoose

Shell — Installation
# Install Mongoose (MongoDB ODM for Node.js)
npm install mongoose

# Connect to MongoDB in your server entry file
# Replace the URI with your MongoDB Atlas connection string
mongoose.connect("mongodb+srv://<user>:<pass>@cluster.mongodb.net/dbname")
  .then(() => console.log("DB connected"))
  .catch((err) => console.log(err));
Comparison Operators

$eq equal  $neq not equal  $gt greater than

$gte ≥  $lt less than  $lte

$in value in array  $nin value NOT in array

Array Operators

Query: $all — doc array must contain ALL listed values (order doesn't matter)

Update:

$push — add one item to array

$each — used with $push to add multiple items at once

$pull — remove all items matching a condition

$pop — remove first (-1) or last (1) item

$addToSet — push only if value doesn't already exist (no duplicates)

General Update Operators

$set — set a field to a value (creates it if it doesn't exist)

$unset — completely remove a field from the document

$inc — increment (or decrement with negative value) a numeric field

strict: "throw"

If you try to save a field that isn't defined in the schema, Mongoose will throw an error instead of silently ignoring it. Catches typos and accidental extra fields during development.

timestamps: true

Automatically adds createdAt and updatedAt fields to every document. Mongoose manages these for you — no need to set them manually.

runValidators: true

By default, Mongoose validators (like minLength, required) only run on .save(). You must pass runValidators: true in update options to enforce them on findByIdAndUpdate too.

JavaScript — Mongoose Schema & Model
import { Schema, model } from 'mongoose';

const productSchema = new Schema({
  productName: {
    type: String,
    required: [true, "product name required"],
    minLength: [5, 'min length is 5']
  },
  price: {
    type: Number,
    min: [10, 'min price 10']
  }
}, {
  strict: "throw",   // Error if undefined fields are saved
  timestamps: true,  // Auto-adds createdAt & updatedAt
  versionKey: false  // Removes __v field
});

export const ProductModel = model("product", productSchema);
JavaScript — Database Operations
// Update user cart — push product and populate details
let modifiedUser = await UserModel.findByIdAndUpdate(
  uid,
  { $push: { cart: { product: pid, quantity: 1 } } },
  { new: true, runValidators: true } // runValidators: false by default!
).populate("cart.product", "productName price");
//          ↑ field to populate  ↑ fields to select
// .populate() replaces the stored ObjectId reference with the actual document data from the referenced collection. The second argument is a space-separated list of fields to include (field projection). For example "productName price" returns only those two fields from the Product document, not the entire object.

Mongoose Query Methods

These are the core Mongoose methods for reading and updating documents. Understanding what each method accepts, returns, and how options like new: true change the behaviour is essential — both in real projects and interviews.

Method What it does Arguments Returns
find() Returns all documents that match the filter. If no filter passed, returns the entire collection. filter (optional), projection (optional) Array of documents — empty array if none match, never null
findOne() Returns the first document that matches the filter. Stops scanning after the first match — more efficient than find() when you only need one. filter, projection (optional) Single document | null if not found
findById() Shorthand for findOne({ _id: id }). Looks up a document by its _id field directly. Accepts string or ObjectId. id (string or ObjectId) Single document | null if not found
findOneAndUpdate() Finds the first matching document and applies the update. By default returns the old (pre-update) document. Pass { new: true } to get the updated version back. filter, update, options Old doc by default | New doc with new: true | null if no match
findByIdAndUpdate() Shorthand for findOneAndUpdate({ _id: id }, ...). Same exact behaviour — returns old document by default, new document with { new: true }. id, update, options Old doc by default | New doc with new: true | null if not found
findByIdAndDelete() Finds a document by _id, removes it from the collection entirely, and returns it — useful to confirm what was deleted. id The deleted document | null if not found
⚠ new: false is the defaultfindByIdAndUpdate() and findOneAndUpdate() return the document before the update was applied unless you pass { new: true }. This trips up almost everyone the first time. Always explicitly pass new: true when you need the updated result.
find() vs findOne()

find() always returns an array — even if only one document matches, you get a one-element array. findOne() always returns a single document or null. Use findOne() when you expect exactly one result (by email, username, slug, etc.).

upsert: true

Pass { upsert: true } to findOneAndUpdate() or findByIdAndUpdate() to create the document if it doesn't exist. Without it, if the filter matches nothing, the method just returns null and does nothing.

runValidators: false (default)

Mongoose validators (required, minLength) only run on .save() by default. Update methods skip validators unless you explicitly pass { runValidators: true }.

JavaScript — All Query Methods in Practice
// ===== find() — always returns an Array, never null =====
let allUsers    = await UserModel.find();
let activeUsers = await UserModel.find({ active: true });
let names       = await UserModel.find({}, "name email"); // field projection
// find() returns [] if nothing matches — no null check needed

// ===== findOne() — returns single doc or null =====
let user = await UserModel.findOne({ email: "user@example.com" });
if (!user) return res.status(404).json({ message: "User not found" });

// ===== findById() — same as findOne({ _id: id }) =====
let userById = await UserModel.findById(req.params.id); // doc or null

// ===== findByIdAndUpdate() — default returns OLD document =====
let oldDoc = await UserModel.findByIdAndUpdate(
  req.params.id,
  { $set: { name: "New Name" } }
);
// oldDoc.name is STILL the original name — the update already happened in DB
// but this variable holds the pre-update snapshot

// ✓ Pass { new: true } to get the UPDATED document back
let updatedDoc = await UserModel.findByIdAndUpdate(
  req.params.id,
  { $set: { name: "New Name" } },
  { new: true, runValidators: true }
);
// updatedDoc.name is now "New Name" ✓

// ===== findOneAndUpdate() — filter by any field, not just _id =====
let updated = await ProductModel.findOneAndUpdate(
  { sku: "ABC123" },
  { $inc: { stock: -1 } },
  { new: true }
);

// ===== findByIdAndDelete() — removes doc, returns the deleted doc =====
let deleted = await UserModel.findByIdAndDelete(req.params.id);
if (!deleted) return res.status(404).json({ message: "Not found" });
// deleted holds the document that WAS in the DB before deletion

// ===== .select() — field projection on find/findOne/findById =====
let safeUser = await UserModel
  .findById(id)
  .select("-password -__v"); // "-" prefix = exclude these fields
// Quick return reference:
find() → always an Array (empty array if no match — never null)
findOne() / findById()document or null
findByIdAndUpdate()old doc by default | new doc with new: true | null if not found
findOneAndUpdate() → same as above, but filter by any field
findByIdAndDelete() → the deleted doc | null if not found

10c-DB Connection Pattern

The correct pattern is to wrap the MongoDB connection in an async function with try/catch, and only start the HTTP server inside the try block after the DB connects successfully. This ensures your server never starts with a broken database connection.

JavaScript — connectDB() — Server Entry File
import { connect } from 'mongoose';
import 'dotenv/config';
import { app } from './app.js'; // your express app

/**
 * Connects to MongoDB, then starts the HTTP server.
 * Server only starts AFTER DB connects — prevents serving
 * requests with no database available.
 */
const connectDB = async () => {
  try {
    await connect(process.env.DB_URL);
    console.log("DB connection success");

    // Start HTTP server only after DB is ready
    app.listen(process.env.PORT, () => console.log("server started!"));

  } catch (err) {
    console.log("error occurred", err.message);
    process.exit(1); // Exit process on DB failure — don't hang
  }
};

connectDB(); // Call it to kick everything off
// Why this pattern? app.listen() is inside try so if connect() throws, the server never starts. process.exit(1) in catch kills the process immediately with an error code instead of silently hanging. The DB_URL and PORT come from .env via dotenv.
Shell — .env DB URLs
# Local MongoDB (MongoDB installed on your machine)
DB_URL=mongodb://localhost:27017/dbName
#              ↑               ↑      ↑
#              host            port   your database name

# MongoDB Atlas (cloud — production)
DB_URL=mongodb+srv://<username>:<password>@cluster0.xxxxx.mongodb.net/dbName

Auth, JWT & Cookies

Authentication answers: "Who are you?"   Authorization answers: "What are you allowed to do?"

In modern web apps, authentication is commonly implemented using JWT (JSON Web Tokens), which are stored inside cookies. The browser automatically attaches these cookies to every request, enabling secure access to protected routes.

Shell — Setup
npm install bcryptjs jsonwebtoken dotenv
🔑 Anatomy of a JWT
HeaderAlgorithm info
.
Payloadid, email, role
.
Signaturevia SECRET_KEY

The Signature proves authenticity — generated by combining the Header, Payload, and your SECRET_KEY. If even one character changes, the signature is invalid.

⚠ Never store passwords in the payload — only safe data like id, email, role.

🔁 Full Login Flow

Step-by-step authentication sequence:

1. User sends credentials (email + password)
2. Server verifies via bcrypt
3. Server generates JWT using jwt.sign()
4. JWT is stored inside an httpOnly cookie
5. Browser auto-sends cookie on every request
6. Protected routes verify token via middleware

JavaScript — Login & Signing
// 1. Verify credentials against DB
let isMatch = await bcrypt.compare(password, userDB.password);

// 2. If match, sign a token (id + email + role combined with Secret)
let token = jwt.sign(
  { id: userDB._id, email: userDB.email, role: userDB.role },
  process.env.JWT_SECRET,
  { expiresIn: '1d' }
);

// 3. Store token in an httpOnly cookie (best security settings)
res.cookie('token', token, {
  httpOnly: true,   // JS cannot read — prevents XSS theft
  secure:   true,   // HTTPS only
  sameSite: "lax",  // CSRF protection
  maxAge: 1000 * 60 * 60 * 24, // 1 day in ms
  path: "/"
});
res.status(200).json({ message: "Valid Token Generated" });
// 🍪 Token Storage
The token is stored in a cookie. Cookies act as a container and are automatically sent with every subsequent request — no manual work needed on the frontend.

🔁 Request Flow

Every incoming request is classified as either Public or Protected. The path each takes is fundamentally different.

Public Routes

📨
Request arrives No authentication required
Reaches route handler No middleware involved
📤
Response sent Operation executes successfully

Protected Routes

📨
Request arrives Cookie auto-attached by browser
🛡️
verifyToken runs first Before the route handler
🍪
Token exists in cookies? ❌ No → 401 Unauthorized
🔑
jwt.verify(token, SECRET) Checks signature ✅ & expiry ✅
👤
req.user is populated Holds id, email, role from token
🎭
Role check (Authorization) Is this user allowed to do this?
📤
Proceeds to route handler Operation executes successfully

The Gatekeeper — verifyToken Middleware

Public routes are served normally. Protected routes first hit this middleware. It checks if the cookie exists, verifies the signature, and checks the expiration.

JavaScript — verifyToken Middleware
export function verifyToken(req, res, next) {
  const token = req.cookies.token;

  // If no token, the protected request fails here → 401 Unauthorized
  if (!token) return res.status(401).json({ message: "No token found" });

  try {
    // Verify Signature ✅ & Expiry ✅
    const decoded = jwt.verify(token, process.env.JWT_SECRET);

    // Store payload in req.user for cleaner downstream logic
    req.user = decoded;  // { id, email, role }

    next(); // Proceed to Role Check or Route Handler
  } catch (err) {
    return res.status(401).json({ message: "Invalid or Expired Token" });
  }
}
// Why store in req.user?
After verification, req.user contains the decoded token. This avoids repeatedly parsing the token and keeps route handlers clean.

⚠️ For sensitive operations, you may still want to validate against the DB.
🎭 Authorization

After verifyToken succeeds, check whether this user actually has permission for this action. Only role: "admin" should delete other users. Authentication proves who you are — authorization decides what you can do.

⚡ Stateless JWT

No session storage on server
All user info lives inside the token ✅
Server only verifies the signature.

Makes JWT fast and scalable, but requires careful handling of expiry and security.

🚨 Common Mistakes

Forgetting withCredentials on axios
Missing cookie-parser middleware
Manually sending token in headers instead of cookies
Using secure: true on localhost (breaks dev)
Not handling token expiry on the frontend

Logout — Clearing the Cookie

JWTs are stateless. Logout simply instructs the browser to empty the cookie container. Future requests arrive without a token and fail the verifyToken check. The token is not destroyed on the server — the cookie is simply removed from the browser.

JavaScript — Logout
res.clearCookie('token', {
  httpOnly: true,
  secure:   true,
  sameSite: "lax",
  path:     "/"
});
res.status(200).json({ message: "Logged out successfully" });

// Summary

JWT authentication works by generating a signed token on login, storing it in an httpOnly cookie, and verifying it on each protected request using middleware. If the token is valid and not expired, access is granted — otherwise the request is rejected. The role stored inside the token is then used for authorization, controlling what each authenticated user is permitted to do.

// Frontend Requirement ⚠️
When using cookies, every axios request must include:
axios.get(url, { withCredentials: true })
Without this, cookies will not be sent and authentication will fail.

DOM Manipulation

Vanilla JS DOM manipulation is imperative — you tell the browser exactly what to do, step by step. React is declarative — you describe what the UI should look like and React handles the DOM.

JavaScript — DOM Events
let submitbtn = document.querySelector(".sb");
let parent    = document.querySelector(".parent");

submitbtn.addEventListener('click', (e) => {
  e.preventDefault(); // Stop default form submission

  let newChild = document.createElement('p');
  newChild.textContent = "Hello World!";
  newChild.style.color  = "red";
  parent.appendChild(newChild);
});

CSS Fundamentals & Layouts

Default Browser CSS

Before you write a single line of CSS, the browser already applies its own built-in stylesheet called the User-Agent Stylesheet. This is why an unstyled <h1> is big and bold, <a> tags are blue and underlined, and <ul> has bullets — you didn't add any of that, the browser did.

User-Agent Stylesheet

Every browser ships with default styles. Chrome, Firefox, and Safari all have slightly different defaults — which is why sites can look inconsistent across browsers without a reset.

CSS Reset

A CSS reset removes all browser defaults so you start from zero. The most common is * { margin:0; padding:0; box-sizing:border-box; }. Tailwind's Preflight does this automatically.

Cascade & Specificity

When multiple rules target the same element, the browser picks the winner by specificity: inline style > ID > class > tag. Equal specificity = last rule wins (the "cascade").

CSS — What the browser applies by default (you never wrote this)
/* Browser default styles — applied before your CSS */
h1 { font-size: 2em; font-weight: bold; margin: 0.67em 0; }
p  { margin: 1em 0; }
a  { color: blue; text-decoration: underline; }
ul { list-style-type: disc; padding-left: 40px; }
body { margin: 8px; } /* That gap around the page edge? This. */

/* Common reset to neutralize browser defaults */
* {
  margin:     0;
  padding:    0;
  box-sizing: border-box; /* padding/border included in width */
}

Common Properties in style.css

These are the properties you'll use constantly in every project — spacing, typography, sizing, and color.

CSS — Spacing, Typography & Sizing
.box {
  /* SPACING */
  margin:  20px;           /* space OUTSIDE the element (pushes away neighbors) */
  margin:  10px 20px;       /* top/bottom  left/right */
  margin:  10px 20px 5px 0; /* top right bottom left (clockwise) */
  padding: 16px;            /* space INSIDE the element (between border & content) */

  /* SIZING */
  width:      300px;
  max-width:  100%;          /* never exceed this width */
  height:     200px;
  min-height: 100vh;         /* at least full viewport height */

  /* TYPOGRAPHY */
  font-size:   16px;
  font-weight: 700;           /* 400=normal 700=bold */
  line-height: 1.6;           /* 1.6× the font-size, improves readability */
  letter-spacing: 0.5px;
  text-transform: uppercase;  /* lowercase | capitalize */
  text-decoration: none;      /* removes underline from links */
  color: #333;

  /* BACKGROUND & BORDER */
  background-color: #fff;
  border: 1px solid #ccc;    /* width style color */
  border-radius: 8px;         /* rounds corners */
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}

text-align vs align-items

These are the most commonly confused CSS properties. They look similar but work completely differently.

text-align

Aligns inline content (text, inline elements) horizontally within its block container.

Works on: the parent that contains the text.

Values: left center right justify

e.g. centering text inside a <p> or <div>

align-items

Aligns flex/grid children along the cross axis (perpendicular to the main axis).

Works on: the flex/grid container — has no effect without display: flex.

Values: flex-start center flex-end stretch

e.g. vertically centering items inside a flex row

CSS — text-align vs align-items
/* text-align: for text/inline content inside a block */
.heading {
  text-align: center; /* centers the text horizontally */
}

/* align-items: for flex/grid children on the cross axis */
.navbar {
  display:      flex;
  align-items:  center; /* vertically centers children in the row */
  justify-content: space-between; /* main axis (horizontal here) */
}

/* Common confusion: text-align on a flex container */
.card {
  display: flex;
  text-align: center;  /* ✓ still works for text inside children */
  align-items: center; /* ✓ vertically centers the flex children */
}
// Rule of thumb: text-align = for text. align-items = for boxes/elements inside a flex or grid container. If you want to center both text AND elements, you often need both.

CSS Positions

static

Default for every element. Follows normal document flow. top / left / right / bottom have no effect whatsoever.

relative

Offset from its own normal position. Original space is still reserved — neighbours don't move. Good for minor nudges or creating a positioned parent for absolute children.

absolute

Removed from normal flow entirely. Positioned relative to the nearest ancestor that has position: relative/absolute/fixed. Other elements act like it doesn't exist.

fixed

Positioned relative to the viewport. Doesn't move on scroll. Used for navbars, floating buttons, modals. Removed from normal flow.

sticky

Hybrid of relative + fixed. Acts relative while in view, then sticks when you scroll past its threshold. Requires top (or left/right/bottom) to activate.

CSS — Positioning Examples
/* relative: small nudge, still in flow */
.badge { position: relative; top: -2px; }

/* absolute inside relative parent — classic pattern */
.card    { position: relative; }
.overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }

/* fixed navbar — always at top of viewport */
.navbar { position: fixed; top: 0; left: 0; width: 100%; }

/* sticky table header — sticks when you scroll table */
th { position: sticky; top: 0; background: #fff; }
/* ⚠ Won't work if a parent has overflow: hidden/auto */

Flex Layout

Flexbox is a one-dimensional layout system — it arranges items in a row or a column. Perfect for navbars, card rows, centering content, and any time items need to be spaced or aligned along one axis.

Main Axis vs Cross Axis

The main axis is the direction items flow (row = horizontal, column = vertical). The cross axis is always perpendicular to it. justify-content controls main axis, align-items controls cross axis.

flex-wrap

By default, flex items squish to fit one line (nowrap). Set flex-wrap: wrap to let them overflow onto the next line when they run out of space — great for responsive grids.

CSS — Flexbox Complete Reference
/* === CONTAINER PROPERTIES === */
.container {
  display:         flex;
  flex-direction:  row;            /* row | row-reverse | column | column-reverse */
  flex-wrap:       wrap;           /* nowrap | wrap | wrap-reverse */

  /* Main axis (horizontal by default) */
  justify-content: space-between;  /* flex-start | center | flex-end | space-around | space-evenly */

  /* Cross axis (vertical by default) */
  align-items:     center;         /* flex-start | center | flex-end | stretch | baseline */

  gap: 16px;                        /* space between items (row-gap column-gap) */
}

/* === ITEM PROPERTIES === */
.item {
  flex: 1;          /* shorthand for flex-grow:1 flex-shrink:1 flex-basis:0 */
                    /* all items grow equally to fill available space */
  flex-grow: 1;     /* how much this item grows relative to others */
  flex-shrink: 0;  /* 0 = refuse to shrink below flex-basis */
  flex-basis: 200px; /* starting size before grow/shrink kicks in */
  align-self: flex-end; /* override align-items for THIS item only */
}

/* Common pattern: perfect centering */
.center {
  display:         flex;
  justify-content: center;
  align-items:     center;
}

Grid Layout

Grid is a two-dimensional layout system — it controls both rows and columns simultaneously. Perfect for page layouts, dashboards, image galleries, and any structure that has both horizontal and vertical arrangement.

fr unit

The fr (fraction) unit divides the remaining available space. 1fr 1fr 1fr = three equal columns. 2fr 1fr = first column is twice as wide as the second.

repeat()

repeat(4, 1fr) is shorthand for 1fr 1fr 1fr 1fr. Use repeat(auto-fill, minmax(200px, 1fr)) for a responsive grid with no media queries.

CSS — Grid Complete Reference
/* === CONTAINER PROPERTIES === */
.grid {
  display:               grid;
  grid-template-columns: 1fr 1fr 1fr 1fr;      /* 4 equal columns */
  grid-template-columns: repeat(4, 1fr);       /* same, shorthand */
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); /* responsive! */
  grid-template-rows:    100px auto 60px;       /* header auto footer */
  gap:        16px;                             /* gap between all cells */
  row-gap:    20px;                             /* gap between rows only */
  column-gap: 10px;                             /* gap between columns only */
}

/* === ITEM PLACEMENT (two ways) === */

/* Longhand */
.hero {
  grid-column-start: 1;   /* start at column line 1 */
  grid-column-end:   3;   /* end at column line 3 (spans 2 columns) */
  grid-row-start:    1;
  grid-row-end:      2;
}

/* Shorthand (preferred) */
.hero {
  grid-column: 1 / 3;  /* start / end */
  grid-column: 1 / span 2; /* start / span count (same result) */
  grid-row:    1 / 2;
}

/* span entire row */
.full-width { grid-column: 1 / -1; } /* -1 = last line */
// Flex vs Grid: Use Flex when you have a single row/column of items and want them to flow and space themselves. Use Grid when you have a two-dimensional layout with both rows and columns to control.

Breakpoints, Media Queries & Units

Responsive design means your layout adapts to different screen sizes. A breakpoint is a specific screen width at which the design needs to change. When a breakpoint is breached, media queries apply new CSS to re-arrange elements for that viewport.

Media Queries

CSS — Media Query Syntax
/*  @media  media-type  operator  (media-feature)  { styles }  */
/*          ----------  --------  ---------------             */
/*          screen      and       max-width: 768px            */
/*          print       not       min-width: 480px            */
/*          speech      or        orientation: landscape      */
/*          all                   prefers-color-scheme: dark  */

/* screen — for any screen device (phone, tablet, desktop) */
@media screen and (max-width: 768px) {
  .container { flex-direction: column; }
  .sidebar   { display: none; }
}

/* print — styles only applied when printing the page */
@media print {
  .navbar, .ads { display: none; }
}

/* multiple conditions with and */
@media screen and (min-width: 768px) and (max-width: 1024px) {
  /* tablet only */
}

/* dark mode media query */
@media (prefers-color-scheme: dark) {
  body { background: #111; color: #fff; }
}

Common Breakpoints

Mobile
max-width: 480px — phones in portrait. Usually the default since mobile-first is the standard approach.
→ < 480px
Tablet
481px – 768px — tablets and large phones in landscape. Often switch from single to two column layouts.
→ 481px – 768px
Laptop
769px – 1024px — small laptops and tablets in landscape. Most complex layouts start here.
→ 769px – 1024px
Desktop
1025px and above — full desktop layouts with sidebars, multi-column grids, large typography.
→ > 1025px
CSS — Mobile-First Approach (recommended)
/* Mobile-First: write base styles for mobile, override upward */
/* Use min-width queries — "at this size and ABOVE, apply this" */

.grid {
  display: grid;
  grid-template-columns: 1fr;     /* mobile: single column */
  gap: 16px;
}

@media (min-width: 768px) {
  .grid { grid-template-columns: 1fr 1fr; }  /* tablet: 2 col */
}

@media (min-width: 1024px) {
  .grid { grid-template-columns: repeat(4, 1fr); } /* desktop: 4 col */
}

/* Desktop-First uses max-width (override downward) — less preferred */
@media (max-width: 768px) {
  .grid { grid-template-columns: 1fr; }
}
// Mobile-first vs Desktop-first: Mobile-first (min-width) is the recommended approach — design for the smallest screen first, then enhance upward. Desktop-first (max-width) starts large and overrides downward, which often leads to more complex overrides.

CSS Units

The root of any HTML document is the <html> tag. This matters because rem is always calculated relative to the font size set on it. Understanding when to use which unit is critical for accessible, responsive designs.

px — Pixels (avoid for text)

Fixed, absolute unit. Does not scale when the user changes their browser font size in settings. Using px for text breaks accessibility — never use it for font-size.

OK for: borders, box-shadow, fine-tuned details

em — Relative to parent

1em = the font-size of the parent element. Compounds up the tree — if a parent is 1.2em and a child is 1.2em, the child is actually 1.44× the root. Can cause unexpected sizes in nested elements.

Use for: padding/margin that should scale with its own component's font size

rem — Relative to root ✓

1rem = the font-size of the <html> (root) element. Defaults to 16px in all browsers. Does NOT compound. Adjusts when the user changes their browser font settings. Use this as your primary unit.

Use for: font-size, spacing, layout — almost everything

% — Percentage

Relative to the parent element's width (for width/padding/margin). Very useful for fluid layouts. width: 50% = half of whatever the parent is.

Use for: fluid widths, responsive containers

vw / vh — Viewport

1vw = 1% of the viewport width. 1vh = 1% of the viewport height. Independent of any parent — purely based on the current screen size.

Use for: full-screen heroes, fixed-height sections, font scaling at large sizes

CSS — Units in Practice
/* Root font-size — 1rem = 16px by default */
/* Trick: set to 62.5% so 1rem = 10px (easy math) */
html { font-size: 62.5%; } /* now 1.6rem = 16px */

/* ✗ Bad — won't respect browser font size settings */
p { font-size: 16px; }

/* ✓ Good — scales with browser settings */
p    { font-size: 1rem; }    /* 16px by default */
h1   { font-size: 2.5rem; }  /* 40px by default */
small{ font-size: 0.875rem;}  /* 14px by default */

/* em: scales with parent — good for component-relative spacing */
.btn {
  font-size: 1rem;
  padding: 0.75em 1.5em; /* scales with button's own font-size */
}

/* % for fluid widths */
.container { width: 90%; max-width: 1200px; }

/* vw/vh for viewport-based sizing */
.hero   { height: 100vh; }         /* full screen height */
.banner { font-size: 5vw; }         /* text scales with screen width */
⚠ The em compounding trap: if .parent has font-size: 1.2em and .child inside it also has font-size: 1.2em, the child ends up at 1.44em of the root — not 1.2em. Use rem to avoid this cascading effect entirely.
// Rule of thumb: Use rem for almost everything. Use em only when you specifically want something to scale relative to its own component's font-size (like button padding). Use px only for fine details like borders and shadows.

Tailwind CSS

Tailwind is a utility-first CSS framework. Instead of writing custom CSS, you compose styles by applying small utility classes directly in your HTML. The build process scans your HTML and generates only the CSS you actually used — nothing more.

How the Build Works

The Tailwind CLI scans your HTML, finds every utility class you used (e.g. text-red-500, flex, p-4), and writes a minimal output.css containing only those styles. The result is an extremely lean CSS file in production.

Utility-First Philosophy

No switching between HTML and CSS files. Every style lives directly on the element as a class. Faster to build, and you can see what an element looks like just by reading its HTML markup.

Tailwind Preflight

Preflight is Tailwind's built-in CSS reset, automatically injected when you add @tailwind base to your input CSS. It removes all browser default styles so you start from a completely clean, consistent baseline across every browser.

What Preflight Removes

All default margins and padding. Default h1–h6 sizes and bold weights. Bullet points on ul/ol. Blue underline on <a>. Default border on <button>. The 8px body margin.

What Preflight Adds

Sets box-sizing: border-box on everything. Makes images display: block by default (removes inline bottom gap). Inherits font on form elements. Sets line-height to a sane default.

Why It Matters

Without Preflight, a Tailwind <h1> would still be huge and bold — you'd need text-base font-normal to fight the browser. With Preflight, <h1> looks identical to <p> until YOU add classes.

CSS — What Preflight does under the hood (simplified)
/* Preflight essentially does this for you: */
*, *::before, *::after {
  box-sizing: border-box; /* padding/border included in width */
  border-width: 0;
  border-style: solid;
}

body { margin: 0; line-height: inherit; }

/* headings reset — no size, no weight */
h1, h2, h3, h4, h5, h6 { font-size: inherit; font-weight: inherit; }

/* links reset — no color, no underline */
a { color: inherit; text-decoration: inherit; }

/* lists reset — no bullets, no padding */
ol, ul { list-style: none; margin: 0; padding: 0; }

/* images block — removes bottom whitespace gap */
img, svg, video { display: block; vertical-align: middle; }

/* buttons reset */
button { background-color: transparent; background-image: none; }
⚠ Preflight side effect: After adding Tailwind, all your existing unstyled HTML elements will look plain and unstyled. A <button> loses its 3D border. An <h1> looks like normal text. This is intentional — you now add all styles explicitly via Tailwind classes.
Shell — Step 1: Install Tailwind CLI
# Install Tailwind CSS as a dev dependency
npm install -D tailwindcss @tailwindcss/cli

# OR use it without installing via npx (no install needed)
npx @tailwindcss/cli --help
CSS — Step 2: Create your input style.css
/* style.css — this is your INPUT file, not what you link in HTML */
/* These 3 directives import all of Tailwind's layers */
@tailwind base;        /* Resets + base element styles */
@tailwind components;  /* Component classes (rarely used) */
@tailwind utilities;   /* All the utility classes like flex, p-4, etc. */
Shell — Step 3: Run the CLI in Watch Mode
# -i ./style.css   → your input file with @tailwind directives
# -o ./output.css  → the generated file (link THIS in your HTML)
# --watch          → auto-rebuilds whenever you save any file
npx @tailwindcss/cli -i ./style.css -o ./output.css --watch

# Add as an npm script in package.json for convenience:
# "scripts": { "tw": "npx @tailwindcss/cli -i ./style.css -o ./output.css --watch" }
# Then just run: npm run tw
HTML — Step 4: Link the OUTPUT file in your HTML
<head>
  <!-- Link output.css, NOT style.css -->
  <link rel="stylesheet" href="./output.css">
</head>
<body>
  <!-- Now use Tailwind classes freely -->
  <div class="flex gap-4 p-6 bg-gray-900 rounded-lg">

    <button class="px-4 py-2 bg-emerald-400 text-black font-bold
                   rounded hover:bg-emerald-300 transition-colors">
      Click me
    </button>

    <!-- sm: md: lg: are responsive breakpoint prefixes -->
    <p class="text-sm md:text-base lg:text-lg text-gray-400">
      Responsive text
    </p>

  </div>
</body>

Tailwind Intellisense in VS Code

Without Intellisense, you're typing Tailwind classes from memory. The official VS Code extension gives you autocomplete, hover previews, and linting — essential for real development speed.

Autocomplete

As you type inside a class="" attribute, it suggests every matching Tailwind class. Press Tab to complete. Shows the actual CSS value it maps to alongside.

Hover Preview

Hover over any Tailwind class in your HTML and a tooltip shows you the exact CSS it generates. No more guessing what p-6 or text-xl actually outputs.

Class Sorting (Prettier)

Install the Prettier plugin for Tailwind to auto-sort your classes into a consistent recommended order every time you save. Keeps code readable across teams.

Shell — Installing Intellisense & Prettier Plugin
# Step 1: Install the VS Code extension
# Open VS Code → Extensions (Ctrl+Shift+X)
# Search: "Tailwind CSS IntelliSense" by Tailwind Labs → Install

# Step 2 (Optional but recommended): Auto-sort classes with Prettier
npm install -D prettier prettier-plugin-tailwindcss

# Create a .prettierrc file in your project root:
# {
#   "plugins": ["prettier-plugin-tailwindcss"]
# }

# Now every time you save, classes sort automatically!
// Intellisense tip: The extension works automatically once installed — it detects Tailwind classes in class="", className="" (React), and even inside template literals. No extra config needed for basic use.
⚠ Common mistake: Linking style.css (input) instead of output.css (generated) in your HTML. Your page will have zero styles. Always link the output file.

React Core Concepts

React uses a Virtual DOM — a lightweight in-memory representation of the real DOM. When state changes, React diffs the virtual DOM and updates only the changed parts in the real DOM.

Declarative vs Imperative

React is declarative: you describe what the UI should look like for a given state. React figures out the DOM operations. Vanilla JS is imperative: you write each DOM step manually.

Rendering Lists

Use .map() to loop through data and return JSX. Always provide a unique key prop to help React track elements efficiently.

JSX — Conditional Rendering Alternatives
// Alt 1: Ternary Operator — if/else, always use this for two outcomes
{ isActive ? <ActiveComponent /> : <InactiveComponent /> }

// Alt 2: Logical AND — render only if true (no else needed)
{ isActive && <ActiveComponent /> }

// Rendering a list with .map()
{ marks.map((m, i) => <div key={i}>{m}</div>) }

State & Mutations

What useState Does

useState is a React Hook that lets you add state to functional components. It returns an array with two values: the current state and a function to update it.

Why We Need It

State allows components to re-render dynamically when data changes. Without state, components would always display static values. It also persists values across renders, unlike local variables which reset each time.

Perfect for handling user interactions (form inputs, toggles, counters) and keeping logic self-contained at the component level.

JSX — useState Example
// Import useState from React
import { useState } from "react";

function Counter() {
  // Declare state variable 'count' with initial value 0
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      // Update state using setCount
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}
JSX — Array & Object State
let [marks, setMarks] = useState([1, 2]);

// Add: Spread creates new array, triggers re-render
const pushed = () => {
  setMarks([...marks, 123]);
};

// Delete: filter() returns a brand new array
const delMark = (index) => {
  setMarks(marks.filter((_, i) => i !== index));
};

// Object State: spread existing, then override property
let [user, setUser] = useState({ email: "test@gmail.com" });

const updateCity = () => {
  setUser({ ...user, city: "hyd" }); // New object, all old fields kept
};
⚠ Golden Rule: NEVER mutate state directly. Always create a new copy using spread or array methods that return new arrays. Direct mutation won't trigger a re-render.

Form Handling

Shell — Installation
# Install react-hook-form
npm install react-hook-form
Alt 1: Vanilla React

Verbose. Requires separate useState for every single input field, plus manual onChange handlers and value bindings. Gets messy fast.

Alt 2: react-hook-form

Designed for performance. Minimal re-renders, built-in validation, single register call per input. Preferred approach.

JSX — react-hook-form
import { useForm } from 'react-hook-form';

function MyForm() {
  const { register, handleSubmit } = useForm();

  const submitForm = (data) => {
    console.log(data); // { username: "...", email: "..." }
  };

  return (
    // handleSubmit: validates then passes data to submitForm
    <form onSubmit={handleSubmit(submitForm)}>
      // register: injects onChange/onBlur, names the field
      <input {...register("username")} />
      <input {...register("email")}    />
      <button>Submit</button>
    </form>
  );
}
JSX — react-hook-form with Validation Rules
function MyForm() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  return (
    <form onSubmit={handleSubmit(data => console.log(data))}>

      <input
        {...register("username", {
          required: "Username is required",         // message shown on error
          minLength: { value: 3, message: "Min 3 chars" },
          maxLength: { value: 20, message: "Max 20 chars" },
        })}
      />
      { errors.username && <span>{errors.username.message}</span> }

      <input
        {...register("email", {
          required: "Email required",
          pattern: {
            value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
            message: "Invalid email format"
          }
        })}
      />
      { errors.email && <span>{errors.email.message}</span> }

      <button>Submit</button>
    </form>
  );
}

useEffect & Side Effects

Manages operations outside the normal render cycle — API calls, subscriptions, timers, DOM manipulation. Runs after the component renders to the screen.

No Dependency Array

Runs after every single render. Dangerous — causes infinite loops if you set state inside. Almost always wrong.

Empty Array []

Runs only once after the initial render. Perfect for initial data fetching, subscriptions, and setup.

[state1] With Dependencies

Runs on initial render AND whenever any listed dependency changes. Great for re-fetching on filter/search changes.

JSX — useEffect
import { useEffect, useState } from 'react';

const [data, setData] = useState([]);
const [filter, setFilter] = useState('all');

// Run once on mount — initial data fetch
useEffect(() => {
  fetch('https://api.example.com/data')
    .then(r => r.json())
    .then(d => setData(d));
}, []); // ← Empty array = mount only

// Re-run whenever 'filter' changes
useEffect(() => {
  fetch(`https://api.example.com/data?type=${filter}`)
    .then(r => r.json())
    .then(d => setData(d));
}, [filter]); // ← Re-runs when filter changes

React Router

React has no built-in routing. React Router is the standard external library. It intercepts URL changes and renders the matching component — the browser never makes a full page reload, giving you a true Single Page Application (SPA).

Shell — Installation
# React Router v7+ (both packages merged into one)
npm install react-router

# Before v7 you needed both:
# npm install react-router react-router-dom

Core Concepts

Root Layout

Decide your root layout before routing. The root component (App.jsx) renders the persistent shell — Navbar, Footer, etc. The main content area is a placeholder that swaps per route.

<Outlet />

Placed in the root layout where child routes should render. Think of it as a slot — when you navigate to /login, the Login component fills the Outlet. The rest of the layout stays unchanged.

createBrowserRouter

Creates the routing configuration object. Takes an array of route objects, each with a path and element. Nested routes use a children array. Pass the result to <RouterProvider>.

NavLink vs <a>

Never use <a href> in React Router — it triggers a full page reload, losing all state. <NavLink> navigates without reload and automatically adds an active class to the current link.

JSX — Setting Up Routes (main.jsx)
import { createBrowserRouter, RouterProvider, Navigate } from 'react-router';

const router = createBrowserRouter([
  {
    path: "/",
    element: <RootLayout />,
    children: [

      // ⚠ WHITE PAGE FIX — without this, "/" shows RootLayout with empty Outlet
      // Option A: path:"" redirects "/" to first route
      { path: "", element: <Navigate to="home" /> },

      // Option B: index:true — marks the default child, cleaner syntax (same result)
      // { index: true, element: <Home /> }

      { path: "home",     element: <Home />     },
      { path: "register", element: <Register /> },
      { path: "login",    element: <Login />    },
      { path: "products", element: <Products /> },
    ]
  }
]);

export default function App() {
  return <RouterProvider router={router} />;
}
⚠ White Page Problem: When the app loads at /, it renders RootLayout — but which child fills the <Outlet />? Nothing! Blank page. Fix: add path: "" with a <Navigate> redirect, or use index: true to mark which child loads by default.
JSX — RootLayout.jsx with Outlet & NavLink
import { Outlet, NavLink } from 'react-router';

function RootLayout() {
  return (
    <div>
      <nav>
        {/* NavLink auto-adds 'active' class — use className fn for custom styling */}
        <NavLink
          to="home"
          className={({ isActive }) => isActive ? "text-white font-bold" : "text-gray-400"}
        >Home</NavLink>

        <NavLink to="products"
          className={({ isActive }) => isActive ? "text-white font-bold" : "text-gray-400"}
        >Products</NavLink>
      </nav>

      {/* Child route components render here */}
      <main>
        <Outlet />
      </main>
    </div>
  );
}
// Outlet at every level: If a route has its own children, the parent component must render <Outlet /> somewhere inside it — otherwise child routes have nowhere to render. This applies at every nesting level, not just the root.

Programmatic Navigation

Sometimes you need to navigate from code — after a form submit, a button click, or a successful API call. Use the useNavigate hook. To pass data between routes without putting it in the URL, use the state option.

useNavigate

Returns a navigate() function. Call it with a path string to go there. Use navigate(-1) to go back (like browser back button). Can pass a state object as the second argument.

useLocation

When you navigate with a state object, the destination component uses useLocation() to read it. Returns the current location object including pathname, search, and state.

Navigate Component

Use <Navigate to="path" /> inside JSX for declarative redirects — e.g. redirect the bare / route to /home, or redirect unauthenticated users away from protected pages.

JSX — useNavigate with State Transfer
import { useNavigate, useLocation } from 'react-router';

// ===== SENDER COMPONENT (e.g. ProductsList) =====
function ProductsList() {
  const navigate = useNavigate();

  const gotoProduct = (productObj) => {
    // Pass product data through state — no URL params needed
    navigate('/products', { state: { product: productObj } });
  };

  return (
    <div>
      {/* Function NEEDS a parameter → wrap in arrow to pass the value */}
      <div onClick={() => gotoProduct(item)}>Click me</div>

      {/* Function has NO parameters → pass directly, no wrapper needed */}
      <button onClick={handleClose}>Close</button>
    </div>
  );
}

// ===== RECEIVER COMPONENT (e.g. ProductDetail) =====
function ProductDetail() {
  const { state } = useLocation(); // Read state from previous navigate()
  const product = state?.product;   // Optional chain — safe if navigated directly

  return <h1>{product?.title}</h1>;
}

// navigate(-1) — go back to previous route (browser back)
// navigate('/login', { replace: true }) — replace history entry (no back button)
// Interview Q: What's the difference between Link, NavLink, and useNavigate? — Link = basic navigation, no active styling. NavLink = same + automatic active class for current route. useNavigate = programmatic navigation from JS code (not from JSX).

Full Pattern: Fetch + Filter + Sort + Navigate

This is the complete real-world pattern combining useEffect for fetching, useState for loading/error/data, react-hook-form for search, and useNavigate for row clicks. This comes up constantly in interviews.

loading / error / data Pattern

Always manage 3 states for async data: loading (show spinner), error (show error message), data (show content). Return early for the first two before rendering the main JSX.

filteredData vs rawData

Keep the original fetched array in products and a separate filteredProducts for display. Search and sort operate on filteredProducts. Reset to products when search is cleared.

.sort() mutates — always copy first

.sort() modifies the array in place. In React, mutating state directly skips re-renders. Always spread first: [...filteredProducts].sort(...) before calling setFilteredProducts.

JSX — ProductsList Complete Pattern
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';

function ProductsList() {
  const { register, handleSubmit } = useForm();
  const navigate = useNavigate();

  const [products, setProducts]               = useState([]);
  const [filteredProducts, setFiltered]      = useState([]);
  const [loading, setLoading]               = useState(false);
  const [error, setError]                   = useState(null);

  // 1. FETCH on mount
  useEffect(() => {
    setLoading(true);
    async function getProducts() {
      try {
        let res = await fetch('https://fakestoreapi.com/products');
        if (res.status !== 200) throw new Error('Failed to fetch');
        let data = await res.json();
        setProducts(data);
        setFiltered(data);     // both start as the full list
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);      // always runs, success or fail
      }
    }
    getProducts();
  }, []);

  // 2. EARLY RETURNS for loading/error states
  if (loading) return <p>Loading...</p>;
  if (error)   return <p>Error: {error}</p>;

  // 3. SEARCH by title or exact category
  const searchProduct = ({ searchInput }) => {
    const term = searchInput.toLowerCase().trim();
    if (!term) { setFiltered(products); return; }
    setFiltered(products.filter(p =>
      p.title.toLowerCase().includes(term) // substring match for title
      || p.category.toLowerCase() === term  // exact match for category
    ));
  };

  // 4. SORT by price — spread first! .sort() mutates
  const sortByPrice = (dir) => {
    const copy = [...filteredProducts];
    copy.sort((a, b) => dir === 'low' ? a.price - b.price : b.price - a.price);
    setFiltered(copy);
  };

  return (
    <div>
      <form onSubmit={handleSubmit(searchProduct)}>
        <input {...register('searchInput')} placeholder="Search title or category" />
        <button type="submit">Search</button>
        <select onChange={e => sortByPrice(e.target.value)}>
          <option value="">Sort</option>
          <option value="low">Low → High</option>
          <option value="high">High → Low</option>
        </select>
      </form>

      {/* Grid of product cards */}
      <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
        {
          filteredProducts.length === 0
            ? <p>No products found</p>
            : filteredProducts.map(p => (
                <div
                  key={p.id}
                  onClick={() => navigate('/products', { state: { product: p } })}
                >
                  <img src={p.image} alt={p.title} />
                  <p>{p.title}</p>
                  <p>${p.price}</p>
                </div>
              ))
        }
      </div>
    </div>
  );
}
// .sort() comparison: negative = a before b, positive = b before a, 0 = unchanged
// a.price - b.price → ascending (low first)
// b.price - a.price → descending (high first)
⚠ Common interview mistake: Using .includes() for category matching. This causes partial matches — searching "men" would also return "women's clothing". Use === (strict equality) for categories, and .includes() only for title substring search.

19 — API Requests from Components

There are two main ways to make HTTP requests from a React component: the built-in fetch API and the third-party axios library.

1. fetch (built-in)

Native browser API — no install needed. More verbose; you handle headers, body serialization, and error detection manually.

// GET request — method defaults to "GET" if omitted
const res  = await fetch("https://api.example.com/data", { method: "GET" });
const data = await res.json();  // must manually extract JSON
// data → { message: "", payload: "" }  — you destructure yourself

// fetch only rejects on network failure, NOT on 4xx/5xx — check manually:
if (!res.ok) throw new Error(`HTTP error: ${res.status}`);

// POST request
const resObj = await fetch("https://api.example.com/users", {
  method:  "POST",
  headers: { "Content-Type": "application/json" },  // must set manually
  body:    JSON.stringify({ name: "Alice", age: 25 }),  // must serialise manually
});
if (!resObj.ok) throw new Error("Request failed");
const created = await resObj.json();
⚠️ fetch does NOT throw on 4xx/5xx. A 404 or 500 response is still a "resolved" promise. Always check res.ok (true when status 200–299) or compare res.status manually.

2. axios (third-party)

Install: npm i axios — then import per file. Less verbose, auto-parses JSON, and automatically throws on 4xx/5xx responses.

import axios from "axios";

// GET request
const resObj = await axios.get("https://api.example.com/data");
const data   = resObj.data;  // JSON already extracted — no .json() call needed

// POST request — pass object directly, axios serialises it automatically
const resObj2 = await axios.post("https://api.example.com/users", { name: "Alice", age: 25 });
const created  = resObj2.data;

// Accessing a protected route — send JWT in Authorization header
const token = localStorage.getItem("token");
const profile = await axios.get("/api/profile", {
  headers: { Authorization: `Bearer ${token}` },
});

// Error handling — axios throws on 4xx/5xx automatically
try {
  const res = await axios.get("/api/secret");
} catch (err) {
  console.log(err.response.status);   // e.g. 401
  console.log(err.response.data);     // server error body
  console.log(err.message);           // human-readable message
}

fetch vs axios — quick comparison

Feature fetch axios
Install needed ❌ built-in ✅ npm i axios
Auto JSON parse ❌ call .json() ✅ resObj.data
Throws on 4xx/5xx ❌ manual check ✅ automatic
Set Content-Type ❌ manual ✅ automatic
Stringify body ❌ JSON.stringify() ✅ automatic
Error object basic Error err.response.data/status

19b — React Fragments & Toast Notifications

React Fragment <></>

A Fragment lets you group multiple sibling elements without adding an extra DOM node. Use it when a wrapper <div> would break layout or add unwanted styling.

// Without fragment — adds an unnecessary div to the DOM
return (
  <div>
    <h1>Title</h1>
    <p>Subtitle</p>
  </div>
);

// With fragment — no extra DOM node
return (
  <>
    <h1>Title</h1>
    <p>Subtitle</p>
  </>
);

// Long-form (needed if you want to pass a key prop, e.g. in lists)
import { Fragment } from "react";
items.map(item => (
  <Fragment key={item.id}>
    <dt>{item.term}</dt>
    <dd>{item.def}</dd>
  </Fragment>
));
💡 Rule of thumb: if the parent wrapper has no className, id, or event handler — use <></> instead.

Toast Notifications — react-hot-toast

Toasts are small, auto-dismissing UI messages used to give the user non-blocking feedback (success, error, loading, etc.).

// 1. Install
npm i react-hot-toast

// 2. Add <Toaster> once in your root component (App.jsx)
import { Toaster } from "react-hot-toast";

function App() {
  return (
    <>
      <Toaster position="top-right" reverseOrder={false} />
      {/* rest of your app */}
    </>
  );
}

// 3. Trigger toasts from any component
import { toast } from "react-hot-toast";

toast.success("Logged in successfully!");   // green ✓
toast.error("Invalid credentials");          // red ✗
toast.loading("Submitting...");             // spinner
toast("Copied to clipboard");               // neutral

// Promise toast — auto switches loading → success/error
toast.promise(
  axios.post("/api/login", credentials),
  {
    loading: "Logging in...",
    success: "Welcome back!",
    error:   "Login failed",
  }
);
📌 position options: "top-left", "top-center", "top-right", "bottom-left", "bottom-center", "bottom-right"
reverseOrder — when true, new toasts stack below older ones instead of above.

State Management

As apps grow, passing state through props between many components becomes painful (prop drilling). State management solutions create a shared store outside the component tree so any component can read or update state directly. It means sharing the state across the application and achieve sync in state across the application

The Problem — Prop Drilling

Prop Drilling

Passing state through every intermediate component just to reach a deeply nested child. App → Layout → Sidebar → UserMenu → Avatar — all components in the chain have to pass user even if they don't use it. Brittle and hard to maintain.

Lifting State Up

Moving state to the nearest common ancestor of all components that need it. Works for single-level sharing or siblings. Gets impractical when the common ancestor is far up the tree.

Global State

State that lives outside the component tree entirely. Components subscribe to it directly. When the state changes, only the components that use it re-render. No prop drilling at any level.

Three Solutions

1. Context API — Built-in React

Best for: small/medium apps, theme toggling, auth state, locale settings.

Create a context, wrap the app in a Provider, consume with useContext(). No external library needed. Downside: every consumer re-renders when context value changes — not optimised for high-frequency updates.

2. Redux / Redux Toolkit — External

Best for: large, complex apps with many interconnected states, time-travel debugging.

Centralised store with strict unidirectional data flow. action → reducer → store → component. Redux Toolkit (RTK) is the modern way — removes boilerplate of classic Redux.

3. Zustand — External (lightweight)

Best for: medium/large apps that want Redux-style global state without the boilerplate.

Very minimal API. Create a store with create(), consume directly in components with a hook. No Provider needed. Much simpler than Redux but still powerful.

JSX — Context API (Built-in)
import { createContext, useContext, useState } from 'react';

// 1. Create the context object
const AuthContext = createContext(null);

// 2. Create a Provider component that holds the state
export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  return (
    {/* Wrap your app — all children can now access user + setUser */}
    <AuthContext.Provider value={{ user, setUser }}>
      {children}
    </AuthContext.Provider>
  );
}

// 3. Custom hook — cleaner than calling useContext directly
export const useAuth = () => useContext(AuthContext);

// 4. In main.jsx — wrap app with Provider
// <AuthProvider><App /></AuthProvider>

// 5. In any nested component — consume directly, no prop drilling
function Navbar() {
  const { user, setUser } = useAuth();
  return <p>Welcome, {user?.name}</p>;
}
Shell + JSX — Zustand (no Provider needed)
# Install Zustand
npm install zustand

// store.js — create the store
import { create } from 'zustand';

export const useCartStore = create((set) => ({
  cart: [],
  addItem:    (item) => set(state => ({ cart: [...state.cart, item] })),
  removeItem: (id) =>   set(state => ({ cart: state.cart.filter(i => i.id !== id) })),
  clearCart:  () =>       set({ cart: [] }),
}));

// In ANY component — no Provider, no prop drilling
function CartIcon() {
  const { cart, addItem } = useCartStore();
  return <span>Cart: {cart.length}</span>;
}
// When to use what: useState = local component state. Context API = shared state in small/medium apps, infrequent updates (auth, theme). Zustand = shared state with frequent updates and simpler code. Redux Toolkit = large enterprise apps needing strict patterns and devtools.
// Key points: Do not directly call the useTest hook; instead, extract only the specific state(s) you need. With Zustand, select the required states per component so that updates only re-render components using those states, avoiding unnecessary renders.
⚠ Context API re-render trap: Every component consuming a context re-renders when any part of the context value changes — even if that component only uses one field. For performance-critical state, split contexts by domain or use Zustand/Redux which support selective subscriptions.

React Hooks Deep Dive

Beyond useState and useEffect, React provides hooks for performance optimisation (useMemo, useCallback), DOM access (useRef), and reusable logic (custom hooks). These come up constantly in interviews.

useRef — Mutable Reference Without Re-render

What useRef Does

Returns a mutable object { current: value }. Changing ref.current does NOT trigger a re-render. The value persists across renders unlike a regular variable which resets every time.

Two Main Uses

1. DOM access: Attach to JSX via ref={myRef} to directly access the DOM node (focus, measure, animate).

2. Persist values: Store values between renders without causing re-renders — e.g. previous state, timer IDs, interval references.

useRef Hook Explained

useRef is used to access the reference of the real DOM directly.

Reasons to use: If some action needs to happen instantly (improving user experience), to maintain previous values, or when changes occur but the component should not re-render.

ref attribute: Connects reference elements like <input ref={inputRef} type="text" />.


JSX — useRef: DOM Access & Persisting Values
import { useRef, useEffect } from 'react';

// USE 1: DOM access — focus an input on mount
function SearchBox() {
  const inputRef = useRef(null);

  useEffect(() => {
    inputRef.current.focus(); // directly call DOM method
  }, []);

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

// USE 2: Persist a timer ID without re-rendering
function Timer() {
  const timerRef = useRef(null);

  const start = () => {
    timerRef.current = setInterval(() => console.log('tick'), 1000);
  };
  const stop = () => {
    clearInterval(timerRef.current); // always have access to the ID
  };
  return <><button onClick={start}>Start</button><button onClick={stop}>Stop</button></>;
}

// DIFFERENCE: useState vs useRef for storing values
// useState  → triggers re-render when updated
// useRef    → NEVER triggers re-render, value just silently updates

Extra Info: Unlike useState, updating ref.current does not cause a re-render. This makes it ideal for storing mutable values like DOM nodes, timers, or previous state snapshots.

useMemo — Cache Expensive Computations

Every time a component re-renders, all code inside it runs again from scratch. If a computation is expensive (filtering thousands of items, complex math), useMemo caches the result and only recalculates when its dependencies change.

Without useMemo — Problem

Heavy computation runs on every render, even when unrelated state changes. If a parent re-renders because of a different piece of state, the expensive filter runs again for no reason.

With useMemo — Solution

Wraps the computation. React caches the result. On re-render, React checks if the dependencies changed. If not, it returns the cached value and skips the computation entirely.

JSX — useMemo
import { useMemo, useState } from 'react';

function ProductList({ products, searchTerm }) {

  // ✗ Without useMemo — runs on EVERY render, even for unrelated state changes
  const filteredBad = products.filter(p => p.title.includes(searchTerm));

  // ✓ With useMemo — only recalculates when products or searchTerm changes
  const filteredGood = useMemo(() => {
    return products.filter(p => p.title.toLowerCase().includes(searchTerm));
  }, [products, searchTerm]); // ← dependency array, same as useEffect

  return filteredGood.map(p => <div key={p.id}>{p.title}</div>);
}

// useMemo returns the RESULT of a function (a value)
// Don't overuse it — for small arrays or cheap ops, the overhead isn't worth it

useCallback — Cache Function References

In JavaScript, every time a component re-renders, every function defined inside it is recreated as a new object. This means child components receiving that function as a prop will see it as a "new" prop and re-render unnecessarily. useCallback memoizes the function itself.

The Problem

Parent re-renders → handleClick is recreated as a new function reference → Child sees a new prop → Child re-renders. This chain happens even when the function logic hasn't changed at all.

The Solution

useCallback returns the same function reference across renders unless dependencies change. Combined with React.memo on the child, it prevents unnecessary re-renders.

JSX — useCallback vs useMemo comparison
import { useCallback, useMemo, memo } from 'react';

// useMemo → memoizes a VALUE (result of a computation)
const sortedList = useMemo(() => [...items].sort(), [items]);

// useCallback → memoizes a FUNCTION REFERENCE
const handleDelete = useCallback((id) => {
  setItems(prev => prev.filter(i => i.id !== id));
}, []); // stable reference — setItems never changes

// React.memo — wraps a child component to skip re-render
// if ALL its props are the same reference (shallow equal)
const ItemRow = memo(({ item, onDelete }) => {
  return <button onClick={() => onDelete(item.id)}>{item.name}</button>;
});

// Without useCallback on onDelete → ItemRow re-renders on every parent render
// With useCallback on onDelete → ItemRow only re-renders when items actually change

/*
  SUMMARY:
  useMemo(fn, deps)      → returns fn()    — cached VALUE
  useCallback(fn, deps)  → returns fn      — cached FUNCTION
  React.memo(Component)  → skips render if props unchanged
*/
// Interview Q: What's the difference between useMemo and useCallback? — useMemo memoizes the result of calling a function (a value). useCallback memoizes the function itself (its reference). useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).

Custom Hooks

Custom hooks let you extract and reuse stateful logic across multiple components. Any function that starts with use and calls other hooks is a custom hook. It's not a React feature — it's a naming convention that enables the rules of hooks to work correctly.

When to Create One

When you find yourself copy-pasting the same useState + useEffect logic across multiple components — extract it. Common examples: useFetch, useLocalStorage, useWindowSize, useDebounce.

Rules of Hooks

Only call hooks at the top level — never inside loops, conditions, or nested functions. Only call hooks from React components or other hooks — never from regular JS functions. These rules allow React to track state correctly.

JSX — Custom Hook: useFetch
// hooks/useFetch.js — reusable data fetching logic
import { useState, useEffect } from 'react';

export function useFetch(url) {
  const [data, setData]       = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError]     = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch(url)
      .then(r => {
        if (!r.ok) throw new Error(r.status);
        return r.json();
      })
      .then(d => setData(d))
      .catch(e => setError(e.message))
      .finally(() => setLoading(false));
  }, [url]); // re-fetch if url changes

  return { data, loading, error };
}

// Usage in ANY component — no copy-paste, just import the hook
function Users() {
  const { data, loading, error } = useFetch('https://api.example.com/users');
  if (loading) return <p>Loading...</p>;
  if (error)   return <p>Error: {error}</p>;
  return data.map(u => <div key={u.id}>{u.name}</div>);
}

function Products() {
  // Same hook, different URL — logic is shared, not duplicated
  const { data, loading } = useFetch('https://fakestoreapi.com/products');
}

Controlled vs Uncontrolled Components

This is one of the most common React interview topics. It describes who owns the truth of the form input value — React state or the DOM itself.

Controlled Component

React controls the value. Input value is bound to useState, and every keystroke calls onChange to update state. React is always the single source of truth.

Pro: easy validation, instant derived state. Con: verbose for large forms.

Uncontrolled Component

The DOM controls the value. You access it via useRef when needed (e.g. on submit) rather than syncing on every keystroke. Less re-renders.

Pro: less code, fewer re-renders. Con: harder to do real-time validation.

react-hook-form approach

Uses uncontrolled inputs under the hood (via refs) but gives you a controlled-like API. This is why it has much fewer re-renders than fully controlled forms — validation happens on submit/blur, not every keystroke.

JSX — Controlled vs Uncontrolled
// CONTROLLED — React owns the value via useState
function Controlled() {
  const [val, setVal] = useState('');
  return <input value={val} onChange={e => setVal(e.target.value)} />;
  // Re-renders on EVERY keystroke — React state is always in sync
}

// UNCONTROLLED — DOM owns the value, React reads it via ref when needed
function Uncontrolled() {
  const inputRef = useRef();
  const handleSubmit = () => {
    console.log(inputRef.current.value); // read value only when needed
  };
  return <input ref={inputRef} defaultValue="initial" />;
  // NO re-renders on keystrokes — DOM handles it internally
}
// Key interview insight: React's own docs recommend controlled components for most cases. But react-hook-form cleverly uses uncontrolled inputs to give you the best of both worlds — minimal re-renders + powerful validation API.

Interview Questions

These are the most commonly asked conceptual questions in full stack interviews. Understanding the why behind each answer is what separates a candidate who has read docs from one who has built things.

JavaScript Concepts

var vs let vs const

var — function-scoped, hoisted to top of function, can be redeclared. let — block-scoped, not hoisted usably (temporal dead zone), can be reassigned. const — block-scoped, must be initialised, cannot be reassigned. Note: const objects/arrays are still mutable — only the binding is locked.

Closure

A function that remembers its outer scope even after the outer function has returned. The inner function has a reference to the variables in the enclosing scope. Used in: factory functions, memoization, module pattern, event handlers, custom hooks.

== vs ===

== (loose equality) — performs type coercion before comparing. 0 == "0" is true. === (strict equality) — compares value AND type, no coercion. 0 === "0" is false. Always use === in production code.

Hoisting

JavaScript moves declarations (not initialisations) to the top of their scope before execution. var declarations are hoisted and initialised as undefined. function declarations are fully hoisted. let/const are hoisted but not usable until declared (TDZ = Temporal Dead Zone).

null vs undefined

undefined — a variable declared but not assigned a value. JavaScript sets this automatically. null — intentional absence of value. A developer explicitly sets this. typeof undefined is "undefined". typeof null is "object" — a famous JS bug from 1995 kept for backward compatibility.

Event Bubbling & Capturing

When an event fires on a child, it bubbles up through parent elements by default. Capturing is the opposite — top-down. e.stopPropagation() stops bubbling. e.preventDefault() stops the browser's default action (e.g. form submit) but does NOT stop bubbling — they're different things.

JavaScript — Closure Example (Classic Interview Q)
// Classic closure — counter factory
function makeCounter() {
  let count = 0; // this variable is "closed over"
  return function() {
    count++;
    return count;
  };
}

const counter1 = makeCounter();
const counter2 = makeCounter();
counter1(); // 1 — own private count
counter1(); // 2
counter2(); // 1 — separate closure, separate count

// The Temporal Dead Zone (TDZ) — let/const before declaration
console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5;

console.log(y); // undefined — var IS hoisted with a value
var y = 5;

React Concepts

Virtual DOM — What & Why

A lightweight in-memory copy of the real DOM. When state changes, React builds a new Virtual DOM, diffs it against the previous one (reconciliation), and updates only the changed parts in the real DOM. Direct DOM manipulation is slow — React batches and optimises it.

Why the key Prop Matters

When rendering lists, React uses key to identify which items changed, were added, or removed. Without a stable key, React re-renders the entire list. With a unique key (use id, not array index), React updates only the changed item. Never use array index as key — it breaks when items are reordered or deleted.

Why Direct State Mutation Doesn't Work

React uses shallow reference equality to detect state changes. If you mutate an object directly (state.name = "new"), the reference stays the same — React thinks nothing changed and skips the re-render. You must always return a new reference ({...state, name:"new"}) to trigger a re-render.

What Causes a Re-render?

1. State changes via setState. 2. Props change (new reference passed). 3. Parent component re-renders. 4. Context value changes. Optimisations: React.memo skips render if props are shallow-equal. useMemo/useCallback stabilise values and functions.

useEffect Cleanup

The function returned from useEffect is the cleanup function. It runs before the component unmounts OR before the effect runs again (on next dependency change). Used to clear intervals, cancel fetch requests, remove event listeners — prevents memory leaks.

Reconciliation

The algorithm React uses to diff two Virtual DOM trees and figure out the minimum set of DOM operations needed. React assumes: elements of different types produce different trees (replace entirely), and key prop identifies stable elements across renders. This is why keys on lists are critical for performance.

JSX — useEffect Cleanup Pattern
useEffect(() => {
  // Setup: subscribe, start timer, add event listener
  const id = setInterval(() => setCount(c => c + 1), 1000);

  // Cleanup: runs when component UNMOUNTS or before effect runs again
  return () => {
    clearInterval(id); // prevent memory leak if component is removed
  };
}, []); // runs once on mount, cleanup runs on unmount

// Without cleanup: the interval keeps running even after unmount!
// With cleanup: React calls clearInterval when component leaves the DOM

Backend / Node Concepts

REST vs GraphQL

REST: multiple endpoints, each returns a fixed shape. GET /users, GET /posts. Simple, cacheable, widely understood. GraphQL: single endpoint, client specifies exactly what data it needs. Solves over-fetching and under-fetching. More complex to set up but powerful for flexible clients.

JWT Structure

Three Base64-encoded parts separated by dots: header.payload.signature. Header = algo type. Payload = data (userId, role, expiry) — NOT encrypted, just encoded. Signature = HMAC of header+payload using the secret. Anyone can read the payload, but cannot fake the signature without the secret.

XSS vs CSRF

XSS (Cross-Site Scripting) — attacker injects malicious JS into your page that runs in other users' browsers. Defense: httpOnly cookies (JS can't read them), sanitize input. CSRF (Cross-Site Request Forgery) — tricks the browser into making requests to your server using existing cookies. Defense: sameSite cookie flag, CSRF tokens.

Middleware in Express

Functions with signature (req, res, next) that run between request and response. Must call next() to pass control forward, or res.send() to terminate. Order matters — register error handlers with 4 parameters (err, req, res, next) last.

SQL vs NoSQL

SQL (Relational): structured tables, predefined schema, joins, ACID transactions. Great for complex relationships. NoSQL (MongoDB): flexible schema, documents (JSON-like), horizontal scaling. Great for variable data shapes, rapid iteration. MongoDB stores data as BSON (Binary JSON).

Async vs Sync in Node.js

Node.js is single-threaded but handles concurrency via the event loop. Async I/O (file reads, DB queries, HTTP requests) is handed to libuv (C++ thread pool). Node continues processing other requests. Callback fires when I/O completes. This is why Node handles 10,000 concurrent connections but would choke on heavy CPU computation.

JavaScript — Quick Fire Conceptual Answers
// Q: What is the output?
console.log(typeof null);          // "object" — historic JS bug
console.log(typeof undefined);     // "undefined"
console.log(typeof []);              // "object" — arrays are objects!
console.log(Array.isArray([]));     // true — correct way to check
console.log(0 == false);             // true — type coercion (both falsy)
console.log(0 === false);            // false — different types
console.log("" == false);            // true — coercion
console.log(null == undefined);     // true — special case
console.log(null === undefined);    // false — different types
console.log(NaN === NaN);           // false — NaN !== NaN always!
console.log(Number.isNaN(NaN));    // true — correct way to check

// Q: What is the output? (closure in loop)
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// Output: 3 3 3 — var is function-scoped, all closures share same i
// Fix: use let (block-scoped) → each iteration gets its own i → 0 1 2
Authentication vs Authorization

Authentication = proving who you are (verifyToken — validates the JWT). Authorization = proving what you can do (requireRole — checks req.user.role). Both are middleware, they chain together: verifyToken → requireRole('admin') → handler.

Why httpOnly cookies over localStorage?

localStorage is readable by any JS on the page — an XSS attack can steal the token. httpOnly cookies are invisible to JS entirely. Only the browser and server see them. The browser also attaches cookies automatically, so no frontend code is needed to send the token.

Is it safe to put userId in a JWT payload?

Yes. The payload is readable (Base64, not encrypted) but unforgeable. Anyone can decode and see the userId, but changing even one character invalidates the signature. The secret key is what makes it tamper-proof. Never put passwords or sensitive data in the payload.

// Interview tip — the "why" matters more than the "what": Don't just say "Virtual DOM is faster". Say: "React uses a Virtual DOM to batch and minimise real DOM operations, because reading/writing the real DOM is expensive — it triggers layout recalculation and repaints. The diff algorithm (reconciliation) figures out the minimum change set." That's the answer that gets you the job.