Sunday, August 17, 2025
0 comments

JavaScript Learning Path: From Zero to Hero – Chapter 10: Final Projects & Best Practices

 

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:

Featured Post

Master Angular 20 Basics: A Complete Beginner’s Guide with Examples and Best Practices

Welcome to the complete Angular 20 learning roadmap ! This series takes you step by step from basics to intermediate concepts , with hands...

Subscribe

 
Toggle Footer
Top