How to Organize and Structure a Vanilla JavaScript Project with Multiple Modules
Treat each feature as a self-contained folder bundling its own HTML, CSS, and JavaScript files, then use ES modules and a central entry point to keep the codebase scalable and maintainable.
When building a pure JavaScript web application with many independent features, learning how to organize and structure a vanilla JavaScript project with multiple modules is essential for long-term maintainability. The dom-projects repository by jisan-mia demonstrates an elegant folder-per-feature architecture that keeps each mini-app isolated while sharing common utilities where needed.
The Folder-Per-Feature Pattern
The dom-projects repository organizes code by placing each distinct feature inside its own directory under projects/. This approach eliminates global namespace pollution and makes dependencies explicit.
Consider the repository structure:
dom-projects/
├── index.html # Central launcher
├── index.js # Optional central script
└── projects/ # Container for all modules
├── advanced-todo/
│ ├── index.html
│ ├── script.js
│ └── style.css
├── basic-calculator/
│ ├── index.html
│ ├── script.js
│ └── style.css
├── camera-app/
│ ├── index.html
│ └── camera.js
├── flappy-bird/
│ ├── index.html
│ ├── flappybird.js
│ └── flappybird.css
├── js-todo/
│ ├── index.html
│ └── script.js
└── … (other mini-apps)
Each folder functions as an independent module. The camera-app module, for example, contains camera.js which handles MediaDevices API logic without interfering with the flappy-bird game loop defined in flappybird.js.
Implementing ES Modules in Vanilla JavaScript
Modern browsers support ES modules natively, allowing you to maintain strict scope isolation without bundlers. In dom-projects, modules use type="module" to enable import/export syntax.
Module Entry Points
Each module's HTML file declares its script as a module:
<!-- projects/weight-converter/index.html -->
<link rel="stylesheet" href="style.css">
<h1>Weight Converter</h1>
<input id="kg" placeholder="kg">
<span id="lb"></span>
<script src="script.js" type="module"></script>
Scoped Module Logic
The corresponding JavaScript runs in module scope, preventing global variable leakage:
// projects/weight-converter/script.js
const kgInput = document.getElementById('kg');
const lbSpan = document.getElementById('lb');
kgInput.addEventListener('input', () => {
const kg = parseFloat(kgInput.value) || 0;
lbSpan.textContent = (kg * 2.20462).toFixed(2) + ' lb';
});
Sharing Utilities Across Modules
When multiple modules need common functionality, place shared code in a utilities folder and import it explicitly. The dom-projects repository demonstrates this pattern in projects/retro-calculator/js/utils.js.
// projects/retro-calculator/js/utils.js
export function formatNumber(num) {
return num.toLocaleString('en-US');
}
export function saveToStorage(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
Other modules import these utilities using relative paths:
// projects/advanced-todo/script.js
import { saveToStorage } from '../retro-calculator/js/utils.js';
// Module-specific logic using shared utility
saveToStorage('todos', todoList);
Creating a Central Entry Point
The root index.html serves as a project launcher, providing navigation to each module without requiring a build step. This pattern keeps the repository accessible for beginners while remaining scalable.
<!-- index.html (repo root) -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>DOM Projects Collection</title>
</head>
<body>
<h1>Vanilla JavaScript Projects</h1>
<ul id="projects"></ul>
<script type="module">
const list = document.getElementById('projects');
const modules = [
'basic-calculator',
'camera-app',
'flappy-bird',
'advanced-todo'
];
modules.forEach(name => {
const li = document.createElement('li');
const a = document.createElement('a');
a.href = `projects/${name}/index.html`;
a.textContent = name.replace(/-/g, ' ');
li.appendChild(a);
list.appendChild(li);
});
</script>
</body>
</html>
Documentation and Module Templates
Maintainability requires documentation. The dom-projects repository includes a README.md template in projects/dialog/README.md that new contributors can copy when adding features.
A well-documented module includes:
- Purpose: What the feature does
- Files: List of contained assets
- APIs: Any external browser APIs used (e.g., MediaDevices, Canvas)
- Usage: How to run or test the module
Summary
- Folder-per-feature: Isolate each module in its own directory under
projects/with dedicated HTML, CSS, and JS files to prevent namespace collisions. - ES modules: Use
type="module"and explicitimport/exportstatements to maintain strict scope isolation without bundlers. - Shared utilities: Place common helper functions in a utilities folder (e.g.,
projects/retro-calculator/js/utils.js) and import them via relative paths. - Central launcher: Use a root
index.htmlto dynamically generate navigation links, providing a unified entry point for the entire collection. - Documentation: Include a
README.mdin each module folder explaining purpose, file structure, and external dependencies.
Frequently Asked Questions
How do I prevent global namespace pollution in a multi-module vanilla JavaScript project?
Use ES modules by adding type="module" to your script tags. This forces each file into its own scope, preventing variable leakage. In the dom-projects repository, every module—from camera-app/camera.js to flappy-bird/flappybird.js—runs as an isolated module, ensuring that variables like score or stream remain local to their respective features.
What is the best way to share utility functions between modules without a bundler?
Place shared utilities in a dedicated JavaScript file and export them as named exports. For example, projects/retro-calculator/js/utils.js exports helper functions like formatNumber and saveToStorage. Other modules import these using relative paths: import { saveToStorage } from '../retro-calculator/js/utils.js'. This pattern keeps dependencies explicit and avoids the complexity of build tools.
Should I use a single HTML entry point or separate HTML files for each module?
Use separate HTML files for each module to maintain true isolation, then provide a central launcher page at the repository root. In dom-projects, each feature like advanced-todo or basic-calculator has its own index.html, allowing developers to work on individual modules without loading unnecessary code. The root index.html dynamically generates a navigation list, serving as a project showcase and entry point.
How do I add a new module to an existing vanilla JavaScript project?
Create a new folder under projects/ containing index.html, script.js, and style.css. Follow the naming convention of existing modules (kebab-case folder names) and include a README.md explaining the feature's purpose and any external APIs used. As shown in projects/dialog/README.md, documenting the module's file structure and dependencies ensures other contributors can understand and test your addition without examining the source code.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →