Back
In print settings: Save as PDF, turn headers and footers off, turn background graphics on.

Modern JavaScript & Ecosystem

An advanced journey through JavaScript, covering the core language, asynchronous patterns, functional programming, React, and server-side Node.js.

Official Documentation

July 2026

Contents

Foundations

  • Strict Mode and Modern ECMAScript Evolution

Functional Foundations

  • Closures and Lexical Scope

Object-Oriented JavaScript

  • The Prototype Chain and Inheritance
  • Modern OOP: Classes and Encapsulation

Advanced Foundations

  • The Mechanics of 'this' and Execution Context

Functional Foundations

  • Functional Programming: Higher-Order Functions
  • Pure Functions and Immutability

Asynchronous JavaScript

  • The Runtime and The Event Loop
  • Promises and Asynchronous Orchestration
  • Async/Await: Syntactic Asynchrony

Ecosystem and Tooling

  • Modules and Modular Architecture

Web Engineering

  • DOM Manipulation and Browser APIs

Frameworks: React

  • React Essentials: The Virtual DOM and Components
  • React Hooks: Managing Logic and Lifecycle
  • Advanced React: Context and Optimization

Ecosystem and Tooling

  • TypeScript Foundations: Static Typing for JavaScript

Server-Side Development

  • Node.js: Server-Side JavaScript Runtime
  • REST API Design with Express

Engineering Excellence

  • Testing Strategies: Unit and Integration
  • Security: Defending the Modern Web

Server-Side Development

  • Databases and Data Persistence

Engineering Excellence

  • Web Performance and Optimization

Foundations

Section Detail

Strict Mode and Modern ECMAScript Evolution

The Evolution of the ECMAScript Standard

JavaScript, or more formally ECMAScript (ES), has transformed from a simple scripting language for browser-side validation into a versatile, high-performance general-purpose language. This evolution is marked by the pivot from the chaotic “sloppy mode” of early implementations to the disciplined and optimized environment provided by “strict mode” and the ES6 (ES2015) revolution.

Strict Mode: Enforcing Discipline

Introduced in ECMAScript 5, "use strict"; is a literal expression that instructs the JavaScript engine to execute the code in a more rigorous manner. Strict mode is not just a style choice; it intentionally changes the semantics of the language to prevent common errors and enable certain optimizations.

Key Restrictions in Strict Mode

  1. Elimination of Silent Failures: In sloppy mode, assigning a value to a non-existent variable implicitly creates a global variable. In strict mode, this throws a ReferenceError.
  2. This Binding: In a standard function call, this defaults to the global object (window or global). In strict mode, this remains undefined, preventing accidental pollution of the global namespace.
  3. Prohibiting Duplicates: Strict mode forbids duplicate property names in object literals and duplicate parameter names in functions.
  4. Security Measures: It prevents access to fn.caller and fn.arguments for security reasons, making it harder to “spy” on the call stack.
javascript
1"use strict";
2 
3try {
4 x = 3.14; // Throws ReferenceError because x is not declared
5} catch (e) {
6 console.log(e.name + ": " + e.message);
7}

Modern Variables: Scoping

ES6 introduced let and const to address the flaws of var. While var is functionally scoped and subject to “hoisting”, let and const are block-scoped.

The Temporal Dead Zone (TDZ)

let and const variables exist in a “Temporal Dead Zone” from the start of the block until declaration. Accessing them before declaration results in a ReferenceError.

Featurevarletconst
ScopeFunctionBlockBlock
Re-assignmentYesYesNo
HoistingYesTDZTDZ

Arrow Functions and Lexical Context

Arrow functions (() => {}) provide a more concise syntax, but their primary utility lies in their handling of the this keyword. Unlike standard functions, arrow functions do not have their own this context. Instead, they capture the this value of the enclosing lexical scope at the time they are defined.

This makes arrow functions ideal for callbacks and methods where you want to maintain access to the parent object’s context without using .bind(this) or aliases like var self = this;.

Which of the following is TRUE regarding strict mode in JavaScript?

Functional Foundations

Section Detail

Closures and Lexical Scope

Understanding Scope

In computer science, scope refers to the region of a program where a binding (a variable or function name) is valid. JavaScript utilizes lexical scoping, also known as static scoping. This means that the scope of a variable is determined by its position within the source code during the parsing phase, rather than how the code is called at runtime.

The Execution Context and Variable Environment

When the JavaScript engine executes code, it creates an Execution Context. Every context has a Lexical Environment, which consists of:

  1. Environment Record: A map of variable names to their values.
  2. Outer Environment Reference: A link to the parent lexical environment.

This linkage of environments forms the Scope Chain. When a variable is accessed, the engine searches the current environment record. If not found, it moves up the reference to the parent environment, repeating this until it reaches the global scope.

The Nature of Closures

A closure is the combination of a function bundled together with references to its surrounding state (the lexical environment). In JavaScript, closures are created every time a function is created, at function creation time.

Essentially, a closure gives a function access to its outer scope even after the outer function has finished executing. This is possible because the inner function maintains a reference to the outer environment record, preventing it from being garbage collected.

javascript
1function createCounter() {
2 let count = 0; // Encapsulated variable
3 
4 return function() {
5 count++;
6 return count;
7 };
8}
9 
10const counter = createCounter();
11console.log(counter()); // 1
12console.log(counter()); // 2

In the example above, createCounter returns an anonymous function. Even though createCounter has finished its execution, the returned function “remembers” the environment where count was defined.

Applications of Closures

Private State and Encapsulation

Before the introduction of private class fields (#field), closures were the primary mechanism for implementing privacy in JavaScript. By defining variables within a function scope and returning an object with methods that access those variables, you can create “private” data that cannot be manipulated from the outside.

Function Factories

Closures allow for the creation of functions that are pre-configured with certain parameters.

javascript
1function makeAdder(x) {
2 return function(y) {
3 return x + y;
4 };
5}
6 
7const add5 = makeAdder(5);
8const add10 = makeAdder(10);
9 
10console.log(add5(2)); // 7
11console.log(add10(2)); // 12

Common Pitfalls: The Loop Problem

A classic issue involving closures occurs when using var inside a loop with asynchronous callbacks. Because var is function-scoped (or global if outside a function), all callbacks share the same variable instance.

Fixing the Loop Closure

for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log("Index:", );
  }, 100);
}

Memory Considerations

While closures are powerful, they must be used judiciously. Since a closure maintains a reference to its outer scope, variables within that scope cannot be garbage collected as long as the closure exists. If you create many closures that maintain references to large objects (like DOM nodes or large arrays) that are no longer needed, it can lead to memory leaks.

Object-Oriented JavaScript

Section Detail

The Prototype Chain and Inheritance

Prototypal vs. Classical Inheritance

Unlike languages like Java or C++, which utilize class-based inheritance, JavaScript is a prototype-based language. While ES6 introduced the class keyword, it is primarily “syntactic sugar” over JavaScript’s existing prototypal inheritance mechanism. In classical inheritance, classes are blueprints and objects are instances. In prototypal inheritance, objects inherit directly from other objects.

Every object in JavaScript has an internal property called [[Prototype]]. This is a link to another object. When you attempt to access a property on an object, JavaScript first looks at the object itself. If the property is not found, it follows the [[Prototype]] link to the next object and searches there. This continues until the property is found or the end of the chain (usually Object.prototype, which points to null) is reached.

The [[Prototype]] of an object can be accessed via Object.getPrototypeOf(obj) or the deprecated __proto__ property.

