Introduction
Congratulations on reaching Chapter 10 of our JavaScript Learning Path: From Zero to Hero! After mastering advanced browser APIs and tooling in Chapter 9, it’s time to tie everything together with design patterns, best practices, and a capstone project. We’ll explore principles like DRY, KISS, and YAGNI, implement patterns like Singleton, Observer, Factory, and MVC, and address security concerns like XSS and CSRF. The highlight is a hands-on mini ERP-like app that integrates invoices, to-dos, notes, and weather data, showcasing DOM manipulation, events, async APIs, LocalStorage, OOP, error handling, and testing. Let’s build something amazing and become a JavaScript hero!
1. Patterns & Best Practices
Design patterns and best practices ensure your code is maintainable, scalable, and efficient.
DRY (Don’t Repeat Yourself)
Avoid code duplication by reusing functions or modules.
// Bad: Repeated logic
function calculateTax(price) {
return price * 0.1;
}
function calculateTotal(price) {
return price + price * 0.1;
}
// Good: DRY
function calculateTax(price) {
return price * 0.1;
}
function calculateTotal(price) {
return price + calculateTax(price);
}
KISS (Keep It Simple, Stupid)
Favor simple, readable solutions over complex ones.
// Bad: Overcomplicated
function getUserStatus(user) {
return user && user.isActive !== undefined && user.isActive ? 'Active' : 'Inactive';
}
// Good: KISS
function getUserStatus(user) {
return user?.isActive ? 'Active' : 'Inactive';
}
YAGNI (You Aren’t Gonna Need It)
Avoid adding unnecessary features until they’re needed.
// Bad: Over-engineering
function fetchData(url, cache = false, retry = 3, timeout = 5000) {
// Complex logic for unused features
}
// Good: YAGNI
function fetchData(url) {
return fetch(url).then(res => res.json());
}
Real-World Use: Simplifying code in a startup’s MVP to focus on core features.
Pros:
DRY reduces maintenance overhead.
KISS improves readability and debugging.
YAGNI speeds up development by avoiding bloat.
Cons:
DRY can lead to over-abstraction if misapplied.
KISS may oversimplify complex requirements.
YAGNI risks under-engineering critical features.
Best Practices:
Refactor repeated code into reusable functions or modules.
Prioritize clarity over cleverness in KISS.
Validate feature necessity with stakeholders for YAGNI.
Alternatives:
Functional programming for reusable logic.
Frameworks with built-in patterns (e.g., React’s component model).
2. Modular JavaScript
Modular JavaScript organizes code into reusable, independent modules using ES Modules.
Example: Modular Utility
// utils.js
export function formatDate(date) {
return new Date(date).toLocaleDateString();
}
// main.js
import { formatDate } from './utils.js';
console.log(formatDate(new Date())); // e.g., 8/17/2025
Real-World Use: Separating API logic from UI in a dashboard app.
Pros:
Improves maintainability and testability.
Reduces global scope pollution.
Cons:
Requires bundlers (e.g., Webpack) for browsers.
Increases setup complexity.
Best Practices:
Keep modules small and single-purpose.
Use named exports for utilities, default exports for main components.
Alternatives:
CommonJS for Node.js environments.
IIFEs for legacy modularization.
3. Singleton, Observer, Factory Patterns
Singleton Pattern
Ensures a class has only one instance, useful for shared resources.
class Database {
static #instance;
constructor() {
if (Database.#instance) return Database.#instance;
Database.#instance = this;
this.connection = 'connected';
}
}
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true
Observer Pattern
Allows objects to subscribe to and react to events.
class EventEmitter {
#listeners = new Map();
on(event, callback) {
if (!this.#listeners.has(event)) this.#listeners.set(event, []);
this.#listeners.get(event).push(callback);
}
emit(event, data) {
const callbacks = this.#listeners.get(event) || [];
callbacks.forEach(cb => cb(data));
}
}
const emitter = new EventEmitter();
emitter.on('update', data => console.log('Updated:', data));
emitter.emit('update', 'New data'); // Updated: New data
Factory Pattern
Creates objects without specifying the exact class.
class Invoice {
constructor(amount) {
this.type = 'Invoice';
this.amount = amount;
}
}
class Task {
constructor(description) {
this.type = 'Task';
this.description = description;
}
}
class ItemFactory {
static createItem(type, data) {
switch (type) {
case 'invoice': return new Invoice(data.amount);
case 'task': return new Task(data.description);
default: throw new Error('Invalid type');
}
}
}
const invoice = ItemFactory.createItem('invoice', { amount: 100 });
console.log(invoice); // { type: 'Invoice', amount: 100 }
Real-World Use:
Singleton: Managing a single API client.
Observer: Updating UI on data changes (e.g., notifications).
Factory: Creating different product types in an e-commerce app.
Pros:
Singleton ensures resource efficiency.
Observer decouples event producers and consumers.
Factory simplifies object creation logic.
Cons:
Singleton can complicate testing and state management.
Observer can lead to memory leaks if listeners aren’t removed.
Factory adds complexity for simple use cases.
Best Practices:
Use Singleton sparingly; prefer dependency injection.
Clean up Observer listeners to prevent leaks.
Keep Factory logic simple and extensible.
Alternatives:
Dependency injection for Singletons.
Event emitters or Pub/Sub libraries for Observer.
Class inheritance for Factory.
4. MVC Pattern
The Model-View-Controller (MVC) pattern separates data (Model), UI (View), and logic (Controller).
Example: Simple MVC
// Model
class TodoModel {
#todos = [];
addTodo(todo) {
this.#todos.push({ id: Date.now(), ...todo });
return this.#todos;
}
getTodos() {
return [...this.#todos];
}
}
// View
class TodoView {
render(todos) {
const list = document.getElementById('todoList');
list.innerHTML = todos.map(todo => `<li>${todo.text}</li>`).join('');
}
}
// Controller
class TodoController {
constructor(model, view) {
this.model = model;
this.view = view;
}
addTodo(text) {
const todos = this.model.addTodo({ text });
this.view.render(todos);
}
}
const controller = new TodoController(new TodoModel(), new TodoView());
controller.addTodo('Buy groceries');
Real-World Use: Structuring large apps like task managers or CRMs.
Pros:
Separates concerns for maintainability.
Makes testing easier by isolating logic.
Cons:
Adds complexity for small apps.
Requires discipline to maintain separation.
Best Practices:
Keep Models pure (no UI logic).
Use Views only for rendering.
Centralize logic in Controllers.
Alternatives:
Flux/Redux for state management.
Component-based architectures (e.g., React).
5. Security Considerations (XSS, CSRF)
Cross-Site Scripting (XSS)
XSS occurs when malicious scripts are injected into web pages.
// Bad: Vulnerable to XSS
document.getElementById('output').innerHTML = userInput;
// Good: Sanitize input
function sanitizeInput(input) {
const div = document.createElement('div');
div.textContent = input;
return div.innerHTML;
}
document.getElementById('output').innerHTML = sanitizeInput(userInput);
Cross-Site Request Forgery (CSRF)
CSRF tricks users into executing unwanted actions on a trusted site.
// Good: Include CSRF token in forms
function createForm() {
return `
<form action="/submit" method="POST">
<input type="hidden" name="_csrf" value="${generateCsrfToken()}">
<button type="submit">Submit</button>
</form>
`;
}
Real-World Use: Protecting user inputs in forms or API endpoints.
Pros:
Prevents data breaches and unauthorized actions.
Enhances user trust.
Cons:
Sanitization adds overhead.
CSRF tokens require server-side support.
Best Practices:
Use textContent over innerHTML for user inputs.
Implement CSRF tokens for POST requests.
Use libraries like DOMPurify for XSS sanitization.
Alternatives:
Content Security Policy (CSP) for XSS prevention.
SameSite cookies for CSRF protection.
6. Final Capstone Project: Mini ERP-like App
Let’s build a mini ERP-like app that integrates invoices, to-dos, notes, and weather data, using MVC, OOP, async APIs, LocalStorage, and testing.
Project Structure
erp-app/
├── src/
│ ├── models/
│ │ ├── invoice.js
│ │ ├── todo.js
│ │ ├── note.js
│ │ ├── weather.js
│ ├── views/
│ │ ├── invoiceView.js
│ │ ├── todoView.js
│ │ ├── noteView.js
│ │ ├── weatherView.js
│ ├── controllers/
│ │ ├── appController.js
│ ├── utils.js
│ ├── index.js
├── index.html
├── package.json
├── tests/
│ ├── invoice.test.js
Models
// models/invoice.js
export class Invoice {
constructor(id, amount, description) {
this.id = id;
this.amount = amount;
this.description = description;
}
}
// models/todo.js
export class Todo {
constructor(id, text, completed = false) {
this.id = id;
this.text = text;
this.completed = completed;
}
}
// models/note.js
export class Note {
constructor(id, text) {
this.id = id;
this.text = text;
}
}
// models/weather.js
export class Weather {
static async fetchWeather(city) {
try {
const response = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=YOUR_API_KEY`);
if (!response.ok) throw new Error('City not found');
const data = await response.json();
return { city: data.name, temp: data.main.temp };
} catch (error) {
return { error: error.message };
}
}
}
Views
// views/invoiceView.js
export class InvoiceView {
render(invoices) {
document.getElementById('invoiceList').innerHTML = invoices
.map(inv => `<li>${inv.description}: $${inv.amount}</li>`)
.join('');
}
}
// views/todoView.js
export class TodoView {
render(todos) {
document.getElementById('todoList').innerHTML = todos
.map(todo => `<li class="${todo.completed ? 'completed' : ''}">${todo.text}</li>`)
.join('');
}
}
// views/noteView.js
export class NoteView {
render(notes) {
document.getElementById('noteList').innerHTML = notes
.map(note => `<li>${note.text}</li>`)
.join('');
}
}
// views/weatherView.js
export class WeatherView {
render(weather) {
document.getElementById('weather').innerHTML = weather.error
? `<span class="error">${weather.error}</span>`
: `${weather.city}: ${weather.temp}°K`;
}
}
Controller
// controllers/appController.js
import { Invoice } from '../models/invoice.js';
import { Todo } from '../models/todo.js';
import { Note } from '../models/note.js';
import { Weather } from '../models/weather.js';
import { InvoiceView } from '../views/invoiceView.js';
import { TodoView } from '../views/todoView.js';
import { NoteView } from '../views/noteView.js';
import { WeatherView } from '../views/weatherView.js';
import { saveToStorage, loadFromStorage } from '../utils.js';
export class AppController {
constructor() {
this.invoices = loadFromStorage('invoices') || [];
this.todos = loadFromStorage('todos') || [];
this.notes = loadFromStorage('notes') || [];
this.invoiceView = new InvoiceView();
this.todoView = new TodoView();
this.noteView = new NoteView();
this.weatherView = new WeatherView();
}
addInvoice(amount, description) {
const invoice = new Invoice(Date.now(), amount, description);
this.invoices.push(invoice);
saveToStorage('invoices', this.invoices);
this.invoiceView.render(this.invoices);
}
addTodo(text) {
const todo = new Todo(Date.now(), text);
this.todos.push(todo);
saveToStorage('todos', this.todos);
this.todoView.render(this.todos);
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
saveToStorage('todos', this.todos);
this.todoView.render(this.todos);
}
}
addNote(text) {
const note = new Note(Date.now(), text);
this.notes.push(note);
saveToStorage('notes', this.notes);
this.noteView.render(this.notes);
}
async fetchWeather(city) {
const weather = await Weather.fetchWeather(city);
this.weatherView.render(weather);
}
}
Utils
// utils.js
export function saveToStorage(key, data) {
try {
localStorage.setItem(key, JSON.stringify(data));
} catch (error) {
console.error(`Storage error: ${error.message}`);
}
}
export function loadFromStorage(key) {
try {
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : null;
} catch (error) {
console.error(`Load error: ${error.message}`);
return null;
}
}
export function sanitizeInput(input) {
const div = document.createElement('div');
div.textContent = input;
return div.innerHTML;
}
Main App
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Mini ERP App</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
input, button { padding: 10px; margin: 5px; }
ul { list-style: none; padding: 0; }
li { margin: 10px 0; padding: 10px; border: 1px solid #ccc; }
.completed { text-decoration: line-through; color: gray; }
.error { color: red; }
section { margin: 20px 0; }
</style>
</head>
<body>
<h1>Mini ERP App</h1>
<!-- Invoices -->
<section>
<h2>Invoices</h2>
<input type="text" id="invoiceDesc" placeholder="Description">
<input type="number" id="invoiceAmount" placeholder="Amount">
<button onclick="controller.addInvoice(Number(document.getElementById('invoiceAmount').value), document.getElementById('invoiceDesc').value)">Add Invoice</button>
<ul id="invoiceList"></ul>
</section>
<!-- To-Dos -->
<section>
<h2>To-Dos</h2>
<input type="text" id="todoInput" placeholder="Enter task">
<button onclick="controller.addTodo(document.getElementById('todoInput').value)">Add Task</button>
<ul id="todoList"></ul>
</section>
<!-- Notes -->
<section>
<h2>Notes</h2>
<input type="text" id="noteInput" placeholder="Enter note">
<button onclick="controller.addNote(document.getElementById('noteInput').value)">Add Note</button>
<ul id="noteList"></ul>
</section>
<!-- Weather -->
<section>
<h2>Weather</h2>
<input type="text" id="cityInput" placeholder="Enter city">
<button onclick="controller.fetchWeather(document.getElementById('cityInput').value)">Get Weather</button>
<div id="weather"></div>
</section>
<script type="module">
import { AppController } from './src/controllers/appController.js';
window.controller = new AppController();
</script>
</body>
</html>
Testing
// tests/invoice.test.js
import { Invoice } from '../src/models/invoice.js';
test('Invoice creation', () => {
const invoice = new Invoice(1, 100, 'Service Fee');
expect(invoice).toEqual({ id: 1, amount: 100, description: 'Service Fee' });
});
package.json
{
"name": "erp-app",
"version": "1.0.0",
"scripts": {
"build": "webpack",
"test": "jest"
},
"devDependencies": {
"@babel/preset-env": "^7.20.0",
"eslint": "^8.0.0",
"prettier": "^2.7.0",
"webpack": "^5.0.0",
"webpack-cli": "^4.0.0",
"jest": "^29.0.0"
}
}
Note: Replace YOUR_API_KEY in weather.js with a valid OpenWeatherMap API key. Run with a local server (e.g., npx serve) due to ES Modules. Jest requires setup for ES Modules (e.g., @babel/preset-env).
How It Works:
MVC: Separates models (data), views (UI), and controller (logic).
OOP: Uses classes for invoices, todos, notes, and weather.
Async: Fetches weather data with fetch and async/await.
LocalStorage: Persists data across sessions.
Error Handling: Sanitizes inputs and handles API errors.
Testing: Includes a basic Jest test for the Invoice class.
Security: Sanitizes user inputs to prevent XSS.
Why It’s Useful: Mimics ERP systems like Odoo, integrating multiple business functions.
Best Standards for Patterns & Best Practices
DRY: Refactor repeated logic into functions or modules.
KISS: Prioritize simple solutions; avoid over-engineering.
YAGNI: Focus on current requirements; defer speculative features.
Modules: Use ES Modules for all modern projects.
Patterns: Apply Singleton for single instances, Observer for events, Factory for object creation, MVC for app structure.
Security: Sanitize all user inputs; use CSRF tokens for forms.
Testing: Write unit tests for critical logic; aim for high coverage.
Conclusion
You’ve completed your journey to JavaScript hero status! By mastering design patterns, best practices, and building a mini ERP-like app, you’re ready to tackle real-world projects. This capstone project combined DOM manipulation, events, async APIs, LocalStorage, OOP, error handling, and testing, showcasing your skills in a practical application.
What’s Next? Continue your growth by exploring frameworks like React or Vue, contributing to open-source projects, or building your own portfolio app. Keep practicing, and try adding a “delete item” feature or more API integrations to the ERP app!
Interactive Challenge: Enhance the ERP app with a dashboard summarizing totals or real-time updates via WebSockets. Share your solution with #JavaScriptHero!
0 comments:
Post a Comment