Module 03: Objects & API Integration
Learning Focus: Object-Oriented Programming (OOP), REST APIs, async/await, and full-stack integration
Table of Contents
- Module Overview
 - Core Concepts
 - Code Walkthrough
 - API Integration
 - Testing Strategy
 - Bugs Fixed
 - 2021 vs 2025 Comparison
 - Key Takeaways
 
Module Overview
What We're Building
This module represents a major milestone: building a full-stack application. We create:
- Bank Account System - OOP with classes and methods
 - Geography/City Management - Complex class hierarchies
 - REST API Client - Fetch data from Flask backend
 - Integration Tests - Full client-server testing
 
Why This Matters
This is where everything comes together:
- OOP: Organize code like real-world objects
 - APIs: Communicate between frontend and backend
 - Async/Await: Handle asynchronous operations elegantly
 - Full-Stack: Frontend (JavaScript) βοΈ Backend (Flask/Python)
 
Module Statistics
- 30 tests total (largest test suite yet!)
 - 14 bugs fixed during audit
 - 3 main classes: Account, City, Community
 - 5 API endpoints: /add, /all, /read, /update, /delete, /clear
 
Core Concepts
1. Object-Oriented Programming (OOP)
What is a Class? A class is a blueprint for creating objects:
// Class definition (blueprint)
class Car {
    constructor(make, model, year) {
        this.make = make;
        this.model = model;
        this.year = year;
    }
    
