Skip to main content

JavaScript Code Organization

As your JavaScript projects grow in complexity, organizing your code becomes increasingly important. Well-organized code is easier to understand, maintain, and extend. This guide covers essential techniques for structuring your JavaScript codebase effectively.

Introduction​

Code organization is the process of structuring your code in a logical and maintainable way. Poor code organization can lead to:

  • Difficulty finding specific functionality
  • Code duplication
  • Naming conflicts
  • Challenges when debugging
  • Problems when multiple developers work on the same codebase

By implementing good organizational practices, you'll create code that's easier to work with and more robust.

Basic Principles of Code Organization​

1. Separate Concerns​

One of the most fundamental principles is to separate your code according to its purpose or "concern."

javascript
// Bad approach: mixing UI, data, and business logic
function processUserData() {
// Get form data
const name = document.getElementById('name').value;
const age = document.getElementById('age').value;

// Validate data
if (name === '' || age === '') {
alert('Please fill all fields');
return;
}

// Process data
const formattedName = name.toUpperCase();
const userDetails = {
name: formattedName,
age: parseInt(age),
registrationDate: new Date()
};

// Update UI
document.getElementById('output').textContent = `Welcome, ${formattedName}!`;
}
javascript
// Better approach: separate concerns
// UI handling
function getUserInputData() {
return {
name: document.getElementById('name').value,
age: document.getElementById('age').value
};
}

function displayWelcomeMessage(name) {
document.getElementById('output').textContent = `Welcome, ${name}!`;
}

// Data validation
function validateUserData(userData) {
if (userData.name === '' || userData.age === '') {
return { valid: false, message: 'Please fill all fields' };
}
return { valid: true };
}

// Business logic
function processUserData(userData) {
const formattedName = userData.name.toUpperCase();
return {
name: formattedName,
age: parseInt(userData.age),
registrationDate: new Date()
};
}

// Main controller function
function handleFormSubmission() {
const userData = getUserInputData();
const validation = validateUserData(userData);

if (!validation.valid) {
alert(validation.message);
return;
}

const processedData = processUserData(userData);
displayWelcomeMessage(processedData.name);
}

2. Use Meaningful Names​

Clear, descriptive names make your code self-documenting:

javascript
// Poor naming
function fn1(a, b) {
return a + b;
}

// Better naming
function calculateSum(firstNumber, secondNumber) {
return firstNumber + secondNumber;
}

3. Follow Consistent Formatting​

Consistent formatting makes code easier to read:

javascript
// Inconsistent formatting
function calculateArea(width,height){
return width*height;
}

function calculateVolume( width, height, depth )
{
return width * height * depth;
}

// Consistent formatting
function calculateArea(width, height) {
return width * height;
}

function calculateVolume(width, height, depth) {
return width * height * depth;
}

File Organization Strategies​

1. Group by Feature or Domain​

Organize files by the features or business domains they serve:

/src
/authentication
login.js
register.js
passwordReset.js
/products
productList.js
productDetail.js
productSearch.js
/cart
cartItems.js
checkout.js

2. Group by Type​

Organize files by their technical purpose:

/src
/components
Button.js
Modal.js
Form.js
/services
apiService.js
authService.js
/utils
formatting.js
validation.js

JavaScript Modules​

Modern JavaScript provides a module system to help organize code. Modules allow you to:

  1. Split code into separate files
  2. Control what's exposed from each file
  3. Clearly specify dependencies

ES Modules Example​

javascript
// mathUtils.js - create and export functions
export function add(a, b) {
return a + b;
}

export function multiply(a, b) {
return a * b;
}

export const PI = 3.14159;
javascript
// app.js - import and use the functions
import { add, multiply, PI } from './mathUtils.js';

console.log(add(5, 3)); // Output: 8
console.log(multiply(4, 2)); // Output: 8
console.log(PI); // Output: 3.14159

Default Exports​

You can also have a default export:

javascript
// user.js
export default class User {
constructor(name, email) {
this.name = name;
this.email = email;
}

getFullDetails() {
return `${this.name} (${this.email})`;
}
}

