cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Structuring an application

Structure JavaScript apps with MVC, MVVM, components, dependency injection, SOLID principles, and clear module boundaries.

By the end, you can
  • 01
    Separate concernsMove fetching, rules, storage, rendering, and input handling behind clear boundaries.
  • 02
    Recognize architecture stylesExplain MVC, MVVM, component-based UI, and how frameworks map to those ideas.
  • 03
    Apply clean design pressureUse dependency injection, SOLID, KISS, DRY, YAGNI, and module boundaries without over-abstracting.

Architecture means boundaries

A JavaScript app starts as a few event listeners. Then it gains fetching, validation, storage, rendering, state, tests, and several screens. Application architecture is the set of boundaries that decides which part owns each responsibility.

Plain definition

Structuring an application means separating data rules, rendering, input handling, side effects, and wiring so each part can be changed, tested, and replaced without dragging the whole app with it.

This lesson links back to the pattern lessons instead of re-teaching them: factories in Creational patterns, subscriptions in Observer & pub/sub, strategies and adapters in Structural & behavioral patterns, and single-source-of-truth ideas in State management.

Real-life analogyA restaurant separates the kitchen, waiter, and menu

If one person cooks, prints menus, takes orders, charges cards, and books suppliers, every change interrupts every job. A restaurant works because each role has a clear handoff. Application architecture creates the same handoffs in code.

In real life: Kitchen recipes and ingredients
In JavaScript: Model data and business rules
In real life: Menu the guest reads
In JavaScript: View rendering
In real life: Waiter takes the order and coordinates
In JavaScript: Controller or view-model commands
In real life: Supplier delivers ingredients
In JavaScript: Injected services such as storage or API clients

Where the analogy stops: A restaurant has people and rooms. A JavaScript app is still one program: the separation is made by functions, modules, objects, and data flow, not by physical walls.

Three ways to organize UI code
StyleCore boundaryWhere you see it
MVCModel holds data and rules; View renders; Controller handles input and coordinates.Forms, older SPA code, server-rendered pages with small widgets.
MVVMView-model exposes observable state and commands; binding keeps the view in sync.Knockout-style bindings, Vue-like mental models, form-heavy screens.
Component-basedSelf-contained UI units receive props, emit events, and compose into larger screens.React, Vue, Svelte, Web Components, design systems.

Keep the fundamentals nearby: functions, objects, classes, inheritance, OOP principles, modules, Web Components, and coding style. Architecture is those ideas arranged for a growing app.

Start with the one big script

STEP THROUGH

The todo app below is realistic because each line is reasonable in isolation. It loads data, clears the DOM, renders items, handles form submission, validates input, writes storage, and appends new DOM. The problem is not that any line is evil. The problem is that one function now has too many reasons to change.

One big todo scriptPop out in the code editor (opens in a new tab)JavaScript
function fetchTodos(url) {  console.log("fetch " + url);  return [{ id: 1, title: "Read architecture", done: false }];} function startTodoPage() {  const todos = fetchTodos("/todos.json");  app.innerHTML = "";  for (const todo of todos) {    const item = document.createElement("li");    item.textContent = todo.title;    app.append(item);  }   form.addEventListener("submit", (event) => {    event.preventDefault();    const title = input.value.trim();    if (title.length < 3) {      error.textContent = "Use at least 3 characters";      return;    }    const next = { id: Date.now(), title, done: false };    todos.push(next);    localStorage.setItem("todos", JSON.stringify(todos));    app.insertAdjacentHTML("beforeend", "<li>" + next.title + "</li>");  });   console.log(app.textContent.trim());} startTodoPage();

Refactor one seam at a time. First name the rule: normalizeTitle. Then hide storage behind load and save. Then make rendering a function that receives data and a root. The startup code still wires everything together, but each unit can now be tested in isolation.

