MODULE 1

React: Components & JSX

Function vs class components, JSX to createElement, props, keys, and controlled inputs.

Function vs Class Components

A React component is a function (or class) that takes props and returns a description of UI. Since hooks arrived in React 16.8, function components are the default; class components persist in legacy code and are still the only way to write error boundaries.

AspectFunction componentClass component
StateuseState/useReducerthis.state + setState
Side effectsuseEffectlifecycle methods
this bindingnone — no thismust bind handlers
Error boundarynot possiblecomponentDidCatch
Reuse logiccustom hooksHOCs / render props
// Function component — props destructured in the signature
function Greeting({ name, count = 0 }) {
  return <p>Hello {name}, you clicked {count} times</p>;
}

// Equivalent class component
class Greeting extends React.Component {
  render() {
    const { name, count = 0 } = this.props;
    return <p>Hello {name}, you clicked {count} times</p>;
  }
}

JSX Compiles to React.createElement

JSX is not HTML and not a runtime feature — it is syntax sugar that Babel/SWC transpiles into function calls. Understanding the output demystifies keys, children, and why className exists.

// You write:
const el = <button className="btn" onClick={fn}>Save</button>;

// Classic transform emits:
const el = React.createElement(
  "button",
  { className: "btn", onClick: fn },
  "Save"
);
// createElement returns a plain object (a React element), NOT a DOM node:
// { type: "button", props: { className, onClick, children: "Save" }, ... }
  • Attributes become the props object; children become props.children.
  • classclassName and forhtmlFor because class/for are reserved JS words.
  • A capitalized tag compiles to the variable, so the component must be imported/in scope — the new JSX transform (React 17+) auto-imports the runtime, so you no longer need import React just for JSX.
  • Elements are immutable, cheap plain objects — React builds a tree of them each render and diffs it.

Props vs Children

Props flow one way: parent to child. They are read-only — a child must never mutate its props. children is just a special prop holding whatever you nest between the tags, which is what makes composition work.

function Card({ title, children }) {
  return (
    <section className="card">
      <h2>{title}</h2>
      <div className="body">{children}</div>
    </section>
  );
}

// Usage — anything nested becomes props.children
<Card title="Profile">
  <Avatar />
  <p>Bio text</p>
</Card>

Conditional & List Rendering with Keys

Because JSX is expressions, you branch with ternaries and &&, and build lists with .map(). Each list item needs a stable key so React can match elements across renders.

function TodoList({ todos, loading }) {
  if (loading) return <Spinner />;
  return (
    <ul>
      {todos.length === 0
        ? <li>No items</li>
        : todos.map((t) => (
            <li key={t.id}>{t.text}</li>  // stable id, NOT the array index
          ))}
    </ul>
  );
}

Controlled vs Uncontrolled Inputs

A controlled input's value is driven by React state — the DOM is a mirror of state. An uncontrolled input keeps its own value in the DOM and you read it via a ref. Controlled is the default recommendation because state is the single source of truth (enabling validation, formatting, conditional disabling).

// Controlled: value + onChange keep state authoritative
function NameField() {
  const [name, setName] = React.useState("");
  return (
    <input
      value={name}
      onChange={(e) => setName(e.target.value)}
    />
  );
}

// Uncontrolled: DOM owns the value, ref reads it on submit
function NameFieldU() {
  const ref = React.useRef(null);
  const submit = () => console.log(ref.current.value);
  return <input ref={ref} defaultValue="" />;
}

Composition over Inheritance

React has no component inheritance story — you build complex UI by composing simple components and passing elements as props. Two idioms dominate: children for generic containers, and named "slot" props for multiple insertion points.

// Slots via props — a layout with named holes
function SplitPane({ left, right }) {
  return (
    <div className="split">
      <div className="left">{left}</div>
      <div className="right">{right}</div>
    </div>
  );
}

<SplitPane left={<Nav />} right={<Content />} />
MODULE 2

React: Hooks

useState, useEffect, useMemo/useCallback, refs, context, reducers, and the rules of hooks.

useState & State Updates

useState returns a [value, setter] pair preserved across renders by call order. State updates are asynchronous and batched; the setter schedules a re-render rather than mutating immediately. When the next value depends on the previous, use the functional updater.

