Course progress0%
Course content
55 min

Split the Working Application

Create modules from responsibilities you can already explain

By the end of this lesson

  • Assign state, data access, presentation and orchestration to focused modules
  • Trace imports without losing the application flow

Split after understanding

A folder structure does not create good architecture by itself. First make the data flow work in one file. Then separate responsibilities that already have clear boundaries.

Use a small team analogy:

File Team role
state.js Keeps the current facts
data-service.js Talks to the data source
view.js Updates what the user sees
app.js Tells the roles when to work together

No file should try to perform every role.

resource-library/
|-- index.html
|-- css/
|   `-- style.css
|-- data/
|   `-- resources.json
`-- js/
    |-- app.js
    |-- state.js
    |-- data-service.js
    `-- view.js

state.js owns current memory

export const state = {
  items: [],
  search: '',
  topic: 'all',
  availableOnly: false,
  selectedId: null,
  status: 'not-started',
  error: null
};

This simple shared object is suitable for this learning application. The important rule is that updates remain deliberate and traceable. Larger applications may enforce updates differently.

data-service.js owns external data access

export async function fetchResources() {
  const response = await fetch('/data/resources.json');
  if (!response.ok) throw new Error(`Request failed (${response.status})`);

  const data = await response.json();
  if (!Array.isArray(data)) throw new Error('Invalid resource data');
  return data;
}

This module knows HTTP and the expected data shape. It does not call document.querySelector because it does not own presentation.

view.js owns DOM presentation

export function renderResources(items) {
  const listElement = document.querySelector('#resource-list');
  listElement.replaceChildren();

  for (const item of items) {
    const element = document.createElement('li');
    element.textContent = `${item.title} - ${item.minutes} min`;
    listElement.append(element);
  }
}

This module receives values to display. It does not fetch them because a view should not decide where application data comes from.

app.js connects the flow

import { state } from './state.js';
import { fetchResources } from './data-service.js';
import { renderResources } from './view.js';

function getVisibleItems() {
  return state.items.filter((item) =>
    item.title.toLowerCase().includes(state.search.toLowerCase())
  );
}

function render() {
  renderResources(getVisibleItems());
}

async function init() {
  state.items = await fetchResources();
  render();
}

init();

The final version should retain the loading and error handling from the previous lesson. The shortened example exposes only the module connections.

Responsibility test

Ask these questions before placing code:

  • Does it communicate with the data source? Put it in the data service.
  • Does it store current application information? Put it in state.
  • Does it create or update visible elements? Put it in the view.
  • Does it coordinate a user action or application lifecycle? Put it in the app controller.

Modules need dependencies, so the boundaries are not walls. app.js imports and coordinates them. Avoid circular imports where every module controls every other module.

Do not memorize filenames as fixed rules. Memorize the reason for each separation. If you can finish the sentence "this code belongs here because...", the structure is helping you think.

What to remember

Separate code because responsibilities change for different reasons. Services obtain data, state records the present, views update the DOM and the app coordinates the flow.

Lesson complete?

Your progress is saved on this device.