Extract rules, storage, and renderingPop out in the code editor (opens in a new tab)JavaScript
function normalizeTitle(raw) {  const title = raw.trim();  if (title.length < 3) throw new Error("Use at least 3 characters");  return title;} function createTodoStore(storage) {  return {    load() {      return JSON.parse(storage.getItem("todos") ?? "[]");    },    save(todos) {      storage.setItem("todos", JSON.stringify(todos));    },  };} function renderTodoList(root, todos) {  const list = document.createElement("ul");  for (const todo of todos) {    const item = document.createElement("li");    item.textContent = todo.title;    list.append(item);  }  root.replaceChildren(list);} const fakeStorage = {  value: "[]",  getItem() { return this.value; },  setItem(_key, value) { this.value = value; },};const todos = [{ id: 1, title: normalizeTitle("  Write tests  "), done: false }];const store = createTodoStore(fakeStorage);store.save(todos);renderTodoList(app, todos);console.log(JSON.parse(fakeStorage.value)[0].title);console.log(app.textContent.trim());
Step through separating one use case
Step 0 of 10Ready
Your turn: follow the blue line

Step through the same todo feature after splitting validation, data, storage, and rendering responsibilities. Each boundary has one reason to change.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  const title = raw.trim();  if (title.length < 3) throw new Error("Use at least 3 characters");  return title;} function addTodo({ todos, title, storage, render }) {  const cleanTitle = normalizeTitle(title);  const nextTodos = [...todos, { id: todos.length + 1, title: cleanTitle }];  storage.save(nextTodos);  render(nextTodos);  return nextTodos;} const storage = { save: (todos) => console.log("saved:" + todos.length) };const render = (todos) => console.log("rendered:" + todos.length);const state = addTodo({ todos: [], title: "  Plan modules  ", storage, render });console.log(state.length);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The line-by-line replay is deliberately small. Line 8 validates. Line 9 creates the next model state. Lines 9 and 10 call injected collaborators. This is the same feature, but the dependencies are visible.

MVC and MVVM on the same feature

RUNNABLE DOM

MVC names three roles. The model owns data and rules. The view renders and reports user intent. The controller handles input and coordinates model plus view. In the runnable example, the controller exposes an add method so the test path and the submit path share the same model rule.

MVC todo in vanilla JavaScriptPop out in the code editor (opens in a new tab)JavaScript
function createTodoModel(initial = []) {  const todos = [...initial];  return {    all() { return [...todos]; },    add(title) {      const clean = title.trim();      if (clean.length < 3) throw new Error("Use at least 3 characters");      todos.push({ id: todos.length + 1, title: clean, done: false });    },  };} function createTodoView(root) {  root.innerHTML = '<form><input aria-label="Todo"><button>Add</button></form><ul></ul>';  const form = root.querySelector("form");  const input = root.querySelector("input");  const list = root.querySelector("ul");  return {    onAdd(handler) {      form.addEventListener("submit", (event) => {        event.preventDefault();        handler(input.value);        input.value = "";      });    },    render(todos) {      list.replaceChildren(...todos.map((todo) => {        const item = document.createElement("li");        item.textContent = todo.title;        return item;      }));    },  };} function createTodoController(model, view) {  const refresh = () => view.render(model.all());  view.onAdd((title) => { model.add(title); refresh(); });  refresh();  return { add(title) { model.add(title); refresh(); } };} const controller = createTodoController(  createTodoModel(),  createTodoView(document.querySelector("#mvc-demo")),);controller.add("Review architecture");console.log(document.querySelector("#mvc-demo li").textContent);

MVVM shifts the coordination surface. The view-model exposes observable state and commands shaped for the view. The view binds input events to commands and subscribes to state. It feels familiar in frameworks with reactive state: when state changes, rendering follows.

MVVM todo in vanilla JavaScriptPop out in the code editor (opens in a new tab)JavaScript
function observableState(initial) {  let state = initial;  const listeners = new Set();  return {    get() { return state; },    set(updater) {      state = updater(state);      listeners.forEach((listener) => listener(state));    },    subscribe(listener) {      listeners.add(listener);      listener(state);      return () => listeners.delete(listener);    },  };} function createTodoViewModel(storage) {  const todos = observableState(storage.load());  return {    todos,    newTitle: "",    setTitle(value) { this.newTitle = value; },    addTodo() {      const title = this.newTitle.trim();      if (title.length < 3) return;      todos.set((items) => [...items, { id: items.length + 1, title, done: false }]);      storage.save(todos.get());      this.newTitle = "";    },  };} function bindTodoView(root, viewModel) {  root.innerHTML = '<input aria-label="Todo"><button>Add</button><ul></ul>';  const input = root.querySelector("input");  const button = root.querySelector("button");  const list = root.querySelector("ul");  input.addEventListener("input", () => viewModel.setTitle(input.value));  button.addEventListener("click", () => {    viewModel.addTodo();    input.value = viewModel.newTitle;  });  viewModel.todos.subscribe((todos) => {    list.replaceChildren(...todos.map((todo) => {      const item = document.createElement("li");      item.textContent = todo.title;      return item;    }));  });} const memoryStorage = { load: () => [], save: (todos) => console.log("saved:" + todos.length) };const vm = createTodoViewModel(memoryStorage);bindTodoView(document.querySelector("#mvvm-demo"), vm);vm.setTitle("Review architecture");vm.addTodo();console.log(document.querySelector("#mvvm-demo li").textContent);

React is not literally MVC, but React components still separate render output from state owners and event handlers. Vue often feels MVVM-like because templates bind to reactive state. Angular combines components with dependency injection. The labels are useful when they help you find the boundary.

Component-based architecture

PROPS CHANGE

A component is a self-contained UI unit. In plain JavaScript it might be a function that returns DOM or HTML. In React, Vue, Svelte, or Web Components, the component has more tooling, but the design rule stays the same: props in, events out.

Component boundaries

Prefer composition over inheritance. A presentational component receives props and emits intent. A container owns data, passes props down, handles events, and re-renders children. Data flows one way, so a child does not secretly edit its parent.

Component props change and child re-render
Step 0 of 7Ready
Your turn: follow the blue line

Step through a tiny component system: props go in, rendered output comes out, and a state change re-renders the child.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  return "<li>" + (todo.done ? "Done: " : "Todo: ") + todo.title + "</li>";} function TodoList({ todos }) {  return "<ul>" + todos.map((todo) => TodoItem({ todo })).join("") + "</ul>";} function render(component, props, root) {  root.innerHTML = component(props);} let todos = [{ id: 1, title: "Draft lesson", done: false }];render(TodoList, { todos }, document.querySelector("#component-demo"));todos = todos.map((todo) => todo.id === 1 ? { ...todo, done: true } : todo);render(TodoList, { todos }, document.querySelector("#component-demo"));console.log(document.querySelector("#component-demo").textContent.trim());
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The source below is the same tiny renderer without the step player. TodoItem does not know where the data came from. TodoList composes children. The container changes todos and calls render again. In real apps, the framework optimizes the DOM update, but the mental model is the same.

Tiny component rendererPop out in the code editor (opens in a new tab)JavaScript
function TodoItem({ todo }) {  return "<li>" + (todo.done ? "Done: " : "Todo: ") + todo.title + "</li>";} function TodoList({ todos }) {  return "<ul>" + todos.map((todo) => TodoItem({ todo })).join("") + "</ul>";} function render(component, props, root) {  root.innerHTML = component(props);} let todos = [{ id: 1, title: "Draft lesson", done: false }];render(TodoList, { todos }, document.querySelector("#component-demo"));todos = todos.map((todo) => todo.id === 1 ? { ...todo, done: true } : todo);render(TodoList, { todos }, document.querySelector("#component-demo"));console.log(document.querySelector("#component-demo").textContent.trim());

Dependency injection: pass collaborators in

SWAP STORAGE

A dependency is another object or function your code needs to do its job: storage, an API client, an ID generator, a clock, a logger. Dependency injection means the caller passes that collaborator in. The module depends on a small contract instead of importing a concrete global.

Swap the storage implementation
Repository with injected storagePop out in the code editor (opens in a new tab)JavaScript
function memoryStorage(seed = {}) {  const data = { ...seed };  return {    get(key) { return data[key] ?? "[]"; },    set(key, value) { data[key] = value; },  };} function createTodoRepository({ storage, key = "todos", nextId = () => "todo-1" }) {  return {    all() { return JSON.parse(storage.get(key)); },    add(title) {      const todos = [...this.all(), { id: nextId(), title }];      storage.set(key, JSON.stringify(todos));      return todos.at(-1);    },  };} const repo = createTodoRepository({ storage: memoryStorage() });console.log(repo.add("Inject storage").title);console.log(repo.all().length);
Run resultmemory
Added todoSwap storage
Repository count1
Storage log
  1. stored:Swap storage
Try it yourself
Choose the collaborator passed into the repository

The repository code does not change. Only the object supplied at the composition root changes.

The empty memory storage run added memory-1 and now reports 1 todo.

This is dependency injection without a framework: pass the collaborator in, then test or replace it at the edge.