Constructors and the prototype Property

When a function is used with the new keyword, it acts as a constructor.

  1. A new empty object is created.
  2. The new object’s [[Prototype]] is set to the function’s prototype property.
  3. The constructor function is executed with this bound to the new object.
  4. The object is returned (unless the constructor returns a different object).

It is crucial to distinguish between an object’s prototype ([[Prototype]]) and a function’s prototype property. The prototype property is only used when the function is used as a constructor to set the [[Prototype]] of the new instance.

Code
skinparam monochrome true

object "myArray (Instance)" as instance {
length = 2
}

object "Array.prototype" as arrayProto {
push()
pop()
slice()
}

object "Object.prototype" as objProto {
toString()
hasOwnProperty()
}

instance ..> arrayProto : [[Prototype]]
arrayProto ..> objProto : [[Prototype]]
objProto ..> null : [[Prototype]]
myArray (Instance)length = 2Array.prototypepush()pop()slice()Object.prototypetoString()hasOwnProperty()nullPrototypePrototypePrototype

Method Delegation

The primary benefit of the prototype chain is reference-based delegation. Instead of every object instance having its own copy of a method (which consumes memory), methods are defined on the prototype. All instances share the same function implementation.

javascript
1function Animal(name) {
2 this.name = name;
3}
4 
5Animal.prototype.speak = function() {
6 return this.name + " makes a noise.";
7};
8 
9const dog = new Animal("Rex");
10console.log(dog.speak()); // "Rex makes a noise."
11console.log(dog.hasOwnProperty('speak')); // false (it's on the prototype)

Shadowing Properties

If you define a property on an instance with the same name as a property on its prototype, the instance property “shadows” the prototype property. The lookup mechanism finds the instance property first and stops searching.

The Object.create() Method

Object.create(proto) provides a cleaner way to create an object with a specific prototype without needing a constructor function or the new keyword.

javascript
1const machine = {
2 type: "universal",
3 compute: function() { return "Computing..."; }
4};
5 
6const turing = Object.create(machine);
7turing.id = "Alan";
8 
9console.log(turing.compute()); // "Computing..."
10console.log(Object.getPrototypeOf(turing) === machine); // true

Performance Considerations

Traversal of the prototype chain can be expensive if the chain is long. Checking for a property using in scans the entire chain, whereas hasOwnProperty() only checks the instance itself. Modern engines like V8 optimize property access using “Hidden Classes”, but deeply nested prototype structures should still be used with caution.

Section Detail

Modern OOP: Classes and Encapsulation

The Class Abstraction

ECMAScript 2015 introduced the class syntax to provide a more familiar organizational structure for developers coming from languages like Java or C++. However, as previously established, this is an abstraction over the prototype chain. Classes in JavaScript are essentially special functions.

Structure of a Class

A class definition typically includes a constructor, methods, and optionally, static members.

class Spacecraft {
  constructor(name, range) {
    this.name = name;
    this.range = range;
  }

  // Prototype method
  launch() {
    return `${this.name} is entering orbit.`;
  }

  // Static method (on the class itself, not instances)
  static getManufacturer() {
    return "Void Dynamics Corp.";
  }
}

Static Members and the this Context

Static methods and properties are defined on the class itself rather than on the instance. Inside a static method, this refers to the class constructor, not an object instance. Static members are often used for utility functions or factory methods.

Private Fields and Encapsulation

Historically, JavaScript developers used a convention of prefixing properties with an underscore (e.g., _secret) to signal that they should be treated as private. However, this was purely a convention and did not prevent external access.

Modern JavaScript (ES2022+) supports Private Class Fields using the # prefix. These fields are truly encapsulated; they cannot be accessed or modified from outside the class body, even through property indexing.

