MODULE 01
JavaScript
The written round leans on closures, hoisting, the event loop and coercion. Learn to answer in one sentence, then back it with a two-line example.
Write these answers
var vs let vs const
var is function-scoped and hoisted with value undefined. let and const are block-scoped and uninitialized until declared (the temporal dead zone). const forbids reassignment, but not mutation of the object it holds.
== vs ===
== compares values after coercing types, so "5" == 5 is true. === compares type and value, so "5" === 5 is false. Default to ===.
Closure
A function that remembers the variables of the scope it was created in, even after its outer function has returned. It is how you build private state — counters, caches, once() helpers.
Hoisting
Declarations are moved to the top of their scope before execution. Only the declaration is hoisted, not the assignment: var reads undefined early, let/const throw a ReferenceError.
Promise and async/await
A Promise is a future value with three states: pending → fulfilled | rejected. async/await is syntax over promises — await pauses inside an async function until it settles; errors are caught with try/catch.
Event loop
JS runs one call stack. When it empties, the event loop drains all microtasks (promise callbacks) first, then takes one macrotask (setTimeout, I/O). That is why a resolved promise wins over setTimeout(…, 0).
map / filter / reduce
map returns a new array of the same length, transformed. filter returns a new array of the items that passed a test. reduce folds the array into one value — a sum, an object, anything.
null vs undefined
undefined: declared but no value assigned (also missing arguments). null: an intentional empty value set by the programmer.
Working examples
// A closed-over variable + returns it
function makeCounter() {
let count = 0; // private — nothing outside can touch it
return function () {
count += 1;
return count;
};
}
const c = makeCounter();
console.log(c()); // 1
console.log(c()); // 2
console.log(c()); // 3
// Fetch a user and handle errors the way graders expect
async function getUser(id) {
try {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
return data;
} catch (err) {
console.error("Failed:", err.message);
return null;
}
}
getUser(1);
// filter today, take prices, add them up — in one chain
const items = [
{ name: "pen", price: 20, inStock: true },
{ name: "bag", price: 500, inStock: true },
{ name: "eraser", price: 5, inStock: false },
];
const total = items
.filter((i) => i.inStock)
.map((i) => i.price)
.reduce((sum, p) => sum + p, 0);
console.log(total); // 520
// call, bind, apply — all fix what `this` points to
const person = { name: "Afjal", greet() { return "Hi " + this.name; } };
console.log(person.greet()); // Hi Afjal
console.log(person.greet.call({ name: "Sam" })); // Hi Sam — args passed one by one
console.log(person.greet.apply({ name: "Ria" })); // Hi Ria — args passed as an array
const g = person.greet.bind({ name: "Dev" }); // bind returns a new function
console.log(g()); // Hi Dev
// debounce: run fn only after the calls have stopped for `ms`
function debounce(fn, ms) {
let timer;
return function (...args) { // closure keeps `timer` private
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), ms);
};
}
const onType = debounce((q) => console.log("searching:", q), 500);
onType("h"); // wait…
onType("ha"); // restart the timer…
onType("hai"); // …only this one runs, 500ms after the last call
// fire requests in parallel, wait for all — fails fast on the first error
async function loadAll() {
try {
const [user, posts] = await Promise.all([
fetch("/api/users/1").then((r) => r.json()),
fetch("/api/posts").then((r) => r.json()),
]);
console.log(user.name, posts.length);
} catch (err) {
console.error("one request failed:", err.message);
}
}
loadAll();
Predict the output
QWhat does console.log(typeof null) print?+
"object" — a historical JavaScript bug kept forever for backwards compatibility.
QWhat does "5" + 3 print, and "5" - 3?+
+ prefers strings, so it prints "53". - has no string meaning, so it coerces to number and prints 2.
QIn what order do a, b, c, d appear in the log?+
console.log("a");
setTimeout(() => console.log("b"), 0);
Promise.resolve().then(() => console.log("c"));
console.log("d");
// a d c b — sync first, then microtask (c), then macrotask (b)
MODULE 1B · FULL DOCS
JavaScript Handbook
The complete concept syllabus for written tests: every core area of the language, one definition + one example at a time. Study it top to bottom.
1 · Language basics
Data types
7 primitives: string, number, boolean, null, undefined, bigint, symbol. Plus object (arrays, functions, dates are all objects). typeof x returns the type as a string; typeof null === "object" is a famous bug.
Type conversion
Explicit: Number("5"), String(5), Boolean(0). Implicit: "5" * 2 → 10 (arithmetic coerces), "5" + 2 → "52" (+ prefers strings). Falsy values: 0, "", null, undefined, NaN, false — everything else is truthy.
let / const / var
var: function-scoped, hoisted as undefined, can be redeclared. let: block-scoped, TDZ, reassignable. const: block-scoped, must be initialised, cannot be reassigned — but objects/arrays held can still be mutated.
Template literals
Backtick strings with ${expression} interpolation and real multi-line support: `Hi ${name}, you have ${n} msgs`.
Functions: 3 forms
Declaration function f() {} — hoisted, callable before definition. Expression const f = function () {} — not hoisted. Arrow const f = () => {} — no own this, inherits from the enclosing scope.
Default & rest parameters
function f(a, b = 10) {} — b defaults to 10 when undefined. function g(...nums) {} collects remaining args into a real array.
2 · Objects, arrays, references
Object basics
Key–value store: const u = { name: "A", age: 21 }. Read: u.name or u["name"]. Add: u.city = "Blr". Delete: delete u.age. Check: "name" in u.
Shorthand & spread
If variable name = key: { name, age }. Copy/merge with spread: { ...u, age: 22 } — new object, original untouched (shallow copy).
Optional chaining & nullish coalescing
user?.address?.city — stops at undefined instead of throwing. count ?? 0 — uses 0 only when count is null/undefined (unlike ||, which also replaces 0 and "").
Destructuring
Objects: const { name, age } = u (with rename: { name: n }). Arrays: const [first, second] = arr. Swap: [a, b] = [b, a]. Function params: function f({ name }) {}.
Reference vs value
Primitives copy by value; objects copy by reference. const b = a for objects means both point to the same object — mutating b mutates a. True copy: structuredClone(a) (deep) or spread (shallow).
Array essentials
Mutating: push, pop, shift, unshift, splice, sort, reverse. Non-mutating: slice, concat, map, filter, find, some, every, includes, flat. Array.isArray(x) to test.
// pull fields out, merge, clone — the everyday syntax
const user = { name: "Afjal", age: 21, city: "Blr" };
const { name, age, ...rest } = user; // name="Afjal" age=21 rest={city:"Blr"}
const updated = { ...user, age: 22 }; // new object, user unchanged
const [first, , third] = ["a", "b", "c"]; // skip middle → first="a" third="c"
function greet({ name, city = "Unknown" } = {}) { // defaults inside params
return `${name} from ${city}`;
}
console.log(greet(user)); // Afjal from Blr
console.log(greet({})); // undefined from Unknown
3 · Scope, closures, this
Scope chain
Inner functions can read outer variables. Lookup walks outward: local → enclosing → global. var ignores blocks; let/const respect them.
Closures everywhere
A closure = function + the variables it captured. Powers: private state (counters), data hiding, debounce/throttle, currying, module pattern.
this — 5 rules
1) Global call → undefined (strict) / window. 2) obj.fn() → obj. 3) fn.call(obj)/apply → explicit obj. 4) obj.fn.bind(obj) → permanently obj. 5) Arrow functions have no own this — they use the enclosing one.
Currying
Transform f(a, b, c) into f(a)(b)(c) using closures: build specialised functions from a general one.
// f(a, b, c) → f(a)(b)(c)
const curry = (fn) =>
(a) => (b) => (c) => fn(a, b, c);
const volume = (l, w, h) => l * w * h;
const v = curry(volume);
console.log(v(2)(3)(4)); // 24
// why? pre-fill arguments: const box10 = v(10); box10(2)(3) → 60
4 · Async JavaScript
Call stack → event loop
JS is single-threaded. Long tasks block everything, so async work (timers, network) is handed to the browser, results queue up, and the event loop feeds them back when the stack is empty. Microtasks (promises) drain fully before each macrotask (timers).
Callbacks → promises → async/await
Three generations of the same idea. Callbacks invert control ("callback hell"). Promises chain with .then()/.catch(). async/await reads like sync code with try/catch.
Promise combinators
all: all must fulfil, fails fast. allSettled: waits for all, never rejects, reports each outcome. race: first settled wins. any: first fulfilled wins.
fetch in browser
const res = await fetch(url) — note fetch does NOT throw on 404/500, only on network failure. Check res.ok / res.status, then res.json().
// all vs allSettled — the exact difference testers ask
const ok = Promise.resolve(1);
const bad = Promise.reject("oops");
Promise.allSettled([ok, bad]).then(console.log);
// [{status:"fulfilled",value:1}, {status:"rejected",reason:"oops"}]
Promise.all([ok, bad]).catch(console.log);
// "oops" — all rejects as soon as anything rejects
5 · OOP, classes & prototypes
Prototypes
Every object has a hidden [[Prototype]]. Property lookup goes up this chain until found: that is prototypal inheritance. Object.getPrototypeOf(obj) inspects it.
class syntax
Sugar over prototypes. constructor() runs on new; methods live on the prototype, shared by all instances. extends / super() for inheritance.
Getters/setters & static
get area() {} exposes a method as a property. static method() belongs to the class itself, not instances — called as Class.method().
class Animal {
constructor(name) { this.name = name; }
speak() { return `${this.name} makes a sound`; }
}
class Dog extends Animal {
constructor(name) {
super(name); // must call before using this
this.legs = 4;
}
speak() { return `${this.name} barks`; } // method overriding
}
console.log(new Dog("Rocky").speak()); // Rocky barks
6 · DOM, browser storage & error handling
DOM selection & events
document.querySelector(".btn") — first match, querySelectorAll — all. Events: el.addEventListener("click", fn). Bubbling: event travels child → parent; event.stopPropagation() halts it. Delegation: one listener on a parent handles children via event.target.
localStorage / sessionStorage
Key–value stores for strings only — JSON.stringify on write, JSON.parse on read. localStorage survives restarts; sessionStorage dies with the tab. ~5 MB limit, synchronous.
try / catch / finally
try risky code, catch (err) handles it, finally always runs (cleanup). Throw your own: throw new Error("msg").
Error types
TypeError (not a function, undefined access), ReferenceError (undeclared variable), SyntaxError (bad syntax), RangeError (bad value range). Custom: class AppError extends Error {}.
// 1 listener handles 100 buttons — via bubbling
document.querySelector("#list").addEventListener("click", (e) => {
const btn = e.target.closest("button");
if (!btn) return;
console.log("clicked:", btn.dataset.id);
});
MODULE 02
Express
Expect one-liners on middleware and route signatures, and one question asking you to sketch a small API. This working server covers both.
Write these answers
Middleware
A function with the signature (req, res, next) that runs in registration order. It can inspect or change the request, respond, or call next() to hand control to the next handler.
express.json()
Built-in middleware that parses incoming JSON and puts the result on req.body. Without it, req.body is undefined for JSON payloads.
params vs query
req.params holds values from the path pattern (/books/:id → req.params.id). req.query holds the query string (?sort=asc → req.query.sort).
Error-handling middleware
Same as normal middleware but with four parameters: (err, req, res, next). Express sends every next(err) or thrown error there. Register it last, after all routes.
app.use vs app.get
app.use runs for every HTTP method and matches path prefixes — it is how middleware is mounted. app.get answers only GET requests on the exact path.
Ending a route
res.json(data), res.status(201).json(obj), res.redirect(url). A route sends exactly one response or calls next() — sending twice throws ERR_HTTP_HEADERS_SENT.
404 handler
Registered as the very last app.use: app.use((req, res) => res.status(404).json({ error: "Not found" })). It catches every URL no earlier route matched.
Working example — a complete small API
const express = require("express");
const app = express();
app.use(express.json()); // parse JSON bodies → req.body
app.use((req, res, next) => { // logger middleware
console.log(req.method, req.url);
next();
});
const books = [{ id: 1, title: "Dune" }];
app.get("/books", (req, res) => {
res.json(books);
});
app.get("/books/:id", (req, res) => {
const book = books.find((b) => b.id === Number(req.params.id));
if (!book) return res.status(404).json({ error: "Book not found" });
res.json(book);
});
app.post("/books", (req, res) => {
const { title } = req.body;
if (!title) return res.status(400).json({ error: "title is required" });
const book = { id: books.length + 1, title };
books.push(book);
res.status(201).json(book);
});
// error middleware — the 4 args are what mark it as an error handler
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: "Something broke" });
});
app.listen(3000, () => console.log("http://localhost:3000"));
node server.js
curl localhost:3000/books
curl -X POST localhost:3000/books -H "Content-Type: application/json" -d '{"title":"1984"}'
curl localhost:3000/books/9 # → 404 {"error":"Book not found"}
More working examples
// login → JWT → protected route: the full-stack interview staple
const express = require("express");
const jwt = require("jsonwebtoken");
const app = express();
app.use(express.json());
const SECRET = "dev-secret"; // real apps: process.env.JWT_SECRET
// 1. login → check credentials, issue a token
app.post("/login", (req, res) => {
const { username, password } = req.body;
// demo check — real apps compare a bcrypt hash in the DB
if (username !== "afjal" || password !== "pass123") {
return res.status(401).json({ error: "invalid credentials" });
}
const token = jwt.sign({ username }, SECRET, { expiresIn: "1h" });
res.json({ token });
});
// 2. middleware that protects routes
function auth(req, res, next) {
const token = (req.headers.authorization || "").replace("Bearer ", "");
try {
req.user = jwt.verify(token, SECRET); // throws if bad or expired
next();
} catch {
res.status(401).json({ error: "invalid or expired token" });
}
}
// 3. only requests with a valid token reach here
app.get("/me", auth, (req, res) => res.json(req.user));
app.listen(3000, () => console.log("auth API on :3000"));
curl -X POST localhost:3000/login -H "Content-Type: application/json" \
-d '{"username":"afjal","password":"pass123"}' # → {"token":"eyJ…"}
curl localhost:3000/me -H "Authorization: Bearer eyJ…" # → {"username":"afjal","iat":…}
MODULE 03
React
Written tests focus on hooks, keys and data flow. Describe what each hook does and when it runs — that is the half most people miss.
Write these answers
Component and props
A component is a function that returns JSX, re-run whenever its inputs change. Props are read-only inputs passed from parent to child — a component never edits its own props.
useState
const [v, setV] = useState(0) declares state. setV schedules a re-render; never assign to v directly. When the next value depends on the old one, use the functional form setV(v => v + 1).
useEffect
Runs side effects after render: fetching, timers, subscriptions. The dependency array decides when: [] once on mount, [id] whenever id changes, none = after every render. Return a function to clean up.
Keys in lists
React matches list items between renders by key. Give each row a stable id — using the array index breaks reconciliation when items are added, removed or reordered.
Controlled input
An input whose value comes from state and updates via onChange: React state is the single source of truth for the form field.
Lifting state
When two components need the same data, move that state to their closest common parent, then pass it down as props plus callback props to change it.
Virtual DOM
On state change React re-renders the component tree in memory, diffs it against the previous tree (reconciliation), and applies only the minimal real-DOM changes.
Hooks cheat sheet
| Hook | What it does | Typical use |
|---|---|---|
useState | Local state that re-renders on change | form fields, counters, toggles |
useEffect | Side effect after render, with cleanup | fetch, timers, subscriptions |
useRef | Mutable box that never triggers a re-render | DOM node, timer id, previous value |
useMemo | Recomputes only when a dependency changes | expensive derived lists |
useContext | Reads context without prop drilling | theme, auth user |
Working example
import { useState, useEffect } from "react";
function TodoApp() {
const [todos, setTodos] = useState([]);
const [text, setText] = useState("");
useEffect(() => {
// [] → run once after the component mounts
fetch("http://localhost:3000/todos")
.then((r) => r.json())
.then(setTodos);
}, []);
function addTodo(e) {
e.preventDefault(); // stop the page reload
if (!text.trim()) return;
setTodos([...todos, { id: Date.now(), title: text }]);
setText("");
}
return (
<>
<form onSubmit={addTodo}>
<input
value={text} // controlled input
onChange={(e) => setText(e.target.value)}
placeholder="New todo"
/>
<button type="submit">Add</button>
</form>
<ul>
{todos.map((t) => (
<li key={t.id}>{t.title}</li> // stable key, not the index
))}
</ul>
</>
);
}
export default TodoApp;
More working examples
import { useState, useEffect } from "react";
function Users() {
const [users, setUsers] = useState([]);
const [state, setState] = useState("loading"); // loading | error | done
useEffect(() => {
fetch("http://localhost:3000/users")
.then((r) => {
if (!r.ok) throw new Error("HTTP " + r.status);
return r.json();
})
.then((data) => { setUsers(data); setState("done"); })
.catch(() => setState("error"));
}, []); // [] → run once on mount
if (state === "loading") return <p>Loading…</p>;
if (state === "error") return <p>Could not load users</p>;
return (
<ul>
{users.map((u) => <li key={u.id}>{u.name}</li>)}
</ul>
);
}
export default Users;
import { useState, useEffect } from "react";
function Timer() {
const [sec, setSec] = useState(0);
useEffect(() => {
const id = setInterval(() => setSec((s) => s + 1), 1000);
return () => clearInterval(id); // cleanup on unmount — no leaks
}, []);
return <p>Seconds passed: {sec}</p>;
}
export default Timer;
MODULE 04
REST API
Second half of HTTP knowledge: URL design, methods, idempotency, and status codes you can recite. The two tables below are the exam.
Write these answers
What REST is
An API style where URLs name resources (nouns), HTTP methods name the action, requests are stateless, and responses carry status codes and JSON. GET /books/3, never /getBook?id=3.
Idempotency
A method is idempotent when repeating the request leaves the same result: GET, PUT and DELETE are; POST is not. Favourite trap: PUT sent twice = same state; POST sent twice = two records.
PUT vs PATCH
PUT replaces the whole resource with the request body — missing fields get reset. PATCH applies a partial update to a few fields.
401 vs 403
401: not authenticated — missing or invalid credentials. 403: authenticated but not allowed. Trap to avoid: 401 really means "unauthenticated".
URL design
Plural nouns, lowercase, hierarchy in the path, filters in the query string: /users/7/orders?status=open.
Testing with Postman
Pick the method and URL; for POST/PUT/PATCH go to Body → raw → JSON for the payload. The response panel shows status and body. Same calls work in curl for CI scripts.
Methods
| Method | Purpose | Idempotent | Typical success |
|---|---|---|---|
GET | Read a resource or collection | Yes | 200 |
POST | Create a resource | No | 201 |
PUT | Replace a resource entirely | Yes | 200 / 204 |
PATCH | Update some fields | Not guaranteed | 200 |
DELETE | Remove a resource | Yes | 204 |
Status codes worth memorising
| Code | Name | Say it in the exam as… |
|---|---|---|
| 200 | OK | standard success, body carries data |
| 201 | Created | after POST, return the new resource |
| 204 | No Content | success with an empty body, typical for DELETE |
| 400 | Bad Request | malformed JSON or a missing required field |
| 401 | Unauthorized | not logged in / credentials invalid |
| 403 | Forbidden | logged in, but not allowed to do this |
| 404 | Not Found | no such resource |
| 409 | Conflict | duplicate key or a version clash |
| 500 | Internal Server Error | server-side crash — check the logs |
A full request/response
POST /books HTTP/1.1
Host: localhost:3000
Content-Type: application/json
{ "title": "1984", "author": "George Orwell" }
HTTP/1.1 201 Created
Content-Type: application/json
{ "id": 3, "title": "1984", "author": "George Orwell" }
curl -i -X POST localhost:3000/books -H "Content-Type: application/json" \
-d '{"title":"1984","author":"George Orwell"}'
curl -i "localhost:3000/books?sort=asc" # filters live in the query string
curl -i -X PUT localhost:3000/books/3 -H "Content-Type: application/json" \
-d '{"title":"Animal Farm"}' # full replace → 200
curl -i -X DELETE localhost:3000/books/3 # → 204 No Content
MODULE 3B · FULL DOCS
React Handbook
From JSX to Redux-level architecture: every concept a written test touches, with the strict rule of hooks, lifecycle mapping table, and a full component-tree example.
1 · Foundations
What React is
A library for building UIs from components — reusable, composable functions that return JSX. React re-renders on state change and updates only what changed in the real DOM via reconciliation.
JSX
HTML-like syntax compiled to React.createElement(...) calls. Rules: one parent element (use <>…</> fragments), close all tags, className not class, htmlFor not for, JS goes in { }.
Components & props
Props flow one way: parent → child. Props are read-only — never assign to them. Children: <Card><p>hi</p></Card> — the inner content arrives as props.children.
Rendering/lists
Arrays of elements render inline. Each list item needs a unique key — stable ids, not array indexes, especially with insert/remove/reorder. Conditional render: {isAdmin && <AdminPanel />} or ternary.
Component types
Modern React = function components + hooks. Class components (this.state, lifecycle methods) are legacy — read them, don't write them.
2 · State & hooks — the full set
useState
const [v, setV] = useState(initial). Setter schedules a re-render. Functional update setV(prev => …) when next depends on prev. Batched within one event handler.
useEffect
After-render side effects. Deps: [] = mount only, [a, b] = when they change, none = every render. Return a cleanup function for timers/subscriptions; it also runs before the next effect.
useRef
const ref = useRef(null). Two uses: a mutable box that survives re-renders without causing them (timer ids, previous values), and ref={ref} to grab a real DOM node.
useRef
const ref = useRef(null). Two uses: a mutable box that survives re-renders without causing them (timer ids, previous values), and ref={ref} to grab a real DOM node.
useContext
Reads a Context value anywhere below its Provider without prop drilling. Create with createContext, wrap the tree with <Ctx.Provider value={…}>, consume with useContext(Ctx).
useMemo & useCallback
Cache a value (useMemo(() => compute(a), [a])) or a function (useCallback = useMemo returning a function) between renders. Use only for real cost or when a memoized child would otherwise re-render.
useReducer
Alternative to useState for complex state: dispatch(action) → reducer(state, action) → new state. Centralises updates: actions describe what happened, the reducer decides the next state.
Custom hooks
Any function starting with use that calls other hooks. Extract shared logic (fetching, form input, window size): function useFetch(url) { … } — each component calling it gets its own state.
3 · The rules of hooks
Two rules
1) Only at the top level — never inside conditions, loops or nested functions (hook order must be identical every render — that is how React pairs state to hooks). 2) Only from React functions: components or custom hooks, never plain JS functions or event handlers.
Why deps matter
React compares deps with Object.is between renders; if you forget a dep, the effect silently uses stale data. Missing deps are the #1 source of "why is this not updating" bugs.
import { useState, useEffect } from "react";
// reusable data-fetching hook — every caller gets its own state
function useFetch(url) {
const [data, setData] = useState(null);
const [status, setStatus] = useState("loading");
useEffect(() => {
let alive = true; // guard against out-of-order responses
setStatus("loading");
fetch(url)
.then((r) => {
if (!r.ok) throw new Error("HTTP " + r.status);
return r.json();
})
.then((d) => { if (alive) { setData(d); setStatus("done"); } })
.catch(() => { if (alive) setStatus("error"); });
return () => { alive = false; }; // cleanup
}, [url]);
return { data, status };
}
// use it in any component:
// const { data, status } = useFetch("http://localhost:3000/users");
4 · Data flow patterns
Lifting state up
When two siblings need the same data, move the state to their closest common parent; parent passes state down as props and handlers up as callback props.
Controlled vs uncontrolled
Controlled: input value in React state, updated in onChange — state is the single source of truth (default choice). Uncontrolled: read the DOM (ref) on submit — fine for file inputs or huge forms.
Prop drilling & Context
Passing props through 5 layers that never use them? That is drilling. Context solves it for low-frequency data: theme, locale, current user, auth state.
Context + useReducer = mini store
The standard interview answer for "how would you manage global state without Redux": put a reducer in Context, expose state + dispatch via a custom hook useStore().
When Redux
Interview framing: "Redux when many components share complex state with devtools/time-travel needs; Context for infrequently-changing global values. Redux Toolkit for the modern setup: createSlice instead of action types + switch."
Rendering perf
React.memo(Component) skips re-render when props are unchanged. Combine with useCallback/useMemo so props stay referentially equal. useTransition marks updates non-urgent.
import { createContext, useContext, useState } from "react";
const ThemeCtx = createContext(null);
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("light");
return (
<ThemeCtx.Provider value={{ theme, setTheme }}>
{children}
</ThemeCtx.Provider>
);
}
function ThemeButton() {
const { theme, setTheme } = useContext(ThemeCtx); // no drilling
return <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
{theme === "light" ? "🌙 Dark" : "☀️ Light"}
</button>;
}
// wrap the app: <ThemeProvider><App /></ThemeProvider>
import { useReducer } from "react";
const initial = { items: [], total: 0 };
function reducer(state, action) {
switch (action.type) {
case "add":
return { ...state, items: [...state.items, action.item] };
case "remove":
return { ...state, items: state.items.filter((i) => i.id !== action.id) };
case "clear":
return initial;
default:
throw new Error("unknown action: " + action.type);
}
}
function Cart() {
const [state, dispatch] = useReducer(reducer, initial);
// dispatch({ type: "add", item: { id: 1, name: "Pen" } })
// dispatch({ type: "clear" })
}
5 · Forms, refs & side effects
Form pattern
One state object for the whole form: const [form, setForm] = useState({}), update one field: setForm({ ...form, email: v }); or computed-name setter setForm(f => ({ ...f, [e.target.name]: e.target.value })).
Submit handling
<form onSubmit={fn}> — always e.preventDefault() first. Validate, call API, reset with setForm({}). Buttons inside forms default to type="submit" — set type="button" to opt out.
Refs to DOM
const inputRef = useRef(null) + <input ref={inputRef} />. After mount: inputRef.current.focus(), .value, .scrollIntoView(). Never read/write ref.current during render.
Fetching — the 3 states
Every fetch UI needs loading / error / data branches. Component logic: if (loading) return <Spinner />; if (error) return <ErrorBox />; return <List data={data} />. Race-safety: ignore stale responses with an alive flag in cleanup.
import { useState } from "react";
function Signup() {
const [form, setForm] = useState({ name: "", email: "" });
const [err, setErr] = useState("");
const set = (e) => setForm((f) => ({ ...f, [e.target.name]: e.target.value }));
const [ok, setOk] = useState(false);
async function submit(e) {
e.preventDefault();
if (!form.name || !form.email) {
setErr("Name and email are required");
return;
}
setErr("");
setOk(true);
// await fetch("/api/signup", { method: "POST", body: JSON.stringify(form) })
}
return (
<form onSubmit={submit}>
<input name="name" value={form.name} onChange={set} placeholder="Name" />
<input name="email" value={form.email} onChange={set} placeholder="Email" />
{err && <p style={{ color: "red" }}>{err}</p>}
<button>Sign up</button>
{ok && <p>Signed up {form.name}!</p>}
</form>
);
}
6 · Lifecycle, routing, misc
Lifecycle mapping (class → hooks)
componentDidMount → useEffect(fn, []). componentDidUpdate → useEffect(fn, [deps]). componentWillUnmount → the cleanup function returned from useEffect. That mapping is the whole "migrate this class to hooks" question.
import { BrowserRouter, Routes, Route, Link, useParams, useNavigate } from "react-router-dom";
function App() {
return (
<BrowserRouter>
<nav><Link to="/">Home</Link> | <Link to="/users/7">User 7</Link> | <Link to="*">404 route is "*"</Link></nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/users/:id" element={<User />} />
<Route path="*" element={<p>404</p>} /> // catch-all 404 route
</Routes>
</BrowserRouter>
);
}
function User() {
const { id } = useParams(); // read URL params
const navigate = useNavigate(); // programmatic navigation
return <button onClick={() => navigate("/")}>Go home</button>;
}
SPA vs MPA
SPA: one HTML page, JS renders everything, routing happens client-side without reloads. MPA: server returns a fresh page per URL.
Why Next.js exists
SPAs ship an empty div and wait; SEO and first paint suffer. Next.js pre-renders: SSG at build time, SSR per request, or hybrid. Same React code, server-rendered output.
StrictMode
Dev-only double render/effects to surface missing cleanup early. If effects run twice in dev but fine in prod, that is why — fix the effect, don't remove StrictMode.
HTTP-first data libs
Server state is not client state. TanStack Query (React Query) or RTK Query handle caching, retries, refetching; Redux/Zustand for client state. Interview: "I'd use React Query for server data, Context/Redux for auth/theme."
Q: keys, portals, fragments, Suspense/lazy
Portals: render children in another DOM node — modals. Fragments: group without extra DOM nodes. lazy + Suspense: code-split components, <Suspense fallback={<Spin />}>. Error boundaries: class components that catch render errors of children.
MODULE 05
SQL & PostgreSQL
The usual ask: one CREATE-table sketch, one JOIN + GROUP BY query, and crisp definitions of the keys. Then a NULL trap to finish.
Write these answers
Primary key vs foreign key
Primary key: the column that uniquely identifies each row, implicitly NOT NULL. Foreign key: a column referencing another table's key, so the DB rejects orphan rows (books.author_id → authors.id).
WHERE vs HAVING
WHERE filters individual rows before grouping; HAVING filters the groups after GROUP BY, so it is where aggregate conditions go. Mixing the two up is the classic lost mark.
INNER vs LEFT JOIN
INNER JOIN keeps only rows matching in both tables. LEFT JOIN keeps every row of the left table and fills unmatched columns with NULL — use it to find authors with zero books.
GROUP BY rule
Every selected column must be either listed in GROUP BY or wrapped in an aggregate: COUNT, SUM, AVG, MIN, MAX.
NULL comparisons
NULL means "unknown": col = NULL is never true and silently returns nothing. Use IS NULL / IS NOT NULL. Note COUNT(col) skips NULLs while COUNT(*) does not.
DELETE vs TRUNCATE
DELETE removes rows one by one, can take a WHERE, is fully logged. TRUNCATE empties the table instantly, resets identity, no WHERE. DROP removes the table itself.
What an index buys
A sorted B-tree structure over column(s) so lookups skip the full scan. Cost: extra disk and slower writes. Primary keys get one automatically.
SERIAL and normal forms
SERIAL = auto-incrementing integer id. 1NF: atomic values, no repeating groups. 2NF: no partial dependency on part of a composite key. 3NF: non-key columns depend only on the key.
Working example — schema, join, aggregate
-- run with: psql -f bookstore.sql
CREATE TABLE authors (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL
);
CREATE TABLE books (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
price NUMERIC(10,2),
author_id INTEGER REFERENCES authors(id)
);
INSERT INTO authors (name) VALUES ('Frank Herbert'), ('Ursula K. Le Guin');
INSERT INTO books (title, price, author_id) VALUES
('Dune', 499.00, 1),
('A Wizard of Earthsea', 349.00, 2);
-- each author with book count and total value
SELECT a.name,
COUNT(b.id) AS books,
SUM(b.price) AS total
FROM authors a
LEFT JOIN books b ON b.author_id = a.id
GROUP BY a.name
HAVING SUM(b.price) > 100;
-- update and delete
UPDATE books SET price = 449.00 WHERE title = 'Dune';
DELETE FROM books WHERE title = 'A Wizard of Earthsea';
More working examples
-- Which author's books earn the most? (JOIN + GROUP BY + ORDER)
SELECT a.name, SUM(b.price) AS earned
FROM authors a
JOIN books b ON b.author_id = a.id
GROUP BY a.name
ORDER BY earned DESC
LIMIT 3;
-- Authors with NO books (LEFT JOIN + IS NULL)
SELECT a.name
FROM authors a
LEFT JOIN books b ON b.author_id = a.id
WHERE b.id IS NULL;
-- Books priced above the average (subquery)
SELECT title, price
FROM books
WHERE price > (SELECT AVG(price) FROM books);
-- Bucket rows with CASE
SELECT title,
CASE WHEN price < 100 THEN 'cheap'
WHEN price < 400 THEN 'mid'
ELSE 'premium' END AS band
FROM books;
-- Newest first, page 2 (10 per page)
SELECT * FROM books ORDER BY id DESC LIMIT 10 OFFSET 10;
-- Change tables later without dropping data
ALTER TABLE books ADD COLUMN isbn VARCHAR(13);
ALTER TABLE authors ADD CONSTRAINT authors_name_uniq UNIQUE (name);
Predict the result
QHow many rows does SELECT * FROM books WHERE price = NULL return?+
None, always. NULL is "unknown" and never equals anything — the correct test is WHERE price IS NULL.
QWrite the query for "authors with no books".+
SELECT a.name
FROM authors a
LEFT JOIN books b ON b.author_id = a.id
WHERE b.id IS NULL;
MODULE 06
MongoDB
Tests love the SQL↔Mongo vocabulary table and one small aggregation. Memorise the find operators — they appear in nearly every paper.
Write these answers
Document model
MongoDB stores JSON-like documents (BSON) grouped in collections. Documents in one collection may differ in fields — the schema lives in the application, not the storage.
_id
Every document gets an _id primary key; a ObjectId is auto-generated if you do not supply one.
Operators in find
Comparisons are objects: { price: { $gt: 100 } } means WHERE price > 100. Others: $gte, $lt, $in (list), $ne. Fields in one object are AND; $or: [ … ] for OR.
Embed vs reference
Embed the sub-document when it is read together and bounded (a book's reviews). Reference another collection by _id when the data is large or shared. Frame it as: embed until you need the other side independently.
$lookup = JOIN
An aggregation stage that joins another collection: from, localField, foreignField, as. The standard answer to "how would you join in MongoDB?"
Aggregation pipeline
Documents flow through ordered stages: $match (filter) → $group (aggregate) → $sort. Each stage's output is the next stage's input.
Indexes
db.coll.createIndex({ field: 1 }) builds a B-tree used by find and sort. explain() shows whether a query used it: IXSCAN good, COLLSCAN full scan.
SQL ↔ MongoDB vocabulary
| SQL | MongoDB |
|---|---|
| Database | use shop (a database) |
| Table | Collection |
| Row | Document |
| Column | Field |
| Primary key | _id |
| INSERT | insertOne / insertMany |
| SELECT … WHERE | find({ … }) |
| UPDATE | updateOne(filter, { $set }) |
| DELETE | deleteOne / deleteMany |
| JOIN | $lookup |
| GROUP BY | aggregation $group |
Working example
use shop; // switch (and create) the database
db.products.insertOne({ name: "Pen", price: 20, tags: ["school"] });
db.products.insertMany([
{ name: "Bag", price: 500 },
{ name: "Eraser", price: 5, tags: ["school"] },
]);
db.products.find({ price: { $gt: 100 } }); // Bag
db.products.find({ tags: "school", price: { $lte: 20 } }); // Pen, Eraser
db.products.updateOne({ name: "Pen" }, { $set: { price: 25 } });
db.products.deleteOne({ name: "Eraser" });
// aggregation: total value of "school" products (≈ GROUP BY)
db.products.aggregate([
{ $unwind: "$tags" },
{ $match: { tags: "school" } },
{ $group: { _id: "$tags", total: { $sum: "$price" } } },
]); // [ { _id: "school", total: 25 } ]
db.products.createIndex({ price: 1 }); // index for price lookups
More working examples
// Mongoose: schema → model → full CRUD from Node
const mongoose = require("mongoose");
mongoose.connect("mongodb://127.0.0.1:27017/shop");
const productSchema = new mongoose.Schema({
name: { type: String, required: true },
price: { type: Number, min: 0 },
tags: [String],
});
const Product = mongoose.model("Product", productSchema);
async function main() {
// create
await Product.create({ name: "Pen", price: 20, tags: ["school"] });
// read
const cheap = await Product.find({ price: { $lte: 20 } });
// update
await Product.updateOne({ name: "Pen" }, { $set: { price: 25 } });
// delete
await Product.deleteOne({ name: "Pen" });
// aggregate (≈ GROUP BY over tags)
const byTag = await Product.aggregate([
{ $unwind: "$tags" },
{ $group: { _id: "$tags", total: { $sum: "$price" } } },
]);
console.log(cheap, byTag);
}
main();
MODULE 07
Docker
Know image vs container cold, the meaning of every Dockerfile line, and the two flags of docker run. Then one compose file for the database.
Write these answers
Image vs container
An image is a read-only template built from a Dockerfile, stored in cached layers. A container is a running instance of an image with a writable layer on top. Say it like: image = class, container = object.
FROM · WORKDIR · COPY · RUN
FROM picks the base image; WORKDIR sets the working directory (creating it); COPY puts files in; RUN executes a command at build time and stores the result as a new layer.
CMD vs ENTRYPOINT
Both define what runs when the container starts. ENTRYPOINT is the fixed program; CMD supplies default arguments that docker run image args overrides. CMD alone is fine for simple Node apps.
COPY vs ADD
ADD can also unpack tar archives and fetch URLs; COPY just copies. Rule: use COPY unless you specifically need the extras.
Why package.json is copied first
Layers are cached — a layer rebuilds only when its inputs change. COPY package*.json → npm ci before COPY . . means dependency install is skipped on pure code changes, giving the fastest rebuilds.
docker run flags
-d background, -p 3000:3000 host:container port mapping, --name a stable name, -e an env var, -v a volume for data that must survive the container.
docker compose
One YAML file describing a multi-container setup (app + database): services, ports, environment, volumes. docker compose up builds and wires everything; the database keeps its data in a named volume.
Working example
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev // cached layer — skipped when only code changed
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
docker build -t books-api .
docker run -d -p 3000:3000 --name books books-api
docker ps # running containers
docker logs books # app output
docker stop books; docker rm books
services:
api:
build: .
ports:
- "3000:3000"
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
More working examples
# stage 1: build the React app into static files
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build # Vite → /app/dist, CRA → /app/build
# stage 2: serve only those static files from a tiny nginx image
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html # (CRA: /app/build)
EXPOSE 80
node_modules
.git
dist
*.log
.env
docker tag books-api:latest yourname/books-api:1.0
docker login
docker push yourname/books-api:1.0 # anyone can now: docker run yourname/books-api:1.0
Dockerfile instructions at a glance
| Instruction | Meaning |
|---|---|
FROM | base image to build on |
WORKDIR | working directory for the rest of the build |
COPY | copy files from the build context into the image |
RUN | execute at build time (installs, compile) |
ENV | set an environment variable |
EXPOSE | document the port the app listens on |
CMD | default command when a container starts |
MODULE 08
CI/CD
Define the pipeline, name its stages, and be able to read a workflow file line by line. That is the whole syllabus — the file below is the answer sheet.
Write these answers
CI — Continuous Integration
Every push triggers an automatic build and test run on a server, so broken code is caught in minutes instead of at release time. The checks you see on a pull request are CI running.
CD — two meanings
Continuous Delivery: after tests, the artifact is packaged and ready — a human presses deploy. Continuous Deployment: the pipeline releases to production automatically. Delivery = auto-prepared; Deployment = auto-released.
Pipeline stages
Install → lint → test → build → publish artifact → deploy. Fail fast: if tests fail, deploy never runs.
Triggers
on: push (branch-filtered) runs for each commit; on: pull_request runs checks on the PR before merge. Scheduled and manual triggers exist too.
Secrets
Passwords and keys live in the CI service's secret store and are read as environment variables. Never commit them to YAML or push a .env. A leaked credential in a public repo is an incident.
Caching and artifacts
Cache node_modules with setup-node's cache: npm so installs take seconds; store the build output as an artifact for the deploy job to pick up.
Working example — real workflow
name: CI
on:
push:
branches: [main]
pull_request: # same checks on every PR
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # get the code
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm # cached node_modules
- run: npm ci # clean install from the lockfile
- run: npm run lint # fail fast on style
- run: npm test # unit tests
- run: npm run build # does it even build?
More working examples
name: CI/CD
on:
push:
branches: [main]
jobs:
build-test: # job 1: prove the code works
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npm test
- run: npm run build
- uses: actions/upload-artifact@v4 # save the build output
with:
name: app-build
path: dist/ # (CRA: build/)
deploy: # job 2: ship it — only after tests pass
needs: build-test
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: app-build
path: dist/
- run: echo "deploying dist/…" # real job: scp/rsync or your host's CLI