This is also why tests use mocks and fakes. A fake storage can record set calls, return a seed list, or simulate a failure without touching browser storage. In production, a composition root wires the real storage, API client, clock, and root UI together.

A tiny DI container as an asidePop out in the code editor (opens in a new tab)JavaScript
function createContainer(registry) {  const cache = new Map();  const container = {    get(name) {      if (!cache.has(name)) cache.set(name, registry[name](container));      return cache.get(name);    },  };  return container;} const container = createContainer({  storage: () => {    const data = { todos: "[]" };    return { get: (key) => data[key], set: (key, value) => { data[key] = value; } };  },  todos: ({ get }) => ({    all: () => JSON.parse(get("storage").get("todos")),    add(title) {      const todos = [...this.all(), { id: "container-1", title }];      get("storage").set("todos", JSON.stringify(todos));      return todos.at(-1);    },  }),});console.log(container.get("todos").add("Container aside").title);console.log(container.get("todos") === container.get("todos"));

A container can centralize object creation when wiring is genuinely repetitive. Many JavaScript apps do not need one. Explicit parameters and one composition root are easier to trace until the repeated wiring becomes real.

Clean code and SOLID in JavaScript terms

SORTER

Clean architecture starts with clean code: meaningful names, small functions, and code that says what it changes. SOLID is not a Java-only checklist. In JavaScript, it becomes focused functions, small objects, and replaceable strategies.

Principles that keep architecture practical
PrincipleJavaScript versionUseful check
KISSPrefer the simple direct function until complexity is proven.A named directTitle beats a factory-builder-adapter for one string transform.
YAGNIDo not add plugin systems, containers, or base classes for imagined futures.Add a strategy map when a second real strategy appears.
DRYRemove duplicated knowledge, not every similar-looking line.One validation rule is good; a vague abstraction for two unrelated forms is not.
SOLIDUse focused modules that can be replaced and tested through small contracts.A storage object with get and set is easier to fake than direct localStorage calls.

Single responsibility: split reasons to change

The first function mixes parsing, saving, and rendering. The after version names each reason to change. This is small, but the habit scales to whole modules.

Single responsibility before and afterPop out in the code editor (opens in a new tab)JavaScript
function addTodoMixed(raw, storage, list) {  const title = raw.trim();  if (title.length < 3) throw new Error("Use at least 3 characters");  storage.saved = title;  list.push(title);} function normalizeTodoTitle(raw) {  const title = raw.trim();  if (title.length < 3) throw new Error("Use at least 3 characters");  return title;}function saveTodo(storage, todo) { storage.saved = todo.title; }function renderTodo(list, todo) { list.push(todo.title); } const storage = {};const list = [];const todo = { title: normalizeTodoTitle("  Learn SOLID  ") };saveTodo(storage, todo);renderTodo(list, todo);console.log(storage.saved);console.log(list.join(", "));

Open/closed: extend with strategies

A strategy is a small function chosen from the outside. Adding a new discount means adding a function, not editing the stable calculation. This links back to the strategy pattern from the structural and behavioral patterns lesson.

Open/closed with discount strategiesPop out in the code editor (opens in a new tab)JavaScript
const discountStrategies = {  none: (total) => total,  tenPercent: (total) => total * 0.9,  fiveOff: (total) => Math.max(0, total - 5),}; function totalAfterDiscount(total, strategy) {  return strategy(total).toFixed(2);} console.log(totalAfterDiscount(10, discountStrategies.tenPercent));console.log(totalAfterDiscount(10, discountStrategies.fiveOff));

Liskov: replacements must keep their promises

If code accepts a rectangle, it expects setSize(4, 5) to mean width 4 and height 5. A Square subtype that ignores height breaks that contract. Prefer a smaller shared contract, such as an object with area(), when shapes do not support the same operations.

Liskov violation and safer shape contractPop out in the code editor (opens in a new tab)JavaScript
class Rectangle {  setSize(width, height) { this.width = width; this.height = height; }  area() { return this.width * this.height; }} class Square extends Rectangle {  setSize(width) { this.width = width; this.height = width; }} function areaAfterResize(shape) {  shape.setSize(4, 5);  return shape.area();} console.log(areaAfterResize(new Rectangle()));console.log(areaAfterResize(new Square())); const square = (size) => ({ area: () => size * size });console.log(square(5).area());