javascript
1class SecureVault {
2 #accessCode; // Private field
3 
4 constructor(code) {
5 this.#accessCode = code;
6 }
7 
8 checkCode(attempt) {
9 return attempt === this.#accessCode;
10 }
11}
12 
13const vault = new SecureVault(1234);
14console.log(vault.checkCode(1234)); // true
15try {
16 console.log(vault.#accessCode); // Throws SyntaxError
17} catch (e) {
18 console.log("Access Denied: Private Field.");
19}

Inheritance with extends and super

The extends keyword allows a class to inherit from another class. The super() call inside the constructor is mandatory before accessing this, as it initializes the parent class and binds the instance.

class Rocket extends Spacecraft {
  constructor(name, range, fuelType) {
    super(name, range); // Call parent constructor
    this.fuelType = fuelType;
  }

  // Method overriding
  launch() {
    return `${super.launch()} Igniting ${this.fuelType} boosters.`;
  }
}

Getters and Setters

Classes support get and set keywords to define computed properties or interpose logic during property access/assignment. This allows for validation and transformation while maintaining a property-like interface.

What happens if you attempt to access a private field (e.g., #data) from outside the class where it is defined?

Performance of Classes

While classes provide a clean syntax, they carry the same performance characteristics as prototypal inheritance. The method lookup still involves traversing the prototype chain. However, modern engines optimize class-based structures very effectively because the fixed shape of class instances allows for stable inline caches and optimized machine code generation.

Advanced Foundations

Section Detail

The Mechanics of 'this' and Execution Context

The Ambiguity of ‘this’

In many object-oriented languages, this (or self) always refers to the current instance of a class. In JavaScript, however, this is a dynamic reference that is determined not by where a function is defined, but by how the function is called. This is known as call-site binding.

Understanding this requires evaluating four primary binding rules in order of precedence.

1. Default Binding

When a function is called as a standalone function (e.g., func()), this defaults to the global object (window in browsers, global in Node.js). However, if strict mode is active, this will be undefined.

javascript
1function showThis() {
2 return this;
3}
4 
5console.log(showThis() === global); // true (in non-strict Node environment)
6 
7"use strict";
8function showThisStrict() {
9 return this;
10}
11console.log(showThisStrict()); // undefined

2. Implicit Binding

When a function is called as a method of an object (e.g., obj.method()), this is bound to the object that “owns” or contains the function call.

const user = {
  name: "Alice",
  greet() {
    return `Hello, I'm ${this.name}`;
  }
};

console.log(user.greet()); // "Hello, I'm Alice"

Implicit Lost: A common source of bugs occurs when a method is assigned to a variable or passed as a callback. In these cases, the implicit binding is “lost,” and the function reverts to default binding.

3. Explicit Binding: Call, Apply, and Bind

JavaScript provides three methods to explicitly set the this context for a function call.

  • call(context, arg1, arg2, ...): Invokes the function immediately with the provided context and individual arguments.
  • apply(context, [args]): Invokes the function immediately with the provided context and an array of arguments.
  • bind(context, ...args): Returns a new function with the this context permanently bound to the provided value.
javascript
1function introduce(location, hobby) {
2 return `I&#039;m ${this.name} from ${location} who likes ${hobby}.`;
3}
4 
5const traveler = { name: "Bob" };
6 
7console.log(introduce.call(traveler, "London", "Chess"));
8const boundIntro = introduce.bind(traveler);
9console.log(boundIntro("Paris", "Cycling"));

4. New Binding

When a function is called with the new keyword (as a constructor), this is bound to the newly created object. This binding takes precedence over implicit and explicit binding.

Lexical ‘this’ in Arrow Functions

Arrow functions do not have their own this binding. Instead, they inherit this from the surrounding (parent) lexical scope. This is extremely useful for maintaining context in asynchronous callbacks or nested functions without needing to use bind() or closure hacks like const self = this.

javascript
1const timer = {
2 seconds: 0,
3 start() {
4 // Standard function would lose binding here
5 setInterval(() => {
6 this.seconds++;
7 if (this.seconds <= 3) console.log(this.seconds);
8 }, 1000);
9 }
10};
11 
12timer.start();

Binding Precedence Summary

  1. New Binding: Was the function called with new?
  2. Explicit Binding: Was the function called with call, apply, or bind?
  3. Implicit Binding: Was the function called as a method on an object?
  4. Default Binding: Is it a standalone function call? (Mind strict mode).

Functional Foundations

Section Detail

Functional Programming: Higher-Order Functions

The Functional Paradigm

JavaScript is a multi-paradigm language that treats functions as first-class citizens. This means functions can be assigned to variables, passed as arguments to other functions, and returned from functions. This capability is the foundation of Functional Programming (FP).

Higher-Order Functions (HOFs)

A Higher-Order Function is a function that either:

  1. Takes one or more functions as arguments.
  2. Returns a function as its result.

HOFs allow for the abstraction of actions, not just values. This leads to code that is more modular, reusable, and declarative (focusing on “what” to do rather than “how” to do it).

Declarative Array Transformations

The most common application of HOFs in JavaScript is manipulating collections. Instead of using imperative for loops, we use declarative methods on the Array.prototype.

Map: Transforming Elements

The map() method creates a new array by applying a callback function to every element of the calling array. It does not mutate the original array.

const temperaturesCelsius = [0, 10, 25, 30];
const temperaturesFahrenheit = temperaturesCelsius.map(c => (c * 9/5) + 32);

Filter: Selecting Elements

The filter() method creates a new array containing only the elements that satisfy a provided predicate function.

const users = [{ name: 'Alice', active: true }, { name: 'Bob', active: false }];
const activeUsers = users.filter(user => user.active);

Reduce: Consolidating Data

The reduce() method is the most powerful HOF. It executes a “reducer” function on each element, resulting in a single output value (an object, a number, or even another array).

javascript
1const inventory = [
2 { item: 'Laptop', price: 1200 },
3 { item: 'Mouse', price: 25 },
4 { item: 'Monitor', price: 300 }
5];
6 
7const totalValue = inventory.reduce((total, current) => {
8 return total + current.price;
9}, 0); // 0 is the initial value
10 
11console.log("Total Inventory Value:", totalValue);

Currying and Partial Application

Currying is the technique of converting a function that takes multiple arguments into a sequence of functions that each take a single argument. This allows for Partial Application, where you fix some arguments of a function and produce a new function for the remaining ones.

javascript
1// Curried function
2const multiply = x => y => x * y;
3 
4const double = multiply(2);
5const triple = multiply(3);
6 
7console.log(double(10)); // 20
8console.log(triple(10)); // 30

Why Functional Programming?

  1. Predictability: Functional code often relies on pure functions, making it easier to reason about and test.
  2. Concurrency: Because FP emphasizes immutability, it avoids race conditions associated with shared mutable state.
  3. Brevity: Declarative code is typically more concise than imperative equivalents.

Which array method should be used to create a subset of an array based on a condition, without modifying the original array?

Section Detail

Pure Functions and Immutability

The Concept of Purity

In functional programming, a Pure Function is a function that possesses two defining characteristics:

  1. Determinism: Given the same input, it will always return the same output.
  2. No Side Effects: It does not modify any state outside its scope or interact with the “outside world” (e.g., logging to console, network calls, modifying global variables).

Determinism and Idempotency

A deterministic function depends solely on its input arguments. If a function uses Math.random(), Date.now(), or reads from a global configuration that can change, it is impure.

Pure functions are idempotent in the context of their return values: calling them multiple times with the same input yields identical results. This makes them significantly easier to test and allows for memoization (caching the results of expensive computations).

Side Effects: The Enemies of Predictability

A side effect is any observable change outside the function’s return value.

  • Modifying an argument (if it’s a reference type).
  • Mutating a global variable.
  • Writing to a database or file system.
  • Triggering a UI update.

While applications must have side effects to be useful (otherwise they wouldn’t perform I/O), the goal of functional programming is to isolate side effects to thin wrappers at the edges of the system, keeping the core logic pure.

Immutability: Protecting State

Immutability means that once a piece of data is created, it cannot be changed. In JavaScript, primitive types (strings, numbers, booleans) are naturally immutable. However, objects and arrays are mutable by default.

The Problem with Mutation

Mutation makes it difficult to track how state changes over time. If multiple parts of a program share a reference to the same object and one part mutates it, the other parts may crash or behave unpredictably.

javascript
1const original = { x: 1, y: 2 };
2const shallowCopy = original;
3 
4shallowCopy.x = 99; // Mutation affects both variables
5 
6console.log(original.x); // 99

Achieving Immutability in JavaScript

To maintain immutability, we create new versions of data structures instead of modifying them in place. This is facilitated by the Spread Operator (...).

javascript
1const state = { user: "Alice", roles: ["admin"] };
2 
3// Create a new state object with an updated field
4const nextState = {
5 ...state,
6 user: "Bob",
7 roles: [...state.roles, "editor"] // Deep copy the array too
8};
9 
10console.log(state.user); // "Alice" (Unchanged)
11console.log(nextState.user); // "Bob"

Structural Sharing

One might worry that creating new objects for every change is inefficient. However, modern JavaScript engines and functional libraries use “Structural Sharing”. If you create a new object from an old one, the new object’s properties can point to the same memory locations as the old object’s properties for any data that hasn’t changed. Only the modified parts are allocated fresh memory.

Benefits of Immutability

  1. Time-Travel Debugging: Because state is never overwritten, you can keep a history of past states and move back and forth between them.
  2. Referential Equality: Checking if an object has changed becomes a simple identity check (obj1 === obj2) rather than a deep recursive comparison. This is critical for performance optimizations in frameworks like React.
  3. Thread Safety: While JavaScript is single-threaded, immutable data structures prevent complex bugs in asynchronous code where multiple closures might otherwise struggle over a shared mutable object.

Asynchronous JavaScript

Section Detail

The Runtime and The Event Loop

Single-Threaded Nature

JavaScript is a single-threaded language, meaning it has one Call Stack and can execute only one command at a time. However, modern web applications handle multiple network requests, user interactions, and timers simultaneously. This is achieved through the Concurrency Model and the Event Loop, which allow JavaScript to perform non-blocking I/O even while running on a single thread.

The Components of the Runtime

The JavaScript runtime is composed of several key parts:

  1. Memory Heap: Where memory allocation happens (objects, variables).
  2. Call Stack: Where the engine keeps track of function execution (LIFO - Last In, First Out).
  3. Web APIs / Node APIs: External environments (provided by the browser or Node.js) that handle things like setTimeout, DOM events, and fetch requests.
  4. Callback Queue (Task Queue): Where callbacks from Web APIs wait to be pushed onto the stack.
  5. Microtask Queue: A high-priority queue for Promises and process.nextTick.

The Mechanics of the Event Loop

The Event Loop has a very simple job: monitor the Call Stack and the Task Queues. If the Call Stack is empty, it takes the first task from the queue and pushes it onto the stack, which effectively runs it.

The Execution Order

  1. Execute all synchronous code in the Call Stack.
  2. Check the Microtask Queue. Execute all pending microtasks until the queue is empty.
  3. Check the Callback Queue (Macrotasks). Execute the first task in the queue.
  4. Repeat.

This means that microtasks (like Promise resolutions) are always executed before the next macrotask (like setTimeout), even if the macrotask was scheduled first.

Code
skinparam componentStyle rectangle

component "Call Stack" as stack
component "Microtask Queue" as micro
component "Callback Queue" as macro
actor "Event Loop" as loop

stack --> loop : "Empty?"
loop --> micro : "Drain All"
micro --> stack : "Run"
loop --> macro : "Run One"
macro --> stack : "Run"

note bottom of micro : Promises
note bottom of macro : setTimeout, etc
Call StackMicrotask QueueCallback QueueEvent LoopPromisessetTimeout, etcEmpty?Drain AllRunRun OneRun

Blocking the Event Loop

Since the Event Loop can only process one thing at a time, performing a heavy computation (like calculating prime numbers or processing a large image) synchronously will “block” the event loop. While the loop is blocked, the browser cannot render updates, handle user input, or process callbacks. This results in a “frozen” user interface.

Visualizing Asynchrony

Consider the following code:

console.log("Start");

setTimeout(() => {
  console.log("Timeout");
}, 0);

Promise.resolve().then(() => {
  console.log("Promise");
});

console.log("End");

Output sequence:

  1. Start (Synchronous)
  2. End (Synchronous)
  3. Promise (Microtask - higher priority)
  4. Timeout (Macrotask - lower priority)

Even though setTimeout has a delay of 0 ms, its callback is placed in the Macrotask queue, which must wait for the current execution and the entire Microtask queue to finish.

If the Call Stack is busy with a long-running while loop, when will a setTimeout callback execute?

Section Detail

Promises and Asynchronous Orchestration

The Death of the Callback

Before ECMAScript 2015, asynchronous operations were handled using callbacks. While functional, this led to “Callback Hell”—deeply nested, unreadable code that made error handling extremely difficult. Promises were introduced to provide a cleaner, more robust way to manage asynchronous operations by representing a value that may be available now, in the future, or never.

The Promise Lifecycle

A Promise is an object that exists in one of three mutually exclusive states:

  1. Pending: Initial state, neither fulfilled nor rejected.
  2. Fulfilled: The operation completed successfully, and the promise has a value.
  3. Rejected: The operation failed, and the promise has a reason (an error).

Once a promise is either fulfilled or rejected, it is settled. A settled promise cannot change its state or value.

Creating and Consuming Promises

A promise is created using the Promise constructor, which takes an “executor” function. This function receives two arguments: resolve and reject.

javascript
1const fetchData = new Promise((resolve, reject) => {
2 const success = true;
3 if (success) {
4 resolve({ id: 1, data: "Orbit Telemetry" });
5 } else {
6 reject("Connection Failure");
7 }
8});
9 
10fetchData
11 .then(data => console.log("Received:", data))
12 .catch(err => console.error("Error:", err))
13 .finally(() => console.log("Request Complete."));

Promise Chaining

The true power of Promises lies in their ability to be chained. Every call to .then() returns a new promise, allowing you to sequence multiple asynchronous operations without nesting.

If a .then() handler returns a value, the new promise is fulfilled with that value. If it returns another promise, the new promise “waits” for that promise to resolve.

fetchUser(id)
  .then(user => fetchOrders(user.id)) // returns a promise
  .then(orders => processPayment(orders[0])) // returns another promise
  .then(receipt => console.log(receipt))
  .catch(handleAnyError); // One catch handles the entire chain

Parallelism and Coordination

JavaScript provides static methods on the Promise class to handle multiple concurrent operations:

  • Promise.all([p1, p2, ...]): Waits for all promises to fulfill. If any single promise rejects, the entire aggregate promise rejects immediately.
  • Promise.allSettled([p1, p2, ...]): Waits for all promises to settle, regardless of outcome.
  • Promise.race([p1, p2, ...]): Resolves or rejects as soon as the first promise in the array settles.
  • Promise.any([p1, p2, ...]): Resolves as soon as the first promise fulfills. It only rejects if all promises reject.

Promise.all behavior

const p1 = Promise.resolve(1);
const p2 = Promise.reject("Error");
const p3 = Promise.resolve(3);

Promise.([p1, p2, p3])
  .catch(e => console.log(e)); // Logs "Error"

Error Propagation

Unlike callbacks, where errors must be manually passed up the chain, Promises naturally propagate errors. A rejection in any link of a .then() chain will skip subsequent .then() handlers and jump directly to the nearest .catch(). This mimics the behavior of try/catch in synchronous code.

Section Detail

Async/Await: Syntactic Asynchrony

Bridging the Gap

While Promises were a massive improvement over callbacks, chaining many .then() calls can still lead to complex logic flows. ECMAScript 2017 introduced async and await, which act as syntactic sugar over Promises. They allow developers to write asynchronous code that looks and behaves more like traditional synchronous code, making it easier to read and debug.

The async Keyword

By prefixing a function with the async keyword, you guarantee that the function will always return a Promise.

  • If the function returns a non-promise value, that value is wrapped in a resolved Promise.
  • If the function throws an error, the returned Promise is rejected with that error.

The await Operator

The await keyword can only be used inside an async function (or at the top level of an ES module). It pauses the execution of the async function until the Promise it is waiting for settles.

Importantly, await does not block the main thread. It only pauses the execution of the specific async function, allowing the Event Loop to continue processing other tasks.

javascript
1async function getAtmosphereData() {
2 console.log("Initiating request...");
3
4 // Simulated network delay using a Promise
5 const telemetry = await new Promise(res =>
6 setTimeout(() => res({ oxygen: 21, nitrogen: 78 }), 1000)
7 );
8 
9 console.log("Data received:", telemetry);
10 return telemetry.oxygen;
11}
12 
13getAtmosphereData().then(o2 => console.log("O2 level:", o2));

Error Handling with Try/Catch

One of the greatest benefits of async/await is that it allows the use of standard try/catch/finally blocks for error management, consolidating asynchronous and synchronous error handling into a single pattern.

async function uploadLogs(logs) {
  try {
    const response = await api.post('/logs', logs);
    return response.status;
  } catch (error) {
    console.error("Upload failed:", error.message);
    throw new Error("System failure during upload");
  } finally {
    clearBuffer();
  }
}

The “Sequential” Trap

A common mistake when using async/await is accidentally making independent asynchronous operations sequential, which increases the total execution time.

javascript
1async function parallelFetch() {
2 const p1 = fetch('https://api.test/1');
3 const p2 = fetch('https://api.test/2');
4 
5 // Using await on both separately but starting them together
6 // or using Promise.all is preferred
7 const [r1, r2] = await Promise.all([p1, p2]);
8 return [r1, r2];
9}

What happens if you use 'await' on a value that is NOT a Promise?

Implementation Detail: Generators and Iterators

Under the hood, async/await is typically implemented using Generators and yield. An async function is essentially a generator function that is automatically stepped forward by a runner (like the co library used to do) whenever a yielded promise resolves. This allows the function to “pause” its state (local variables, execution point) and resume exactly where it left off, all without blocking the system.

Ecosystem and Tooling

Section Detail

Modules and Modular Architecture

The Need for Modularity

As JavaScript applications grew from simple scripts to complex systems, managing the global namespace became impossible. Modularity allows developers to break code into small, self-contained units that export specific functionality and keep their internal logic private.

Pre-Module Patterns: The IIFE

Before any formal module system existed, developers used Immediately Invoked Function Expressions (IIFEs) to create private scopes and avoid polluting the global object.

const counterModule = (function() {
  let count = 0; // Private
  return {
    increment: () => ++count,
    get: () => count
  };
})();

CommonJS: The Node.js Standard

CommonJS (CJS) was developed for server-side environments like Node.js. It uses require() for importing and module.exports for exporting. It is synchronous, which is appropriate for server environments where modules are loaded from the local disk.

// math.js
const add = (a, b) => a + b;
module.exports = { add };

// index.js
const { add } = require('./math');

ESM: The Modern Standard

ECMAScript Modules (ESM) are the official, standardized module system for JavaScript, supported by both modern browsers and Node.js. ESM uses import and export statements.

Key Characteristics of ESM

  1. Static Analysis: Imports and exports are determined at compile-time/parsing-time, not runtime. This enables “Tree Shaking”—the ability for bundlers to remove unused code.
  2. Strict Mode: ESM always operates in strict mode by default.
  3. Asynchronous Loading: Modules can be loaded asynchronously, which is essential for web performance.
  4. Live Bindings: Modifying an exported variable in the source module updates it in the importing module (unlike CommonJS, which exports copies).

Export Types

  • Named Exports: Multiple exports per module. Must be imported using the exact name.
  • Default Export: Only one per module. Can be imported using any name.
javascript
1// Simulation of ESM syntax in a single block
2const module = {
3 export: {
4 PI: 3.14,
5 calculate: (r) => 3.14 * r * r
6 },
7 default: "Geographic Utils"
8};
9 
10const { PI, calculate } = module.export;
11console.log("Area:", calculate(10));
12console.log("Default:", module.default);

Dynamic Imports

While most imports happen at the top of the file, ESM supports dynamic imports via the import() function. This returns a Promise and allows for “Lazy Loading”—loading code only when it is actually needed, which significantly reduces the initial bundle size of a web application.

if (userIsAdmin) {
  const { loadDashboard } = await import('./admin.js');
  loadDashboard();
}

Bundling and the Web

Browsers must download every module individually. For a large application with hundreds of modules, this creates massive network overhead. Tools like Webpack, Rollup, and Vite are used to “bundle” these modules into fewer, optimized files, while also handling transpilation (via Babel) and minification.

Web Engineering

Section Detail

DOM Manipulation and Browser APIs

The Document Object Model (DOM)

The DOM is a programming interface for web documents. It represents the page as a structured tree of nodes, where each node is an object representing a part of the document (elements, text, comments). JavaScript acts as the language that interacts with this interface to create dynamic, interactive websites.

Selecting and Traversing Elements

Modern DOM manipulation favors querySelector and querySelectorAll, which allow selecting elements using standard CSS selectors.

const mainHeader = document.querySelector('#header-1');
const allButtons = document.querySelectorAll('.btn-action');

Traversal refers to moving between nodes in the tree: parentNode, children, nextElementSibling, etc. Accessing the DOM is computationally expensive, so traversal should be minimized.

The Event System

Events are the primary way users interact with the DOM. JavaScript uses an event-driven model.

Event Bubbling and Capturing

When an event occurs on an element, it doesn’t just trigger the listener on that element. It moves through phases:

  1. Capture Phase: From window down to the target.
  2. Target Phase: At the target element.
  3. Bubbling Phase: From the target back up to window.

Event Delegation

Instead of attaching a listener to dozens of individual items (like list items in a large table), you can attach a single listener to a parent element. Because of bubbling, the parent can catch events from its children. You can then check event.target to see which child was actually clicked. This is a crucial optimization for memory and performance.

Event Prevention

const link = document.querySelector('a');
link.addEventListener('click', (event) => {
  // Stop the browser from navigating to the URL
  event.();
});

Reflow and Repaint: Performance Costs

Every time you modify the DOM, the browser may need to recalculate the positions and sizes of elements (Reflow) and then redraw them on the screen (Repaint).

Reflow is particularly expensive because a change to one element can trigger changes in its parent, children, and siblings.

Best Practices to Minimize Reflow:

  • Batch DOM updates using DocumentFragment.
  • Avoid reading layout properties (like offsetHeight) in the same loop where you are writing style changes.
  • Use requestAnimationFrame for animations to synchronize with the browser’s refresh rate.

Storage and Persistence

Browsers provide several APIs for persisting data on the client side:

  1. LocalStorage: Non-expiring, string-based storage (per domain).
  2. SessionStorage: Cleared when the tab is closed.
  3. IndexedDB: A low-level API for client-side storage of significant amounts of structured data, including files/blobs.
  4. Cookies: Small pieces of data sent with every HTTP request, primarily used for session management and tracking.

Why is Event Delegation considered a best practice for large dynamic lists?

Frameworks: React

Section Detail

React Essentials: The Virtual DOM and Components

The Declarative Shift

Traditional DOM manipulation is imperative: you tell the browser exactly which steps to take (e.g., “Find this element, change its color, append a child”). React introduced a declarative model: you describe what the UI should look like for a given state, and React handles the updates.

Components: The Building Blocks

In React, the UI is decomposed into small, isolated, and reusable pieces called Components. Each component is a JavaScript function that returns a description of its UI using JSX (JavaScript XML).

JSX is an extension to JavaScript that allows you to write HTML-like structures directly in your code. It is transpiled by tools like Babel into React.createElement() calls.

The Virtual DOM and Reconciliation

Direct manipulation of the real DOM is slow. To optimize updates, React maintains a Virtual DOM (VDOM)—a lightweight, in-memory representation of the real DOM.

The Diffing Algorithm

When a component’s state changes:

  1. React creates a new VDOM tree.
  2. It compares the new tree with the previous VDOM tree. This process is called Diffing.
  3. It calculates the minimum set of changes needed to synchronize the real DOM with the VDOM.
  4. It applies those changes to the real DOM in a single batch. This phase is called Commit.
Code
node "State Change" as state
node "New VDOM Tree" as vdom
node "Reconciliation (Diff)" as diff
database "Real DOM" as dom

state -> vdom
vdom -> diff
diff -> dom : "Minimal Patches Only"
State ChangeNew VDOM TreeReconciliation (Diff)Real DOMMinimal Patches Only

One-Way Data Flow

Data in React flows in a single direction: from parent to child through Props (properties). A child component can receive data and functions via props, but it cannot directly modify the props it receives. If a child needs to communicate back to the parent, it invokes a callback function passed down through props.

This unidirectional flow makes the application state more predictable and easier to trace.

Pure Components and Immutability

React relies heavily on the concept of Purity. A component should be a “pure” function of its props and state. This means it should not have side effects during the rendering phase.

Furthermore, React uses shallow comparison of props and state to decide if a component needs to re-render. This is why immutability is critical: if you mutate an object, its reference stays the same, and React may fail to detect the change, resulting in a UI that is out of sync with your data.

What is the primary purpose of the Virtual DOM in React?

Section Detail

React Hooks: Managing Logic and Lifecycle

The Functional Revolution

Before React 16.8, state and lifecycle methods were exclusive to Class Components. Hooks were introduced to allow functional components to “hook into” React state and lifecycle features. This led to flatter component trees and better logic reuse.

useState: Preserving Local State

The useState hook allows a component to maintain persistent data across re-renders. It returns an array with two elements: the current state value and a function to update it.

const [count, setCount] = useState(0);

When setCount is called, React schedules a re-render of the component.

useEffect: Handling Side Effects

Side effects, such as data fetching, subscriptions, or manual DOM manipulations, must be isolated from the rendering logic. useEffect runs after the component renders.

It takes two arguments:

  1. Effect Function: The logic to execute.
  2. Dependency Array: An array of values that the effect depends on. The effect only re-runs if one of these values changes.

The Cleanup Phase

If an effect creates a subscription or a timer, it must return a cleanup function to prevent memory leaks when the component unmounts or before the effect re-runs.

javascript
1// Conceptual simulation of useEffect cleanup
2function ChatRoom({ roomId }) {
3 console.log("Connecting to room:", roomId);
4 
5 // Return cleanup function
6 return () => {
7 console.log("Disconnecting from room:", roomId);
8 };
9}
10 
11// Imagine roomId changes from 1 to 2
12const cleanup = ChatRoom({ roomId: 1 });
13cleanup(); // React runs cleanup before switching
14ChatRoom({ roomId: 2 });

useRef: Persistent References

Unlike useState, changing a value in useRef does not trigger a re-render. It is primarily used for:

  1. Accessing DOM nodes directly.
  2. Storing values that need to persist for the lifetime of the component but don’t affect the UI (e.g., timer IDs).

The Rules of Hooks

To ensure that React can correctly associate internal state with functional calls, hooks must follow two strict rules:

  1. Only Call Hooks at the Top Level: Do not call hooks inside loops, conditions, or nested functions.
  2. Only Call Hooks from React Functions: Call them from React functional components or custom hooks.

Custom Hooks: Extracts and Reuses

Custom hooks allow you to extract component logic into reusable functions. This is the primary way React developers share stateful logic between different components without using complex patterns like Render Props or HOCs.

function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth);
  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  return width;
}