// Helper function (named export)
export function validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
javascript
// app.js
import User, { validateEmail } from './user.js';

const email = 'jane@example.com';

if (validateEmail(email)) {
const user = new User('Jane Doe', email);
console.log(user.getFullDetails()); // Output: Jane Doe (jane@example.com)
} else {
console.log('Invalid email');
}

Design Patterns for Code Organization​

The Module Pattern (Pre-ES6)​

Before ES6 modules, the Module Pattern was a common way to organize code:

javascript
// Creating a module with private and public members
const Calculator = (function() {
// Private variables and functions
let result = 0;

function validateNumber(num) {
return typeof num === 'number' && !isNaN(num);
}

// Public API
return {
add: function(num) {
if (validateNumber(num)) {
result += num;
}
return this;
},

subtract: function(num) {
if (validateNumber(num)) {
result -= num;
}
return this;
},

getResult: function() {
return result;
},

reset: function() {
result = 0;
return this;
}
};
})();

// Using the calculator
Calculator.add(5).add(10).subtract(3);
console.log(Calculator.getResult()); // Output: 12
Calculator.reset();
console.log(Calculator.getResult()); // Output: 0

The Revealing Module Pattern​

A variant that makes it clearer which functions are public:

javascript
const ShoppingCart = (function() {
// Private members
const items = [];

function addItem(name, price, quantity) {
items.push({
name,
price,
quantity,
total: price * quantity
});
}

function getItemCount() {
return items.reduce((total, item) => total + item.quantity, 0);
}

function getTotalPrice() {
return items.reduce((total, item) => total + item.total, 0);
}

function clearCart() {
items.length = 0;
}

// Reveal public methods
return {
add: addItem,
count: getItemCount,
total: getTotalPrice,
clear: clearCart
};
})();

// Using the shopping cart
ShoppingCart.add('Book', 19.99, 2);
ShoppingCart.add('Coffee', 3.99, 1);
console.log(`Items count: ${ShoppingCart.count()}`); // Output: Items count: 3
console.log(`Total: $${ShoppingCart.total().toFixed(2)}`); // Output: Total: $43.97

Real-World Example: Building a Todo App​

Let's see how to organize a simple todo application:

/todo-app
/js
/models
todo.js # Data structure definition
/services
todoService.js # Data handling and business logic
/views
todoView.js # UI rendering and DOM manipulation
/utils
helpers.js # General utility functions
app.js # Main application initialization
index.html
styles.css

Implementation​

javascript
// models/todo.js
export class Todo {
constructor(id, text, completed = false) {
this.id = id;
this.text = text;
this.completed = completed;
this.createdAt = new Date();
}

toggle() {
this.completed = !this.completed;
}
}
javascript
// services/todoService.js
import { Todo } from '../models/todo.js';

export class TodoService {
constructor() {
this.todos = JSON.parse(localStorage.getItem('todos')) || [];
// Convert plain objects back to Todo instances
this.todos = this.todos.map(t => Object.assign(new Todo(), t));
}

getTodos() {
return this.todos;
}

addTodo(text) {
const id = Date.now().toString();
const todo = new Todo(id, text);
this.todos.push(todo);
this._save();
return todo;
}

toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.toggle();
this._save();
}
return todo;
}

deleteTodo(id) {
const index = this.todos.findIndex(t => t.id === id);
if (index !== -1) {
this.todos.splice(index, 1);
this._save();
return true;
}
return false;
}