Interface segregation and dependency inversion

JavaScript does not need formal interfaces for these principles to help. Pass the smallest focused object or function. Depend on a storage with get and set, not direct localStorage calls deep inside a module.

Small contracts instead of fat servicesPop out in the code editor (opens in a new tab)JavaScript
function sendReceipt({ mailer }, address) {  mailer.send(address, "Receipt");} function loadPreference(settingsStore) {  return settingsStore.get("theme") ?? "system";} const fakeMailer = { sent: [], send(to, text) { this.sent.push(to + ":" + text); } };sendReceipt({ mailer: fakeMailer }, "ada@example.com");console.log(fakeMailer.sent[0]);console.log(loadPreference({ get: () => "dark" }));

KISS, YAGNI, and DRY: avoid architecture theater

Over-abstracting hurts when the abstraction has no real variation yet. DRY removes duplicated knowledge, not every similar line. KISS and YAGNI say to keep the direct code until the second real use case appears.

KISS beats an imaginary abstractionPop out in the code editor (opens in a new tab)JavaScript
function createFactoryBuilderStrategyAdapter(value) {  return { execute: () => value.trim().toUpperCase() };} const directTitle = (value) => value.trim().toUpperCase();console.log(directTitle("  ship it  "));console.log(createFactoryBuilderStrategyAdapter("  ship it  ").execute());
Which SOLID pressure is this?
  • A submit handler fetches, validates, saves, and renders all by itself.
  • Adding a coupon means editing a long `if/else` inside `checkoutTotal`.
  • `Square` extends `Rectangle`, but `setSize(4, 5)` returns area 16.
  • A component receives one huge `services` object but only calls `services.mailer.send`.
  • A repository calls `localStorage` directly, so tests must patch browser globals.
Try it yourself
0 of 5 correct

Sort each smell by the principle it most clearly violates. Some designs have more than one problem; choose the main pressure described.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Modules, folders, and circular dependencies

BOUNDARIES

Folder structure is a signal, not the architecture itself. Organizing by feature keeps a screen or domain close together. Organizing by type is easy to start and can be clear for small apps. Most professional front ends become a hybrid.

Feature folders versus type folders
StructureExampleTrade-off
By featurefeatures/todos/TodoList.js, features/todos/todoRepository.jsKeeps related code close; good when features are owned by teams or screens.
By typecomponents/TodoList.js, models/todoModel.js, services/todoRepository.jsEasy to start and teach; can make feature work require many folders.
HybridFeature folders plus shared/ for cross-cutting helpersOften the practical default once repeated utilities are real.
Feature-first and type-first folder sketchestext
src/
  features/
    todos/
      TodoList.js
      todoModel.js
      todoRepository.js
    settings/
      SettingsPanel.js
  shared/
    dom.js
    storage.js

src/
  components/
    TodoList.js
    SettingsPanel.js
  models/
    todoModel.js
  services/
    todoRepository.js

Module boundaries should point in one direction. Models should not import views to render, and views should not import models to save. A controller, view-model, or composition root can import both and wire them together. If you hit a circular import, review module resolution and then move the wiring to a higher layer.

Circular dependency smellJavaScript
// todoModel.jsimport { renderTodo } from "./todoView.js"; // model now knows the viewexport function addTodo(title) {  const todo = { title };  renderTodo(todo);  return todo;} // todoView.jsimport { addTodo } from "./todoModel.js"; // view now knows the modelbutton.addEventListener("click", () => addTodo(input.value));

Common misconceptions

  • “Architecture is folder names.” Folders help, but runtime dependencies and responsibilities are the real design.
  • “MVC means old code.” Modern frameworks still separate data rules, rendering, and input handling.
  • “MVVM means magic two-way binding everywhere.” A view-model can expose explicit commands and observable state without hiding rules.
  • “Components remove the need for architecture.” Components still need state owners, effects, module boundaries, and tests.
  • “Dependency injection requires a container.” Passing collaborators as parameters is DI.
  • “SOLID means adding classes.” In JavaScript, functions and plain objects often express the principles better.
Misconception checks
ClaimBetter questionPractical move
We need a base class for every component.Is there shared behavior or just similar markup?Prefer composition and shared helpers.
Everything should be DRY immediately.Is this duplicated knowledge or just two similar cases?Wait for a real pattern before abstracting.
The model can render itself for convenience.Who owns data rules and who owns presentation?Let a controller or view-model coordinate.
Tests need real localStorage.What contract does the code actually need?Inject a storage with get and set.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict the model count