What happens if you provide an empty array [] as the second argument to useEffect?

Section Detail

Advanced React: Context and Optimization

Scaling Component Architecture

As applications grow, passing props through multiple levels of components (known as “Prop Drilling”) becomes unmaintainable. React provides patterns and APIs to handle global cross-cutting concerns and to optimize rendering performance.

The Context API

Context provides a way to share values between components without having to explicitly pass a prop through every level of the tree. It is designed for “global” data like the current user, theme, or preferred language.

Components of Context

  1. createContext: Initializes the context object.
  2. Provider: A component that wraps part of your app to make the context value available to its descendants.
  3. useContext: A hook used by consumer components to access the current context value.

Performance Note: Every time the Provider value changes, all components that consume that context will re-render. Context should not be used as a high-frequency state management tool (like for real-time mouse positions or animations).

Optimization Hooks: memo, useMemo, and useCallback

React is generally fast, but unnecessary re-renders in large trees can lead to “jank” (visual stutter).

React.memo

A Higher-Order Component that wraps a functional component. It performs a shallow comparison of props; if the props are identical to the last render, React skips re-rendering the component and reuses the last rendered result.

useMemo

Returns a memoized value. It is used to cache the result of expensive computations so they aren’t recalculated on every render unless their dependencies change.

useCallback