function Counter() {
  const [count, setCount] = React.useState(0);

  const addThree = () => {
    // WRONG: all three read the same stale `count`, net +1
    // setCount(count + 1); setCount(count + 1); setCount(count + 1);

    // RIGHT: functional updater queues off the latest value, net +3
    setCount((c) => c + 1);
    setCount((c) => c + 1);
    setCount((c) => c + 1);
  };
  return <button onClick={addThree}>{count}</button>;
}

useEffect: Deps, Cleanup, Timing

useEffect runs a side effect after the render is painted. The dependency array controls when it re-runs; the returned function cleans up before the next run and on unmount. This is where subscriptions, timers, and data fetching live.

React.useEffect(() => {
  const id = setInterval(tick, 1000);
  const sub = socket.subscribe(onMsg);
  return () => {          // cleanup: runs before re-run & on unmount
    clearInterval(id);
    sub.unsubscribe();
  };
}, [onMsg]);              // re-subscribes only when onMsg changes
Deps arrayEffect runs
omittedafter every render
[]once after mount (cleanup on unmount)
[a, b]on mount + whenever a or b changes (Object.is)

useMemo & useCallback: Referential Stability

Both exist to preserve reference identity across renders. useMemo caches a computed value; useCallback caches a function (it is useMemo(() => fn, deps)). They matter only when a stable reference is a dependency of something — a memoized child, an effect dep, or an expensive recompute.

const sorted = React.useMemo(
  () => bigList.slice().sort(cmp),   // recompute only when inputs change
  [bigList, cmp]
);

const handleSelect = React.useCallback(
  (id) => dispatch({ type: "select", id }),
  [dispatch]                        // stable identity for a memo'd child
);

useRef: Mutable Boxes & DOM Access

useRef returns a mutable { current } object that persists across renders and — critically — mutating it does not trigger a re-render. Two uses: grabbing a DOM node, and holding an instance value (timer id, previous value, latest callback) that shouldn't drive rendering.

function TextInput() {
  const inputRef = React.useRef(null);
  const renders = React.useRef(0);
  renders.current++;                 // mutate freely, no re-render
  return (
    <input
      ref={inputRef}
      onFocus={() => console.log("focus #", renders.current)}
    />
  );
}

useContext & useReducer

useContext reads the nearest provider's value, avoiding prop drilling. useReducer centralizes complex state transitions into a pure reducer — preferable to multiple useState calls when the next state depends on the current state and an action, or when transitions are interrelated.

const initial = { count: 0, step: 1 };
function reducer(state, action) {
  switch (action.type) {
    case "inc":  return { ...state, count: state.count + state.step };
    case "step": return { ...state, step: action.value };
    default:     throw new Error("unknown action");
  }
}
function Counter() {
  const [state, dispatch] = React.useReducer(reducer, initial);
  return <button onClick={() => dispatch({ type: "inc" })}>{state.count}</button>;
}

Custom Hooks & the Rules of Hooks

A custom hook is any function named useX that calls other hooks — the mechanism for extracting and sharing stateful logic. It shares logic, not state: each call site gets its own independent state.

function useDebounced(value, ms) {
  const [debounced, setDebounced] = React.useState(value);
  React.useEffect(() => {
    const id = setTimeout(() => setDebounced(value), ms);
    return () => clearTimeout(id);
  }, [value, ms]);
  return debounced;
}
  1. Only call hooks at the top level — never inside loops, conditions, or nested functions.
  2. Only call hooks from React function components or other custom hooks.
MODULE 3

Rendering & Reconciliation

The virtual DOM, keys, re-render triggers, React.memo, and concurrent rendering.

The Virtual DOM & Diffing

The virtual DOM is React's in-memory tree of element objects. On each render React builds a new tree, diffs it against the previous one ("reconciliation"), and applies the minimal set of real DOM mutations. Real DOM writes are expensive; comparing plain JS objects is cheap.

  • A full tree diff is O(n³) in general; React uses heuristics to make it O(n).
  • Different type at a position → tear down the old subtree and build fresh (state is lost).
  • Same type → keep the DOM node, update changed attributes, recurse into children.
  • Lists are matched by key, not by position.

Keys & List Reconciliation