// Private method to persist todos
_save() {
localStorage.setItem('todos', JSON.stringify(this.todos));
}
}
javascript
// views/todoView.js
export class TodoView {
constructor(container, callbacks) {
this.container = container;
this.callbacks = callbacks;
}

render(todos) {
this.container.innerHTML = '';

const list = document.createElement('ul');
list.className = 'todo-list';

todos.forEach(todo => {
const item = document.createElement('li');
item.className = todo.completed ? 'todo-item completed' : 'todo-item';

// Create todo content
const textSpan = document.createElement('span');
textSpan.textContent = todo.text;
textSpan.className = 'todo-text';

// Create toggle button
const toggleBtn = document.createElement('button');
toggleBtn.textContent = todo.completed ? 'Undo' : 'Complete';
toggleBtn.className = 'btn-toggle';
toggleBtn.addEventListener('click', () => {
this.callbacks.onToggle(todo.id);
});

// Create delete button
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.className = 'btn-delete';
deleteBtn.addEventListener('click', () => {
this.callbacks.onDelete(todo.id);
});

// Append all elements
item.appendChild(textSpan);
item.appendChild(toggleBtn);
item.appendChild(deleteBtn);
list.appendChild(item);
});

this.container.appendChild(list);

// Create input form for new todos
const form = document.createElement('form');
form.className = 'todo-form';

const input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Add a new todo...';
input.className = 'todo-input';

const addBtn = document.createElement('button');
addBtn.type = 'submit';
addBtn.textContent = 'Add';
addBtn.className = 'btn-add';

form.appendChild(input);
form.appendChild(addBtn);

form.addEventListener('submit', (e) => {
e.preventDefault();
if (input.value.trim()) {
this.callbacks.onAdd(input.value.trim());
input.value = '';
}
});

this.container.appendChild(form);
}
}
javascript
// utils/helpers.js
export function formatDate(date) {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}

export function debounce(func, delay = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => func(...args), delay);
};
}
javascript
// app.js
import { TodoService } from './services/todoService.js';
import { TodoView } from './views/todoView.js';

class TodoApp {
constructor() {
this.todoService = new TodoService();
this.todoView = new TodoView(
document.getElementById('todo-container'),
{
onAdd: this.handleAddTodo.bind(this),
onToggle: this.handleToggleTodo.bind(this),
onDelete: this.handleDeleteTodo.bind(this)
}
);

this.init();
}

init() {
this.renderTodos();
}

renderTodos() {
const todos = this.todoService.getTodos();
this.todoView.render(todos);
}

handleAddTodo(text) {
this.todoService.addTodo(text);
this.renderTodos();
}

handleToggleTodo(id) {
this.todoService.toggleTodo(id);
this.renderTodos();
}

handleDeleteTodo(id) {
this.todoService.deleteTodo(id);
this.renderTodos();
}
}

// Initialize the app when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
new TodoApp();
});
html
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo App</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<h1>Todo App</h1>
<div id="todo-container"></div>
</div>

<script type="module" src="js/app.js"></script>
</body>
</html>

This example demonstrates separation of concerns:

  • Model: Defines the data structure (Todo class)
  • Service: Handles business logic and data persistence
  • View: Manages the UI rendering and user interaction
  • Utils: Contains reusable utility functions
  • App: Acts as the controller, connecting all parts together

Summary​

Effective JavaScript code organization creates maintainable, scalable applications. Key takeaways include:

  1. Separate Concerns: Keep UI code, business logic, and data handling separate.
  2. Use Meaningful Names: Choose descriptive names for variables, functions, and files.
  3. Modularize Code: Break your application into smaller, reusable modules.
  4. Apply Design Patterns: Use established patterns to organize your code effectively.
  5. Structure Your Files: Group related files together, either by feature or by type.
  6. Be Consistent: Follow a consistent style throughout your codebase.

By following these principles, you'll build applications that are easier to maintain, debug, and extend.

Additional Resources​

Exercises​

  1. Refactor the following code to separate UI and business logic:
javascript
function calculateAndDisplay() {
const num1 = parseFloat(document.getElementById('num1').value);
const num2 = parseFloat(document.getElementById('num2').value);
const operation = document.getElementById('operation').value;

let result;
if (operation === 'add') {
result = num1 + num2;
} else if (operation === 'subtract') {
result = num1 - num2;
} else if (operation === 'multiply') {
result = num1 * num2;
} else if (operation === 'divide') {
result = num1 / num2;
}

document.getElementById('result').textContent = result;
}
  1. Create a simple module for handling user authentication with methods for login, logout, and checking auth status.

  2. Organize a file structure for a blog application with features like posts, comments, user profiles, and admin functionality.



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)