    describe() {
        return `${this.year} ${this.make} ${this.model}`;
    }
}
// Creating instances (actual objects)
const myCar = new Car('Honda', 'Civic', 2020);
const yourCar = new Car('Toyota', 'Camry', 2021);
console.log(myCar.describe());  // "2020 Honda Civic"
The Four Pillars of OOP:
Encapsulation: Bundle data with methods
class BankAccount { #balance = 0; // Private field deposit(amount) { this.#balance += amount; // Controlled access } }Inheritance: Reuse code from parent classes
class SavingsAccount extends BankAccount { constructor(interestRate) { super(); // Call parent constructor this.interestRate = interestRate; } }Polymorphism: Different classes, same interface
class Cat { speak() { return "Meow"; } } class Dog { speak() { return "Woof"; } } [new Cat(), new Dog()].forEach(animal => { console.log(animal.speak()); // Different behavior, same method });Abstraction: Hide complexity, show only essentials
class EmailSender { send(to, subject, body) { // Hides SMTP complexity this.#connect(); this.#authenticate(); this.#sendMessage(to, subject, body); this.#disconnect(); } }
2. REST API Communication
What is REST? REST (Representational State Transfer) is a pattern for client-server communication:
Client (Browser)          Server (Flask)
     |                         |
     |---- GET /cities ------->|
     |                         |
     |<--- 200 OK + JSON ------|
     |    [{id:1, name:"..."}] |
HTTP Methods:
GET     /cities        // Read all cities
GET     /cities/1      // Read one city
POST    /cities        // Create new city
PUT     /cities/1      // Update city
DELETE  /cities/1      // Delete city
Our Flask Endpoints:
POST /add       # Add new city
POST /all       # Get all cities
POST /read      # Get city by key
POST /update    # Update city
POST /delete    # Delete city
GET  /clear     # Clear all data
3. Async/Await Pattern
The Problem: Callback Hell (2015)
// Old way - nested callbacks
fetchData(url, function(data) {
    processData(data, function(result) {
        saveData(result, function(response) {
            console.log('Done!');  // Deeply nested!
        });
    });
});
The Solution: Async/Await (2025)
// Modern way - linear code
async function workflow() {
    const data = await fetchData(url);
    const result = await processData(data);
    const response = await saveData(result);
    console.log('Done!');  // Readable!
}
How It Works:
// 1. Mark function as async
async function getCities() {
    // 2. Use await for promises
    const response = await fetch('/api/cities');
    const data = await response.json();
    return data;
}
// 3. Call async function
getCities().then(cities => console.log(cities));
Code Walkthrough
File Structure
03-objects/
βββ src/
β   βββ scripts/
β   β   βββ account.js          # Bank account class
β   β   βββ account.test.js
β   β   βββ geography.js        # β City/Community classes
β   β   βββ geography.test.js   # β Integration tests
β   β   βββ fetch.js            # β API utilities
β   β   βββ fetch.test.js
β   βββ index.html
βββ jest.config.js              # jsdom + node-fetch config
βββ jest.setup.js               # TextEncoder polyfill
βββ package.json
account.js - Bank Account System
class Account {
    constructor(name, balance = 0) {
        this.name = name;
        this.balance = balance;
    }
    // Deposit money
    deposit(amount) {
        if (amount > 0) {
            this.balance += amount;
        }
        return this.balance;
    }
    // Withdraw money
    withdraw(amount) {
        if (amount > 0 && amount <= this.balance) {
            this.balance -= amount;
        }
        return this.balance;
    }
    // Display account info
    display() {
        return `${this.name}: $${this.balance}`;
    }
    // Validate new balance
    isNewAmount(newBalance) {
        if (typeof newBalance !== 'number') {
            return 'ERROR: Not a number';
        }
        if (newBalance < 0) {
            return 'ERROR: Negative balance';
        }
        this.balance = newBalance;
        return this.balance;
    }
}
export default Account;
Design Decisions:
- Default Parameters: 
balance = 0allows creating accounts without initial balance - Guard Clauses: Check conditions before modifying state
 - Return Values: Methods return new balance for chaining/verification
 - Validation: 
isNewAmountprevents invalid states 
Real-World Usage:
const myAccount = new Account('Brennan', 1000);
myAccount.deposit(500);      // $1500
myAccount.withdraw(200);     // $1300
myAccount.display();         // "Brennan: $1300"
geography.js - City Class
class City {
    constructor(name, population, latitude, longitude, key) {
        this.name = name;
        this.population = population;
        this.latitude = latitude;
        this.longitude = longitude;
        this.key = key;
    }
    // Display city information
    show() {
        return `${this.name}, population: ${this.population}`;
    }
    // Population movement methods
    transferIn(amount) {
        this.population += amount;
        return this.population;
    }
    transferOut(amount) {
        this.population -= amount;
        return this.population;
    }
    // Determine hemisphere
    whichHemiphere() {
        if (this.latitude > 0) return 'Northern Hemisphere';
        if (this.latitude < 0) return 'Southern Hemisphere';
        return 'Equator';
    }
    // Classify city by population
    classification() {
        const pop = this.population;
        
        if (pop < 100) return 'Hamlet';
        if (pop < 1000) return 'Village';
        if (pop < 20000) return 'Town';
        if (pop < 100000) return 'Large Town';
        if (pop >= 100000) return 'City';  // β οΈ BUG WAS HERE: used >
        
        return 'ERROR';
    }
}
export default City;
Why These Methods?
- transferIn/Out: Model population migration
 - whichHemiphere: Demonstrate conditional logic on instance data
 - classification: Graduated thresholds (like tax brackets!)
 
geography.js - Community Class (Complex!)
import functions from './fetch.js';
class Community {
    constructor() {
        this.url = 'http://localhost:5002/';
        this.community = [];
    }
    /**
     * Create a new city and store it on the server
     * β οΈ This method had THE MOST bugs!
     */
    async createCity(city, latitude, longitude, population) {
        try {
            // 1. Get all existing cities to determine next key
            let data = await functions.postData(this.url + 'all');
            let i;
            
            if (data.length === 0) {
                i = 0;  // First city
            } else {
                // Find highest key
                i = data.sort((a, b) => b.key - a.key);
                i = i[0].key;
            }
            
            // 2. Create new city with next key
            // β οΈ BUG WAS HERE: Parameter order was wrong!
            let myCity = new City(city, population, latitude, longitude, i + 1);
            
            // 3. Send to server
            data = await functions.postData(this.url + 'add', myCity);
            
            if (data.status === 200) {
                return data;
            }
            return 'SERVER ERROR';
        } catch (error) {
            console.error("Error:", error);
        }
    }
    /**
     * Get all cities from server
     */
    async getCommunity() {
        try {
            let data = await functions.postData(this.url + 'all');
            
            if (data.length > 0) {
                this.community = await JSON.parse(JSON.stringify(data));
                return this.community;
            }
            return 'SERVER ERROR';
        } catch (error) {
            console.error("Error:", error);
        }
    }
    /**
     * Get most northern city
     */
    getMostNorthern() {
        let data = this.community;
        if (data.length === 0) return 'ERROR';
        
        data.sort((a, b) => b.latitude - a.latitude);
        return data[0].name;
    }
    /**
     * Get most southern city
     */
    getMostSouthern() {
        let data = this.community;
        if (data.length === 0) return 'ERROR';
        
        data.sort((a, b) => a.latitude - b.latitude);
        return data[0].name;
    }
    /**
     * Calculate total population
     */
    async getTotalPopulation() {
        try {
            let data = await functions.postData(this.url + 'all');
            
            if (data.length > 0) {
                let population = data.map(city => city.population);
                population = population.reduce((a, b) => Number(a) + Number(b));
                return Number(population).toLocaleString();
            }
            return 'ERROR';
        } catch (error) {
            console.error("Error:", error);
        }
    }
    /**
     * Update city population on server
     */
    async updatePopulation(city) {
        try {
            // β οΈ BUG WAS HERE: Had unnecessary /all check
            let data = await functions.postData(this.url + 'update', {
                key: city.key,
                name: city.name,
                latitude: city.latitude,
                longitude: city.longitude,
                population: city.population
            });
            
            if (data.status === 200) {
                return data;
            }
            return 'SERVER ERROR';
        } catch (error) {
            console.error("Error:", error);
        }
    }
    /**
     * Delete city from server
     */
    async deleteCity(key) {
        try {
            let data = await functions.postData(this.url + 'delete', { key: key });
            
            if (data.status === 200) {
                return data;
            }
            return 'SERVER ERROR';
        } catch (error) {
            console.error("Error:", error);
        }
    }
}
export { City, Community };
API Integration
fetch.js - API Utility Functions
const functions = {
    url: "http://127.0.0.1:5002/",
    /**
     * Make POST request to server
     * Handles both array and object responses
     */
    async postData(url = "", info = {}) {
        const response = await fetch(url, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "Accept": "application/json"
            },
            body: JSON.stringify(info)
        });
        
        const text = await response.text();
        
        // β οΈ BUG WAS HERE: Empty responses caused JSON.parse to fail
        let JSON_DATA = text ? JSON.parse(text) : {};
        
        // If it's an array, just return it (for /all endpoint)
        if (Array.isArray(JSON_DATA)) {
            return JSON_DATA;
        }
        
        // For objects, add status information
        JSON_DATA.status = response.status;
        JSON_DATA.statusText = response.statusText;
        return JSON_DATA;
    },
    /**
     * Extract first names from array of objects
     */
    retrieveAllNames(info) {
        try {
            return info.map(person => person.first_name);
        } catch (error) {
            console.error("Error:", error);
        }
    }
};
export default functions;
Critical Design Decisions:
Why differentiate array vs object responses?
// /all endpoint returns array: [city1, city2, ...] // /add endpoint returns object: {status: 200, ...} // Need to handle both! if (Array.isArray(JSON_DATA)) { return JSON_DATA; // No status needed for arrays }Why parse empty text?
// Some endpoints return empty body with 200 status const text = await response.text(); let JSON_DATA = text ? JSON.parse(text) : {};Why add status to response?
// Tests need to verify status codes JSON_DATA.status = response.status; // 200, 400, etc.
Flask Backend (web.py)
from flask import Flask, request, jsonify
from flask_cors import CORS
app = Flask(__name__)
CORS(app)  # Allow requests from browser
data = {}
firstKeyType = None
@app.route("/add", methods=['POST'])
def add():
    global data, firstKeyType
    
    # Get data from request
    req = request.get_json()
    key = req.get('key')
    
    # Check if key already exists
    if key in data:
        return jsonify({}), 400  # Bad request
    
    # Store data
    data[key] = req
    return jsonify({}), 200
@app.route("/all", methods=['POST', 'GET'])
def get_all():
    # Return all data as array
    return jsonify(list(data.values())), 200
@app.route("/update", methods=['POST'])
def update():
    req = request.get_json()
    key = req.get('key')
    
    if key not in data:
        return jsonify({}), 400
    
    data[key] = req
    return jsonify({}), 200
@app.route("/delete", methods=['POST'])
def delete():
    req = request.get_json()
    key = req.get('key')
    
    if key not in data:
        return jsonify({}), 400
    
    del data[key]
    return jsonify({}), 200
@app.route("/clear", methods=['POST', 'GET'])
def clear():
    global data, firstKeyType
    data = {}
    firstKeyType = None  # β οΈ BUG WAS HERE: Didn't reset this
    return jsonify(data), 200
if __name__ == '__main__':
    print("--- Starting", __file__)
    # β οΈ Changed: debug=False to prevent hanging
    app.run(debug=False, use_reloader=False, port=5002)
Testing Strategy
Integration Test Setup
// geography.test.js
import { City, Community } from "./geography.js"
import functions from "./fetch.js"
import fetch from "node-fetch";
// Use real fetch for integration tests
global.fetch = fetch;
const url = "http://localhost:5002/";
// Clear data before each test
beforeEach(async () => {
    await fetch(url + "clear");
    await new Promise(resolve => setTimeout(resolve, 50));
})
Why this setup?
- node-fetch: Jest runs in Node.js, not browserβneed fetch polyfill
 - beforeEach: Ensures clean state for every test
 - setTimeout: Small delay lets Flask process clear request
 - Integration: Tests full stack, not just JavaScript
 
Jest Configuration
// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',           // Simulate browser
  setupFiles: ['<rootDir>/jest.setup.js'],  // Global setup
  maxWorkers: 1,                      // Run tests serially
  transformIgnorePatterns: [
    'node_modules/(?!(node-fetch)/)'  // Transpile node-fetch
  ],
};
// jest.setup.js
const { TextDecoder, TextEncoder } = require('util');
global.TextDecoder = TextDecoder;
global.TextEncoder = TextEncoder;
Sample Tests
test("Does the createCity function work?", async () => {
    const community = new Community;
    
    // Create first city
    let info = await community.createCity("Chestermere", 51.0382, 113.8425, 19887);
    expect(info.status).toBe(200);
    
    // Create second city
    info = await community.createCity("Winnipeg", 49.895138, 97.138374, 749534);
    expect(info.status).toBe(200);
});
test("Does the getTotalPopulation function work?", async () => {
    const community = new Community;
    
    await community.createCity("Gadsby", 52.2954, 112.3564, 40);
    await community.createCity("Rainbow Lake", 58.4999, 119.3996, 795);
    
    expect(await community.getTotalPopulation()).toBe("835");
});
test("Does the updatePopulation function work?", async () => {
    const community = new Community;
    
    // Create city with 749,534 population
    let info = await community.createCity("Winnipeg", 49.895138, 97.138374, 749534);
    info = await community.getCommunity();
    
    // Get the city from server
    let serverCity = info[0];
    let testCity = new City(
        serverCity.name,
        serverCity.population,
        serverCity.latitude,
        serverCity.longitude,
        serverCity.key
    );
    
    // Add 100,000 people
    testCity.transferIn(100000);
    
    // Update server
    await community.updatePopulation(testCity);
    
    // Verify update worked
    let update = await community.getCommunity();
    expect(update[0].population).toBe(849534);
});
Bugs Fixed
This module had 14 bugsβthe most of any module! Let me detail the critical ones:
Bug #1: City Constructor Parameter Order
// β WRONG: createCity passes parameters in wrong order
async createCity(city, latitude, longitude, population) {
    let myCity = new City(city, latitude, longitude, population, key);
    //                         β         β          β
    //                      WRONG     WRONG      WRONG
}
// β
 CORRECT: Match City constructor signature
async createCity(city, latitude, longitude, population) {
    let myCity = new City(city, population, latitude, longitude, key);
    //                         β           β          β
    //                      RIGHT       RIGHT      RIGHT
}
Impact: Cities were created with latitude as population and vice versa!
Bug #2: updatePopulation Unnecessary Check
// β WRONG: Checks /all status, but /all returns array!
async updatePopulation(city) {
    let data = await functions.postData(this.url + 'all');
    if (data.status === 200) {  // Arrays don't have .status!
        // This never runs!
    }
}
// β
 CORRECT: Remove unnecessary check
async updatePopulation(city) {
    let data = await functions.postData(this.url + 'update', cityData);
    if (data.status === 200) {  // Now checking the right response
        return data;
    }
}
Impact: Updates never executed because condition always failed.
Bug #3: Classification Threshold
// β WRONG: City classification uses > instead of >=
if (pop > 100000) return 'City';
// Results: 100,000 population returns undefined!
// β
 CORRECT: Use >=
if (pop >= 100000) return 'City';
Impact: Cities with exactly 100,000 people fell through all conditions.
Bug #4: Empty JSON Response Handling
// β WRONG: Fails when response is empty
const json = await response.json();  // Throws on empty body!
// β
 CORRECT: Handle empty responses
const text = await response.text();
const json = text ? JSON.parse(text) : {};
Impact: Crashed when Flask returned 200 with empty body.
Bug #5: Array vs Object Response
// β WRONG: Always adds status, even to arrays
let JSON_DATA = await response.json();
JSON_DATA.status = response.status;  // Adds to array!
// Result: [{city}, {city}].status = 200 (weird!)
// β
 CORRECT: Only add status to objects
if (Array.isArray(JSON_DATA)) {
    return JSON_DATA;  // Arrays don't need status
} else {
    JSON_DATA.status = response.status;
    return JSON_DATA;
}
Impact: Tests failed because
data.status was undefined for array responses.
Bug #6: Port 5000 Conflict
// β WRONG: Port 5000 is used by macOS ControlCenter!
url: "http://127.0.0.1:5000/"  // Connection refused!
// β
 CORRECT: Use port 5002
url: "http://127.0.0.1:5002/"
Impact: Fetch requests returned 403 Forbidden from system service.
Bug #7: Flask Clear Endpoint
# β WRONG: Doesn't reset firstKeyType
@app.route("/clear")
def clear():
    global data
    data = {}
    return jsonify(data), 200
# β
 CORRECT: Reset all global state
@app.route("/clear")
def clear():
    global data, firstKeyType
    data = {}
    firstKeyType = None  # Must reset this too!
    return jsonify(data), 200
Impact: State leaked between tests, causing unpredictable failures.
2021 vs 2025 Comparison
Async Patterns
2021: Promises & Callbacks
// Old way - promise chains
function getCities() {
    return fetch(url)
        .then(response => response.json())
        .then(data => {
            return processData(data);
        })
        .then(result => {
            return saveResult(result);
        })
        .catch(error => {
            console.error(error);
        });
}
2025: Async/Await
// Modern way - sequential syntax
async function getCities() {
    try {
        const response = await fetch(url);
        const data = await response.json();
        const result = await processData(data);
        return await saveResult(result);
    } catch (error) {
        console.error(error);
    }
}
Class Syntax
2021: Constructor Functions
// Old way - function constructors
function City(name, population) {
    this.name = name;
    this.population = population;
}
City.prototype.show = function() {
    return this.name;
};
2025: ES6 Classes
// Modern way - class keyword
class City {
    constructor(name, population) {
        this.name = name;
        this.population = population;
    }
    
    show() {
        return this.name;
    }
}
Module System
2021: CommonJS
// Old way
const City = require('./city');
module.exports = City;
2025: ES6 Modules
// Modern way
import { City } from './city.js';
export { City };
CORS Handling
2021: Manual CORS
// Old way - lots of options
fetch(url, {
    method: 'POST',
    mode: 'cors',
    cache: 'no-cache',
    credentials: 'same-origin',
    headers: { 'Content-Type': 'application/json' },
    redirect: 'follow',
    referrer: 'no-referrer',
    body: JSON.stringify(data)
});
2025: Simplified
// Modern way - just the essentials
fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data)
});
Key Takeaways
Technical Skills
- OOP Mastery: Classes, inheritance, encapsulation
 - Async Programming: Promise handling, error management
 - REST APIs: HTTP methods, status codes, JSON
 - Full-Stack Integration: Client-server architecture
 - Test-Driven Development: Integration testing
 
Architecture Patterns
Pattern 1: Repository Pattern
class CityRepository {
    async getAll() { return await fetch('/api/cities'); }
    async getOne(id) { return await fetch(`/api/cities/${id}`); }
    async create(city) { return await fetch('/api/cities', {...}); }
    async update(city) { return await fetch(`/api/cities/${city.id}`, {...}); }
    async delete(id) { return await fetch(`/api/cities/${id}`, {method: 'DELETE'}); }
}
Pattern 2: Facade Pattern
// Hide complex API calls behind simple interface
class Community {
    async addCity(name, lat, lng, pop) {
        // Internally: fetch all, calculate key, create city, post to server
        // Externally: just call one method
    }
}
Pattern 3: Error Handling
async function safeApiCall() {
    try {
        const data = await riskyOperation();
        return data;
    } catch (error) {
        console.error('Operation failed:', error);
        return 'ERROR';  // Graceful degradation
    }
}
Debugging Lessons
- Parameter Order Matters: Use named parameters for clarity
 - Type Checking: Arrays vs Objects need different handling
 - Port Conflicts: Check system services (lsof -i :port)
 - State Management: Clear global state between tests
 - Response Handling: Always check for empty responses
 
Further Learning
Practice Exercises
Extend City Class:
- Add 
timezoneproperty - Implement 
distanceTo(otherCity)method - Add 
nearestNeighbor()functionality 
- Add 
 Add Features:
- Search cities by name
 - Filter by population range
 - Sort by multiple criteria
 - Paginate results
 
Error Handling:
- Retry failed requests
 - Implement timeout logic
 - Add loading states
 - Handle network errors gracefully
 
Real-World Applications
- Social Media: User management, posts, comments
 - E-commerce: Products, cart, orders
 - Mapping: Locations, routes, POIs
 - CRM: Contacts, companies, deals
 
Recommended Resources
Next Module
Now that we understand full-stack integration, we're ready for modern frontend frameworks in Module 04: React Applications β
Module Status: β
 Complete (30/30 tests
passing)
Key Bugs Fixed: 14 (parameter order, response handling,
port conflicts)
Time Investment: ~8 hours (most complex module)
Key Achievement: Built first full-stack application
with working API integration!