Keys tell React which element is which across renders. Without them, React diffs list children positionally, so inserting at the front makes every row look "changed". Stable keys let React move DOM nodes and preserve their state instead of rebuilding them.

// Prepending "New" with index keys re-labels every row's state.
// With key={item.id}, React moves nodes and keeps each row's input intact.
{items.map((item) => (
  <Row key={item.id} item={item} />
))}

What Triggers a Re-render

A component re-renders when its own state changes, when its parent re-renders, or when a context it consumes changes. Notably, a parent re-render re-renders all children by default — even children whose props did not change.

TriggerScope
setState in a componentthat component + its subtree
Parent re-rendersall children (unless memoized)
Context value changesevery consumer of that context
Props changethe receiving component

React.memo & Referential Equality

React.memo wraps a component so it skips re-rendering when its props are shallow-equal to the previous render. It only helps if the props actually stay referentially stable — which is why it pairs with useMemo/useCallback.

const Row = React.memo(function Row({ item, onSelect }) {
  return <li onClick={() => onSelect(item.id)}>{item.text}</li>;
});

// Parent must keep onSelect stable, or memo is defeated every render:
const onSelect = React.useCallback((id) => select(id), []);

Pitfall: Inline Objects & Functions

Inline object literals, array literals, and arrow functions in JSX create a new reference on every render. That silently breaks React.memo, adds unstable useEffect deps, and forces context consumers to update.

// Every render: new object + new function -> memoized child re-renders anyway
<Child style={{ margin: 8 }} onClick={() => save()} />

// Fix: hoist stable values / memoize callbacks
const STYLE = { margin: 8 };                       // module scope
const onClick = React.useCallback(() => save(), []);
<Child style={STYLE} onClick={onClick} />

Batching, Concurrent Rendering & Suspense

React batches multiple state updates in the same tick into one render. React 18's automatic batching extends this to promises, timeouts, and native event handlers — not just React events. Concurrent rendering lets React interrupt, pause, and resume rendering to keep the UI responsive.

// startTransition marks an update as low-priority/interruptible
const [isPending, startTransition] = React.useTransition();
function onType(e) {
  setQuery(e.target.value);              // urgent: keeps input snappy
  startTransition(() => setResults(filter(e.target.value))); // deferrable
}

// Suspense shows a fallback while a child is waiting (lazy code / data)
<React.Suspense fallback={<Spinner />}>
  <LazyDashboard />
</React.Suspense>
MODULE 4

State Management

Local vs global state, Context limits, Redux/Zustand, and server-state with React Query.

Local vs Lifted vs Global State

State placement is the first decision. Keep state as local as possible; lift it to the nearest common ancestor when siblings must share it; reach for global state only when many unrelated parts of the tree need it. Over-globalizing state is a top source of accidental complexity.

ScopeWhere it livesUse when
LocaluseState in the componentonly that component cares (form field, toggle)
Liftednearest common parent, passed as propsa few siblings share it
GlobalContext / storecross-cutting: auth, theme, cart
Serverquery cache (React Query)data owned by the backend

Context and Its Limits

Context solves distribution (avoiding prop drilling), not performance. It is not a state manager — you still need useState/useReducer to hold the value. Its key limitation: any change to the context value re-renders every consumer, with no built-in selector to subscribe to a slice.

const ThemeCtx = React.createContext("light");

function App() {
  const [theme, setTheme] = React.useState("light");
  // Memoize so consumers don't re-render on unrelated App renders
  const value = React.useMemo(() => ({ theme, setTheme }), [theme]);
  return (
    <ThemeCtx.Provider value={value}>
      <Toolbar />
    </ThemeCtx.Provider>
  );
}

Redux & Redux Toolkit

Redux is a single immutable store updated only by dispatching actions through pure reducers — a predictable, time-travel-debuggable data flow. Redux Toolkit (RTK) is the modern standard: it removes the boilerplate and uses Immer so you can "mutate" draft state safely.

import { createSlice, configureStore } from "@reduxjs/toolkit";

const cart = createSlice({
  name: "cart",
  initialState: { items: [] },
  reducers: {
    added(state, action) {
      state.items.push(action.payload);   // Immer -> safe "mutation"
    },
    cleared(state) { state.items = []; },
  },
});