Returns a memoized function. This is particularly important when passing callbacks to memoized child components. In JavaScript, functions are objects, so a function defined inside a component is “re-created” on every render (forming a new reference). useCallback ensures the function reference stays the same.

Component Composition vs. Inheritance

React strongly advocates for Composition over Inheritance. Instead of building complex hierarchies where classes inherit behavior from parents, you build components by combining smaller, specific components.

Containment

Using the children prop to pass elements into a component without the component needing to know what those elements are.

Specialization

Creating a “generic” component (e.g., Dialog) and then creating specialized versions (e.g., WelcomeDialog) that configure the generic one with specific props.

When should you reach for useMemo?

Summary of Optimization Patterns

  1. Keep State Local: Don’t lift state higher than necessary.
  2. Memoize Components: Use React.memo for expensive leaf components.
  3. Optimize Callbacks: Pair useCallback with React.memo to prevent cascading re-renders.
  4. Virtualization: For extremely large lists, use libraries like react-window to render only the items currently visible on the screen.

Ecosystem and Tooling

Section Detail

TypeScript Foundations: Static Typing for JavaScript

The Problem with Dynamic Typing

JavaScript is a dynamically typed language, meaning types are associated with values at runtime, not variables at compile-time. While this offers flexibility, it often leads to “Runtime Errors”—bugs that only appear when a specific line of code is executed (e.g., Cannot read property 'x' of undefined).

