In Lab 04 you built a live token counter. It works. But every line of code utility functions, history management, and DOM logic lives inside a single file: tokenCounter.js. As projects grow, this becomes a serious problem. A 50-line file is easy to read. A 500-line file is not. A 5,000-line file is a liability.
Refactoring is the practice of restructuring existing code without changing what it does. It is one of the most important skills in software development, and it is also one of the areas where AI tools are least reliable. An AI assistant can write new features quickly, but reorganising existing code correctly requires a deep understanding of what depends on what and AI often gets this wrong in subtle ways that introduce hard-to-spot bugs. In this lab, that responsibility is yours.
You will split tokenCounter.js into three focused modules:
utils.jspure text-processing functions with no dependencieshistory.jssnapshot history state and managementdom.jsDOM queries and event listeners
By the end, the token counter will behave exactly as before. But the code will be organised, readable, and ready to grow.
By the end of this lab you will be able to:
- Explain what an ES Module is and why modules exist
- Use
exportto make a function or variable available to other files - Use
importto bring exported items into another file - Add
type="module"to a<script>tag - Trace dependencies between modules and identify what needs to be imported where
- Use VS Code Live Server to run a local development server (required for ES Modules)
- Verify a refactor is correct by testing every feature manually in the browser
You are continuing in the same project. No new repository needed. Open your existing tokenCounter.html and tokenCounter.js files from Lab 04 those are your starting point.
Before you write a single line of code, read all of Part 1. Understanding the module system conceptually will save you significant debugging time later.
A module is a JavaScript file that explicitly declares what it shares with the outside world and what it needs from other files. Nothing leaks in or out by accident. Variables declared in a module are scoped to that file they do not end up on the global window object.
There are two keywords:
exportmarks a function or variable as available to other modulesimportbrings something exported by another module into the current file
The most common pattern is a named export:
// utils.js
export function cleanText(text) {
return text.trim();
}
export function splitIntoWords(text) {
return text.split(' ');
}Each function is exported individually by name. You can have as many named exports as you like in one file.
To use those functions in another file:
// dom.js
import { cleanText, splitIntoWords } from './utils.js';Two things to notice:
- The names inside
{}must match exactly what was exported capitalisation matters - The path must start with
./JavaScript will not resolve bare names like'utils.js'on its own
For the browser to treat a file as a module, the <script> tag that loads it must include type="module":
<script type="module" src="dom.js"></script>Without this attribute, import and export are syntax errors.
ES Modules do not work when you open an HTML file directly from your filesystem (the URL will start with file://). You will see a CORS error in the console and nothing will load.
You need to serve the files over HTTP. The easiest way in VS Code is the Live Server extension:
- Right-click
tokenCounter.htmlin the file explorer - Select Open with Live Server
- The page opens at something like
http://127.0.0.1:5500/tokenCounter.html
If Live Server is not installed, find it in the Extensions panel (Ctrl+Shift+X) and install it before continuing.
Before creating any new files, you need to understand what depends on what. Open tokenCounter.js and read through it carefully.
List every function in tokenCounter.js and decide which module it belongs to. Use this table as a guide:
| Name | Where it lives now | Where it will go |
|---|---|---|
cleanText |
tokenCounter.js |
? |
splitIntoWords |
tokenCounter.js |
? |
removeEmptyWords |
tokenCounter.js |
? |
estimateTokens |
tokenCounter.js |
? |
countTokens |
tokenCounter.js |
? |
analyzeText |
tokenCounter.js |
? |
renderHistory |
tokenCounter.js |
? |
history array |
tokenCounter.js |
? |
| DOM selectors | tokenCounter.js |
? |
| Event listeners | tokenCounter.js |
? |
Apply this rule to each item:
- Does it touch
document? βdom.js - Does it manage the
historyarray? βhistory.js - Does it only transform text or numbers, with no side effects? β
utils.js
Fill in the column before writing any code.
Now figure out which files will need to import from which. Answer these questions:
- Does
history.jsneed anything fromutils.js? - Does
dom.jsneed anything fromutils.js? - Does
dom.jsneed anything fromhistory.js? - Does
utils.jsneed anything from anyone?
Sketch it out on paper or as a comment block. One rule to keep in mind: a module should never import from a file that imports from it. Circular dependencies will cause errors that are very hard to trace.
Create a new file called utils.js. Move the pure text-processing functions into it.
Each function that other files will need must be exported. Add the export keyword in front of each function definition.
Test your exports are correct before moving on. A typo in an export name will not throw an error immediately it will only fail silently when another module tries to import it.
Create a new file called history.js. This file is responsible for the history array and all operations on it.
Think carefully about one design decision: should you export the history array directly, or is it better to export functions that interact with it? Consider what happens if dom.js can modify the array freely could that cause problems as the project grows?
If history.js needs to call analyzeText, import it from utils.js.
Export everything that dom.js will need to call.
Create a new file called dom.js. This file handles all user interaction:
- All
document.querySelectorcalls - The
inputevent listener on the textarea - The
clickevent listener on the save button - The
renderHistoryfunction
At the top of the file, import whatever you need from utils.js and history.js. Do not import anything you do not actually use.
The HTML currently loads tokenCounter.js. Change the <script> tag to load dom.js instead, and add type="module":
<script type="module" src="dom.js"></script>Leave tokenCounter.js in place for now. In Step 9 you will confirm it is no longer needed.
Open tokenCounter.html using Live Server. The URL in the browser must begin with http://, not file://.
Open the browser Console tab (F12). If you see errors, read them carefully. Here are the most common ones and what they mean:
| Error message | Likely cause |
|---|---|
Failed to resolve module specifier 'utils.js' |
Missing ./ prefix use './utils.js' |
SyntaxError: Cannot use import statement |
The <script> tag is missing type="module" |
cleanText is not exported from './utils.js' |
The function exists but export was not added |
cleanText is not a function |
Imported under a wrong name, or not imported at all |
Cross-Origin Request Blocked |
You opened the file directly use Live Server |
Fix errors one at a time. The first error in the console is usually the root cause of all the others.
No errors in the console does not mean everything works. Test each feature manually:
β Type text into the textarea character, word, and token counts update in real time β Click Save Snapshot a new item appears in the history list β Click Save Snapshot a second time it is labelled "Snapshot 2" β Refresh the page the counter still starts correctly from a clean state
Once all tests pass, delete tokenCounter.js (or empty its contents completely). Refresh the page. If anything breaks, your new modules are still relying on something that was left in the old file. Trace it and move it to the right module.
When the page works with tokenCounter.js gone, the refactor is complete.
After this lab your project should look like this:
index.html
how-it-works.html
tokenCounter.html β <script type="module" src="dom.js">
styles.css
utils.js β pure text-processing functions
history.js β history array and snapshot management
dom.js β DOM selectors and event listeners
tokenCounter.js should be empty or deleted.
- Add a Clear History button. Write a
clearHistoryfunction inhistory.js, export it, import it indom.js, and connect it to the button. Practice the full cycle from scratch. - Research
export defaultand rewrite one of your exports to use it. How does the import syntax change? When would you preferdefaultover named exports? - Open the browser Sources (or Debugger) panel and expand the module tree. You should be able to see each of your
.jsfiles listed separately. Does the loading order match the dependency graph you drew in Step 2? - Add a
timestampproperty to each snapshot object inhistory.jsusingnew Date().toLocaleTimeString(). Display it in the rendered history list. Trace the full path the data takes: from the function inhistory.jsto the element on the page.
β€οΈ Happy coding!