export const { added, cleared } = cart.actions;
export const store = configureStore({ reducer: { cart: cart.reducer } });
  • Store: one object tree, the single source of truth.
  • Action: a plain object { type, payload } describing what happened.
  • Reducer: (state, action) => newState, pure, no side effects.
  • Selector: useSelector(s => s.cart.items) subscribes to a slice.

Zustand: Minimal Global State

Zustand is a small store built on hooks with no provider and no boilerplate. You define a store as a hook and subscribe to precise slices with a selector, so only components reading a changed slice re-render.

import { create } from "zustand";

const useStore = create((set) => ({
  count: 0,
  inc: () => set((s) => ({ count: s.count + 1 })),
}));

// Component subscribes only to `count`; `inc` never changes identity
function Counter() {
  const count = useStore((s) => s.count);
  const inc = useStore((s) => s.inc);
  return <button onClick={inc}>{count}</button>;
}

Server State: React Query & RTK Query

Server-state libraries treat remote data as an asynchronous cache keyed by a query key. They give you loading/error states, deduping, background refetching, cache invalidation, and optimistic updates — features you'd otherwise reinvent inside Redux.

import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

function Todos() {
  const qc = useQueryClient();
  const { data, isLoading } = useQuery({
    queryKey: ["todos"],
    queryFn: () => fetch("/api/todos").then((r) => r.json()),
    staleTime: 30_000,               // serve cache for 30s before refetch
  });

  const add = useMutation({
    mutationFn: (t) => fetch("/api/todos", { method: "POST", body: JSON.stringify(t) }),
    onSettled: () => qc.invalidateQueries({ queryKey: ["todos"] }), // refetch
  });
  // ...
}
  1. Caching: results stored per query key; staleTime controls freshness.
  2. Invalidation: invalidateQueries marks data stale to trigger a refetch.
  3. Optimistic update: write the expected result to cache immediately, roll back in onError.
MODULE 5

Node.js & Express

The event loop, non-blocking I/O, streams and backpressure, and the middleware chain.

The Event Loop & Its Phases

Node runs JavaScript on a single thread and offloads I/O to libuv, which uses a thread pool and the OS. The event loop is the scheduler that decides what callback runs next, cycling through ordered phases on each tick.

  1. timerssetTimeout/setInterval callbacks.
  2. pending callbacks — deferred I/O callbacks.
  3. poll — retrieve new I/O events; execute I/O callbacks.
  4. checksetImmediate callbacks.
  5. closeclose event callbacks.

Between every phase transition (and after each macrotask callback), Node drains the microtask queues: process.nextTick first, then Promise callbacks.

Microtasks vs Macrotasks

Microtasks (promises, queueMicrotask, process.nextTick) run to exhaustion before the loop moves to the next macrotask phase. This ordering explains many "why did this log first?" puzzles.

console.log("1: sync");
setTimeout(() => console.log("2: timeout"), 0);
setImmediate(() => console.log("3: immediate"));
Promise.resolve().then(() => console.log("4: promise"));
process.nextTick(() => console.log("5: nextTick"));
console.log("6: sync");
// Order: 1, 6, 5, 4, then 2/3 (timer vs check ordering is context-dependent)

Non-blocking I/O & Blocking the Loop

Node's throughput depends on never blocking the single JS thread. Any synchronous CPU-heavy work or *Sync call freezes the loop, and all pending requests stall until it returns.

// BLOCKING: freezes every other request for the duration
app.get("/hash", (req, res) => {
  const h = crypto.pbkdf2Sync(req.query.pw, "salt", 1_000_000, 64, "sha512");
  res.send(h.toString("hex"));
});

// NON-BLOCKING: async variant runs on the libuv pool, loop stays free
app.get("/hash", (req, res) => {
  crypto.pbkdf2(req.query.pw, "salt", 1_000_000, 64, "sha512", (err, h) =>
    res.send(h.toString("hex"))
  );
});

Streams & Backpressure

Streams process data in chunks instead of buffering everything in memory. Backpressure is the flow-control signal: when a writable's internal buffer fills, write() returns false and you should pause until drain. pipe()/pipeline() handle this automatically.

const { pipeline } = require("stream/promises");
const fs = require("fs");
const zlib = require("zlib");