TypeScript (TS) is a syntactical superset of JavaScript that adds static typing. It allows developers to catch type errors during development (via the compiler or IDE) long before the code reaches a user.

The Structural Type System

TypeScript uses Structural Typing (also known as “Duck Typing”). This means that two types are compatible if they have the same shape. If an object has the required properties, it is considered a valid instance of a type, regardless of its explicit declaration.

interface Point {
  x: number;
  y: number;
}

function logPoint(p: Point) {
  console.log(`${p.x}, ${p.y}`);
}

// This works despite not being explicitly declared as a 'Point'
const obj = { x: 10, y: 20, z: 30 };
logPoint(obj);

Core Type Constructs

Basic Types

TypeScript extends JS with types like string, number, boolean, unknown, any, and void. The any type disables type checking and should be avoided in production code.

Interfaces and Type Aliases

  • Interfaces: Primarily used to define the shape of objects. They support “declaration merging” (you can define the same interface multiple times and TS will merge them).
  • Type Aliases: Can represent objects, but also unions, primitives, and tuples.

Union and Intersection Types

  • Unions (|): A value can be one of several types. string | number means the variable can be either.
  • Intersections (&): Combines multiple types into one. The value must satisfy all types.
javascript
1// Conceptual TypeScript
2type Status = "loading" | "success" | "error";
3 
4function handle(s) {
5 if (s === "loading") return "Wait...";
6 if (s === "success") return "Done!";
7 return "Failure";
8}
9 
10console.log(handle("loading"));

Generics: Reusable Logic

Generics allow you to create components (functions, classes, interfaces) that work with a variety of types while maintaining type safety. They act as “type variables.”

function wrapInArray<T>(input: T): T[] {
  return [input];
}

const numArr = wrapInArray<number>(5); // T becomes number
const strArr = wrapInArray("text");    // T is inferred as string

The TypeScript Compiler (tsc)

TypeScript does not run in the browser. It must be transpiled into standard JavaScript. The tsc compiler performs two main tasks:

  1. Type Checking: Validates that the code adheres to the defined types.
  2. Downleveling: Converts modern TS/JS into older versions (like ES5) for compatibility with older browsers.

Which keyword in TypeScript creates a type that represents the 'either-or' relationship between multiple types?

Why Use TypeScript?

  • Self-Documenting Code: Types reveal the developer’s intent and expectations.
  • Refactoring Confidence: The compiler instantly flags all locations that break when a function signature or object shape changes.
  • Improved Tooling: Enables powerful IDE features like “Go to Definition,” “Find Usages,” and intelligent autocomplete.

Server-Side Development

Section Detail

Node.js: Server-Side JavaScript Runtime

Beyond the Browser

Node.js is not a programming language or a framework; it is a runtime environment that allows JavaScript to be executed outside of a web browser. Built on Chrome’s V8 JavaScript engine, it enables developers to use JavaScript to write command-line tools and server-side scripts.

Core Architecture: V8 and Libuv