Read the model-only code and type the number printed by the final line.

Starter codePop out in the code editor (opens in a new tab)JavaScript
function createTodoModel(initial = []) {
  const todos = [...initial];
  return { add(title) { todos.push({ title }); }, all() { return [...todos]; } };
}
const model = createTodoModel([{ title: "A" }]);
model.add("B");
console.log(model.all().length);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeSpot the responsibility mix

    Name one concern you would extract first.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    form.addEventListener("submit", async (event) => {
      event.preventDefault();
      const response = await fetch("/todos");
      const title = input.value.trim();
      localStorage.setItem("draft", title);
      list.insertAdjacentHTML("beforeend", "<li>" + title + "</li>");
    });

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeInject a storage dependency

      Predict the text printed by the injected fake storage.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function saveTheme(storage, theme) {
        storage.set("theme", theme);
      }
      const fakeStorage = { value: "", set(key, value) { this.value = key + ":" + value; } };
      saveTheme(fakeStorage, "dark");
      console.log(fakeStorage.value);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeRead a presentational component

        Type the exact text printed by the component-style functions.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const TodoItem = ({ todo }) => todo.title;
        const TodoList = ({ todos }) => todos.map((todo) => TodoItem({ todo })).join(", ");
        console.log(TodoList({ todos: [{ title: "Draft" }, { title: "Review" }] }));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeFind the Liskov violation

          Which class breaks the caller’s contract?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          class Rectangle {
            setSize(width, height) { this.width = width; this.height = height; }
            area() { return this.width * this.height; }
          }
          class Square extends Rectangle {
            setSize(width) { this.width = width; this.height = width; }
          }
          const shape = new Square();
          shape.setSize(4, 5);
          console.log(shape.area());

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            8 QUESTIONS
            Application architecture quiz · 8 questionsScore: first tries count
            1. Question 1 of 8In MVC, which part should enforce todo title rules?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does this dependency-injected save print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function saveTheme(storage, theme) {
                storage.set("theme", theme);
              }
              const fake = { value: "", set(key, value) { this.value = key + ":" + value; } };
              saveTheme(fake, "dark");
              console.log(fake.value);

              Choose an answer to see the explanation.

            3. Question 3 of 8What does the Liskov example reveal?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              class Rectangle {
                setSize(width, height) { this.width = width; this.height = height; }
                area() { return this.width * this.height; }
              }
              class Square extends Rectangle {
                setSize(width) { this.width = width; this.height = width; }
              }
              const shape = new Square();
              shape.setSize(4, 5);
              console.log(shape.area());

              Choose an answer to see the explanation.

            4. Question 4 of 8Which statement best describes MVVM?

              Choose an answer to see the explanation.

            5. Question 5 of 8What does unidirectional component data flow mean?

              Choose an answer to see the explanation.

            6. Question 6 of 8What does this open/closed strategy code print first?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const discounts = { tenPercent: (total) => total * 0.9 };
              function totalAfterDiscount(total, strategy) {
                return strategy(total).toFixed(2);
              }
              console.log(totalAfterDiscount(10, discounts.tenPercent));

              Choose an answer to see the explanation.

            7. Question 7 of 8Which boundary avoids a circular dependency?

              Choose an answer to see the explanation.

            8. Question 8 of 8When is a DI container worth considering in a JavaScript app?

              Choose an answer to see the explanation.

            Key takeaways

            • Architecture is separation of responsibilities, not just folder names.
            • MVC separates model, view, and controller; MVVM exposes observable state and commands through a view-model.
            • Components work best with props in, events out, composition, and unidirectional data flow.
            • Dependency injection is ordinary parameter passing that makes testing and swapping implementations easier.
            • SOLID in JavaScript means small contracts, honest subtypes, strategies, and depending on abstractions.
            • Use KISS, YAGNI, and DRY to avoid building architecture that solves imaginary problems.

            One-line summary: structure JavaScript apps by making ownership and dependencies explicit, then choose the lightest pattern that keeps change local.

            Up next: Designing good APIs. That lesson focuses on the public function and module shapes exposed by the boundaries you designed here.

            CompleteFrontend Clear concepts. Working examples.