// pipeline propagates backpressure AND errors + cleans up file handles
await pipeline(
  fs.createReadStream("big.log"),
  zlib.createGzip(),
  fs.createWriteStream("big.log.gz")
);

Express Middleware & Routing

Express is a chain of middleware functions (req, res, next) executed in registration order. Each either responds or calls next() to pass control along. Routing is just middleware scoped to a method + path.

const express = require("express");
const app = express();

app.use(express.json());                       // parse JSON body
app.use((req, res, next) => {                  // logging middleware
  console.log(req.method, req.url);
  next();                                       // MUST call to continue
});

app.get("/users/:id", (req, res) => {
  res.json({ id: req.params.id });
});

Error Handling & Scaling the Loop

Express recognizes error middleware by its four-argument signature (err, req, res, next). In Express 4, errors thrown in async handlers aren't caught automatically — you must forward them to next(err) (Express 5 fixes this). To use multiple CPU cores, run a cluster or offload CPU work to worker threads.

app.get("/x", async (req, res, next) => {
  try { res.json(await getData()); }
  catch (e) { next(e); }                       // forward to error handler
});

// Central error handler — 4 args signals Express it's for errors
app.use((err, req, res, next) => {
  res.status(err.status || 500).json({ error: err.message });
});
ToolSolvesShares memory?
clusteruse all CPU cores (fork N processes)no (separate processes)
worker_threadsoffload CPU-bound work off the main loopyes (SharedArrayBuffer)
MODULE 6

Spring Boot Core

IoC/DI, beans and scopes, stereotype annotations, and auto-configuration.

IoC Container & Dependency Injection

Inversion of Control means the framework, not your code, creates and wires objects. Spring's IoC container reads your configuration, instantiates beans, resolves their dependencies, and manages their lifecycle. Dependency Injection is how those dependencies are supplied — you declare what you need and Spring hands it over.

@Service
public class OrderService {
    private final PaymentGateway gateway;

    // Constructor injection: Spring passes the PaymentGateway bean in
    public OrderService(PaymentGateway gateway) {
        this.gateway = gateway;
    }
}
Injection typeHowNotes
Constructorparams on the constructorpreferred: enables final, fails fast, testable
Setter@Autowired on a setterfor optional dependencies
Field@Autowired on a fieldconcise but hides deps, hard to unit test

Beans & Scopes

A bean is any object the Spring container manages. By default every bean is a singleton — one shared instance per container. Scope controls how many instances exist and how long they live.

ScopeInstancesUse for
singleton (default)one per containerstateless services
prototypenew one per injection/lookupstateful, short-lived objects
requestone per HTTP requestweb request-scoped data
sessionone per HTTP sessionper-user session data

Stereotype Annotations

Stereotype annotations mark a class as a Spring-managed component and convey its role. They are all specializations of @Component — functionally similar for scanning, but they document layering and enable role-specific behavior.

  • @Component — generic Spring-managed bean.
  • @Service — business-logic layer (semantic marker).
  • @Repository — data-access layer; also translates persistence exceptions into Spring's DataAccessException hierarchy.
  • @Controller / @RestController — web layer.
@Repository
public class JdbcUserRepo {
    // Persistence exceptions thrown here get translated to
    // Spring's unchecked DataAccessException automatically.
}

@Autowired & Resolving Ambiguity

Spring resolves dependencies by type. When two beans satisfy the same type, injection is ambiguous and startup fails unless you disambiguate with @Primary or @Qualifier.

public interface Notifier { void send(String msg); }

@Component @Primary
class EmailNotifier implements Notifier { public void send(String m) {} }

@Component("sms")
class SmsNotifier implements Notifier { public void send(String m) {} }

@Service
class AlertService {
    // @Qualifier picks a specific bean; without it @Primary (Email) wins
    AlertService(@Qualifier("sms") Notifier notifier) { }
}

@RestController & Request Mapping

@RestController = @Controller + @ResponseBody: return values are serialized (usually to JSON via Jackson) straight to the response body, rather than resolved as view names. Mapping annotations bind HTTP methods and paths to handler methods.

@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping("/{id}")
    public UserDto get(@PathVariable Long id) {
        return service.find(id);            // serialized to JSON
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public UserDto create(@RequestBody @Valid CreateUserReq req) {
        return service.create(req);
    }

    @GetMapping
    public List<UserDto> search(@RequestParam(defaultValue = "0") int page) {
        return service.page(page);
    }
}