Node.js is fundamentally a wrapper around two major components:

  1. V8: The engine (written in C++) that compiles and executes JavaScript code.
  2. Libuv: A multi-platform C library that handles asynchronous I/O operations. It provides the Event Loop and the Thread Pool that Node uses to offload blocking tasks (like file system access or cryptography).
Code
package "Node.js Architecture" {
[JavaScript Code] --> [Node.js API]
[Node.js API] --> [V8 Engine]
[Node.js API] --> [Libuv]
[Libuv] --> [Event Loop]
[Libuv] --> [Worker Threads]
}
Node.js ArchitectureJavaScript CodeNode.js APIV8 EngineLibuvEvent LoopWorker Threads

The Non-Blocking I/O Model

In traditional web servers (like Apache), every incoming request creates a new thread. This consumes significant memory and CPU. Node.js uses a single-threaded event loop. When it encounters an I/O operation (e.g., reading from a database), it hands the task to Libuv and continues executing the next line of code. When the operation finishes, it places the callback into the queue to be executed.

This makes Node.js exceptionally efficient for I/O-heavy applications (like real-time chats or streaming) but less suitable for CPU-heavy tasks (like video encoding) that would block the single main thread.

Core Modules

Node.js provides built-in modules to interact with the OS:

  • fs (File System): Read, write, and manipulate files and directories.
  • path: Utilities for handling and transforming file paths.
  • http/https: Create servers and make requests.
  • os: Information about the host operating system.

Streams and Buffers

For performance, Node.js processes large amounts of data using Streams. Instead of reading a 4GB file into memory (which would crash the application), a stream reads it in small “chunks.”

  • Buffers: Direct memory allocation outside the V8 heap, used to handle raw binary data.
  • Pipe: A method to connect the output of one stream to the input of another.
const fs = require('fs');
const readable = fs.createReadStream('./large_log.txt');
const writable = fs.createWriteStream('./backup.txt');

// Efficiently move data without loading everything into RAM
readable.pipe(writable);

Why is Node.js considered 'single-threaded' even though it uses multiple threads internally?

NPM: The Package Ecosystem

The power of Node.js lies in NPM (Node Package Manager), the world’s largest software registry. It allows developers to share and reuse code via package.json, which tracks project dependencies and scripts.

Section Detail

REST API Design with Express

The Express Framework

Express is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. It simplifies the process of creating a server by abstracting the low-level http module and providing a powerful Middleware architecture.

Middleware: The Heart of Express

Middleware functions are functions that have access to the request object (req), the response object (res), and the next middleware function in the application’s request-response cycle.

Middleware can:

  • Execute any code.
  • Make changes to the request and the response objects.
  • End the request-response cycle.
  • Call the next middleware in the stack.
app.use((req, res, next) => {
  console.log(`${req.method} request to ${req.url}`);
  next(); // Move to the next function
});

Routing and Parameters

Routing defines how an application responds to a client request to a particular endpoint.

  • Path Parameters: Used for specific resources (e.g., /users/:id).
  • Query Parameters: Used for filtering or sorting (e.g., /search?q=js).
app.get('/api/users/:userId', (req, res) => {
  const id = req.params.userId;
  const user = database.find(u => u.id === id);
  res.status(200).json(user);
});

RESTful Principles

Representational State Transfer (REST) is an architectural style for designing networked applications. It relies on a stateless, client-server, cacheable communications protocol.

MethodActionPurpose
GETRetrieveFetch a resource or list of resources.
POSTCreateCreate a new resource.
PUT/PATCHUpdateReplace or partially update an existing resource.
DELETERemoveDelete a specified resource.

Error Handling Middleware

Express distinguishes error-handling middleware by checking the number of arguments. If a function has four arguments (err, req, res, next), Express recognizes it as an error handler.

javascript
1// Simulation of Express error handling logic
2const errMiddleware = (err, req, res, next) => {
3 console.error("System Error:", err.message);
4 return { status: 500, body: "Internal Server Error" };
5};
6 
7const result = errMiddleware(new Error("Database Timeout"));
8console.log(result);

Environment Configuration

Production applications should never hardcode sensitive data like database URLs or API keys. Express apps typically use the dotenv package to load environment variables from a .env file into process.env.

What is the purpose of calling 'next()' in an Express middleware function?

Engineering Excellence

Section Detail

Testing Strategies: Unit and Integration

The Philosophy of Testing

In modern software engineering, automated testing is not a luxury but a requirement. It ensures that changes don’t break existing functionality (regressions) and serves as executable documentation. We follow the Testing Pyramid model: a broad base of fast Unit Tests, a middle layer of Integration Tests, and a thin top layer of End-to-End (E2E) tests.

Unit Testing with Jest

Jest is a delightful JavaScript Testing Framework with a focus on simplicity. It provides an assertion library, test runner, and mocking support out of the box.

The Anatomy of a Test

Tests are typically organized into describe blocks (grouping related tests) and it or test blocks (individual test cases).

describe('Math Utilities', () => {
  test('adds 1 + 2 to equal 3', () => {
    expect(add(1, 2)).toBe(3);
  });
});

Mocking and Dependency Injection

In isolation, a unit test should not depend on external systems like databases or web APIs. We use Mocks and Spies to simulate these dependencies.

  • jest.fn(): Creates a mock function that tracks its calls.
  • jest.mock('module-name'): Replaces an entire module with mocked versions of its exports.
  • jest.spyOn(object, 'method'): Watches a real method but allows you to track its usage or override its implementation.
const api = require('./api');
jest.mock('./api');

test('fetches data on click', async () => {
  api.getData.mockResolvedValue({ status: 200 });
  await triggerLoad();
  expect(api.getData).toHaveBeenCalledTimes(1);
});

React Component Testing

Testing UI components requires a different approach. Instead of testing internal implementation details (like state variables), we test observable behavior from the user’s perspective. The React Testing Library (RTL) is the industry standard for this.

RTL encourages you to find elements by their role or text content (e.g., getByRole('button', { name: /submit/i })), mimicking how a screen reader or a real user would find them.

Test-Driven Development (TDD)

TDD is a workflow where you write the test before the implementation:

  1. Red: Write a failing test for a small piece of functionality.
  2. Green: Write the minimum code necessary to make the test pass.
  3. Refactor: Improve the code while ensuring the tests stay green.

Why is it generally discouraged to test internal component state in React?

Continuous Integration (CI)

In a professional environment, tests are run automatically on every “Push” or “Pull Request” using CI tools like GitHub Actions. This ensures that only code that passes all verification steps can be merged into the main codebase.

Section Detail

Security: Defending the Modern Web

The Threat Landscape

As JavaScript applications handle increasingly sensitive data, security must be integrated into the development lifecycle. The Open Web Application Security Project (OWASP) maintains a top 10 list of critical vulnerabilities that every developer should understand.

Cross-Site Scripting (XSS)

XSS occurs when an attacker injects malicious scripts into content that is then delivered to a different user. This can be used to steal session cookies or perform actions on behalf of the user.

  • Stored XSS: Malicious script is permanently stored on the server (e.g., in a database as a comment).
  • Reflected XSS: Script is “reflected” off the web server, typically via a URL parameter.

Mitigation:

  • Sanitize Input: Never trust data from the user.
  • Encode Output: Use frameworks like React which automatically escape strings in JSX.
  • Content Security Policy (CSP): A header that tells the browser which sources of scripts are trusted.

Cross-Site Request Forgery (CSRF)

