Structuring Frontend JavaScript
Organizing files
As your project grows, organizing code into files becomes essential.
Basic structure
my-app/
├── index.html
├── css/
│ └── styles.css
├── js/
│ ├── app.js
│ ├── utils.js
│ └── components/
│ ├── todo.js
│ └── user.js
└── assets/
└── images/
Separation of concerns
js/
├── app.js # Main application logic
├── state.js # State management
├── dom.js # DOM manipulation
├── api.js # API calls
└── utils.js # Utility functions
Using es modules in the browser
ES modules let you split code across files and import what you need.
Setting up modules
In your HTML:
<script type="module" src="js/app.js"></script>
Module structure
// utils.js
export function formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount);
}
export function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
// app.js
import { formatCurrency, debounce } from './utils.js';
const price = formatCurrency(100);
Organizing by feature
js/
├── app.js
├── todo/
│ ├── todo.js
│ ├── todo-state.js
│ └── todo-ui.js
├── user/
│ ├── user.js
│ └── user-api.js
└── utils/
└── helpers.js
Separating concerns
Keep different responsibilities separate:
Logic vs DOM updates
// state.js - manages data
export const todos = {
items: [],
add(text) {
this.items.push({ id: Date.now(), text, completed: false });
},
toggle(id) {
const todo = this.items.find(t => t.id === id);
if (todo) todo.completed = !todo.completed;
}
};
// ui.js - handles DOM updates
import { todos } from './state.js';
export function renderTodos() {
const list = document.querySelector('#todo-list');
list.innerHTML = '';
todos.items.forEach(todo => {
const li = document.createElement('li');
li.textContent = todo.text;
if (todo.completed) li.classList.add('completed');
list.appendChild(li);
});
}
// app.js - wires everything together
import { todos } from './state.js';
import { renderTodos } from './ui.js';
document.querySelector('#add-btn').addEventListener('click', () => {
todos.add('New todo');
renderTodos();
});
Events vs logic
// events.js - event handlers
import { todos } from './state.js';
import { renderTodos } from './ui.js';
export function setupEventListeners() {
document.querySelector('#add-btn').addEventListener('click', () => {
const input = document.querySelector('#todo-input');
todos.add(input.value);
input.value = '';
renderTodos();
});
document.querySelector('#todo-list').addEventListener('click', (e) => {
if (e.target.classList.contains('delete')) {
const id = parseInt(e.target.dataset.id);
todos.remove(id);
renderTodos();
}
});
}
// app.js - initialization
import { setupEventListeners } from './events.js';
import { renderTodos } from './ui.js';
setupEventListeners();
renderTodos();
Progressive enhancement mindset
Progressive enhancement means building a functional base that works everywhere, then adding JavaScript enhancements.
HTML first
<!-- Works without JavaScript -->
<form action="/api/submit" method="POST">
<input type="text" name="username" required />
<button type="submit">Submit</button>
</form>
Enhance with JavaScript
// Enhance form with JavaScript
const form = document.querySelector('form');
form.addEventListener('submit', function(e) {
e.preventDefault();
// Enhanced behavior: show loading, handle errors, etc.
submitFormWithFeedback(this);
});
Benefits:
- Works even if JavaScript fails
- Better for SEO
- Faster initial load
- More accessible
Common patterns
App initialization pattern
// app.js
class App {
constructor() {
this.state = {};
this.components = {};
}
init() {
this.loadState();
this.initComponents();
this.setupEvents();
this.render();
}
loadState() {
// Load from localStorage, API, etc.
}
initComponents() {
// Initialize components
}
setupEvents() {
// Setup event listeners
}
render() {
// Initial render
}
}
// Start app when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
const app = new App();
app.init();
});
Component pattern
// components/todo-list.js
export class TodoList {
constructor(container, todos) {
this.container = container;
this.todos = todos;
}
render() {
this.container.innerHTML = '';
this.todos.forEach(todo => {
const item = this.createTodoItem(todo);
this.container.appendChild(item);
});
}
createTodoItem(todo) {
const li = document.createElement('li');
li.textContent = todo.text;
if (todo.completed) li.classList.add('completed');
return li;
}
}
// app.js
import { TodoList } from './components/todo-list.js';
const todoList = new TodoList(
document.querySelector('#todo-list'),
todos.items
);
todoList.render();
Service pattern
// services/api.js
export class ApiService {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async get(endpoint) {
const response = await fetch(`${this.baseUrl}${endpoint}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
async post(endpoint, data) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}
}
// app.js
import { ApiService } from './services/api.js';
const api = new ApiService('https://api.example.com');
const users = await api.get('/users');
Best practices
1. One responsibility per file
// Good: focused file
// utils/formatting.js
export function formatCurrency() { }
export function formatDate() { }
// Bad: everything in one file
// utils.js
export function formatCurrency() { }
export function formatDate() { }
export function apiCall() { }
export function domManipulation() { }
2. Use descriptive file names
// Good
todo-manager.js
user-service.js
form-validator.js
// Bad
stuff.js
helpers.js
utils.js
3. Export what's needed
// Good: explicit exports
export function publicFunction() { }
export const publicConstant = 123;
function privateFunction() { } // Not exported
// Bad: everything exported
export function everything() { }
export function isPublic() { }
4. Keep dependencies clear
// Good: clear dependencies
import { formatCurrency } from './utils/formatting.js';
import { ApiService } from './services/api.js';
// Bad: unclear where things come from
import * from './utils.js';
5. Document structure
/**
* Manages todo items state and operations
* @module todo/state
*/
/**
* Adds a new todo item
* @param {string} text - Todo text
*/
export function addTodo(text) {
// ...
}