Starters, Auto-configuration & Profiles

Spring Boot starters are curated dependency bundles (e.g. spring-boot-starter-web pulls Spring MVC, Jackson, and embedded Tomcat). Auto-configuration inspects the classpath and your properties, then conditionally creates beans so you get sensible defaults with zero XML. Profiles let one build behave differently per environment.

// Conditional bean creation is the heart of auto-config
@Configuration
@ConditionalOnClass(DataSource.class)
@ConditionalOnMissingBean(DataSource.class)
class DataSourceAutoConfig {
    @Bean DataSource dataSource() { /* HikariCP default */ }
}
# application.properties — profile-specific overrides in application-prod.properties
spring.profiles.active=prod
server.port=8080
spring.datasource.url=jdbc:postgresql://db:5432/app
MODULE 7

Spring Boot: Data & Web

Spring Data JPA, the N+1 problem, transactions, DTOs, and REST error handling.

Spring Data JPA Repositories

Spring Data JPA generates repository implementations at runtime from an interface. You extend JpaRepository<Entity, Id> and get CRUD plus paging for free; derived query methods are parsed from the method name into JPQL.

public interface UserRepository extends JpaRepository<User, Long> {
    // Parsed into: SELECT u FROM User u WHERE u.email = ?1
    Optional<User> findByEmail(String email);

    List<User> findByActiveTrueOrderByCreatedAtDesc();

    @Query("SELECT u FROM User u JOIN FETCH u.roles WHERE u.id = :id")
    Optional<User> findWithRoles(@Param("id") Long id);
}

Entities & Relationships

An @Entity maps a class to a table. Relationships model foreign keys; the critical decision is fetch type. Associations default to LAZY for collections (@OneToMany, @ManyToMany) and EAGER for single-valued (@ManyToOne, @OneToOne).

@Entity
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)   // override the EAGER default
    @JoinColumn(name = "customer_id")
    private Customer customer;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderLine> lines = new ArrayList<>();
}

The N+1 Problem & Fetch Strategies

The N+1 problem: you fetch N parent rows in one query, then lazy-loading each parent's association fires one more query per parent — 1 + N total. It's the most common JPA performance bug.

// Triggers N+1: 1 query for orders, then 1 per order for its customer
List<Order> orders = orderRepo.findAll();
orders.forEach(o -> o.getCustomer().getName());  // N extra queries

// Fix 1: JOIN FETCH in a single query
@Query("SELECT o FROM Order o JOIN FETCH o.customer")
List<Order> findAllWithCustomer();

// Fix 2: entity graph
@EntityGraph(attributePaths = "customer")
List<Order> findAll();
StrategyQueriesCaveat
Lazy + loop1 + Nthe bug
JOIN FETCH1row explosion with multiple collections
@EntityGraph1declarative, per-query
@BatchSize1 + N/batchfewer round-trips, still lazy

Transactions with @Transactional

@Transactional wraps a method in a database transaction via a proxy: begin on entry, commit on normal return, roll back on exception. Within the transaction the persistence context tracks entities so dirty changes flush automatically.

@Service
public class TransferService {
    @Transactional
    public void transfer(Long from, Long to, BigDecimal amt) {
        Account a = repo.findById(from).orElseThrow();
        Account b = repo.findById(to).orElseThrow();
        a.debit(amt);     // no explicit save needed — dirty checking flushes
        b.credit(amt);
    }                     // commit here; any RuntimeException -> rollback
}

DTOs & Validation

Never expose JPA entities directly at the API boundary — they leak schema, risk lazy-loading serialization errors, and over-post. Map to DTOs and validate incoming DTOs with Bean Validation annotations, triggered by @Valid.

public record CreateUserReq(
    @NotBlank String name,
    @Email String email,
    @Min(18) int age
) {}

@PostMapping
public UserDto create(@RequestBody @Valid CreateUserReq req) { ... }
  • Serializing a lazy entity outside a transaction throws LazyInitializationException — DTOs sidestep it entirely.
  • Entities in the API let clients set fields they shouldn't (mass-assignment); DTOs whitelist inputs.
  • @Valid failures produce a MethodArgumentNotValidException you translate to a clean 400.