CSRF is an attack that tricks a logged-in user into performing unwanted actions on a web application where they are authenticated. The attack relies on the fact that browsers automatically include cookies (like session IDs) in requests to the associated domain.

Mitigation:

  • CSRF Tokens: Include a unique, secret token in every sensitive request (POST, PUT, DELETE). The server validates the token before processing.
  • SameSite Cookies: Set the SameSite attribute to Strict or Lax to prevent cookies from being sent on cross-site requests.

Secure Authentication: JWT vs. Sessions

Modern applications often use JSON Web Tokens (JWT) for stateless authentication. A JWT is a signed string containing user claims.

  • Storage Consideration: Storing a JWT in localStorage makes it vulnerable to XSS. Storing it in an HttpOnly, Secure Cookie is a more robust approach, as JavaScript cannot access HttpOnly cookies.

Injection Vulnerabilities

Injection (like SQL Injection or NoSQL Injection) happens when untrusted data is sent to an interpreter as part of a command or query.

// VULNERABLE
db.collection('users').find({ username: req.body.username });
// If req.body.username is { $gt: "" }, it returns all users

Mitigation: Use ORMs/ODMs (like Prisma or Mongoose) that use parameterized queries or built-in sanitization.

Which cookie attribute is essential for preventing a Cross-Site Scripting (XSS) attack from stealing a session token?

Dependency Auditing

JavaScript projects often depend on thousands of third-party packages. These packages can contain vulnerabilities or even malicious code (“Supply Chain Attacks”).

  • Use npm audit to check for known vulnerabilities in your project’s dependency tree.
  • Update dependencies regularly using tools like renovate or dependabot.

Server-Side Development

Section Detail

Databases and Data Persistence

The Persistence Layer

Most JavaScript applications require a way to store data permanently. The choice of database depends on the data’s structure, the required consistency guarantees, and the scale of the application. In the Node.js ecosystem, we primarily deal with two paradigms: Relational (SQL) and Non-Relational (NoSQL).

Relational Databases (SQL)

SQL databases like PostgreSQL or MySQL store data in predefined tables with fixed columns. They are excellent for complex queries and ensuring data integrity through ACID (Atomicity, Consistency, Isolation, Durability) transactions.

When to use SQL:

  • Relationships between data are complex (many-to-many).
  • Data structure is stable and requires strict enforcement.
  • Financial or transactional data integrity is paramount.

Non-Relational Databases (NoSQL)

NoSQL databases like MongoDB store data as flexible, JSON-like documents. They are designed for horizontal scalability and high-velocity data.

When to use NoSQL:

  • Rapidly evolving data structures.
  • Big data and real-time logging.
  • High volume of simple read/write operations.
Code
skinparam componentStyle rectangle

package "Data Paradigms" {
[Document (NoSQL)] - [Key/Value]
[Relational (SQL)] - [Graph]
}

note right of [Document (NoSQL)]
Flexible, nested structures.
Example: MongoDB
end note

note right of [Relational (SQL)]
Tables, Rows, Columns.
Example: PostgreSQL
end note
Data ParadigmsDocument (NoSQL)Key/ValueRelational (SQL)GraphFlexible, nested structures.Example: MongoDBTables, Rows, Columns.Example: PostgreSQL

Object-Relational Mapping (ORM)

Working with raw SQL strings in JavaScript can be error-prone and insecure. An ORM (like Prisma or TypeORM) provides a programmable interface to the database. It maps database rows to JavaScript objects and handles schema migrations.

Benefits of Prisma (Modern ORM)

  • Type Safety: Automatically generates TypeScript types based on your database schema.
  • Migrations: Tracks changes to your database structure in version-controlled files.
  • Declarative Schema: You define your data model in a single schema.prisma file.
// Example Prisma Schema
model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  posts Post[]
}

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  author    User    @relation(fields: [authorId], references: [id])
  authorId  Int
}

Caching with Redis

To improve performance for frequently accessed data, we use an in-memory database like Redis. Redis stores data as key-value pairs in RAM, making it orders of magnitude faster than a traditional disk-based database. It is commonly used for session storage, rate limiting, and result caching.

What is the primary advantage of using an ORM over writing raw SQL queries?

ACID vs. BASE

  • SQL databases prioritize ACID: ensuring that every transaction is a complete, reliable unit.
  • NoSQL databases often follow the BASE model (Basically Available, Soft state, Eventual consistency), prioritizing availability over immediate consistency in distributed systems.

Engineering Excellence

Section Detail

Web Performance and Optimization

The Cost of Performance

In web development, performance is not just a feature; it is a fundamental aspect of user experience. Studies consistently show that even millisecond delays in page loading correlate with significantly higher bounce rates and lower user engagement. Mastering web performance requires understanding how the browser processes resources and identifying bottlenecks.

The Critical Rendering Path (CRP)

The CRP is the sequence of steps the browser takes to convert HTML, CSS, and JavaScript into pixels on the screen.

  1. DOM: Parse HTML and build the Document Object Model.
  2. CSSOM: Parse CSS and build the CSS Object Model.
  3. Render Tree: Combine DOM and CSSOM to determine which elements are visible.
  4. Layout: Calculate the geometry (position and size) of each element.
  5. Paint: Fill in the pixels for each element.

JavaScript’s Role: JavaScript is a parser-blocking resource. When the browser encounters a <script> tag, it must stop building the DOM, download the script, and execute it before continuing.

Code
participant "HTML Parser" as P
participant "DOM" as D
participant "Network/Script" as S

P -> D : "Builds"
P -> S : "Encounter <script>"
note right of S: Parser Blocks!
S -> P : "Resume after execution"
HTML ParserDOMNetwork.ScriptHTML ParserDOMNetwork/ScriptHTML ParserDOMNetwork/Script"Builds""Encounter <script>"Parser Blocks!"Resume after execution"

Optimization Techniques

1. Script Attributes: Async and Defer

To prevent parser blocking:

  • async: Download the script in the background and execute it the moment it’s ready (may interrupt the parser).
  • defer: Download in the background and only execute after the DOM is fully parsed. Use defer for most scripts.

2. Code Splitting and Lazy Loading

Instead of sending a single 2MB JavaScript bundle, use Dynamic Imports to split your code into smaller chunks. Load only the code required for the current route or action.

3. Tree Shaking

Modern bundlers use static analysis to identify “dead code”—functions or modules that are imported but never used—and remove them from the final bundle.

Core Web Vitals (CWV)

Google introduced Core Web Vitals as a standardized set of metrics to measure user experience:

  • Largest Contentful Paint (LCP): How long it takes to render the largest visible element (loading performance).
  • First Input Delay (FID): Time from user interaction to the browser’s response (responsiveness).
  • Cumulative Layout Shift (CLS): Measures visual stability (do elements jump around while loading?).

Memory Management and Garbage Collection

JavaScript uses a Mark-and-Sweep garbage collection algorithm. The engine starts from “roots” (like the global object) and marks all reachable objects. Anything not marked is considered unreachable and is cleared.

Common Memory Leaks:

  1. Uncleared Timers: setInterval continuing to run after a component unmounts.
  2. Forgotten Event Listeners: Listeners attached to window or document that are never removed.
  3. Closures: Over-retaining large objects in a long-lived closure scope.

Which script attribute is generally preferred for modern web applications to ensure scripts don't block DOM parsing?

Image Optimization

Images often account for the majority of a page’s weight. Use modern formats like WebP or AVIF, implement responsive images with srcset, and use Lazy Loading for below-the-fold content.