REST Design & @ControllerAdvice

Good REST APIs use nouns for resources, HTTP methods for verbs, and status codes to signal outcomes. Centralize error handling in a @RestControllerAdvice so every endpoint returns a consistent error shape instead of leaking stack traces.

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(EntityNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ApiError notFound(EntityNotFoundException e) {
        return new ApiError("NOT_FOUND", e.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiError invalid(MethodArgumentNotValidException e) {
        var msg = e.getBindingResult().getFieldErrors().stream()
            .map(f -> f.getField() + ": " + f.getDefaultMessage())
            .collect(Collectors.joining(", "));
        return new ApiError("VALIDATION", msg);
    }
}
StatusMeaning
200 / 201 / 204OK / Created / No Content
400 / 401 / 403Bad Request / Unauthenticated / Forbidden
404 / 409Not Found / Conflict
500Server Error (bug — never for validation)
MODULE 8

Full-Stack Architecture & Auth

Client-server flow, REST vs GraphQL, CORS, session/JWT/OAuth2, and the BFF pattern.

Client-Server Data Flow

A full-stack request is a chain: browser → (optional CDN/gateway) → API server → database, and back. Understanding each hop's job — and where latency, caching, and auth live — is what "full-stack" really tests.

  1. Browser issues an HTTP request (fetch/XHR) with headers, cookies, and body.
  2. CDN serves static assets; dynamic calls pass to an API gateway / load balancer.
  3. The API server authenticates, authorizes, runs business logic, queries the DB.
  4. Response returns JSON; the client updates state and re-renders.

REST vs GraphQL from the Client

REST exposes fixed resource endpoints; the client takes whatever shape each returns. GraphQL exposes one endpoint where the client specifies exactly the fields it needs, solving over- and under-fetching at the cost of added server complexity and harder HTTP caching.

AspectRESTGraphQL
Endpointsmany, per resourceone
Over/under-fetchcommonclient picks fields
HTTP cachingnative (URL + verbs)needs client cache layer
Multiple resourcesN round-tripsone query
Errorsstatus codesusually 200 + errors array
{ "query": "{ user(id: 1) { name orders { total } } }" }

CORS

The browser's same-origin policy blocks JS from reading responses of cross-origin requests unless the server opts in with CORS headers. It's enforced by the browser, not the server — a server without CORS headers still processes the request; the browser just hides the response from your script.

// Spring Boot: allow the SPA origin, send credentials
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    public void addCorsMappings(CorsRegistry r) {
        r.addMapping("/api/**")
         .allowedOrigins("https://app.example.com")
         .allowedMethods("GET", "POST", "PUT", "DELETE")
         .allowCredentials(true);   // required to send cookies cross-origin
    }
}

Session vs JWT vs OAuth2/OIDC

These answer different questions. Sessions and JWTs are ways to maintain authentication state; OAuth2 is a delegated authorization framework and OIDC adds an identity layer on top for authentication.

ApproachStateRevocationScale
Server sessionstored server-side, ID in cookieeasy (delete session)needs shared store
JWTstateless, signed claimshard until expiryno server store
OAuth2/OIDCtokens from an authz servervia short expiry + refreshfederated / SSO
// A JWT is three base64url parts: header.payload.signature
// eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiZXhwIjoxNzUwMH0.SIG
// The signature proves integrity; the payload is READABLE, not encrypted.

Cookies: httpOnly, SameSite & CSRF

Where you store the token decides your threat model. An httpOnly cookie can't be read by JS (blocks XSS token theft) but is sent automatically on every request (opens CSRF). localStorage is the reverse — immune to CSRF, exposed to XSS.

# Hardened auth cookie
Set-Cookie: token=abc; HttpOnly; Secure; SameSite=Strict; Path=/
  • HttpOnly — JS can't read it (XSS mitigation).
  • Secure — sent only over HTTPS.
  • SameSite=Lax/Strict — not sent on cross-site requests (primary CSRF defense).
  • CSRF token — extra defense for state-changing requests when using cookie auth.

API Gateway & the BFF Pattern

An API gateway is a single entry point in front of many services, centralizing auth, rate limiting, TLS termination, and routing. A Backend-for-Frontend (BFF) is a dedicated backend per client type that aggregates and reshapes downstream calls into exactly what that UI needs.

// BFF endpoint: one client call fans out to many services server-side
app.get("/bff/dashboard", async (req, res) => {
  const [user, orders, recs] = await Promise.all([
    fetch(USER_SVC + "/me", { headers: auth }).then(r => r.json()),
    fetch(ORDER_SVC + "/recent").then(r => r.json()),
    fetch(REC_SVC + "/for-user").then(r => r.json()),
  ]);
  res.json({ user, orders, recs });   // one round-trip for the client
});
MODULE 9

Build, Bundling & Delivery

Vite vs webpack, tree-shaking, code-splitting, SSR/SSG/hydration, and Core Web Vitals.

Vite vs webpack

Bundlers turn many source modules into optimized assets the browser can load. webpack bundles everything (via a dependency graph) even in dev. Vite serves source over native ES modules in dev — no bundling — and only bundles for production (using Rollup), which makes dev startup near-instant.

AspectwebpackVite
Dev serverbundles then servesnative ESM, no bundle
Cold startslower (grows with app)near-instant
HMRcan slow on large appsfast, module-precise
Prod buildwebpackRollup
Configverbose, very flexibleminimal, opinionated

Tree-shaking & Code-splitting

Tree-shaking removes unused exports from the final bundle; it relies on static import/export (ES modules) so the bundler can prove code is dead. Code-splitting breaks the bundle into chunks loaded on demand, so users download only what a route needs.

// Tree-shakeable: named import, bundler drops the rest of the lib
import { debounce } from "lodash-es";  // NOT: import _ from "lodash"

// Route-level code-splitting via dynamic import + React.lazy
const Settings = React.lazy(() => import("./Settings"));
<React.Suspense fallback={<Spinner />}>
  <Settings />
</React.Suspense>

CSR vs SSR vs SSG vs Hydration

The rendering strategy decides where and when HTML is produced, trading off time-to-first-byte, interactivity delay, and server cost. Hydration is the step where client JS attaches event listeners to server-rendered HTML to make it interactive.

StrategyHTML builtBest for
CSRin the browser, after JS loadsapp-like dashboards behind auth
SSRper request on the serverdynamic, SEO-sensitive, personalized
SSGat build timemostly-static content (blogs, docs)
ISRbuild + periodic regenerationlarge catalogs that change slowly

Next.js Rendering & Env Config

Next.js lets you pick a rendering mode per route and colocate server and client code. Environment config separates build-time constants from runtime secrets — a frequent source of leaked keys.

// Next.js App Router: server component fetches at request time (SSR-like)
export default async function Page() {
  const data = await fetch("https://api/x", { cache: "no-store" }).then(r => r.json());
  return <Dashboard data={data} />;   // rendered on the server
}
# Only NEXT_PUBLIC_* is inlined into client JS (visible to users)
NEXT_PUBLIC_API_URL=https://api.example.com   # safe: public
DATABASE_URL=postgres://...                   # server-only: never exposed

Caching, CDN & Content Hashing

A CDN caches static assets at edge locations near users. The trick to "cache forever but still deploy updates" is content hashing: the build embeds a hash of each file's contents in its name, so a change produces a new filename and the old cache entry is never served stale.

# Build output: hash changes only when file content changes
dist/assets/index-a1b2c3.js     # immutable, cache 1 year
dist/assets/vendor-9f8e7d.css

# Hashed assets: long-lived immutable cache
Cache-Control: public, max-age=31536000, immutable
# index.html: must revalidate so new asset names are picked up
Cache-Control: no-cache

Source Maps & Perf Budgets

Source maps map minified production code back to original source so stack traces and debugging are readable. Lighthouse audits performance against Core Web Vitals; a performance budget is a CI-enforced ceiling on bundle size and metrics to prevent regressions.

  • LCP (Largest Contentful Paint) — loading; target < 2.5 s.
  • INP (Interaction to Next Paint) — responsiveness; target < 200 ms.
  • CLS (Cumulative Layout Shift) — visual stability; target < 0.1.
{
  "budgets": [
    { "type": "bundle", "name": "main", "maximumWarning": "170kb", "maximumError": "250kb" }
  ]
}