diff --git a/courses/backend/advanced-javascript/.gitignore b/courses/backend/advanced-javascript/.gitignore new file mode 100644 index 00000000..fc087237 --- /dev/null +++ b/courses/backend/advanced-javascript/.gitignore @@ -0,0 +1,3 @@ +week*/session-materials/solutions.js +week*/assignment-solutions.js +_teaching-notes.md diff --git a/courses/backend/advanced-javascript/README.md b/courses/backend/advanced-javascript/README.md index 3e06eb47..c0d229cd 100644 --- a/courses/backend/advanced-javascript/README.md +++ b/courses/backend/advanced-javascript/README.md @@ -1,3 +1,164 @@ -# Advanced JavaScript (Backend) +# Advanced JavaScript -Coming soon +> From visual output to data pipelines: your journey into backend development + +Welcome to the Advanced JavaScript module. Over the next 4 weeks, you'll learn the JavaScript patterns that power every backend system: transforming data, handling asynchronous operations, consuming APIs, and modeling your domain with classes. + +We'll build on a single example throughout: a **tea shop e-commerce** system. This gives you a concrete, evolving context to see how each concept connects to real backend work. + +--- + +## Welcome to Backend Development + +Frontend development is visual. You write code, refresh the browser, and see buttons, colors, and layouts. The feedback loop is immediate and satisfying. + +Backend development is different. There's no visual output. Your code runs on a server somewhere, processing requests, querying databases, and returning data. The output isn't pixels on a screen - it's structured data sent over a network. + +This can feel abstract at first. But here's the thing: **terminals existed before graphical interfaces**. The command line isn't a step backward - it's where serious computing has always happened. Get comfortable with it. + +### What Does a Backend Actually Do? + +Think about scrolling Instagram. You don't download all 500 million posts when you open the app. Instead, your app asks: *"Give me the next 10 posts for this user."* + +That's a backend call. The backend: +1. Receives the request +2. Checks if you're allowed to see those posts +3. Queries the database for the right 10 posts +4. Formats them in a way the app can use +5. Sends back just that slice of data + +Backend output = **consumable slices of data in a structured manner**. + +--- + +## What is Data? + +Before we manipulate data, let's understand what it is. Data is just structured information. The same information can be represented in different formats. + +Here are 3 teas from our tea shop: + +### As a Table + +| id | name | origin | pricePerGram | organic | +|----|------|--------|--------------|---------| +| 1 | Sencha | Japan | 0.12 | true | +| 2 | Earl Grey | India | 0.08 | false | +| 3 | Dragon Well | China | 0.25 | true | + +### As CSV (Comma-Separated Values) + +``` +id,name,origin,pricePerGram,organic +1,Sencha,Japan,0.12,true +2,Earl Grey,India,0.08,false +3,Dragon Well,China,0.25,true +``` + +### As JSON (JavaScript Object Notation) + +```json +[ + { "id": 1, "name": "Sencha", "origin": "Japan", "pricePerGram": 0.12, "organic": true }, + { "id": 2, "name": "Earl Grey", "origin": "India", "pricePerGram": 0.08, "organic": false }, + { "id": 3, "name": "Dragon Well", "origin": "China", "pricePerGram": 0.25, "organic": true } +] +``` + +### As JSONL (JSON Lines - one object per line) + +``` +{"id":1,"name":"Sencha","origin":"Japan","pricePerGram":0.12,"organic":true} +{"id":2,"name":"Earl Grey","origin":"India","pricePerGram":0.08,"organic":false} +{"id":3,"name":"Dragon Well","origin":"China","pricePerGram":0.25,"organic":true} +``` + +Same data, different formats. In this course, we'll primarily work with JSON since that's what JavaScript handles natively. + +--- + +## The Three Layers: Data → Logic → Rendering + +Every application, from a simple website to a complex enterprise system, follows this pattern: + +``` +Data → Logic → Rendering +``` + +- **Data Layer:** The raw truth. Databases, files, external services. This is where information lives. +- **Logic Layer:** The brain. What to fetch, who's allowed, how to transform. **This is where backends live.** +- **Rendering Layer:** The face. HTML, mobile UI, PDF - whatever users actually see. + +### Example: Filtering Teas + +You browse a tea shop website and click "Organic only." + +1. **Rendering** (frontend): Sends your filter choice to the server +2. **Logic** (backend): Receives the request, queries the database, applies your filter, formats the response +3. **Data** (database): Returns all teas matching the query +4. **Logic** (backend): Transforms the raw data into the shape the frontend expects +5. **Rendering** (frontend): Displays the filtered teas as cards with images and prices + +The backend sits in the middle, orchestrating the flow between stored data and user-facing output. + +--- + +## Course Overview + +| Week | Topic | Backend Connection | +|------|-------|-------------------| +| 1 | Array Methods | Transforming data between "as stored" and "as served" | +| 2 | Callbacks & Async | Why backends can't run top-to-bottom | +| 3 | Promises & APIs | Speaking to databases and external services | +| 4 | Classes | Modeling your business domain | + +Each week builds on the previous. By the end, you'll have the JavaScript fundamentals needed to build real backend systems. + +### Week 1: Array Methods - The application layer's daily bread + +The database gives you raw rows. You filter out unauthorized items, map to a cleaner shape, reduce to calculate totals. This is the transformation step between "data as stored" and "data as served." + +> You search for "green tea" on a tea shop. The database returns 200 matches. But you only want organic ones, sorted by price, showing just name and price - not the 15 other fields stored internally. That's `filter`, then `sort`, then `map`. Three lines of code, happens on every single request. + +### Week 2: Callbacks & Async - Why backends can't run top-to-bottom + +Databases are slow. File systems are slow. Network calls are slow. Callbacks are how JavaScript says "go do this slow thing, and here's what to do when you're done." + +> A café orders 50 teas from your shop. Your backend needs to: check inventory, calculate shipping, charge their card, send confirmation email. If each step took 500ms and you waited frozen, that's 2 seconds. Meanwhile 100 other customers are trying to browse. Callbacks let you say "start charging the card, and while you wait, go handle those other customers." + +### Week 3: Promises & API Consumption - Speaking to other services + +Real backends rarely work alone. They call databases, payment providers, email services, other APIs. Promises manage these conversations cleanly with proper error handling. + +> Customer wants to checkout. Your backend calls Stripe for payment, calls PostNord for shipping rates, calls your inventory service to reserve stock. Three external calls, any could fail. Promises let you say "do all three, wait for all to succeed, and if any fails, here's how to handle it." Try writing that with nested callbacks - you'll see why promises exist. + +### Week 4: Classes - Modeling your domain + +A tea isn't just a JavaScript object floating around. It has behaviors: validate itself, calculate shipping weight, check stock. Classes model your business domain. + +> Every time you handle a tea, you write the same checks: is the price valid? is stock above zero? is steep time reasonable? Scattered across your codebase, you write these 50 times. With a `Tea` class, you write them once. `Tea.create(data)` either gives you a valid tea or throws an error. The logic lives with the data it protects. + +--- + +## The Tea Shop + +Throughout this course, we'll work with data from a fictional tea shop. You'll encounter: + +- **Teas**: Different varieties with origins, prices, brewing instructions +- **Equipment**: Teapots, cups, infusers +- **Orders**: Customer purchases with items and totals +- **Customers**: People who buy from the shop + +This consistent context means you're not learning abstract concepts in isolation - you're solving real problems a real backend would need to solve. + +--- + +## Getting Started + +Each week has: +- `README.md` - Overview and learning goals +- `preparation.md` - What to do before class +- `session-plan.md` - What happens during class (for teachers) +- `assignment.md` - Homework +- `session-materials/` - Code examples and exercises + +Start with [Week 1: Array Methods](./week1/README.md). diff --git a/courses/backend/advanced-javascript/data/teas.js b/courses/backend/advanced-javascript/data/teas.js new file mode 100644 index 00000000..b53beb68 --- /dev/null +++ b/courses/backend/advanced-javascript/data/teas.js @@ -0,0 +1,22 @@ +export const teas = [ + { id: 1, name: "Sencha", type: "green", origin: "Japan", pricePerGram: 0.12, caffeineLevel: "medium", organic: true, inStock: true, stockCount: 150 }, + { id: 2, name: "Earl Grey", type: "black", origin: "India", pricePerGram: 0.08, caffeineLevel: "high", organic: false, inStock: true, stockCount: 200 }, + { id: 3, name: "Dragon Well", type: "green", origin: "China", pricePerGram: 0.25, caffeineLevel: "medium", organic: true, inStock: true, stockCount: 45 }, + { id: 4, name: "Chamomile", type: "herbal", origin: "Egypt", pricePerGram: 0.10, caffeineLevel: "none", organic: true, inStock: true, stockCount: 180 }, + { id: 5, name: "Darjeeling", type: "black", origin: "India", pricePerGram: 0.18, caffeineLevel: "high", organic: false, inStock: true, stockCount: 90 }, + { id: 6, name: "Oolong", type: "oolong", origin: "Taiwan", pricePerGram: 0.22, caffeineLevel: "medium", organic: true, inStock: true, stockCount: 60 }, + { id: 7, name: "Peppermint", type: "herbal", origin: "USA", pricePerGram: 0.08, caffeineLevel: "none", organic: true, inStock: true, stockCount: 220 }, + { id: 8, name: "Matcha", type: "green", origin: "Japan", pricePerGram: 0.45, caffeineLevel: "high", organic: true, inStock: true, stockCount: 30 }, + { id: 9, name: "Assam", type: "black", origin: "India", pricePerGram: 0.09, caffeineLevel: "high", organic: false, inStock: true, stockCount: 175 }, + { id: 10, name: "White Peony", type: "white", origin: "China", pricePerGram: 0.30, caffeineLevel: "low", organic: true, inStock: true, stockCount: 55 }, + { id: 11, name: "Rooibos", type: "herbal", origin: "South Africa", pricePerGram: 0.11, caffeineLevel: "none", organic: true, inStock: true, stockCount: 140 }, + { id: 12, name: "Gyokuro", type: "green", origin: "Japan", pricePerGram: 0.56, caffeineLevel: "high", organic: false, inStock: false, stockCount: 0 }, + { id: 13, name: "Lapsang Souchong", type: "black", origin: "China", pricePerGram: 0.15, caffeineLevel: "medium", organic: false, inStock: true, stockCount: 85 }, + { id: 14, name: "Silver Needle", type: "white", origin: "China", pricePerGram: 0.50, caffeineLevel: "low", organic: true, inStock: true, stockCount: 25 }, + { id: 15, name: "Genmaicha", type: "green", origin: "Japan", pricePerGram: 0.10, caffeineLevel: "low", organic: false, inStock: true, stockCount: 110 }, + { id: 16, name: "Tie Guan Yin", type: "oolong", origin: "China", pricePerGram: 0.30, caffeineLevel: "medium", organic: true, inStock: true, stockCount: 40 }, + { id: 17, name: "English Breakfast", type: "black", origin: "Sri Lanka", pricePerGram: 0.06, caffeineLevel: "high", organic: false, inStock: true, stockCount: 250 }, + { id: 18, name: "Hibiscus", type: "herbal", origin: "Sudan", pricePerGram: 0.09, caffeineLevel: "none", organic: true, inStock: true, stockCount: 95 }, + { id: 19, name: "Pu-erh", type: "black", origin: "China", pricePerGram: 0.35, caffeineLevel: "medium", organic: false, inStock: false, stockCount: 0 }, + { id: 20, name: "Jasmine Pearl", type: "green", origin: "China", pricePerGram: 0.32, caffeineLevel: "medium", organic: true, inStock: true, stockCount: 70 } +]; diff --git a/courses/backend/advanced-javascript/slides/index.html b/courses/backend/advanced-javascript/slides/index.html new file mode 100644 index 00000000..f3d24b11 --- /dev/null +++ b/courses/backend/advanced-javascript/slides/index.html @@ -0,0 +1,457 @@ + + + + + + Advanced JavaScript - Introduction + + + + + + + +
+
+ +
+

Advanced JavaScript

+

From visual output to data pipelines

+

Your journey into backend development

+
+ + +
+
+

Welcome to Backend Development

+

🍵

+
+ +
+

Frontend vs Backend

+

Frontend: Write code, refresh browser, see buttons and colors

+

Backend: Write code, run it... see text in a terminal

+

Where did my pixels go?

+
+ +
+

Here's the thing...

+

Terminals existed before graphical interfaces

+

The command line isn't a step backward

+

It's where serious computing has always happened

+
+ +
+

Get comfortable with it

+
$ node index.js
+Server running on port 3000
+GET /api/teas - 200 OK (12ms)
+GET /api/teas/42 - 200 OK (3ms)
+POST /api/orders - 201 Created (45ms)
+

This is your new feedback loop

+
+
+ + +
+
+

What Does a Backend Actually Do?

+
+ +
+

Think about Instagram

+

You open the app...

+

Do you download all 500 million posts?

+

Of course not.

+
+ +
+

Instead, your app asks:

+
+ "Give me the next 10 posts for this user" +
+

That's a backend call.

+
+ +
+

The Backend...

+
    +
  1. Receives the request
  2. +
  3. Checks if you're allowed to see those posts
  4. +
  5. Queries the database for the right 10
  6. +
  7. Formats them for the app
  8. +
  9. Sends back just that slice
  10. +
+
+ +
+

Backend Output

+

+ Consumable slices of data
in a structured manner +

+
+
+ + +
+
+

What is Data?

+

Before we manipulate it, let's understand it

+
+ +
+

Data is structured information

+

The same information can look different

+

Let's look at 3 teas from our tea shop...

+
+ +
+

As a Table

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
idnameoriginpricePerGramorganic
1SenchaJapan0.12true
2Earl GreyIndia0.08false
3Dragon WellChina0.25true
+

Familiar. Easy to read. Not great for computers.

+
+ +
+

As CSV

+
id,name,origin,pricePerGram,organic
+1,Sencha,Japan,0.12,true
+2,Earl Grey,India,0.08,false
+3,Dragon Well,China,0.25,true
+

Compact. Good for spreadsheets and imports.

+
+ +
+

As JSON

+
[
+  { "id": 1, "name": "Sencha", "origin": "Japan", "pricePerGram": 0.12, "organic": true },
+  { "id": 2, "name": "Earl Grey", "origin": "India", "pricePerGram": 0.08, "organic": false },
+  { "id": 3, "name": "Dragon Well", "origin": "China", "pricePerGram": 0.25, "organic": true }
+]
+

JavaScript's native format. This is what we'll use.

+
+ +
+

As JSONL (JSON Lines)

+
{"id":1,"name":"Sencha","origin":"Japan","pricePerGram":0.12,"organic":true}
+{"id":2,"name":"Earl Grey","origin":"India","pricePerGram":0.08,"organic":false}
+{"id":3,"name":"Dragon Well","origin":"China","pricePerGram":0.25,"organic":true}
+

One object per line. Great for streaming large datasets.

+
+ +
+

Same Data, Different Formats

+

The information is identical

+

Only the representation changes

+

We'll primarily work with JSON

+
+
+ + +
+
+

The Three Layers

+
+
Data
+ +
Logic
+ +
Rendering
+
+
+ +
+

Data Layer

+
+
Data
+ +
Logic
+ +
Rendering
+
+

The raw truth

+

Databases, files, external services

+

Where information lives

+
+ +
+

Logic Layer

+
+
Data
+ +
Logic
+ +
Rendering
+
+

The brain

+

What to fetch, who's allowed, how to transform

+

This is where backends live.

+
+ +
+

Rendering Layer

+
+
Data
+ +
Logic
+ +
Rendering
+
+

The face

+

HTML, mobile UI, PDF

+

Whatever users actually see

+
+ +
+

Example: "Show me organic teas"

+

User clicks "Organic only" on the tea shop

+
+ +
+

The Flow

+
    +
  1. Rendering sends filter choice to server
  2. +
  3. Logic receives request, queries database
  4. +
  5. Data returns matching teas
  6. +
  7. Logic transforms to the shape frontend expects
  8. +
  9. Rendering displays tea cards
  10. +
+
+ +
+

The Backend's Role

+

Sits in the middle

+

Orchestrating the flow between stored data and user-facing output

+
+
+ + +
+
+

Our Example: The Tea Shop

+

🍵

+
+ +
+

Same Context, 4 Weeks

+

You're building a backend for a tea e-commerce site

+
+ +
+

Our Data

+
    +
  • Teas - varieties, origins, prices, brewing instructions
  • +
  • Equipment - teapots, cups, infusers
  • +
  • Orders - customer purchases
  • +
  • Customers - people who buy
  • +
+
+ +
+

Not Abstract Exercises

+

You're solving problems a real backend would solve

+

Same tea shop, growing complexity, 4 weeks

+
+
+ + +
+
+

The Next 4 Weeks

+
+ +
+

Week 1: Array Methods

+

The application layer's daily bread

+

filter, map, reduce

+

Transform data between "as stored" and "as served"

+
+ +
+

Week 1: Real Example

+
+ You search for "green tea". Database returns 200 matches. But you only want organic ones, sorted by price, showing just name and price. +
+

filtersortmap

+

Three lines of code. Happens on every request.

+
+ +
+

Week 2: Callbacks & Async

+

Why backends can't run top-to-bottom

+

Databases are slow. Networks are slow.

+

"Do this slow thing, here's what to do when you're done"

+
+ +
+

Week 2: Real Example

+
+ A cafe orders 50 teas. Check inventory, calculate shipping, charge card, send email. If each step takes 500ms and you wait frozen... that's 2 seconds. Meanwhile 100 other customers can't browse. +
+

Callbacks let you handle other requests while waiting

+
+ +
+

Week 3: Promises & APIs

+

Speaking to other services

+

Backends rarely work alone

+

Payment providers, email services, external APIs

+
+ +
+

Week 3: Real Example

+
+ Customer checks out. Call Stripe for payment, PostNord for shipping, inventory service to reserve stock. Three external calls, any could fail. +
+

"Do all three, if any fails, handle it gracefully"

+

Try writing that with nested callbacks...

+
+ +
+

Week 4: Classes

+

Modeling your domain

+

A tea isn't just a floating object

+

It has behaviors: validate itself, calculate weight, check stock

+
+ +
+

Week 4: Real Example

+
+ Every time you handle a tea: Is price valid? Stock above zero? Steep time reasonable? You write these checks 50 times across your codebase. +
+

With a Tea class, you write them once

+

The logic lives with the data it protects

+
+
+ + +
+

Let's Begin

+
+
+
+ + + + + + + \ No newline at end of file diff --git a/courses/backend/advanced-javascript/week1/README.md b/courses/backend/advanced-javascript/week1/README.md new file mode 100644 index 00000000..313f0c5b --- /dev/null +++ b/courses/backend/advanced-javascript/week1/README.md @@ -0,0 +1,34 @@ +# Array Methods (Week 1) + +Array methods are the Logic layer's core tools. Every backend request involves transforming data: filtering results, mapping to response formats, reducing to totals. These operations happen on nearly every request. + +This week you'll learn the three essential methods: `forEach`, `map`, and `filter`. You'll understand when to use each one, and how they fit into the Data → Logic → Rendering model. + +## Contents + +- [Preparation](./preparation.md) +- [Slides](./slides/index.html) +- [Session Plan](./session-plan.md) (for mentors) +- [Session Materials](./session-materials/) +- [Assignment](./assignment.md) + +## Learning Goals + +By the end of this session, you will be able to: + +- [ ] Use `forEach` for side effects (logging, rendering) +- [ ] Use `map` to transform arrays (N items → N transformed items) +- [ ] Use `filter` to select items (N items → fewer items, same shape) +- [ ] Combine multiple array methods through chaining +- [ ] Use arrow function syntax with implicit and explicit returns +- [ ] Recognize which methods belong in the Logic layer vs Rendering layer +- [ ] Know how to find and learn about other array methods such as `.find()`, `.some()`, `.reduce()` + +```js +// Example: Get names of organic Japanese teas +const result = teas + .filter(tea => tea.origin === "Japan") + .filter(tea => tea.organic) + .map(tea => tea.name); +// ["Sencha", "Matcha"] +``` diff --git a/courses/backend/advanced-javascript/week1/assignment.md b/courses/backend/advanced-javascript/week1/assignment.md new file mode 100644 index 00000000..9981f055 --- /dev/null +++ b/courses/backend/advanced-javascript/week1/assignment.md @@ -0,0 +1,187 @@ +# Week 1 Assignment + +Complete these exercises after the session. All exercises use the tea data. + +```js +import { teas } from "../data/teas.js"; +``` + +--- + +## Exercise 1: Rewrite with Array Methods + +This code uses a traditional for-loop. Rewrite it using `filter` and `map` with arrow functions: + +```js +const result = []; +for (let i = 0; i < teas.length; i++) { + if (teas[i].caffeineLevel !== "none") { + result.push(teas[i].name.toUpperCase()); + } +} +console.log(result); +``` + +Your solution should be 2-3 lines using chained array methods. + +--- + +## Exercise 2: Inventory Report ⭐ + +Build a function that generates an inventory report: + +```js +function inventoryReport(teas) { + return { + totalTeas: /* total number of teas */, + inStock: /* number of teas where inStock is true */, + outOfStock: /* number of teas where inStock is false */, + totalInventoryValue: /* sum of (pricePerGram * stockCount) for all teas */, + averagePrice: /* average pricePerGram across all teas */ + }; +} + +console.log(inventoryReport(teas)); +``` + +Expected output structure: +```js +{ + totalTeas: 20, + inStock: 18, + outOfStock: 2, + totalInventoryValue: 234.55, // example number + averagePrice: 0.21 // example number +} +``` + +--- + +## Exercise 3: Low Stock Alert + +Create a function that returns teas with low stock (less than 50 items): + +```js +function lowStockAlert(teas) { + // Return array of objects with name and stockCount + // sorted by stockCount (lowest first) +} + +console.log(lowStockAlert(teas)); +``` + +Expected output format: +```js +[ + { name: "Silver Needle", stockCount: 25 }, + { name: "Matcha", stockCount: 30 }, + // ... +] +``` + +--- + +## Exercise 4: Teas by Origin ⭐⭐ + +Create a function that groups teas by their origin country: + +```js +function teasByOrigin(teas) { + // Return object where keys are origins and values are arrays of tea names +} + +console.log(teasByOrigin(teas)); +``` + +Expected output format: +```js +{ + Japan: ["Sencha", "Matcha", "Gyokuro", "Genmaicha"], + India: ["Earl Grey", "Darjeeling", "Assam"], + China: ["Dragon Well", "White Peony", ...], + // ... +} +``` + +--- + +## Exercise 5: Search Function + +Create a search function for the tea shop: + +```js +function searchTeas(teas, query) { + // Return teas where the name contains the query (case-insensitive) + // Return just the names, sorted alphabetically +} + +console.log(searchTeas(teas, "earl")); +// Returns: ["Earl Grey"] + +console.log(searchTeas(teas, "dragon")); +// Returns: ["Dragon Well"] + +console.log(searchTeas(teas, "ch")); +// Returns: ["English Breakfast", "Genmaicha", "Lapsang Souchong"] +``` + +Hint: Use `.toLowerCase()` and `.includes()` for case-insensitive search. + +--- + +## Optional: reduce + +These exercises use `reduce`. Try them if you want a challenge! + +### Exercise 6: Total Inventory Value (Optional) + +Calculate the total value of all tea inventory using `reduce`: + +```js +const totalValue = teas.reduce((sum, tea) => { + // add pricePerGram * stockCount to sum +}, 0); + +console.log("Total inventory value:", totalValue); +``` + +Hint: `reduce` builds up a single value by processing each item. The `0` is your starting value. + +### Exercise 7: Count by Type (Optional) + +Use `reduce` to count how many teas of each type exist: + +```js +const countByType = teas.reduce((counts, tea) => { + // increment counts[tea.type] +}, {}); + +console.log(countByType); +// Expected: { green: 6, black: 6, herbal: 4, oolong: 2, white: 2 } +``` + +--- + +## Submission + +Create a folder `week1-assignment/` with: +- `exercise1.js` - Rewrite with array methods +- `exercise2.js` - Inventory report +- `exercise3.js` - Low stock alert +- `exercise4.js` - Teas by origin +- `exercise5.js` - Search function +- `exercise6.js` - Total inventory value (optional) +- `exercise7.js` - Count by type (optional) + +Each file should be runnable with `node exerciseN.js`. + +```js +// Example structure for exercise1.js +import { teas } from "../data/teas.js"; + +const result = teas + .filter(/* ... */) + .map(/* ... */); + +console.log(result); +``` diff --git a/courses/backend/advanced-javascript/week1/preparation.md b/courses/backend/advanced-javascript/week1/preparation.md new file mode 100644 index 00000000..7dd992ce --- /dev/null +++ b/courses/backend/advanced-javascript/week1/preparation.md @@ -0,0 +1,188 @@ +# Week 1 Preparation + +Read this introduction before class. It explains the concepts we'll practice during the session. + +--- + +## Array Methods: A Different Way of Thinking + +You've written `for` loops before. They work. But there's another way to work with arrays that's more common in professional JavaScript code: **array methods**. + +```js +// Imperative: you describe HOW to do it +const names = []; +for (let i = 0; i < teas.length; i++) { + names.push(teas[i].name); +} + +// Declarative: you describe WHAT you want +const names = teas.map(function(tea) { + return tea.name; +}); +``` + +Both produce the same result. But the second version is **declarative** - you're declaring what you want (the names), not describing the step-by-step process of how to get them. + +By the end of this week, you'll write it even shorter: + +```js +const names = teas.map(tea => tea.name); +``` + +That's called an **arrow function**. We'll get there - but first, let's take things step by step, and understand what's happening with the regular function syntax. + +> 📚 **Glossary: Declarative vs Imperative** +> **Imperative** code describes *how* to do something (step by step). +> **Declarative** code describes *what* you want (the result). +> Array methods are declarative - you say "give me the names" not "create empty array, loop, push each name." + +--- + +## Higher-Order Functions + +`map`, `filter`, and `forEach` are all **higher-order functions**. This sounds fancy, but it just means: they're functions that take another function as an argument. + +```js +teas.map(function(tea) { + return tea.name; +}); +// ^^^^^^^^^^^^^^^^^^^^ +// this function is passed TO map +``` + +> 📚 **Glossary: Higher-Order Function** +> A function that takes another function as an argument (or returns a function). +> All array methods like `map`, `filter`, `forEach` are higher-order functions. + +--- + +## Side Effects + +Here's an important concept. Look at this code: + +```js +let count = 0; +teas.forEach(function(tea) { + count = count + 1; // modifies something OUTSIDE the function +}); +``` + +This modifies `count` - a variable that exists outside the function. That's a **side effect**: the function reaches out and changes something in the world. + +Other examples of side effects: +- Logging to console (`console.log`) +- Updating the DOM +- Writing to a database +- Sending an HTTP request + +`forEach` is designed for side effects. That's why it doesn't return anything - its job is to *do* something for each item, not to produce a new array. + +> 📚 **Glossary: Side Effect** +> When a function modifies something outside its own scope: changing a variable, logging to console, updating the DOM, writing to a database. +> `forEach` is typically used *for* its side effects (like logging). + +--- + +## The Four Methods + +Here's what each method does at a high level: + +### forEach - "Do something for each item" + +- **Input:** N items +- **Output:** Nothing (undefined) +- **Purpose:** Side effects - logging, rendering, sending data + +```js +teas.forEach(function(tea) { + console.log(tea.name); +}); +``` + +### map - "Transform each item" + +- **Input:** N items +- **Output:** N transformed items +- **Purpose:** Convert data from one shape to another + +```js +const names = teas.map(function(tea) { + return tea.name; +}); +// 20 teas in → 20 names out +``` + +### filter - "Keep items that match" + +- **Input:** N items +- **Output:** 0 to N items (same shape as input) +- **Purpose:** Select a subset based on criteria + +The function you pass to `filter` is called a **predicate** - a function that returns `true` or `false`. + +```js +const organic = teas.filter(function(tea) { + return tea.organic; +}); +// Keep only items where predicate returns true +``` + +> 📚 **Glossary: Predicate** +> A function that returns `true` or `false`. Used to test a condition. +> The function you pass to `filter` is a predicate - it decides which items to keep. + +--- + +## Pipelines: Chaining Methods + +Because each method returns a new array, you can chain them together: + +```js +const result = teas + .filter(function(tea) { return tea.organic; }) + .filter(function(tea) { return tea.inStock; }) + .map(function(tea) { return tea.name; }) + .sort(); +``` + +Data flows through each step like water through pipes. This is called a **pipeline**. + +Once you learn arrow functions, this becomes much cleaner: + +```js +const result = teas + .filter(tea => tea.organic) + .filter(tea => tea.inStock) + .map(tea => tea.name) + .sort(); +``` + +Same logic, less noise. That's why arrow functions are popular for array methods. + +> 📚 **Glossary: Pipeline** +> A sequence of operations where each step's output becomes the next step's input. +> Chaining array methods creates a data pipeline. + +--- + +## Summary + +| Term | Meaning | +|------|---------| +| Declarative | Describe *what* you want, not *how* | +| Higher-order function | A function that takes a function as argument | +| Side effect | Modifying something outside the function | +| Predicate | A function returning true/false | +| Pipeline | Chained operations, output → input | + +--- + +## Pre-Reading (Optional) + +If you want to go deeper, read these MDN pages: + +- [Array.prototype.forEach()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach) +- [Array.prototype.map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) +- [Array.prototype.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) +- [Arrow function expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions) + diff --git a/courses/backend/advanced-javascript/week1/session-materials/exercises.md b/courses/backend/advanced-javascript/week1/session-materials/exercises.md new file mode 100644 index 00000000..af054588 --- /dev/null +++ b/courses/backend/advanced-javascript/week1/session-materials/exercises.md @@ -0,0 +1,165 @@ +# Week 1 Exercises: Array Methods + +All exercises use the tea shop data. Start by loading it: + +```js +import { teas } from "../../data/teas.js"; +``` + +--- + +## Part 1: forEach + +`forEach` walks through each item. No return value - just side effects. + +
+📚 Recall: Side Effect + +A side effect is when a function modifies something outside itself - like logging to console or updating a variable. `forEach` is designed for side effects. +
+ +### Exercise 1 +Log each tea's name to the console. + +### Exercise 2 +Log each tea in the format: `"Sencha (Japan)"` + +### Exercise 3 +Count how many teas are organic. Use a variable outside the forEach to track the count. + +> 💡 Notice: You're modifying an external variable (`count`). That's a side effect - and exactly what `forEach` is for. + +--- + +## Part 2: map + +`map` transforms each item. Same count in, same count out. + +> 💡 Unlike `forEach`, `map` has no side effects. It creates a new array without changing the original. + +### Exercise 4 +Create an array containing just the tea names. +```js +// Expected: ["Sencha", "Earl Grey", "Dragon Well", ...] +``` + +### Exercise 5 +Create an array of prices in DKK for 100 grams (multiply `pricePerGram` by 100). +```js +// Expected: [12, 8, 25, ...] +``` + +### Exercise 6 +Create an array of display strings in the format: `"Sencha - 12 DKK/100g"` + +--- + +## Part 3: filter + +`filter` keeps items that match. Fewer items out, same shape. + +
+📚 Recall: Predicate + +The function you pass to `filter` is called a predicate - a function that returns `true` or `false`. Items where the predicate returns `true` are kept. +
+ +### Exercise 7 +Get all organic teas. + +### Exercise 8 +Get all teas from Japan. + +### Exercise 9 +Get all teas with `caffeineLevel` equal to `"high"`. + +### Exercise 10 +Get all teas that are both in stock AND organic. + +> 💡 The items themselves aren't changed - they're just selected. That's the difference between `filter` (select) and `map` (transform). + +--- + +## Part 4: Combining Methods + +Chain methods together: the output of one becomes input to the next. + +
+📚 Recall: Pipeline + +When you chain methods, data flows through like water through pipes. Each step's output becomes the next step's input. This is a pipeline. +
+ +### Exercise 11 +Get the names of all green teas. +```js +// filter to green type, then map to names +``` + +### Exercise 12 +Get display prices (format: `"Sencha - 12 DKK/100g"`) for organic teas only. + +### Exercise 13 ⭐ +Get Japanese teas sorted by price (lowest first). +```js +// Hint: .sort((a, b) => a.pricePerGram - b.pricePerGram) +``` + +--- + +## Part 5: Arrow Functions + +Rewrite the exercises above using arrow function syntax. + +### Exercise 14 +Rewrite exercises 1-3 using arrow functions. + +### Exercise 15 +Rewrite exercises 4-6 using arrow functions with implicit return (no curly braces). + +Example: +```js +// Traditional +teas.map(function(tea) { + return tea.name; +}); + +// Arrow with implicit return +teas.map(tea => tea.name); +``` + +### Exercise 16 ⭐ +When do you need explicit return (curly braces)? + +Rewrite exercise 6 both ways: +- With implicit return (hint: use template literals inline) +- With explicit return (curly braces and `return` keyword) + +--- + +## Part 6: Challenge + +### Exercise 17 ⭐⭐ +Build a `filterTeas(teas, criteria)` function that accepts a filter object: + +```js +filterTeas(teas, { organic: true }); +// Returns all organic teas + +filterTeas(teas, { origin: "Japan" }); +// Returns all Japanese teas + +filterTeas(teas, { organic: true, origin: "Japan" }); +// Returns organic Japanese teas + +filterTeas(teas, { type: "green", inStock: true }); +// Returns green teas that are in stock +``` + +The function should work with any combination of filter properties. + +
+📚 This is declarative programming + +Instead of writing specific filter functions for each case, you're describing *what* you want with a data structure (the criteria object). The function figures out *how* to apply it. That's declarative. +
diff --git a/courses/backend/advanced-javascript/week1/session-plan.md b/courses/backend/advanced-javascript/week1/session-plan.md new file mode 100644 index 00000000..95082f27 --- /dev/null +++ b/courses/backend/advanced-javascript/week1/session-plan.md @@ -0,0 +1,122 @@ +# Session Plan (Week 1: Array Methods) + +> This guide is for mentors. It outlines how to run the session, what to emphasize, and why we introduce certain terminology. + +--- + +## Session Goals + +By the end of this session, trainees should: + +1. Understand when to use `forEach`, `map`, and `filter` +2. Be able to chain array methods together +3. Write arrow functions with implicit and explicit returns +4. Connect array methods to the Data → Logic → Rendering model + +--- + +## Glossary Goals + +| Term | Why we introduce it | +|------|---------------------| +| **Declarative vs Imperative** | Frames the shift from for-loops to array methods. Helps them understand *why* this style is preferred. | +| **Higher-order function** | They'll encounter this term in docs, tutorials, and interviews. Demystifies "functions that take functions." | +| **Side effect** | Critical for understanding `forEach` vs other methods. Foundation for async concepts in Week 2. | +| **Predicate** | Precise term for filter functions. Common in documentation and libraries. | +| **Pipeline** | Mental model for chaining. Useful metaphor they'll use throughout their career. | + +--- + +## Session Outline + +### 1. Introduction + +- Connect to the course theme: Data → Logic → Rendering +- Array methods live in the **Logic layer** - transforming data between storage and display +- Show the tea shop context they'll work with + +**Use:** [Slides](./slides/index.html) (first few sections) + +### 2. forEach + +- Start with `function()` syntax, NOT arrow functions +- Emphasize: **no return value** - it's for side effects +- Live code example: logging tea names + +**Key point:** "If you need to *do* something for each item but don't need a result back, use `forEach`." + +**Exercises:** [Part 1](./session-materials/exercises.md#part-1-foreach) (exercises 1-3) + +### 3. map + +- Still using `function()` syntax +- Emphasize: **same count in, same count out** +- Contrast with `forEach`: map *returns* a new array, forEach doesn't +- Live code: extract tea names, calculate prices + +**Key point:** "If you need to *transform* each item into something else, use `map`." + +**Exercises:** [Part 2](./session-materials/exercises.md#part-2-map) (exercises 4-6) + +### 4. filter +- Introduce the term **predicate** - a function returning true/false +- Emphasize: **items stay the same shape, count changes** +- Live code: organic teas, Japanese teas + +**Key point:** "If you need to *select* some items based on a condition, use `filter`." + +**Exercises:** [Part 3](./session-materials/exercises.md#part-3-filter) (exercises 7-10) + +### 5. Combining methods + +- Introduce **pipeline** metaphor +- Show chaining: filter → map +- Discuss order: usually filter first (reduce data), then map (transform) + +**Exercises:** [Part 4](./session-materials/exercises.md#part-4-combining-methods) (exercises 11-13) + +### 6. Arrow functions + +- NOW introduce arrow syntax +- Show the progression: `function()` → arrow → implicit return +- Practice converting earlier exercises + +**Key point:** "Arrow functions are shorter syntax for the same thing. They're popular because they reduce noise in array methods." + +**Exercises:** [Part 5](./session-materials/exercises.md#part-5-arrow-functions) (exercises 14-16) + +### 7. Wrap-up + +- Recap the three methods and when to use each +- Show the "questions to ask" slide +- Introduce the assignment + +--- + +## Teaching Tips + +### Managing exercises with mixed skill levels + +The exercises are intentionally numerous - students progress at different speeds. Here's how to manage this: + +1. **Let students work on Part 1** - observe how far each gets individually +2. **Solve one exercise in plenum** - pick one that weaker students struggled with but stronger students just finished. This way everyone benefits +3. **Move on to Part 2** - weaker students skip remaining Part 1 exercises (they can revisit later) +4. **Repeat** for each part + +Exercises marked with ⭐ are optional stretch goals. If your fastest students finish all exercises while others haven't solved the first one, that part needs an additional challenge - let us know, or create a PR with a creative challenging exercise! + +### Use the tea data consistently +All examples should use the tea shop data. This builds familiarity and shows how the same dataset can be queried different ways. + +### Connect to backend context +Regularly remind them: "This is what happens on every API request. Data comes in one shape, goes out another." + +--- + +## Materials + +- [Slides](./slides/index.html) - Reveal.js presentation +- [Exercises](./session-materials/exercises.md) - In-class exercises +- [Assignment](./assignment.md) - Take-home work +- [Tea Data](../data/teas.js) - Shared dataset diff --git a/courses/backend/advanced-javascript/week1/slides/index.html b/courses/backend/advanced-javascript/week1/slides/index.html new file mode 100644 index 00000000..eb5ad105 --- /dev/null +++ b/courses/backend/advanced-javascript/week1/slides/index.html @@ -0,0 +1,477 @@ + + + + + + Week 1 - Array Methods + + + + + + + +
+
+ + +
+

Array Methods

+

The application layer's daily bread

+

Week 1

+
+ + +
+
+

Why Array Methods?

+
+ +
+

Backend Reality

+

Data comes in one shape...

+

...and needs to go out in another

+

This happens on every single request

+
+ +
+

Example: Tea Search

+
+ User searches "green tea". Database returns 200 matches.
+ But they want: organic only, sorted by price, just name and price. +
+

filtersortmap

+
+ +
+

Array Methods Are Your Toolkit

+

Transform data between:

+

"as stored" and "as served"

+
+
+ + +
+
+

Three Methods to Learn

+
+ forEach + map + filter +
+
+
+ + +
+
+

forEach

+

"Walk through and do something"

+
+ +
+

The Mental Model

+

N inputsno output

+

Side effects only: logging, rendering, sending

+
+ +
+

Example

+
const teas = ["Sencha", "Earl Grey", "Matcha"];
+
+teas.forEach(function(tea) {
+  console.log(tea);
+});
+// Sencha
+// Earl Grey
+// Matcha
+

No return value. Just does something for each item.

+
+ +
+

When to Use

+
    +
  • Logging items
  • +
  • Rendering elements to DOM
  • +
  • Sending notifications
  • +
+

When you don't need a result back

+
+
+ + +
+
+

map

+

"Transform each item"

+
+ +
+

The Mental Model

+

N inputsN outputs

+

Same count, different shape

+
+ +
+

Visual

+
[ 🟥 🟦 🟩 🟨 ]
+

↓ map ↓

+
[ 🔺 🔷 🔻 🔶 ]
+

Colors stay. Shapes change.

+
+ +
+

Example

+
const teas = [
+  { name: "Sencha", pricePerGram: 0.12 },
+  { name: "Matcha", pricePerGram: 0.45 }
+];
+
+const names = teas.map(function(tea) {
+  return tea.name;
+});
+// ["Sencha", "Matcha"]
+

2 objects in, 2 strings out.

+
+ +
+

Another Example

+
const prices = teas.map(function(tea) {
+  return tea.pricePerGram * 100;  // price per 100g
+});
+// [12, 45]
+

Transform the data, keep the count.

+
+ +
+

When to Use

+
    +
  • Extract one property from objects
  • +
  • Transform values (calculate, format)
  • +
  • Reshape data for API response
  • +
+

When every item needs to become something else

+
+
+ + +
+
+

filter

+

"Keep only what matches"

+
+ +
+

The Mental Model

+

N inputsM outputs (M ≤ N)

+

Same shape, fewer items

+
+ +
+

Visual

+
[ 🟥 🟦 🟩 🟨 ]
+

↓ filter (warm colors) ↓

+
[ 🟥 🟨 ]
+

Shapes stay. Count changes.

+
+ +
+

Example

+
const teas = [
+  { name: "Sencha", organic: true },
+  { name: "Earl Grey", organic: false },
+  { name: "Matcha", organic: true }
+];
+
+const organic = teas.filter(function(tea) {
+  return tea.organic;
+});
+// [{ name: "Sencha", ... }, { name: "Matcha", ... }]
+

3 in, 2 out. Objects unchanged.

+
+ +
+

When to Use

+
    +
  • Select items matching criteria
  • +
  • Remove unwanted data
  • +
  • Search results
  • +
+

When you need fewer items, not different items

+
+
+ + +
+
+

map vs filter

+

The key difference

+
+ +
+

map

+

Changes what items are

+

Objects → strings, numbers → formatted, etc.

+

Count stays the same

+
+ +
+

filter

+

Changes which items you keep

+

All teas → only organic teas

+

Items stay the same

+
+ +
+

Combining Them

+
// Get names of organic teas
+const organicNames = teas
+  .filter(function(tea) { return tea.organic; })
+  .map(function(tea) { return tea.name; });
+
+// ["Sencha", "Matcha"]
+

First reduce count, then transform.

+
+
+ + +
+
+

Where Do These Live?

+
+
Data
+ +
Logic
+ +
Rendering
+
+
+ +
+

In the Logic Layer

+

map - transform data for output

+

filter - select relevant data

+

These prepare data between storage and display

+
+ +
+

forEach is Different

+

Often appears in the Rendering layer

+

Logging, DOM updates, side effects

+

No output = end of the pipeline

+
+
+ + +
+
+

Arrow Functions

+

A cleaner way to write

+
+ +
+

Traditional vs Arrow

+
// Traditional
+teas.map(function(tea) {
+  return tea.name;
+});
+
+// Arrow
+teas.map((tea) => {
+  return tea.name;
+});
+
+// Arrow (implicit return)
+teas.map(tea => tea.name);
+
+ +
+

Implicit Return

+

No curly braces = automatic return

+
// These are identical:
+teas.filter(tea => tea.organic);
+teas.filter(tea => { return tea.organic; });
+
+ +
+

When You Need Braces

+
// Multiple statements need braces + return
+teas.map(tea => {
+  const price = tea.pricePerGram * 100;
+  return `${tea.name}: ${price} DKK`;
+});
+
+
+ + +
+
+

Tea Shop Examples

+

🍵

+
+ +
+

Get All Japanese Teas

+
const japaneseTeas = teas.filter(
+  tea => tea.origin === "Japan"
+);
+
+ +
+

Get Tea Names Only

+
const names = teas.map(tea => tea.name);
+// ["Sencha", "Earl Grey", "Dragon Well", ...]
+
+ +
+

Organic Japanese Tea Names

+
const result = teas
+  .filter(tea => tea.origin === "Japan")
+  .filter(tea => tea.organic)
+  .map(tea => tea.name);
+// ["Sencha", "Matcha"]
+
+
+ + +
+
+

What About reduce?

+

Another powerful array method

+
+ +
+

The Mental Model

+

N inputs1 output

+

Many items become one value

+
+ +
+

Visual

+
[ 🟥 🟦 🟩 🟨 ]
+

↓ reduce ↓

+
🟣
+

Many become one.

+
+ +
+

Example

+
const teas = [
+  { name: "Sencha", stockCount: 150 },
+  { name: "Matcha", stockCount: 30 },
+  { name: "Earl Grey", stockCount: 200 }
+];
+
+const totalStock = teas.reduce((sum, tea) => {
+  return sum + tea.stockCount;
+}, 0);
+// 380
+

3 objects in, 1 number out.

+
+ +
+

We'll Learn This in Week 2

+

reduce is the most powerful array method

+

It needs a solid foundation in map/filter first

+

Optional exercises in this week's assignment!

+
+
+ + +
+
+

Summary

+
+ +
+

The Three Methods

+

forEach - N → nothing (side effects)

+

map - N → N (transform)

+

filter - N → M (select)

+
+ +
+

Questions to Ask

+

Do I need a result? → not forEach

+

Same count, different shape? → map

+

Fewer items, same shape? → filter

+
+ +
+

Time to Practice

+

🍵

+
+
+ +
+
+ + + + + + + diff --git a/courses/backend/advanced-javascript/week2/README.md b/courses/backend/advanced-javascript/week2/README.md new file mode 100644 index 00000000..64a62236 --- /dev/null +++ b/courses/backend/advanced-javascript/week2/README.md @@ -0,0 +1,40 @@ +# Callbacks & Delayed Execution (Week 2) + +Backends can't run top-to-bottom. Database queries take time. File reads take time. Network calls take time. If your server waited for each operation to complete before doing anything else, it could only serve one request at a time. + +This week you'll learn how JavaScript handles operations that take time. You'll understand callbacks, build your own higher-order functions, and see why delayed execution is essential for backends. + +## Contents + +- [Preparation](./preparation.md) +- [Slides](./slides/index.html) +- [Session Plan](./session-plan.md) (for mentors) +- [Session Materials](./session-materials/) +- [Assignment](./assignment.md) + +## Learning Goals + +By the end of this session, you will be able to: + +- [ ] Assign functions to variables and pass them as arguments +- [ ] Return functions from other functions (function factories) +- [ ] Use `reduce()` to aggregate data (totals, grouping) +- [ ] Write and use callback functions +- [ ] Understand how delayed callbacks work +- [ ] Use `setTimeout()` to schedule delayed execution +- [ ] Use Node.js `fs.readFile()` with callbacks +- [ ] Understand the error-first callback pattern + +```js +// Example: Simulated database lookup +function findTeaById(id, callback) { + setTimeout(() => { + const tea = teas.find(t => t.id === id); + callback(tea); + }, 500); +} + +findTeaById(3, tea => { + console.log("Found:", tea.name); +}); +``` diff --git a/courses/backend/advanced-javascript/week2/assignment.md b/courses/backend/advanced-javascript/week2/assignment.md new file mode 100644 index 00000000..188d362d --- /dev/null +++ b/courses/backend/advanced-javascript/week2/assignment.md @@ -0,0 +1,223 @@ +# Week 2 Assignment + +Complete these exercises after the session. They build on callbacks, delayed execution, and reduce. + +```js +import { teas } from "../data/teas.js"; +import fs from "fs"; +``` + +--- + +## Exercise 1: Stock by Caffeine Level + +Use `reduce` to calculate the total stock for each caffeine level: + +```js +function stockByCaffeine(teas) { + return teas.reduce((acc, tea) => { + // Your implementation + }, {}); +} + +console.log(stockByCaffeine(teas)); +// { high: 745, medium: 450, low: 190, none: 635 } +``` + +This tells you how much inventory you have for customers who want high-caffeine vs caffeine-free options. + +--- + +## Exercise 2: Order Processing System ⭐ + +Create an order processing system with simulated delays. + +```js +const order = { + id: 1001, + customerId: 42, + items: [ + { teaId: 1, grams: 100 }, + { teaId: 8, grams: 50 }, + { teaId: 3, grams: 200 } + ] +}; +``` + +Create these functions: + +**1. `validateOrder(order, callback)`** - 200ms delay +- Check all teaIds exist in the teas array +- Callback receives `{ valid: boolean, errors: string[] }` + +**2. `calculateTotal(order, callback)`** - 300ms delay +- Sum up `pricePerGram * grams` for each item +- Callback receives `{ orderId: number, total: number }` + +**3. `checkStock(order, callback)`** - 400ms delay +- Check if each tea has enough stock for the order quantity +- Callback receives `{ orderId: number, inStock: boolean, shortages: string[] }` + +Test each function individually with console.log as callback function to see results after delays: + +You an test `validateOrder` like this: + +```js +validateOrder(order, result => { + console.log("Validation result:", result); +}); +``` + +--- + +## Exercise 3: Sequential Processing + +Using the functions from Exercise 2, process an order through all three steps *in sequence*: + +1. First validate +2. If valid, calculate total +3. If total calculated, check stock +4. Log final result + +This requires "callback nesting" - calling the next function inside the previous callback. + +```js +function processOrder(order) { + console.log("Processing order", order.id); + + validateOrder(order, validation => { + if (!validation.valid) { + console.log("Validation failed:", validation.errors); + return; + } + console.log("Validation passed"); + + calculateTotal(order, pricing => { + console.log("Total:", pricing.total, "DKK"); + + // Continue with checkStock... + }); + }); +} + +processOrder(order); +``` + +> 💡 This nesting is ugly and hard to read. It's called "callback hell." Week 3 will show how Promises solve this! + +--- + +## Exercise 4: Inventory Aggregation from File + +Create a file `inventory-updates.json`: + +```json +[ + { "teaId": 1, "change": -20, "reason": "sale" }, + { "teaId": 1, "change": 50, "reason": "restock" }, + { "teaId": 8, "change": -10, "reason": "sale" }, + { "teaId": 3, "change": -100, "reason": "sale" }, + { "teaId": 8, "change": 30, "reason": "restock" } +] +``` + +Write a function that: +1. Reads this file using a callback +2. Uses `reduce` to calculate net change per tea +3. Combines with original tea data to show new stock levels +4. Logs a report + +```js +function generateInventoryReport(callback) { + fs.readFile("./inventory-updates.json", "utf8", (error, data) => { + if (error) { + callback(error, null); + return; + } + + // Parse updates, calculate changes, build report + // Call callback(null, report) when done + }); +} + +generateInventoryReport((error, report) => { + if (error) { + console.error("Failed:", error.message); + return; + } + console.log(report); +}); +``` + +Expected output format: +``` +Inventory Report: +- Sencha: was 150, change +30, now 180 +- Matcha: was 30, change +20, now 50 +- Dragon Well: was 45, change -100, now -55 (NEGATIVE!) +``` + +--- + +## Exercise 5: Build runSequentially ⭐⭐ + +Create a utility function that runs delayed operations in sequence: + +```js +function runSequentially(tasks, finalCallback) { + // tasks is an array of functions + // each function signature: (done) => { ... done(); } + // run them one after another + // when all done, call finalCallback +} +``` + +Test it: + +```js +const tasks = [ + done => setTimeout(() => { console.log("Task 1"); done(); }, 300), + done => setTimeout(() => { console.log("Task 2"); done(); }, 200), + done => setTimeout(() => { console.log("Task 3"); done(); }, 100) +]; + +runSequentially(tasks, () => { + console.log("All tasks complete!"); +}); +``` + +Expected output (in order, despite different delays): +``` +Task 1 +Task 2 +Task 3 +All tasks complete! +``` + +> 💡 This is challenging! Hint: use recursion or track an index. Each task's `done` callback should trigger the next task. + +--- + +## Submission + +Create a folder `week2-assignment/` with: +- `exercise1.js` +- `exercise2.js` +- `exercise3.js` +- `exercise4.js` + `inventory-updates.json` +- `exercise5.js` + +Each file should be runnable with `node exerciseN.js`. + +```js +// Example structure for exercise1.js +import { teas } from "../data/teas.js"; + +function teasByOrigin(teas) { + return teas.reduce((acc, tea) => { + // ... + }, {}); +} + +console.log(teasByOrigin(teas)); +``` diff --git a/courses/backend/advanced-javascript/week2/preparation.md b/courses/backend/advanced-javascript/week2/preparation.md new file mode 100644 index 00000000..3fb69a98 --- /dev/null +++ b/courses/backend/advanced-javascript/week2/preparation.md @@ -0,0 +1,260 @@ +# Week 2 Preparation + +Read this introduction before class. It explains the concepts we'll practice during the session. + +--- + +## Why Backends Need Async + +Imagine a tea shop that gets 50 online orders at once. Each order requires: +1. Looking up the tea in the database (10ms) +2. Checking inventory (10ms) +3. Processing payment (200ms) + +If the server waited for each step to complete before moving on, one order takes 220ms. Fifty orders? 11 seconds. The 50th customer waits 11 seconds just for their order to *start* processing. + +Real backends don't work this way. Instead of waiting, they say: "Start this operation, and call me back when you're done." While waiting for one database query, they can start processing other requests. + +This is **asynchronous programming** - the foundation of how backends handle multiple requests efficiently. + +--- + +## Functions Are Values + +Before we get to async, we need to understand something fundamental: in JavaScript, functions are values. Just like strings or numbers, you can: + +- Assign them to variables +- Store them in arrays +- Pass them as arguments +- Return them from other functions + +```js +// Assign a function to a variable +const greet = function(name) { + return `Hello, ${name}!`; +}; + +// Call it through the variable +console.log(greet("Alice")); // "Hello, Alice!" + +// Pass it to another function +function runTwice(fn, value) { + console.log(fn(value)); + console.log(fn(value)); +} + +runTwice(greet, "Bob"); +// "Hello, Bob!" +// "Hello, Bob!" +``` + +This is what makes callbacks possible. + +> 📚 **Glossary: First-Class Functions** +> When functions can be treated like any other value (assigned, passed, returned), they're called "first-class citizens." JavaScript has first-class functions. + +--- + +## Callbacks + +A **callback** is a function you pass to another function, to be called later. + +You've already used callbacks! Every time you write `.map()`, `.filter()`, or `.forEach()`, you pass a callback: + +```js +teas.map(tea => tea.name); +// ^^^^^^^^^^^^^^^^ +// This is a callback. map calls it for each item. +``` + +The difference between these callbacks and async callbacks is *when* they're called: + +- **Synchronous callbacks**: Called immediately, during the function's execution (like in `map`) +- **Asynchronous callbacks**: Called later, after some operation completes (like after a file loads) + +> 📚 **Glossary: Callback** +> A function passed to another function to be called at some point. The receiving function decides when to "call back" your function. + +--- + +## Synchronous vs Asynchronous + +**Synchronous** code runs line by line. Each line waits for the previous one to finish: + +```js +console.log("First"); +console.log("Second"); +console.log("Third"); +// Output: First, Second, Third (always in this order) +``` + +**Asynchronous** code doesn't wait. You start an operation and provide a callback for when it's done: + +```js +console.log("First"); + +setTimeout(() => { + console.log("Second"); +}, 1000); + +console.log("Third"); + +// Output: First, Third, Second +// "Second" appears last because setTimeout waits 1000ms +``` + +The key insight: while waiting for the timeout, JavaScript continues running other code. It doesn't freeze. + +> 📚 **Glossary: Synchronous vs Asynchronous** +> **Synchronous**: Code runs in sequence, each operation blocks until complete. +> **Asynchronous**: Code continues running while waiting for operations to complete. Callbacks handle the results when ready. + +--- + +## setTimeout: The Simplest Async + +`setTimeout` is the simplest async function. It waits a specified time, then calls your callback: + +```js +setTimeout(callback, delayInMilliseconds); +``` + +We use it to simulate database delays: + +```js +function simulatedDbLookup(id, callback) { + setTimeout(() => { + const result = { id, name: "Found item" }; + callback(result); + }, 500); // Simulates 500ms database delay +} + +console.log("Starting lookup..."); +simulatedDbLookup(42, result => { + console.log("Got result:", result); +}); +console.log("Lookup started, doing other work..."); + +// Output: +// Starting lookup... +// Lookup started, doing other work... +// Got result: { id: 42, name: "Found item" } (after 500ms) +``` + +--- + +## Node.js File Operations + +Real backend async often involves file operations. Node.js provides `fs.readFile`: + +```js +import fs from "fs"; + +fs.readFile("./data.json", "utf8", (error, data) => { + if (error) { + console.error("Failed to read file:", error.message); + return; + } + console.log("File contents:", data); +}); +``` + +Notice the callback signature: `(error, data)`. This is the **error-first callback** pattern - a Node.js convention. + +> 📚 **Glossary: Error-First Callback** +> Node.js convention where callbacks receive `(error, result)`. If `error` is truthy, something went wrong. If `error` is null/undefined, proceed with `result`. This pattern ensures consistent error handling across all async operations. + +--- + +## reduce: Aggregating Data + +We deferred `reduce` from Week 1 because it's the most complex array method. Now that you understand `map` and `filter`, you're ready. + +`reduce` builds up a single value from an array: + +```js +// Sum all numbers +const numbers = [1, 2, 3, 4, 5]; +const total = numbers.reduce((accumulator, current) => { + return accumulator + current; +}, 0); // 0 is the initial value +// total = 15 +``` + +The **accumulator** is the value being built up. It starts as your initial value and becomes your final result: + +```js +// Count teas by type +const countByType = teas.reduce((acc, tea) => { + acc[tea.type] = (acc[tea.type] || 0) + 1; + return acc; +}, {}); // Start with empty object + +// { green: 6, black: 5, herbal: 4, oolong: 3, white: 2 } +``` + +> 📚 **Glossary: Accumulator** +> In `reduce`, the value being built up across iterations. It starts as the initial value you provide and transforms with each item until it becomes your final result. + +--- + +## Pure Functions and Immutability + +Two concepts become important with async code: + +**Pure function**: A function that always returns the same output for the same input, and has no side effects. + +```js +// Pure - no side effects, same input = same output +function double(x) { + return x * 2; +} + +// Impure - modifies external state +let total = 0; +function addToTotal(x) { + total += x; // side effect! + return total; +} +``` + +**Immutability**: Creating new data instead of modifying existing data. + +```js +// Mutable - modifies original +teas.push(newTea); // Changes the teas array + +// Immutable - creates new array +const newTeas = [...teas, newTea]; // Original unchanged +``` + +Why do these matter? In async code, operations can overlap. If two callbacks modify the same data, bugs happen. Pure functions and immutability make async code predictable. + +> 📚 **Glossary: Pure Function** +> A function that: (1) always returns the same output for the same input, (2) has no side effects. Makes code predictable and testable. + +> 📚 **Glossary: Immutability** +> Creating new data instead of modifying existing data. `map` and `filter` are immutable - they return new arrays without changing the original. + +--- + +## Summary + +| Term | Meaning | +|------|---------| +| First-class functions | Functions treated as values | +| Callback | Function passed to another function, called later | +| Synchronous | Code runs in sequence, blocking | +| Asynchronous | Code continues while waiting, uses callbacks | +| Error-first callback | Node pattern: `(error, result)` | +| Accumulator | The building-up value in reduce | +| Pure function | Same input = same output, no side effects | +| Immutability | Create new data, don't modify existing | + +--- + +## Pre-Reading (Optional) + +- [MDN: Array.prototype.reduce()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) +- [MDN: setTimeout()](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout) +- [Node.js: fs.readFile()](https://nodejs.org/api/fs.html#fsreadfilepath-options-callback) diff --git a/courses/backend/advanced-javascript/week2/session-materials/exercises.md b/courses/backend/advanced-javascript/week2/session-materials/exercises.md new file mode 100644 index 00000000..bb8afa11 --- /dev/null +++ b/courses/backend/advanced-javascript/week2/session-materials/exercises.md @@ -0,0 +1,354 @@ +# Week 2 Exercises: Callbacks & Delayed Execution + +All exercises use the tea shop data. Start by loading it: + +```js +import { teas } from "../../data/teas.js"; +``` + +--- + +## Part 1: Functions as Values + +Functions in JavaScript are "first-class citizens" - they can be assigned to variables, stored in arrays, and passed around like any other value. + +
+📚 Recall: First-Class Functions + +A first-class function can be: assigned to a variable, passed as an argument, returned from a function, stored in data structures. JavaScript treats functions like any other value. +
+ +### Exercise 1 + +Create a function that logs a tea's name and origin in the format `"Sencha (Japan)"`. Assign it to a variable called `logTea`. Call it with the first tea in the array. + +```js +const logTea = function(tea) { + // your code +}; + +logTea(teas[0]); // should log: "Sencha (Japan)" +``` + +### Exercise 2 + +Create a function called `functionRunner` that takes a function as a parameter and calls it. + +```js +function functionRunner(fn) { + // call the function that was passed in +} + +// Test it: +functionRunner(function() { + console.log("I was called!"); +}); + +// Also test with a function variable: +const sayHello = function() { + console.log("Hello!"); +}; +functionRunner(sayHello); +``` + +> 💡 This is the core of callbacks: passing a function to another function that calls it. + +### Exercise 3 + +Create an array containing three different functions. Each function should log something different. Loop through the array and call each function. + +```js +const functions = [ + function() { console.log("First"); }, + // add two more +]; + +for (let i = 0; i < functions.length; i++) { + functions[i](); // call each function +} +``` + +### Exercise 4 ⭐ + +Create a function `createGreeter(greeting)` that returns a new function. The returned function should take a name and log the greeting with the name. + +```js +const sayHello = createGreeter("Hello"); +const sayHi = createGreeter("Hi"); + +sayHello("Alice"); // "Hello, Alice!" +sayHi("Bob"); // "Hi, Bob!" +``` + +> 💡 This pattern is called a "function factory" - a function that creates and returns other functions. + +--- + +## Part 2: reduce + +`reduce` builds up a single value from an array. It's the most powerful array method. + +
+📚 Recall: Accumulator + +The accumulator is the value being "built up" through each iteration. It starts as your initial value (the second argument to reduce) and ends as your final result. +
+ +### Exercise 5 + +Use `reduce` to calculate the total `stockCount` across all teas. + +```js +const totalStock = teas.reduce((sum, tea) => { + // return the new sum +}, 0); + +console.log(totalStock); // sum of all stockCount values +``` + +### Exercise 6 + +Calculate the total inventory value: the sum of `pricePerGram * stockCount` for each tea. + +```js +const inventoryValue = teas.reduce(/* ... */); +console.log(inventoryValue); +``` + +### Exercise 7 + +Use `reduce` to count how many teas of each type exist. + +```js +const countByType = teas.reduce((counts, tea) => { + // increment counts[tea.type] + // hint: counts[tea.type] might be undefined the first time +}, {}); + +console.log(countByType); +// Expected: { green: 6, black: 6, herbal: 4, oolong: 2, white: 2 } +``` + +### Exercise 8 ⭐ + +Use `reduce` to group tea names by their origin country. + +```js +const groupedByOrigin = teas.reduce(/* ... */); +console.log(groupedByOrigin); +// Expected: { Japan: ["Sencha", "Matcha", ...], China: [...], ... } +``` + +> 💡 This solves Exercise 4 from Week 1's assignment! Now you know the proper way to implement it. + +--- + +## Part 3: Building Higher-Order Functions + +Let's build our own versions of array methods to understand how callbacks work under the hood. + +
+📚 Recall: Callback + +A callback is a function you pass to another function, to be called at some point. In `teas.map(tea => tea.name)`, the arrow function IS the callback - map calls it for each item. +
+ +### Exercise 9 + +Create your own `myForEach(array, callback)` function that works like the built-in `forEach`. + +```js +function myForEach(array, callback) { + // loop through array + // call callback for each item +} + +// Test it: +myForEach(teas, function(tea) { + console.log(tea.name); +}); +``` + +### Exercise 10 + +Create your own `myMap(array, callback)` function that works like the built-in `map`. + +```js +function myMap(array, callback) { + const result = []; + // loop through array + // call callback for each item + // push the return value to result + return result; +} + +// Test it: +const names = myMap(teas, function(tea) { + return tea.name; +}); +console.log(names); // ["Sencha", "Earl Grey", ...] +``` + +### Exercise 11 ⭐ + +Create your own `myFilter(array, callback)` function that works like the built-in `filter`. + +```js +function myFilter(array, callback) { + // your implementation +} + +// Test it: +const organic = myFilter(teas, function(tea) { + return tea.organic; +}); +console.log(organic.length); // number of organic teas +``` + +> 💡 Building these yourself demystifies array methods. They're just functions that call your callback at the right time. + +--- + +## Part 4: setTimeout & Delayed Callbacks + +Now the twist: what if the callback runs *later*? `setTimeout` is the simplest example - it waits, then calls your callback. + +
+📚 Recall: Delayed Execution + +With delayed execution, the code doesn't wait. "Start this operation, continue running other code, call the callback when done." Unlike regular code which runs line-by-line and blocks. +
+ +### Exercise 12 + +What order will these console.logs appear? Write your prediction first, then run the code to check. + +```js +console.log("1. Starting"); + +setTimeout(function() { + console.log("2. Timeout done"); +}, 1000); + +console.log("3. Continuing"); +``` + +### Exercise 13 + +Create a function `runAfterDelay(delay, callback)` that waits `delay` milliseconds, then calls the callback. + +```js +function runAfterDelay(delay, callback) { + // use setTimeout +} + +// Test it: +runAfterDelay(2000, function() { + console.log("This runs after 2 seconds"); +}); + +runAfterDelay(1000, function() { + console.log("This runs after 1 second"); +}); + +console.log("This runs immediately"); +``` + +What order do the three messages appear? + +### Exercise 14 + +Create a function `findTeaById(id, callback)` that simulates a database lookup with a 500ms delay. + +```js +function findTeaById(id, callback) { + // Use setTimeout to wait 500ms + // Inside the timeout: find the tea by id, then call the callback with it +} + +// Test it: +console.log("Looking up tea..."); +findTeaById(3, function(tea) { + console.log("Found:", tea.name); +}); +console.log("Request sent, waiting..."); +``` + +Before running: predict the output order. Then run it to check. + +Expected output order: +1. "Looking up tea..." +2. "Request sent, waiting..." +3. (after 500ms) "Found: Dragon Well" + +### Exercise 15 ⭐ + +Call `findTeaById` three times in a row with different IDs. Notice that all three requests start at the same time - they don't wait for each other. + +```js +findTeaById(1, function(tea) { console.log("Got:", tea.name); }); +findTeaById(5, function(tea) { console.log("Got:", tea.name); }); +findTeaById(10, function(tea) { console.log("Got:", tea.name); }); +console.log("All requests sent!"); +``` + +> 💡 "All requests sent!" appears first, then all three results appear together after 500ms. This is how backends handle multiple requests efficiently. + +--- + +## Part 5: File System + +Node.js file operations use callbacks. `fs.readFile` takes a callback that runs when the file is loaded. + +
+📚 Recall: Error-First Callback + +Node.js convention: callbacks receive `(error, result)`. If error exists, something went wrong - handle it and return early. If error is null/undefined, proceed with result. +
+ +### Exercise 16 + +First, create a file called `orders.json` in the same folder with this content: + +```json +[ + { "id": 1, "customerId": 101, "items": [{ "teaId": 1, "grams": 100 }] }, + { "id": 2, "customerId": 102, "items": [{ "teaId": 3, "grams": 50 }, { "teaId": 8, "grams": 30 }] }, + { "id": 3, "customerId": 103, "items": [{ "teaId": 5, "grams": 200 }] } +] +``` + +Then write code that reads the file and logs how many orders there are: + +```js +import fs from "fs"; + +fs.readFile('./orders.json', {encoding: 'utf8'}, function(error, data) { + if (error) { + console.error(error); + return; + } + // 1. Parse the JSON string into an array + // 2. Log the number of orders +}); +``` + +Try to point to a non-existent file to see how the error handling works. + +Expected output: `Number of orders: 3` + +### Exercise 17 ⭐⭐ + +Building on Exercise 16, after reading the orders: +1. For each order, look up the tea prices from the teas array +2. Calculate the total value of each order (`pricePerGram * grams`) +3. Log each order's total + +```js +// Expected output: +// Order 1: 12.00 DKK (1 item) +// Order 2: 26.00 DKK (2 items) +// Order 3: 36.00 DKK (1 item) +``` + +> 💡 You'll need to find each tea by ID, then multiply pricePerGram by grams. diff --git a/courses/backend/advanced-javascript/week2/session-plan.md b/courses/backend/advanced-javascript/week2/session-plan.md new file mode 100644 index 00000000..0033c430 --- /dev/null +++ b/courses/backend/advanced-javascript/week2/session-plan.md @@ -0,0 +1,150 @@ +# Session Plan (Week 2: Callbacks & Delayed Execution) + +> This guide is for mentors. It outlines how to run the session, what to emphasize, and why we introduce certain terminology. + +--- + +## Session Goals + +By the end of this session, trainees should: + +1. Understand functions as first-class values +2. Use `reduce` confidently for aggregation +3. Build their own higher-order functions (myForEach, myMap) +4. Understand how delayed callbacks work (setTimeout, fs.readFile) +5. Write callbacks for `setTimeout` and `fs.readFile` +6. Understand the error-first callback pattern + +--- + +## Glossary Goals + +| Term | Why we introduce it | +|------|---------------------| +| **Callback** | Central concept. Week 1 used them implicitly; now we name them explicitly. | +| **Accumulator** | Essential for understanding `reduce`. Common in documentation. | +| **Pure function** | Foundation for understanding why bugs with delayed code happen. | +| **Immutability** | Connects to state management and safety with delayed code. | +| **Error-first callback** | Node.js convention. They'll see this pattern constantly. | +| **Delayed execution** | Fundamental concept for backend development. | + +--- + +## Session Outline + +### 1. Introduction + +- Recall Week 1: forEach, map, filter all take functions as arguments +- "Those functions are callbacks - but they run immediately" +- This week: callbacks that run *later* +- The cafe metaphor: a waiter doesn't stand at the kitchen waiting for one order + +**Use:** [Slides](./slides/index.html) (introduction sections) + +### 2. Functions as Values + +- Functions are "first-class citizens" - treated like any other value +- Live code: assign function to variable, call via variable +- Live code: `functionRunner(fn)` that calls the passed function - core callback pattern +- Live code: array of functions, call each +- Live code: function that returns a function (factory pattern) + +**Key point:** "You can pass functions around like data. This is what makes callbacks possible." + +**Exercises:** [Part 1](./session-materials/exercises.md#part-1-functions-as-values) (exercises 1-4) + +### 3. reduce Method + +- Connect to Week 1: map transforms, filter selects, forEach does side effects +- reduce: the "build up" pattern +- Walk through the accumulator concept with a simple sum +- Live code: sum of stockCount +- Live code: group by origin (object accumulator) + +**Key point:** "reduce lets you build any single value from an array - a number, object, string, even another array." + +**Exercises:** [Part 2](./session-materials/exercises.md#part-2-reduce) (exercises 5-8) + +### Break + +### 4. Callbacks - Synchronous + +- Key insight: the function you pass to map IS a callback +- "Let's build our own forEach to see how callbacks work under the hood" +- Live code: implement `myForEach(array, callback)` +- Live code: implement `myMap(array, callback)` + +**Key point:** "Building these yourself demystifies how array methods work. They're just functions calling your callback." + +**Exercises:** [Part 3](./session-materials/exercises.md#part-3-building-higher-order-functions) (exercises 9-11) + +### 5. setTimeout & Delayed Callbacks + +- Now the twist: what if the callback runs *later*? +- Show code with surprising output order - let students predict first +- Explain: JavaScript continues running, callback runs when timer fires +- Live code: `runAfterDelay(delay, callback)` utility +- Live code: simulated database lookup with delay + +**Key point:** "The callback doesn't block. JavaScript keeps running. This is how backends serve many users at once." + +**Exercises:** [Part 4](./session-materials/exercises.md#part-4-settimeout--delayed-callbacks) (exercises 12-15) + +### Break + +### 6. File System Callbacks + +- Real backend example: reading files +- Introduce error-first pattern: `(error, data)` +- Live code: read a JSON file, parse it, use the data +- Emphasize: always check error first + +**Key point:** "Node uses error-first callbacks everywhere. Always handle errors before using data." + +**Exercises:** [Part 5](./session-materials/exercises.md#part-5-file-system) (exercises 16-17) + +### 7. Wrap-up + +- Recap: functions as values, reduce, callbacks, delayed callbacks +- Preview Week 3: "Callback nesting gets ugly. Promises solve this." +- Introduce the assignment + +--- + +## Teaching Tips + +### Managing exercises with mixed skill levels + +The exercises are intentionally numerous - students progress at different speeds. Here's how to manage this: + +1. **Let students work on Part 1** - observe how far each gets individually +2. **Solve one exercise in plenum** - pick one that weaker students struggled with but stronger students just finished. This way everyone benefits +3. **Move on to Part 2** - weaker students skip remaining Part 1 exercises (they can revisit later) +4. **Repeat** for each part + +Exercises marked with ⭐ are optional stretch goals. If your fastest students finish all exercises while others haven't solved the first one, that part needs an additional challenge - let us know, or create a PR with a creative challenging exercise! + +### The callback revelation + +Many trainees have used map/filter without realizing they're passing callbacks. Make this explicit: "You've been using callbacks all along!" + +### Show the surprising output order + +For setTimeout, always ask trainees to predict the console.log order *before* running. The surprise helps the concept stick. + +### Use the tea data + +Continue using the tea shop context. Show how reduce solves problems they encountered in Week 1 (like grouping teas by origin). + +### Connect to backend reality + +Regularly remind them: "This is why servers don't freeze when handling multiple requests. They start operations and handle results via callbacks." + +--- + +## Materials + +- [Slides](./slides/index.html) - Reveal.js presentation +- [Exercises](./session-materials/exercises.md) - In-class exercises +- [Assignment](./assignment.md) - Take-home work +- [Tea Data](../data/teas.js) - Shared dataset diff --git a/courses/backend/advanced-javascript/week2/slides/index.html b/courses/backend/advanced-javascript/week2/slides/index.html new file mode 100644 index 00000000..6bec59a8 --- /dev/null +++ b/courses/backend/advanced-javascript/week2/slides/index.html @@ -0,0 +1,386 @@ + + + + + + Week 2 - Callbacks & Delayed Execution + + + + + + + +
+
+ + +
+

Callbacks & Delayed Execution

+

Why backends can't run top-to-bottom

+

Week 2

+
+ + +
+
+

Why Async?

+
+ +
+

The Problem

+

Database queries take time

+

File reads take time

+

Network calls take time

+

If we waited, we'd serve one request at a time

+
+ +
+

The Cafe Analogy

+
+ A waiter doesn't stand at the kitchen waiting for one order.

+ They take the order, give it to the kitchen, and serve other tables while waiting. +
+
+ +
+

The Solution

+

"Start this operation..."

+

"...call me back when you're done"

+

That's a callback

+
+
+ + +
+
+

Functions as Values

+

First, we need to understand this

+
+ +
+

Functions Are Just Values

+
// Assign a function to a variable
+const greet = function(name) {
+  return `Hello, ${name}!`;
+};
+
+// Call it through the variable
+greet("Alice");  // "Hello, Alice!"
+
+ +
+

Pass Functions to Functions

+
function runTwice(fn, value) {
+  console.log(fn(value));
+  console.log(fn(value));
+}
+
+runTwice(greet, "Bob");
+// "Hello, Bob!"
+// "Hello, Bob!"
+

This is what makes callbacks possible

+
+ +
+

You Already Do This!

+
teas.map(tea => tea.name);
+//       ^^^^^^^^^^^^^^^^
+//       This IS a function passed to map
+

map, filter, forEach all take functions as arguments

+
+ +
+

Function Factories

+

A function that returns a function

+
function createGreeter(greeting) {
+  return function(name) {
+    console.log(`${greeting}, ${name}!`);
+  };
+}
+
+const sayHello = createGreeter("Hello");
+const sayHi = createGreeter("Hi");
+
+sayHello("Alice");  // "Hello, Alice!"
+sayHi("Bob");       // "Hi, Bob!"
+
+
+ + +
+
+

Callbacks

+

A function you pass, to be called later

+
+ +
+

Synchronous Callbacks

+

Called immediately, during execution

+
teas.map(tea => tea.name);
+// callback runs NOW, for each item
+
+ +
+

Asynchronous Callbacks

+

Called later, after something completes

+
setTimeout(() => {
+  console.log("Done!");
+}, 1000);
+// callback runs LATER, after 1000ms
+
+
+ + +
+
+

reduce

+

The "build up" method

+
+ +
+

The Mental Model

+

N items1 value

+

Sum, count, group, aggregate

+
+ +
+

The Accumulator

+
const total = numbers.reduce((accumulator, current) => {
+  return accumulator + current;
+}, 0);
+// ^-- initial value
+

The accumulator builds up through each iteration

+
+ +
+

Sum Example

+
const total = teas.reduce((sum, tea) => {
+  return sum + tea.stockCount;
+}, 0);
+// Total stock across all teas
+
+ +
+

Grouping Example

+
const byOrigin = teas.reduce((groups, tea) => {
+  if (!groups[tea.origin]) {
+    groups[tea.origin] = [];
+  }
+  groups[tea.origin].push(tea.name);
+  return groups;
+}, {});
+// { Japan: ["Sencha", ...], China: [...] }
+
+
+ + +
+
+

setTimeout

+

The simplest async function

+
+ +
+

Surprising Output

+
console.log("1. First");
+
+setTimeout(() => {
+  console.log("2. Second");
+}, 1000);
+
+console.log("3. Third");
+

What order do these print?

+
+ +
+

The Answer

+
// Output:
+// 1. First
+// 3. Third
+// 2. Second  (after 1 second)
+

JavaScript doesn't wait. It keeps going.

+
+ +
+

Simulating Database Delay

+
function findTeaById(id, callback) {
+  setTimeout(() => {
+    const tea = teas.find(t => t.id === id);
+    callback(tea);
+  }, 500);  // 500ms "database" delay
+}
+
+findTeaById(3, tea => {
+  console.log("Found:", tea.name);
+});
+
+
+ + +
+
+

File System

+

Real backend async

+
+ +
+

Reading Files

+
import fs from "fs";
+
+fs.readFile("./data.json", "utf8", (error, data) => {
+  if (error) {
+    console.error("Failed:", error.message);
+    return;
+  }
+  console.log("Got data:", data);
+});
+
+ +
+

Error-First Pattern

+

Node.js convention: (error, result)

+
fs.readFile(path, (error, data) => {
+  if (error) {
+    // Handle error FIRST
+    return;
+  }
+  // Then use data
+});
+

Always check error before using result

+
+
+ + +
+
+

Build Your Own

+

Understanding how it works

+
+ +
+

Build myForEach

+
function myForEach(array, callback) {
+  for (let i = 0; i < array.length; i++) {
+    callback(array[i], i, array);
+  }
+}
+
+myForEach(teas, tea => console.log(tea.name));
+

That's all forEach is!

+
+ +
+

Build myMap

+
function myMap(array, callback) {
+  const result = [];
+  for (let i = 0; i < array.length; i++) {
+    result.push(callback(array[i], i, array));
+  }
+  return result;
+}
+
+myMap(teas, tea => tea.name);  // ["Sencha", ...]
+
+
+ + +
+
+

Summary

+
+ +
+

Key Concepts

+

Functions as values - pass them around

+

Callbacks - functions called later

+

reduce - build up a single value

+

Async - don't wait, use callbacks

+
+ +
+

Next Week

+

Callback nesting gets messy...

+
doFirst(result1 => {
+  doSecond(result1, result2 => {
+    doThird(result2, result3 => {
+      // "Callback hell"
+    });
+  });
+});
+

Promises solve this!

+
+ +
+

Time to Practice

+

🍵

+
+
+ +
+
+ + + + + + + diff --git a/courses/backend/advanced-javascript/week3/README.md b/courses/backend/advanced-javascript/week3/README.md new file mode 100644 index 00000000..d0b57360 --- /dev/null +++ b/courses/backend/advanced-javascript/week3/README.md @@ -0,0 +1,48 @@ +# Promises & async/await (Week 3) + +Last week you learned about callbacks - functions that run later. You saw how nesting callbacks gets messy ("callback hell"). This week, you'll learn Promises and async/await - cleaner ways to handle asynchronous operations. + +This week you'll work with a real API: the Tea Shop backend. You'll fetch data, handle errors, and chain operations - just like you would when building real backend services. + +## Contents + +- [Preparation](./preparation.md) +- [Slides](./slides/index.html) +- [Session Plan](./session-plan.md) (for mentors) +- [Session Materials](./session-materials/) +- [Assignment](./assignment.md) + +## Learning Goals + +By the end of this session, you will be able to: + +- [ ] Understand why Promises exist (solving callback hell) +- [ ] Consume Promises with `.then()` and `.catch()` +- [ ] Chain multiple `.then()` calls +- [ ] Create your own Promises with `new Promise()` +- [ ] Use `async`/`await` syntax for cleaner code +- [ ] Handle errors with `try`/`catch` in async functions +- [ ] Run multiple Promises in parallel with `Promise.all()` +- [ ] Fetch data from APIs using `fetch()` + +```js +// From callback hell... +validateOrder(order, (err, valid) => { + if (err) return handleError(err); + calculateTotal(order, (err, total) => { + if (err) return handleError(err); + checkStock(order, (err, inStock) => { + if (err) return handleError(err); + // deeply nested... + }); + }); +}); + +// ...to clean async/await +async function processOrder(order) { + const valid = await validateOrder(order); + const total = await calculateTotal(order); + const inStock = await checkStock(order); + return { valid, total, inStock }; +} +``` diff --git a/courses/backend/advanced-javascript/week3/assignment.md b/courses/backend/advanced-javascript/week3/assignment.md new file mode 100644 index 00000000..fad18445 --- /dev/null +++ b/courses/backend/advanced-javascript/week3/assignment.md @@ -0,0 +1,264 @@ +# Week 3 Assignment + +Build a Tea Shop CLI tool that interacts with the Tea Shop API. + +```js +const API_BASE = "https://tea-api-787553294298.europe-west1.run.app/api"; +``` + +--- + +## Exercise 1: Tea Search + +Create a function that searches for teas by name: + +```js +async function searchTeas(query) { + // 1. Fetch all teas from the API + // 2. Filter to teas where name includes query (case-insensitive) + // 3. Return array of matching tea objects +} + +// Test it: +searchTeas("pearl").then(teas => { + console.log("Search results for 'pearl':"); + teas.forEach(tea => console.log(`- ${tea.name}`)); +}); +``` + +Expected output: +``` +Search results for 'pearl': +- Jasmine Pearl +- Royal Kenya Pearl +- Imperial China Pearl +- Mountain Sri Lanka Pearl +``` + +--- + +## Exercise 2: Tea Details + +Create a function that gets full details for a tea, including its current stock: + +```js +async function getTeaDetails(id) { + // Fetch tea and inventory in PARALLEL using Promise.all + // Return combined object: { ...tea, stock: number } +} + +// Test it: +getTeaDetails(1).then(tea => { + console.log(`${tea.name} (${tea.origin})`); + console.log(`Price: ${tea.pricePerGram} DKK/gram`); + console.log(`Stock: ${tea.stock} grams`); + console.log(`Value: ${(tea.pricePerGram * tea.stock).toFixed(2)} DKK`); +}); +``` + +--- + +## Exercise 3: Order Calculator ⭐ + +Create a function that calculates the total for an order: + +```js +async function calculateOrderTotal(items) { + // items is an array of { teaId, grams } + // 1. Fetch all teas from API + // 2. For each item, find the tea and calculate price + // 3. Return total price + + // Bonus: throw an error if a teaId doesn't exist +} + +const order = [ + { teaId: 1, grams: 100 }, + { teaId: 3, grams: 50 }, + { teaId: 8, grams: 200 } +]; + +calculateOrderTotal(order) + .then(total => console.log(`Order total: ${total.toFixed(2)} DKK`)) + .catch(err => console.error("Error:", err.message)); +``` + +--- + +## Exercise 4: Stock Check ⭐ + +Create a function that checks if all items in an order are in stock: + +```js +async function checkOrderStock(items) { + // items is an array of { teaId, grams } + // 1. Fetch inventory from API + // 2. Check if each item has enough stock + // 3. Return { inStock: boolean, shortages: [...] } +} + +const largeOrder = [ + { teaId: 1, grams: 100 }, + { teaId: 2, grams: 500 }, // might be out of stock + { teaId: 3, grams: 9999 } // definitely out of stock +]; + +checkOrderStock(largeOrder).then(result => { + if (result.inStock) { + console.log("All items in stock!"); + } else { + console.log("Shortages:"); + result.shortages.forEach(s => { + console.log(`- ${s.name}: need ${s.needed}, have ${s.available}`); + }); + } +}); +``` + +--- + +## Exercise 5: Full Order Flow ⭐⭐ + +Combine everything into a complete order processing flow: + +```js +async function processOrder(items) { + console.log("Processing order...\n"); + + // Step 1: Validate items exist + console.log("1. Validating items..."); + // Fetch all teas, check all teaIds exist + // If any don't exist, throw error + + // Step 2: Check stock + console.log("2. Checking stock..."); + const stockResult = await checkOrderStock(items); + if (!stockResult.inStock) { + throw new Error("Items out of stock"); + } + + // Step 3: Calculate total + console.log("3. Calculating total..."); + const total = await calculateOrderTotal(items); + + // Step 4: Create order summary + console.log("4. Creating summary...\n"); + + return { + items: items.length, + total, + status: "ready" + }; +} + +const myOrder = [ + { teaId: 1, grams: 50 }, + { teaId: 5, grams: 100 } +]; + +processOrder(myOrder) + .then(result => { + console.log("Order ready!"); + console.log(`Items: ${result.items}`); + console.log(`Total: ${result.total.toFixed(2)} DKK`); + }) + .catch(err => { + console.error("Order failed:", err.message); + }); +``` + +--- + +## Exercise 6: Authenticated Orders ⭐⭐ + +Create functions to work with the authenticated orders endpoint. + +First, sign up for an account, then use login to get a token: + +> ⚠️ Use a **dummy email and password** — not your real ones! This is a practice API with no security guarantees. + +```js +// Helper: sign up (only needed once) +async function signup(email, password) { + const response = await fetch(`${API_BASE}/auth/signup`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }) + }); + + if (!response.ok) throw new Error("Signup failed"); + return response.json(); +} + +// Helper: login and get token +async function getAuthToken() { + const response = await fetch(`${API_BASE}/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: "yourname@example.com", + password: "mypassword" + }) + }); + + if (!response.ok) throw new Error("Login failed"); + const data = await response.json(); + return data.token; +} + +// Create a new order (POST /orders) +async function createOrder(items) { + const token = await getAuthToken(); + + // POST to /orders with: + // - Authorization header with token + // - Body with items array + + // Return the created order +} + +// Get all orders (GET /orders) +async function getMyOrders() { + const token = await getAuthToken(); + + // GET /orders with Authorization header + // Return array of orders +} + +// Test (sign up first, then create and list orders): +signup("yourname@example.com", "mypassword") + .catch(() => {}) // ignore if already signed up + .then(() => createOrder([{ teaId: 1, grams: 100 }])) + .then(order => console.log("Created order:", order.id)) + .then(() => getMyOrders()) + .then(orders => console.log("All orders:", orders.length)); +``` + +--- + +## Submission + +Create a folder `week3-assignment/` with: +- `exercise1.js` - Tea search +- `exercise2.js` - Tea details +- `exercise3.js` - Order calculator +- `exercise4.js` - Stock check +- `exercise5.js` - Full order flow +- `exercise6.js` - Authenticated orders (optional) + +Each file should be runnable with `node exerciseN.js`. + +```js +// Example structure for exercise1.js +const API_BASE = "https://tea-api-787553294298.europe-west1.run.app/api"; + +async function searchTeas(query) { + // ... +} + +// Run the function +searchTeas("pearl").then(results => { + console.log("Found:", results.length, "teas"); + results.forEach(tea => console.log(`- ${tea.name}`)); +}); +``` diff --git a/courses/backend/advanced-javascript/week3/preparation.md b/courses/backend/advanced-javascript/week3/preparation.md new file mode 100644 index 00000000..fbce6dc1 --- /dev/null +++ b/courses/backend/advanced-javascript/week3/preparation.md @@ -0,0 +1,275 @@ +# Week 3 Preparation + +Read this introduction before class. It explains the concepts we'll practice during the session. + +--- + +## The Problem: Callback Hell + +Last week you wrote code like this: + +```js +validateOrder(order, (err, validation) => { + if (err) return console.error(err); + + calculateTotal(order, (err, total) => { + if (err) return console.error(err); + + checkStock(order, (err, stock) => { + if (err) return console.error(err); + + processPayment(order, total, (err, receipt) => { + if (err) return console.error(err); + // Finally done... but look at this pyramid! + }); + }); + }); +}); +``` + +This is called **callback hell** - deeply nested callbacks that are hard to read and maintain. Each new step adds another level of indentation. Error handling is repetitive. The code grows sideways instead of downward. + +Promises solve this. + +--- + +## What is a Promise? + +A **Promise** is an object representing an operation that hasn't completed yet. Think of it like an order ticket at a cafe: + +1. You place an order and receive a ticket (the Promise) +2. The ticket promises you'll get coffee eventually +3. Two outcomes: you get your coffee (resolved) or they're out (rejected) + +```js +const orderTicket = placeOrder("latte"); +// orderTicket is a Promise - the coffee isn't ready yet + +orderTicket + .then(coffee => console.log("Got my", coffee)) + .catch(error => console.log("No coffee:", error)); +``` + +> 📚 **Glossary: Promise** +> An object representing the eventual completion (or failure) of an asynchronous operation. A Promise is always in one of three states: pending, fulfilled (resolved), or rejected. + +--- + +## Consuming Promises: .then() and .catch() + +When you have a Promise, you can attach callbacks using `.then()` and `.catch()`: + +```js +const API_BASE = "https://tea-api-787553294298.europe-west1.run.app/api/v1"; + +fetch(`${API_BASE}/teas`) + .then(response => response.json()) + .then(teas => { + console.log("Got", teas.length, "teas"); + }) + .catch(error => { + console.error("Failed:", error.message); + }); +``` + +- `.then()` runs when the Promise **resolves** (succeeds) +- `.catch()` runs when the Promise **rejects** (fails) + +> 📚 **Glossary: resolve / reject** +> A Promise "resolves" when the operation succeeds, passing the result to `.then()`. It "rejects" when the operation fails, passing the error to `.catch()`. + +--- + +## Chaining Promises + +The magic of Promises: `.then()` returns a new Promise, so you can chain them: + +```js +const API_BASE = "https://tea-api-787553294298.europe-west1.run.app/api/v1"; + +fetch(`${API_BASE}/teas/1`) + .then(response => response.json()) + .then(tea => { + console.log("Tea:", tea.name); + return fetch(`${API_BASE}/inventory/${tea.id}`); + }) + .then(response => response.json()) + .then(inventory => { + console.log("Stock:", inventory.stockCount); + }) + .catch(error => { + // Catches errors from ANY step above + console.error("Something failed:", error.message); + }); +``` + +No more nesting! The code flows downward, and one `.catch()` handles all errors. + +> 📚 **Glossary: then / catch** +> `.then(callback)` schedules a callback to run when a Promise resolves. `.catch(callback)` schedules a callback to run when a Promise rejects. Both return new Promises, enabling chaining. + +--- + +## Creating Promises + +You can create your own Promises using `new Promise()`: + +```js +function wait(ms) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, ms); + }); +} + +wait(2000).then(() => console.log("2 seconds passed!")); +``` + +The Promise constructor takes a function with two parameters: +- `resolve` - call this when the operation succeeds +- `reject` - call this when the operation fails + +```js +function findTeaById(id) { + return new Promise((resolve, reject) => { + setTimeout(() => { + const tea = teas.find(t => t.id === id); + if (tea) { + resolve(tea); + } else { + reject(new Error(`Tea ${id} not found`)); + } + }, 500); + }); +} +``` + +--- + +## async/await: Even Cleaner Syntax + +`async`/`await` is syntactic sugar over Promises - it makes async code look synchronous: + +```js +const API_BASE = "https://tea-api-787553294298.europe-west1.run.app/api/v1"; + +async function getTea(id) { + const response = await fetch(`${API_BASE}/teas/${id}`); + const tea = await response.json(); + return tea; +} +``` + +- `async` before a function makes it return a Promise +- `await` pauses execution until a Promise resolves +- The code reads top-to-bottom, like synchronous code + +> 📚 **Glossary: async / await** +> `async` declares a function that returns a Promise. `await` pauses execution until a Promise settles, then returns its resolved value. Only works inside `async` functions. + +--- + +## Error Handling with try/catch + +With async/await, you handle errors using `try`/`catch`: + +```js +const API_BASE = "https://tea-api-787553294298.europe-west1.run.app/api/v1"; + +async function getTea(id) { + try { + const response = await fetch(`${API_BASE}/teas/${id}`); + if (!response.ok) { + throw new Error(`HTTP error: ${response.status}`); + } + const tea = await response.json(); + return tea; + } catch (error) { + console.error("Failed to get tea:", error.message); + return null; + } +} +``` + +This is the same pattern as synchronous error handling - familiar and readable. + +--- + +## Promise.all: Parallel Operations + +Sometimes you want to run multiple operations at once: + +```js +const API_BASE = "https://tea-api-787553294298.europe-west1.run.app/api/v1"; + +// Sequential - slow (3 requests, one after another) +const tea1 = await fetch(`${API_BASE}/teas/1`).then(r => r.json()); +const tea2 = await fetch(`${API_BASE}/teas/2`).then(r => r.json()); +const tea3 = await fetch(`${API_BASE}/teas/3`).then(r => r.json()); + +// Parallel - fast (3 requests at the same time) +const [tea1, tea2, tea3] = await Promise.all([ + fetch(`${API_BASE}/teas/1`).then(r => r.json()), + fetch(`${API_BASE}/teas/2`).then(r => r.json()), + fetch(`${API_BASE}/teas/3`).then(r => r.json()), +]); +``` + +`Promise.all()` takes an array of Promises and returns a Promise that resolves when ALL of them resolve. + +> 📚 **Glossary: Promise.all** +> Takes an array of Promises and returns a single Promise that resolves when all input Promises resolve (with an array of results), or rejects when any input Promise rejects. + +--- + +## Summary + +| Term | Meaning | +|---------|-------------| +| Promise | Object representing future completion/failure | +| resolve | Promise succeeded, pass result to .then() | +| reject | Promise failed, pass error to .catch() | +| .then() | Handle successful result | +| .catch() | Handle error | +| async | Makes function return a Promise | +| await | Pause until Promise resolves | +| Promise.all | Run multiple Promises in parallel | + +--- + +## Pre-Reading (Optional) + +If you want to go deeper, read these MDN pages: + +- [Using Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises) +- [async function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function) +- [Promise.all()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) +- [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) + +--- + +## The Tea Shop API + +During class you'll work with a real API: + +``` +Base URL: https://tea-api-787553294298.europe-west1.run.app/api/v1 + +Endpoints: +GET /teas - List all teas +GET /teas/:id - Get single tea +GET /inventory - Get stock levels +POST /auth/login - Get auth token +GET /orders - List orders (requires auth) +POST /orders - Create order (requires auth) +``` + +Example: +```js +const API_BASE = "https://tea-api-787553294298.europe-west1.run.app/api/v1"; + +const response = await fetch(`${API_BASE}/teas`); +const teas = await response.json(); +console.log(teas); +``` diff --git a/courses/backend/advanced-javascript/week3/session-materials/exercises.md b/courses/backend/advanced-javascript/week3/session-materials/exercises.md new file mode 100644 index 00000000..d24616d0 --- /dev/null +++ b/courses/backend/advanced-javascript/week3/session-materials/exercises.md @@ -0,0 +1,416 @@ +# Week 3 Exercises: Promises & async/await + +All exercises use the Tea Shop API: + +```js +const API_BASE = "https://tea-api-787553294298.europe-west1.run.app/api"; +``` + +--- + +## Part 1: Consuming Promises + +`fetch()` returns a Promise. Let's learn to use it. + +
+📚 Recall: Promise + +A Promise represents a future value. It's either pending (waiting), fulfilled (success), or rejected (error). Use `.then()` for success, `.catch()` for errors. +
+ +### Exercise 1 + +Fetch all teas from the API and log how many there are. + +```js +fetch(`${API_BASE}/teas`) + .then(response => { + // response.json() also returns a Promise! + }) + .then(teas => { + // log the count + }); +``` + +Expected output: `Found 50 teas` + +> 💡 Notice: `response.json()` itself returns a Promise - that's why you need a second `.then()` to get the actual data. + +### Exercise 2 + +Fetch a single tea by ID and log its name and origin. + +```js +fetch(`${API_BASE}/teas/3`) + // your code +``` + +Expected output: `Dragon Well from China` + +### Exercise 3 + +Try fetching a tea that doesn't exist (ID 999). Handle the error with `.catch()`. + +```js +fetch(`${API_BASE}/teas/999`) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP error: ${response.status}`); + } + return response.json(); + }) + .then(tea => { + console.log(tea.name); + }) + .catch(error => { + // handle the error + }); +``` + +Expected output: `Error: HTTP error: 404` (or similar) + +### Exercise 4 + +Fetch the inventory endpoint and log which teas are low on stock (less than 50). + +```js +fetch(`${API_BASE}/inventory`) + // your code +``` + +Expected output (will vary): +``` +Low stock: +- Sencha: 0 +- Chamomile: 0 +- Darjeeling: 33 +- ... +``` + +--- + +## Part 2: Chaining Promises + +Chain `.then()` calls to perform sequential operations. + +
+📚 Recall: Chaining + +`.then()` returns a new Promise. The return value of your callback becomes the input to the next `.then()`. This lets you chain operations without nesting. +
+ +### Exercise 5 + +Fetch a tea, then fetch its inventory status. Log both pieces of information. + +```js +fetch(`${API_BASE}/teas/1`) + .then(response => response.json()) + .then(tea => { + console.log("Tea:", tea.name); + // Return a new fetch to chain it + return fetch(`${API_BASE}/inventory`); + }) + .then(response => response.json()) + .then(inventory => { + // Find this tea's stock in the inventory + // Log the stock count + }) + .catch(error => console.error("Error:", error.message)); +``` + +### Exercise 6 ⭐ + +Fetch all teas, filter to only Japanese teas, then for each one log its name and price. All using `.then()` chains. + +```js +fetch(`${API_BASE}/teas`) + .then(response => response.json()) + .then(teas => { + // Filter to Japanese teas + // Log each one's name and price + }) + .catch(error => console.error(error)); +``` + +--- + +## Part 3: Creating Promises + +Build your own Promises with `new Promise()`. + +
+📚 Recall: resolve / reject + +When creating a Promise, call `resolve(value)` for success or `reject(error)` for failure. The value/error is passed to `.then()` or `.catch()`. +
+ +### Exercise 7 + +Create a `wait(ms)` function that returns a Promise which resolves after `ms` milliseconds. + +```js +function wait(ms) { + return new Promise((resolve) => { + // use setTimeout + }); +} + +// Test it: +console.log("Starting..."); +wait(2000).then(() => console.log("2 seconds passed!")); +``` + +> 💡 Wrapping `setTimeout` in a Promise is a common pattern. You're converting callback-based code to Promise-based code - this is called "promisifying." + +### Exercise 8 ⭐ + +Create a `fetchTeaWithTimeout(id, timeoutMs)` function. It should: +- Fetch the tea from the API +- Reject if it takes longer than `timeoutMs` + +Hints: +- Use `setTimeout` to create a timeout that calls `reject` +- Use `clearTimeout` to cancel the timeout if fetch succeeds +- Remember to handle fetch errors too + +```js +function fetchTeaWithTimeout(id, timeoutMs) { + return new Promise((resolve, reject) => { + // Your code here + }); +} + +// Test with a generous timeout (should work) +fetchTeaWithTimeout(1, 5000) + .then(tea => console.log("Got:", tea.name)) + .catch(err => console.log("Failed:", err.message)); + +// Test with a tiny timeout (should fail) +fetchTeaWithTimeout(1, 1) + .then(tea => console.log("Got:", tea.name)) + .catch(err => console.log("Failed:", err.message)); +``` + +### Exercise 9 ⭐ + +Convert this callback-based function to return a Promise: + +```js +import fs from "fs"; + +// Callback version +function readJsonFile(path, callback) { + fs.readFile(path, "utf8", (error, data) => { + if (error) { + callback(error, null); + return; + } + try { + const parsed = JSON.parse(data); + callback(null, parsed); + } catch (parseError) { + callback(parseError, null); + } + }); +} + +// Convert to Promise version +function readJsonFilePromise(path) { + return new Promise((resolve, reject) => { + // your code + }); +} + +// Test it: +readJsonFilePromise("./test.json") + .then(data => console.log(data)) + .catch(error => console.error(error.message)); +``` + +--- + +## Part 4: async/await + +Rewrite Promise chains with cleaner async/await syntax. + +
+📚 Recall: async/await + +`async` before a function makes it return a Promise. `await` pauses execution until a Promise resolves. Together they make async code read like sync code. +
+ +### Exercise 10 + +Rewrite Exercise 1 using async/await: + +```js +async function countTeas() { + // use await instead of .then() +} + +countTeas(); +``` + +### Exercise 11 + +Rewrite Exercise 5 using async/await - fetch a tea, then fetch its inventory. + +```js +async function getTeaWithStock(id) { + // your code +} + +getTeaWithStock(1); +``` + +### Exercise 12 + +Add error handling to Exercise 11 using try/catch: + +```js +async function getTeaWithStock(id) { + try { + // your code + } catch (error) { + console.error("Failed:", error.message); + return null; + } +} + +// Test with valid ID +getTeaWithStock(1); + +// Test with invalid ID +getTeaWithStock(999); +``` + +### Exercise 13 ⭐ + +Create an async function that: +1. Fetches all teas +2. Filters to organic teas +3. Gets inventory for each +4. Returns only those with stock > 100 + +```js +async function getWellStockedOrganicTeas() { + // your code +} + +getWellStockedOrganicTeas().then(teas => { + console.log("Well-stocked organic teas:", teas); +}); +``` + +--- + +## Part 5: Promise.all + +Run multiple Promises in parallel for better performance. + +
+📚 Recall: Promise.all + +`Promise.all([p1, p2, p3])` runs all Promises simultaneously and resolves when ALL complete. Returns an array of results in the same order. +
+ +### Exercise 14 + +Fetch 3 specific teas (IDs 1, 5, and 10) in parallel using `Promise.all`. + +```js +async function getThreeTeas() { + const ids = [1, 5, 10]; + + // 1. Create an array of fetch Promises (use .map()) + // 2. Use Promise.all() to wait for all of them + // 3. Log each tea's name +} + +getThreeTeas(); +``` + +**Bonus:** Modify it to also log how long the operation took using `Date.now()`. + +> 💡 All three fetches run at the same time. `Promise.all` only waits as long as the slowest one - much faster than fetching one after another. + +### Exercise 15 ⭐ + +Create a function that fetches ALL teas and ALL inventory data in parallel, then combines them into a single report: + +```js +async function getFullInventoryReport() { + // Fetch both endpoints in parallel + const [teas, inventory] = await Promise.all([ + // your code + ]); + + // Combine: for each tea, add its stock count + // Return array of { name, origin, stock } +} + +getFullInventoryReport().then(report => { + console.log("Inventory Report:"); + report.forEach(item => { + console.log(`- ${item.name} (${item.origin}): ${item.stock} in stock`); + }); +}); +``` + +--- + +## Part 6: Authentication ⭐⭐ + +Work with protected endpoints using authentication tokens. + +### Exercise 16 + +The `/orders` endpoint requires authentication. First sign up, then log in: + +1. POST to `/auth/signup` with your email and a password to create an account +2. POST to `/auth/login` with the same email and password to get a token + +> ⚠️ Use a **dummy email and password** — not your real ones! This is a practice API with no security guarantees. + +```js +// Step 1: Sign up (only needed once) +async function signup(email, password) { + // POST to ${API_BASE}/auth/signup + // Body: { email, password } + // Return: data.token +} + +// Step 2: Log in +async function login(email, password) { + // POST to ${API_BASE}/auth/login + // Body: { email, password } + // Return: data.token +} + +// Sign up first, then log in (use dummy credentials!) +await signup("yourname@example.com", "mypassword"); +login("yourname@example.com", "mypassword") + .then(token => console.log("Got token:", token)) + .catch(err => console.error(err.message)); +``` + +Hint: Use `method: "POST"` and `headers: { "Content-Type": "application/json" }` in your fetch options. + +### Exercise 17 ⭐⭐ + +Use the token from Exercise 16 to fetch orders: + +```js +async function getOrders() { + // 1. Login to get token (use your login function) + // 2. Fetch /orders with Authorization header: "Bearer " + // 3. Return the orders +} + +getOrders() + .then(orders => console.log("Orders:", orders)) + .catch(err => console.error(err.message)); +``` + +Hint: The Authorization header format is `Bearer ${token}` diff --git a/courses/backend/advanced-javascript/week3/session-plan.md b/courses/backend/advanced-javascript/week3/session-plan.md new file mode 100644 index 00000000..c6992f85 --- /dev/null +++ b/courses/backend/advanced-javascript/week3/session-plan.md @@ -0,0 +1,155 @@ +# Session Plan (Week 3: Promises & async/await) + +> This guide is for mentors. It outlines how to run the session, what to emphasize, and why we introduce certain terminology. + +--- + +## Session Goals + +By the end of this session, trainees should: + +1. Understand why Promises exist (solving callback hell) +2. Consume Promises with `.then()` and `.catch()` +3. Create Promises with `new Promise()` +4. Use `async`/`await` for cleaner async code +5. Handle errors with `try`/`catch` +6. Use `Promise.all()` for parallel operations +7. Fetch data from the Tea Shop API + +--- + +## Glossary Goals + +| Term | Why we introduce it | +|------|---------------------| +| **Promise** | Core concept for modern async JavaScript. They'll use this everywhere. | +| **resolve / reject** | Understanding Promise outcomes is essential for proper error handling. | +| **then / catch** | The foundational API for working with Promises. | +| **async / await** | Modern syntax they'll use daily. Built on Promises. | +| **Promise.all** | Essential for performance - parallel operations. | + +--- + +## Session Outline + +### 1. Introduction + +- Recall Week 2: callback hell example +- "There has to be a better way" - introduce Promises +- Show the transformation: nested callbacks → chained Promises → async/await + +**Use:** [Slides](./slides/index.html) (introduction sections) + +### 2. Promise Consumption + +- Start with consuming, NOT creating Promises +- Use `fetch()` - a real function that returns a Promise +- Live code: fetch teas from the API +- Explain `.then()` and `.catch()` +- Emphasize: `.then()` returns a Promise (enables chaining) + +**Key point:** "When you have a Promise, you can call .then() for success and .catch() for failure." + +**Exercises:** [Part 1](./session-materials/exercises.md#part-1-consuming-promises) (exercises 1-4) + +### 3. Chaining Promises + +- Show chaining: one `.then()` after another +- The return value becomes the next `.then()`'s input +- One `.catch()` at the end handles all errors +- Compare to callback hell: flat vs nested + +**Key point:** "Each .then() returns a new Promise. That's what makes chaining work." + +**Exercises:** [Part 2](./session-materials/exercises.md#part-2-chaining-promises) (exercises 5-6) + +### Break + +### 4. Creating Promises + +- Now show how to CREATE Promises +- `new Promise((resolve, reject) => { ... })` +- Convert callback-based code to Promise-based +- Live code: promisify `setTimeout` +- Live code: promisify `fs.readFile` + +**Key point:** "resolve = success, reject = failure. You decide when to call them." + +**Exercises:** [Part 3](./session-materials/exercises.md#part-3-creating-promises) (exercises 7-9) + +### 5. async/await + +- "Promises are great, but there's even cleaner syntax" +- `async` makes a function return a Promise +- `await` pauses until Promise resolves +- Live code: rewrite `.then()` chains with async/await +- Error handling with `try`/`catch` + +**Key point:** "async/await is just Promises with nicer syntax. The code looks synchronous but runs asynchronously." + +**Exercises:** [Part 4](./session-materials/exercises.md#part-4-asyncawait) (exercises 10-13) + +### Break + +### 6. Promise.all + +- Problem: sequential requests are slow +- Solution: run them in parallel with `Promise.all()` +- Live code: fetch multiple teas at once +- Show timing difference + +**Key point:** "Promise.all runs operations in parallel. Much faster when requests don't depend on each other." + +**Exercises:** [Part 5](./session-materials/exercises.md#part-5-promiseall) (exercises 14-15) + +### 7. Wrap-up + +- Recap: Promises → .then/.catch → async/await → Promise.all +- Mention Part 6 (Authentication) as optional stretch for fast learners +- Preview Week 4: "Next week we organize code with Classes" +- Introduce the assignment + +--- + +## Teaching Tips + +### Managing exercises with mixed skill levels + +The exercises are intentionally numerous - students progress at different speeds. Here's how to manage this: + +1. **Let students work on Part 1** - observe how far each gets individually +2. **Solve one exercise in plenum** - pick one that weaker students struggled with but stronger students just finished. This way everyone benefits +3. **Move on to Part 2** - weaker students skip remaining Part 1 exercises (they can revisit later) +4. **Repeat** for each part + +Exercises marked with ⭐ are optional stretch goals. If your fastest students finish all exercises while others haven't solved the first one, that part needs an additional challenge - let us know, or create a PR with a creative challenging exercise! + +### Teach consumption before creation + +Students often get confused when Promise creation and consumption are mixed. Teach them separately: +1. First: "Here's a Promise (from fetch). How do we USE it?" +2. Later: "How do we CREATE our own Promises?" + +### The order matters + +Don't explain async/await until they understand .then/.catch. async/await is sugar on top of Promises - if they don't understand Promises, async/await will be magic they can't debug. + +### Use real API calls + +The Tea Shop API makes exercises concrete. Students see real network delays, real errors, real data. Much more engaging than setTimeout simulations. + +### Common mistakes to watch for + +- Forgetting `await` (getting Promise objects instead of values) +- Forgetting to return in `.then()` chains +- Not handling errors (no `.catch()` or try/catch) +- Using `await` outside an `async` function + +--- + +## Materials + +- [Slides](./slides/index.html) - Reveal.js presentation +- [Exercises](./session-materials/exercises.md) - In-class exercises +- [Assignment](./assignment.md) - Take-home work +- [Tea Shop API](https://tea-api-787553294298.europe-west1.run.app/api/v1) - Live API for exercises diff --git a/courses/backend/advanced-javascript/week3/slides/index.html b/courses/backend/advanced-javascript/week3/slides/index.html new file mode 100644 index 00000000..ad1f80a7 --- /dev/null +++ b/courses/backend/advanced-javascript/week3/slides/index.html @@ -0,0 +1,431 @@ + + + + + + Week 3 - Promises & async/await + + + + + + + +
+
+ + +
+

Promises & async/await

+

Escaping callback hell

+

Week 3

+
+ + +
+
+

The Problem

+

Remember last week?

+
+ +
+

Callback Hell

+
validateOrder(order, (err, valid) => {
+  if (err) return handleError(err);
+  calculateTotal(order, (err, total) => {
+    if (err) return handleError(err);
+    checkStock(order, (err, stock) => {
+      if (err) return handleError(err);
+      processPayment(total, (err, receipt) => {
+        if (err) return handleError(err);
+        // Finally done... 😫
+      });
+    });
+  });
+});
+
+ +
+

The Problems

+

Code grows sideways instead of down

+

Error handling is repetitive

+

Hard to read and maintain

+

There has to be a better way...

+
+
+ + +
+
+

What is a Promise?

+
+ +
+

The Cafe Analogy

+
+ You order coffee and get a receipt ticket.

+ The ticket is a promise - you'll get coffee eventually.

+ Two outcomes: you get coffee ✓ or they're out ✗ +
+
+ +
+

Promise States

+

Pending → waiting for result

+

Fulfilled → success! (resolved)

+

Rejected → failure! (error)

+
+ +
+

A Promise Object

+
const coffeeOrder = orderCoffee("latte");
+// coffeeOrder is a Promise
+// The coffee isn't ready yet!
+
+coffeeOrder
+  .then(coffee => console.log("Got my", coffee))
+  .catch(error => console.log("No coffee:", error));
+
+
+ + +
+
+

Consuming Promises

+

.then() and .catch()

+
+ +
+

fetch() Returns a Promise

+
const API_BASE = "https://tea-api-787553294298.europe-west1.run.app/api/v1";
+
+const promise = fetch(`${API_BASE}/teas`);
+// promise is pending...
+// The request is happening in the background
+
+ +
+

.then() for Success

+
fetch(`${API_BASE}/teas`)
+  .then(response => {
+    console.log("Got response!", response.status);
+  });
+

.then() runs when the Promise resolves

+
+ +
+

.catch() for Errors

+
fetch(`${API_BASE}/teas`)
+  .then(response => response.json())
+  .then(teas => console.log(teas))
+  .catch(error => {
+    console.error("Something went wrong:", error);
+  });
+

.catch() runs when the Promise rejects

+
+ +
+

The Key Insight

+
+ .then() returns a new Promise

+ That's what makes chaining possible! +
+
+
+ + +
+
+

Chaining Promises

+

The magic of .then()

+
+ +
+

Flat, Not Nested

+
fetch(`${API_BASE}/teas/1`)
+  .then(response => response.json())
+  .then(tea => {
+    console.log("Got tea:", tea.name);
+    return fetch(`${API_BASE}/inventory/${tea.id}`);
+  })
+  .then(response => response.json())
+  .then(inventory => {
+    console.log("Stock:", inventory.stockCount);
+  })
+  .catch(error => {
+    // Catches errors from ANY step!
+    console.error("Failed:", error);
+  });
+
+ +
+

Compare: Callbacks vs Promises

+
+
+

Callbacks

+
step1((err, a) => {
+  step2(a, (err, b) => {
+    step3(b, (err, c) => {
+      // nested...
+    });
+  });
+});
+
+
+

Promises

+
step1()
+  .then(a => step2(a))
+  .then(b => step3(b))
+  .then(c => {
+    // flat!
+  })
+  .catch(handleError);
+
+
+
+
+ + +
+
+

Creating Promises

+

new Promise()

+
+ +
+

The Constructor

+
const myPromise = new Promise((resolve, reject) => {
+  // Do something async...
+  // Call resolve(value) for success
+  // Call reject(error) for failure
+});
+
+ +
+

Example: wait()

+
function wait(ms) {
+  return new Promise((resolve) => {
+    setTimeout(() => {
+      resolve();
+    }, ms);
+  });
+}
+
+wait(2000).then(() => {
+  console.log("2 seconds passed!");
+});
+
+ +
+

Example: With Reject

+
function findTea(id) {
+  return new Promise((resolve, reject) => {
+    const tea = teas.find(t => t.id === id);
+    if (tea) {
+      resolve(tea);
+    } else {
+      reject(new Error(`Tea ${id} not found`));
+    }
+  });
+}
+
+findTea(99)
+  .then(tea => console.log(tea.name))
+  .catch(err => console.error(err.message));
+
+
+ + +
+
+

async/await

+

Even cleaner syntax

+
+ +
+

The Magic Keywords

+

async - makes a function return a Promise

+

await - pauses until Promise resolves

+
+ +
+

Before: .then() chains

+
function getTea(id) {
+  return fetch(`${API_BASE}/teas/${id}`)
+    .then(response => response.json())
+    .then(tea => {
+      console.log(tea.name);
+      return tea;
+    });
+}
+
+ +
+

After: async/await

+
async function getTea(id) {
+  const response = await fetch(`${API_BASE}/teas/${id}`);
+  const tea = await response.json();
+  console.log(tea.name);
+  return tea;
+}
+

Reads like synchronous code!

+
+ +
+

Error Handling: try/catch

+
async function getTea(id) {
+  try {
+    const response = await fetch(`${API_BASE}/teas/${id}`);
+    const tea = await response.json();
+    return tea;
+  } catch (error) {
+    console.error("Failed:", error.message);
+    return null;
+  }
+}
+
+ +
+

Common Mistake

+
// WRONG - forgot await!
+async function getTea(id) {
+  const response = fetch(`${API_BASE}/teas/${id}`);
+  // response is a Promise, not the actual response!
+  const tea = response.json(); // Error!
+}
+
+// RIGHT
+async function getTea(id) {
+  const response = await fetch(`${API_BASE}/teas/${id}`);
+  const tea = await response.json();
+}
+
+
+ + +
+
+

Promise.all

+

Parallel operations

+
+ +
+

Sequential = Slow

+
// One after another - slow!
+const tea1 = await fetch(`${API_BASE}/teas/1`).then(r => r.json());
+const tea2 = await fetch(`${API_BASE}/teas/2`).then(r => r.json());
+const tea3 = await fetch(`${API_BASE}/teas/3`).then(r => r.json());
+// Total time: tea1 + tea2 + tea3
+
+ +
+

Parallel = Fast

+
// All at once - fast!
+const [tea1, tea2, tea3] = await Promise.all([
+  fetch(`${API_BASE}/teas/1`).then(r => r.json()),
+  fetch(`${API_BASE}/teas/2`).then(r => r.json()),
+  fetch(`${API_BASE}/teas/3`).then(r => r.json()),
+]);
+// Total time: max(tea1, tea2, tea3)
+
+ +
+

When to Use

+

Use Promise.all when requests don't depend on each other

+

Use sequential await when they do

+
+
+ + +
+
+

Summary

+
+ +
+

The Journey

+

Callbacks → nested, messy

+

Promises → chainable, flat

+

async/await → reads like sync code

+
+ +
+

Key Concepts

+

.then() - handle success

+

.catch() - handle errors

+

async - function returns Promise

+

await - pause for Promise

+

Promise.all - parallel execution

+
+ +
+

Next Week

+

Organizing code with Classes

+
class Tea {
+  constructor(name, origin) {
+    this.name = name;
+    this.origin = origin;
+  }
+
+  describe() {
+    return `${this.name} from ${this.origin}`;
+  }
+}
+
+ +
+

Time to Practice

+

🍵

+
+
+ +
+
+ + + + + + + diff --git a/courses/backend/advanced-javascript/week4/README.md b/courses/backend/advanced-javascript/week4/README.md new file mode 100644 index 00000000..c18f6245 --- /dev/null +++ b/courses/backend/advanced-javascript/week4/README.md @@ -0,0 +1,48 @@ +# Classes & Object-Oriented Programming (Week 4) + +Over the past three weeks you've transformed data with array methods, managed async flows with callbacks and Promises, and fetched from a real API. But all your data has been plain objects - no validation, no built-in behavior, no structure beyond what you remember to add each time. + +This week you'll learn classes: blueprints for creating objects with consistent structure and built-in behavior. You'll build Tea, Order, and Inventory classes that validate data, calculate totals, and work together - just like the models behind the Tea Shop API you've been using. + +## Contents + +- [Preparation](./preparation.md) +- [Slides](./slides/index.html) +- [Session Plan](./session-plan.md) (for mentors) +- [Session Materials](./session-materials/) +- [Assignment](./assignment.md) + +## Learning Goals + +By the end of this session, you will be able to: + +- [ ] Understand why classes exist (organizing data with behavior) +- [ ] Declare a class using `class`, `constructor`, and `this` +- [ ] Instantiate objects from classes using `new` +- [ ] Add methods to classes and call them on instances +- [ ] Use `this` to access and modify instance state +- [ ] Use static methods for utility operations on a class +- [ ] Use inheritance with `extends` and `super()` +- [ ] Understand the difference between a class (blueprint) and an object (instance) + +```js +// From scattered plain objects... +const tea = { name: "Sencha", pricePerGram: 0.12, origin: "Japan" }; +// No validation, no behavior, no consistency + +// ...to structured classes with built-in behavior +class Tea { + constructor(name, pricePerGram, origin) { + this.name = name; + this.pricePerGram = pricePerGram; + this.origin = origin; + } + + priceFor(grams) { + return this.pricePerGram * grams; + } +} + +const sencha = new Tea("Sencha", 0.12, "Japan"); +console.log(sencha.priceFor(100)); // 12 +``` diff --git a/courses/backend/advanced-javascript/week4/assignment.md b/courses/backend/advanced-javascript/week4/assignment.md new file mode 100644 index 00000000..fb4bce90 --- /dev/null +++ b/courses/backend/advanced-javascript/week4/assignment.md @@ -0,0 +1,364 @@ +# Week 4 Assignment + +Complete these exercises after the session. All exercises use the tea data. + +```js +import { teas } from "../data/teas.js"; +``` + +--- + +## Exercise 1: Tea Class with Validation + +Build a complete `Tea` class with validation and a static factory method. + +```js +class Tea { + constructor(name, type, origin, pricePerGram, organic) { + // Validate: + // - name must be a non-empty string + // - type must be one of: "green", "black", "herbal", "oolong", "white" + // - pricePerGram must be a positive number + // Store all properties + } + + priceFor(grams) { + // Return price for given weight + } + + describe() { + // Return "Sencha (green) from Japan - 12.00 DKK/100g [organic]" + // Only include "[organic]" if the tea is organic + } + + static fromObject(obj) { + // Create a Tea from a plain object (like from the data file) + } +} + +// Test validation: +try { new Tea("", "green", "Japan", 0.12, true); } +catch (e) { console.log(e.message); } // "Name is required" + +try { new Tea("Test", "purple", "Japan", 0.12, true); } +catch (e) { console.log(e.message); } // "Invalid type: purple" + +// Test factory method: +const teaInstances = teas.map(Tea.fromObject); +console.log(teaInstances.length); // 20 +console.log(teaInstances[0].describe()); +// "Sencha (green) from Japan - 12.00 DKK/100g [organic]" +console.log(teaInstances[1].describe()); +// "Earl Grey (black) from India - 8.00 DKK/100g" +``` + +--- + +## Exercise 2: Order System + +Build `OrderItem` and `Order` classes that work together. + +```js +class OrderItem { + constructor(tea, grams) { + // tea is a Tea instance, grams is a positive number + // Validate: grams must be positive + } + + lineTotal() { + // Use tea.priceFor() + } + + describe() { + // "200g Sencha - 24.00 DKK" + } +} + +class Order { + constructor() { + // items array, status starts as "pending" + } + + addItem(orderItem) { + // Add item (only when pending) + } + + getTotal() { + // Sum all line totals using .reduce() + } + + getSummary() { + // Return formatted multi-line string: + // "Order (pending) - 2 items" + // " 200g Sencha - 24.00 DKK" + // " 50g Matcha - 22.50 DKK" + // "Total: 46.50 DKK" + } +} + +// Test: +const teaInstances = teas.map(Tea.fromObject); +const order = new Order(); +order.addItem(new OrderItem(teaInstances[0], 200)); // Sencha +order.addItem(new OrderItem(teaInstances[7], 50)); // Matcha + +console.log(order.getSummary()); +console.log("Total:", order.getTotal().toFixed(2), "DKK"); +``` + +--- + +## Exercise 3: Inventory Manager ⭐ + +Build an `Inventory` class that tracks stock for multiple teas. + +```js +class Inventory { + constructor() { + // Store a Map or object of tea ID → { tea, stockCount } + } + + add(tea, stockCount) { + // Add a tea to inventory + } + + sell(teaName, grams) { + // Reduce stock. Throw if not enough stock. + } + + restock(teaName, grams) { + // Increase stock + } + + getStock(teaName) { + // Return current stock count for a tea + } + + getLowStock(threshold) { + // Return array of { tea, stockCount } where stock < threshold + // Use .filter() + } + + getTotalValue() { + // Sum of (pricePerGram * stockCount) for all items + // Use .reduce() + } +} + +// Test: +const teaInstances = teas.map(Tea.fromObject); +const inventory = new Inventory(); + +teaInstances.forEach(tea => { + const data = teas.find(t => t.name === tea.name); + inventory.add(tea, data.stockCount); +}); + +console.log("Sencha stock:", inventory.getStock("Sencha")); // 150 + +inventory.sell("Sencha", 50); +console.log("After selling 50g:", inventory.getStock("Sencha")); // 100 + +console.log("Low stock (< 50):"); +inventory.getLowStock(50).forEach(item => { + console.log(`- ${item.tea.name}: ${item.stockCount}g`); +}); + +console.log("Total inventory value:", inventory.getTotalValue().toFixed(2), "DKK"); +``` + +--- + +## Exercise 4: Customer with History ⭐ + +Build a `Customer` class that tracks order history and spending. + +```js +class Customer { + constructor(name, email) { + // Store name, email, and empty orders array + } + + placeOrder(order) { + // Confirm the order and add to this.orders + // Return the order + } + + totalSpent() { + // Sum all order totals using .reduce() + } + + getOrderHistory() { + // Return formatted string of all orders + // "Alex (alex@example.com) - 2 orders" + // "" + // "Order 1 (confirmed) - 1 item" + // " 100g Sencha - 12.00 DKK" + // "Total: 12.00 DKK" + // "" + // "Order 2 (confirmed) - 1 item" + // " 50g Matcha - 22.50 DKK" + // "Total: 22.50 DKK" + // "" + // "Lifetime total: 34.50 DKK" + } +} + +// Test: +const teaInstances = teas.map(Tea.fromObject); +const customer = new Customer("Alex", "alex@example.com"); + +const order1 = new Order(); +order1.addItem(new OrderItem(teaInstances[0], 100)); // Sencha +customer.placeOrder(order1); + +const order2 = new Order(); +order2.addItem(new OrderItem(teaInstances[7], 50)); // Matcha +customer.placeOrder(order2); + +console.log(customer.getOrderHistory()); +console.log("Total spent:", customer.totalSpent().toFixed(2), "DKK"); +``` + +--- + +## Exercise 5: Full Tea Shop System ⭐⭐ + +Build a `TeaShop` class that orchestrates all the other classes together. + +```js +class TeaShop { + constructor(teaData) { + // Create a TeaCatalog from the data + // Create an Inventory from the data + // Store customers as an empty array + } + + registerCustomer(name, email) { + // Create and return a new Customer + } + + createOrder(customer, items) { + // items is array of { teaName, grams } + // 1. Find each tea in the catalog + // 2. Check stock in inventory + // 3. Create OrderItems and an Order + // 4. Sell from inventory + // 5. Place order on the customer + // 6. Return the order + } + + getReport() { + // Return a shop report: + // - Total customers + // - Total orders + // - Total revenue + // - Low stock items + } +} + +// Test: +const shop = new TeaShop(teas); + +const alex = shop.registerCustomer("Alex", "alex@example.com"); +const maria = shop.registerCustomer("Maria", "maria@example.com"); + +const order1 = shop.createOrder(alex, [ + { teaName: "Sencha", grams: 100 }, + { teaName: "Matcha", grams: 50 } +]); +console.log(order1.getSummary()); + +const order2 = shop.createOrder(maria, [ + { teaName: "Earl Grey", grams: 200 } +]); +console.log(order2.getSummary()); + +console.log(shop.getReport()); +``` + +--- + +## Exercise 6: Inheritance ⭐⭐ + +Build specialized classes using inheritance. This exercise is optional. + +```js +// 1. PremiumTea extends Tea +class PremiumTea extends Tea { + constructor(name, type, origin, pricePerGram, organic, grade) { + // Call super(), store grade + // Validate grade is "A", "B", or "C" + } + + priceFor(grams) { + // Override: A = 50% markup, B = 25%, C = 10% + } + + describe() { + // Override: include [Grade A] in description + } + + static fromTea(tea, grade) { + // Create a PremiumTea from an existing Tea instance + } +} + +// 2. ExpressOrder extends Order +class ExpressOrder extends Order { + constructor(expressFee) { + // Call super(), store express fee (default 25 DKK) + } + + getTotal() { + // Override: add express fee to parent's total + } + + getSummary() { + // Override: include express fee line in summary + } +} + +// Test PremiumTea: +const gyokuro = new PremiumTea("Gyokuro", "green", "Japan", 0.56, false, "A"); +console.log(gyokuro.describe()); +// "Gyokuro [Grade A] (green) from Japan - 84.00 DKK/100g" +console.log(gyokuro.priceFor(100)); // 84 + +const upgraded = PremiumTea.fromTea(teas.map(Tea.fromObject)[0], "B"); +console.log(upgraded.describe()); + +// Test ExpressOrder: +const express = new ExpressOrder(25); +express.addItem(new OrderItem(gyokuro, 100)); +console.log(express.getSummary()); +// Should show items + express fee + total +console.log(express.getTotal()); // 84 + 25 = 109 +``` + +--- + +## Submission + +Create a folder `week4-assignment/` with: +- `exercise1.js` - Tea class with validation +- `exercise2.js` - Order system +- `exercise3.js` - Inventory manager +- `exercise4.js` - Customer with history +- `exercise5.js` - Full tea shop system +- `exercise6.js` - Inheritance (optional) + +Each file should be runnable with `node exerciseN.js`. + +```js +// Example structure for exercise1.js +import { teas } from "../data/teas.js"; + +class Tea { + // ... +} + +// Test your class +const teaInstances = teas.map(Tea.fromObject); +teaInstances.forEach(t => console.log(t.describe())); +``` diff --git a/courses/backend/advanced-javascript/week4/extra-example/index.html b/courses/backend/advanced-javascript/week4/extra-example/index.html new file mode 100644 index 00000000..30d07c3f --- /dev/null +++ b/courses/backend/advanced-javascript/week4/extra-example/index.html @@ -0,0 +1,361 @@ + + + + + Foxes & Rabbits + + + +

Foxes & Rabbits

+ +
+ +
+

Rules

+
    +
  • All animals walk randomly and wrap through edges
  • +
  • Rabbits reproduce when two meet close enough
  • +
  • Rabbits also have a small chance to reproduce on their own
  • +
  • A fox that touches a rabbit eats it and duplicates
  • +
  • A fox that doesn't eat for too long starves and dies
  • + +
  • The last rabbit cannot be eaten
  • +
  • The last fox cannot starve
  • +
+
+
+ + +
+ + + +
+ Rabbits: 0 + Foxes: 0 + Step 0 +
+
+ +
+

Starting conditions

+
+
+ Rabbits + +
+
+ Mate cooldown + +
+
+ Mate distance + +
+
+ Foxes + +
+
+ Starve after + +
+
+ Catch distance + +
+
+ Spontaneous % + +
+
+
+ + + + diff --git a/courses/backend/advanced-javascript/week4/extra-example/simulation.js b/courses/backend/advanced-javascript/week4/extra-example/simulation.js new file mode 100644 index 00000000..7d046e20 --- /dev/null +++ b/courses/backend/advanced-javascript/week4/extra-example/simulation.js @@ -0,0 +1,173 @@ +export class Animal { + constructor(x, y, speed, worldWidth, worldHeight) { + this.x = x; + this.y = y; + this.speed = speed; + this.worldWidth = worldWidth; + this.worldHeight = worldHeight; + this.angle = Math.random() * Math.PI * 2; + } + + move() { + this.angle += (Math.random() - 0.5) * 0.5; + this.x += Math.cos(this.angle) * this.speed; + this.y += Math.sin(this.angle) * this.speed; + // Walk through edges to the other side + this.x = ((this.x % this.worldWidth) + this.worldWidth) % this.worldWidth; + this.y = ((this.y % this.worldHeight) + this.worldHeight) % this.worldHeight; + } +} + +export class Rabbit extends Animal { + constructor(x, y, cooldown, worldWidth, worldHeight) { + super(x, y, 2, worldWidth, worldHeight); + this.cooldown = 0; + this.maxCooldown = cooldown; + } + + update() { + this.move(); + if (this.cooldown > 0) this.cooldown--; + } + + canReproduce() { + return this.cooldown === 0; + } + + reproduce() { + this.cooldown = this.maxCooldown; + } +} + +export class Fox extends Animal { + constructor(x, y, maxHunger, worldWidth, worldHeight) { + super(x, y, 2.5, worldWidth, worldHeight); + this.hunger = 0; + this.maxHunger = maxHunger; + } + + update() { + this.move(); + this.hunger++; + } + + isStarving() { + return this.hunger >= this.maxHunger; + } + + hungerLevel() { + return Math.min(1, this.hunger / this.maxHunger); + } + + eat() { + this.hunger = 0; + } +} + +export class Universe { + constructor(width, height) { + this.width = width; + this.height = height; + this.rabbits = []; + this.foxes = []; + this.stepCount = 0; + this.catchDistance = 12; + this.mateDistance = 25; + this.mateCooldown = 50; + this.foxHunger = 150; + this.maxRabbits = 500; + this.spontaneous = 0.02; + } + + populate(rabbitCount, foxCount) { + for (let i = 0; i < rabbitCount; i++) { + this.rabbits.push( + new Rabbit(Math.random() * this.width, Math.random() * this.height, this.mateCooldown, this.width, this.height) + ); + } + for (let i = 0; i < foxCount; i++) { + this.foxes.push( + new Fox(Math.random() * this.width, Math.random() * this.height, this.foxHunger, this.width, this.height) + ); + } + } + + distance(a, b) { + const dx = a.x - b.x; + const dy = a.y - b.y; + return Math.sqrt(dx * dx + dy * dy); + } + + moveAll() { + this.rabbits.forEach((r) => r.update()); + this.foxes.forEach((f) => f.update()); + } + + hunt() { + const eatenRabbits = []; + const newFoxes = []; + + this.foxes.forEach((fox) => { + // Keep at least one rabbit alive + if (this.rabbits.length - eatenRabbits.length <= 1) return; + + const prey = this.rabbits.find( + (r) => + !eatenRabbits.includes(r) && this.distance(fox, r) < this.catchDistance + ); + if (!prey) return; + + eatenRabbits.push(prey); + fox.eat(); + const child = new Fox(fox.x, fox.y, this.foxHunger, this.width, this.height); + child.angle = fox.angle + Math.PI; + newFoxes.push(child); + }); + + this.rabbits = this.rabbits.filter((r) => !eatenRabbits.includes(r)); + this.foxes.push(...newFoxes); + } + + starve() { + const alive = this.foxes.filter((fox) => !fox.isStarving()); + // Keep at least one fox alive + this.foxes = alive.length > 0 ? alive : [this.foxes[0]]; + } + + reproduce() { + if (this.rabbits.length >= this.maxRabbits) return; + + const babies = []; + this.rabbits.forEach((a) => { + if (!a.canReproduce()) return; + const mate = this.rabbits.find( + (b) => + b !== a && b.canReproduce() && this.distance(a, b) < this.mateDistance + ); + if (!mate) return; + + const awayAngle = Math.atan2(mate.y - a.y, mate.x - a.x); + a.angle = awayAngle + Math.PI; + mate.angle = awayAngle; + a.reproduce(); + mate.reproduce(); + babies.push(new Rabbit((a.x + mate.x) / 2, (a.y + mate.y) / 2, this.mateCooldown, this.width, this.height)); + }); + + this.rabbits.push(...babies); + + // Spontaneous reproduction: small random chance per step + if (this.rabbits.length < this.maxRabbits && Math.random() < this.spontaneous) { + const parent = this.rabbits[Math.floor(Math.random() * this.rabbits.length)]; + this.rabbits.push(new Rabbit(parent.x, parent.y, this.mateCooldown, this.width, this.height)); + } + } + + step() { + this.stepCount++; + this.moveAll(); + this.hunt(); + this.starve(); + this.reproduce(); + } +} diff --git a/courses/backend/advanced-javascript/week4/preparation.md b/courses/backend/advanced-javascript/week4/preparation.md new file mode 100644 index 00000000..56e9049b --- /dev/null +++ b/courses/backend/advanced-javascript/week4/preparation.md @@ -0,0 +1,297 @@ +# Week 4 Preparation + +Read this introduction before class. It explains the concepts we'll practice during the session. + +--- + +## Why Classes? The Copy-Paste Problem + +Over the past three weeks, you've worked with plain objects like this: + +```js +const tea1 = { name: "Sencha", pricePerGram: 0.12, origin: "Japan" }; +const tea2 = { name: "Earl Grey", pricePerGram: 0.08, origin: "India" }; +``` + +That works fine for reading data. But what happens when you need to *do things* with it? + +```js +// Calculate price for 100 grams +const price1 = tea1.pricePerGram * 100; +const price2 = tea2.pricePerGram * 100; + +// Validate a new tea +function validateTea(tea) { + if (!tea.name) throw new Error("Name required"); + if (tea.pricePerGram < 0) throw new Error("Price must be positive"); +} +``` + +Every time you create a tea, you have to remember its shape. Every time you calculate a price, you repeat the same formula. Every time you validate, you call a separate function that isn't connected to the data. + +Now imagine a backend that creates hundreds of tea objects, order objects, and inventory objects. The same formulas, the same validation, scattered across your codebase. Change the price formula? Find and update every place it appears. + +Classes solve this by bundling **data** and **behavior** together. + +--- + +## The Constructor and `this` + +A **constructor** is a special method that runs when you create an object from a class. It sets up the initial state: + +```js +class Tea { + constructor(name, pricePerGram, origin) { + this.name = name; + this.pricePerGram = pricePerGram; + this.origin = origin; + } +} +``` + +The keyword `this` refers to the specific object being created. When you write `this.name = name`, you're saying "store the `name` parameter on this particular object." + +Think of it like filling in a form. The class is the blank form, and the constructor fills in the fields for each specific instance. + +```js +const sencha = new Tea("Sencha", 0.12, "Japan"); +// this.name = "Sencha" +// this.pricePerGram = 0.12 +// this.origin = "Japan" + +const earlGrey = new Tea("Earl Grey", 0.08, "India"); +// this.name = "Earl Grey" (different object, different "this") +``` + +Each object gets its own copy of the data. Changing `sencha.name` doesn't affect `earlGrey.name`. + +> 📚 **Glossary: constructor** +> A special method that runs automatically when you create a new instance with `new`. It initializes the object's properties. + +> 📚 **Glossary: this** +> Inside a class, `this` refers to the current instance - the specific object being created or used. It's how methods access the object's own data. + +--- + +## Creating Instances with `new` + +The `new` keyword creates an object from a class: + +```js +const sencha = new Tea("Sencha", 0.12, "Japan"); +``` + +Here's what `new` does: +1. Creates a fresh, empty object +2. Calls the constructor, with `this` set to the new object +3. Returns the object + +The class is a **blueprint**. The instance is a **building**. One blueprint can produce many buildings, each with different details but the same structure. + +```js +// One class (blueprint) +class Tea { /* ... */ } + +// Many instances (buildings) +const tea1 = new Tea("Sencha", 0.12, "Japan"); +const tea2 = new Tea("Earl Grey", 0.08, "India"); +const tea3 = new Tea("Matcha", 0.45, "Japan"); +``` + +> 📚 **Glossary: instance** +> A specific object created from a class using `new`. `sencha` and `earlGrey` are both instances of the `Tea` class. + +--- + +## Methods: Behavior Lives with Data + +The real power of classes: you can attach functions directly to your data. These are called **methods**: + +```js +class Tea { + constructor(name, pricePerGram, origin) { + this.name = name; + this.pricePerGram = pricePerGram; + this.origin = origin; + } + + priceFor(grams) { + return this.pricePerGram * grams; + } + + describe() { + return `${this.name} from ${this.origin}`; + } +} + +const sencha = new Tea("Sencha", 0.12, "Japan"); +console.log(sencha.priceFor(100)); // 12 +console.log(sencha.describe()); // "Sencha from Japan" +``` + +Notice: the price formula lives *with* the Tea data. You don't pass the tea to an external function - the tea knows how to calculate its own price. That's **encapsulation**: bundling data and the operations that work on it together. + +Now compare: + +```js +// Without classes: data and behavior are separate +const tea = { name: "Sencha", pricePerGram: 0.12 }; +function priceFor(tea, grams) { return tea.pricePerGram * grams; } +priceFor(tea, 100); + +// With classes: data and behavior live together +const sencha = new Tea("Sencha", 0.12, "Japan"); +sencha.priceFor(100); +``` + +When you add validation, formatting, and calculations to a class, everything stays organized in one place. + +> 📚 **Glossary: encapsulation** +> Bundling data and the operations that work on that data together in one unit (a class). Instead of separate functions operating on plain objects, the object itself knows how to perform its operations. + +--- + +## Static Methods + +Sometimes you need a method that belongs to the **class itself**, not to any particular instance. These are **static methods**: + +```js +class Tea { + constructor(name, pricePerGram, origin) { + this.name = name; + this.pricePerGram = pricePerGram; + this.origin = origin; + } + + // Instance method - called on a specific tea + priceFor(grams) { + return this.pricePerGram * grams; + } + + // Static method - called on the class itself + static fromObject(obj) { + return new Tea(obj.name, obj.pricePerGram, obj.origin); + } + + static findCheapest(teas) { + return teas.reduce((cheapest, tea) => + tea.pricePerGram < cheapest.pricePerGram ? tea : cheapest + ); + } +} +``` + +You call static methods on the class, not on instances: + +```js +// Static methods: called on Tea (the class) +const sencha = Tea.fromObject({ name: "Sencha", pricePerGram: 0.12, origin: "Japan" }); +const cheapest = Tea.findCheapest(teas); + +// Instance methods: called on sencha (an instance) +sencha.priceFor(100); +sencha.describe(); +``` + +The `fromObject` pattern is especially useful on backends: data arrives from APIs or databases as plain objects, and `fromObject` converts them into class instances with methods and validation. This is called a **factory method**. + +--- + +## Inheritance: `extends` and `super()` + +Sometimes you want a specialized version of an existing class. **Inheritance** lets one class build on another: + +```js +class Tea { + constructor(name, pricePerGram, origin) { + this.name = name; + this.pricePerGram = pricePerGram; + this.origin = origin; + } + + priceFor(grams) { + return this.pricePerGram * grams; + } +} + +class PremiumTea extends Tea { + constructor(name, pricePerGram, origin, grade) { + super(name, pricePerGram, origin); // Call parent constructor + this.grade = grade; + } + + priceFor(grams) { + const base = super.priceFor(grams); // Call parent method + return base * 1.2; // 20% premium markup + } +} +``` + +- `extends` says "PremiumTea is a specialized Tea" +- `super()` calls the parent's constructor or method +- PremiumTea **inherits** all of Tea's methods, and can **override** specific ones + +```js +const gyokuro = new PremiumTea("Gyokuro", 0.56, "Japan", "A"); +console.log(gyokuro.priceFor(100)); // 67.2 (56 * 1.2) +console.log(gyokuro.grade); // "A" +``` + +Inheritance is useful when you have an "is-a" relationship: a PremiumTea *is a* Tea with extra features. But don't overuse it - most of the time, composition (classes using other classes) is more flexible. + +--- + +## Classes vs Objects + +This distinction is fundamental: + +| | Class | Instance | +|--|-------|----------| +| What | Blueprint / template | Specific object | +| How many | One per type | Many per class | +| Created with | `class` keyword | `new` keyword | +| Example | `Tea` | `sencha`, `earlGrey` | +| Analogy | Cookie cutter | Cookies | + +```js +// Tea is a class - it describes what teas look like +class Tea { + constructor(name, pricePerGram, origin) { /* ... */ } + priceFor(grams) { /* ... */ } +} + +// sencha is an instance - it IS a specific tea +const sencha = new Tea("Sencha", 0.12, "Japan"); + +// You can check: +console.log(sencha instanceof Tea); // true +``` + +> 📚 **Glossary: class** +> A blueprint for creating objects with shared structure and behavior. Defines what properties and methods instances will have, but isn't itself an object you work with directly. + +--- + +## Summary + +| Term | Meaning | +|------|---------| +| class | Blueprint for creating objects with shared structure | +| constructor | Method that initializes a new instance | +| instance | A specific object created from a class | +| this | Refers to the current instance inside a class | +| encapsulation | Bundling data and operations together | +| static method | Method on the class itself, not on instances | +| extends | Create a specialized version of another class | +| super() | Call the parent class's constructor or method | + +--- + +## Pre-Reading (Optional) + +If you want to go deeper, read these MDN pages: + +- [Classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) +- [constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/constructor) +- [static](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/static) +- [extends](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/extends) diff --git a/courses/backend/advanced-javascript/week4/session-materials/exercises.md b/courses/backend/advanced-javascript/week4/session-materials/exercises.md new file mode 100644 index 00000000..527d8a5d --- /dev/null +++ b/courses/backend/advanced-javascript/week4/session-materials/exercises.md @@ -0,0 +1,514 @@ +# Week 4 Exercises: Classes & Object-Oriented Programming + +All exercises use the tea shop data. Start by loading it: + +```js +import { teas } from "../../data/teas.js"; +``` + +--- + +## Part 1: Creating Classes + +A class is a blueprint for creating objects. The constructor initializes each instance. + +
+📚 Recall: constructor and this + +The `constructor` method runs when you create an instance with `new`. Inside it, `this` refers to the new object being created. `this.name = name` stores the parameter on the instance. +
+ +### Exercise 1 + +Create a `Tea` class with a constructor that accepts `name`, `type`, and `origin`. Create two instances and log them. + +```js +class Tea { + // your constructor +} + +const sencha = new Tea("Sencha", "green", "Japan"); +const earlGrey = new Tea("Earl Grey", "black", "India"); + +console.log(sencha.name); // "Sencha" +console.log(sencha.type); // "green" +console.log(earlGrey.origin); // "India" +``` + +### Exercise 2 + +Extend your `Tea` class to also accept `pricePerGram` and `organic` (boolean). Create an instance from the first tea in the data array. + +```js +const firstTea = teas[0]; +const tea = new Tea(firstTea.name, firstTea.type, firstTea.origin, firstTea.pricePerGram, firstTea.organic); +console.log(tea); +``` + +### Exercise 3 + +Create the Tea instances using `.map()` and your class: + +```js +const teaInstances = teas.map(t => new Tea(t.name, t.type, t.origin, t.pricePerGram, t.organic)); +console.log(teaInstances.length); // 20 +console.log(teaInstances[0].name); // "Sencha" +``` + +### Exercise 4 ⭐ + +Add validation to your constructor. Throw an error if: +- `name` is empty or missing +- `pricePerGram` is negative +- `type` is not one of: "green", "black", "herbal", "oolong", "white" + +```js +// Should work: +const valid = new Tea("Sencha", "green", "Japan", 0.12, true); + +// Should throw: +const noName = new Tea("", "green", "Japan", 0.12, true); +// Error: "Name is required" + +const badPrice = new Tea("Sencha", "green", "Japan", -1, true); +// Error: "Price must be positive" + +const badType = new Tea("Sencha", "purple", "Japan", 0.12, true); +// Error: "Invalid type: purple" +``` + +--- + +## Part 2: Methods + +Methods are functions that belong to a class. They use `this` to access the instance's data. + +
+📚 Recall: encapsulation + +Encapsulation means bundling data and the operations that work on it together. Instead of separate functions that take a tea object as a parameter, the tea *itself* has methods. +
+ +### Exercise 5 + +Add a `priceFor(grams)` method to your Tea class that returns the price for a given weight. + +```js +const sencha = new Tea("Sencha", "green", "Japan", 0.12, true); +console.log(sencha.priceFor(100)); // 12 +console.log(sencha.priceFor(50)); // 6 +``` + +### Exercise 6 + +Add a `describe()` method that returns a formatted string: + +```js +const sencha = new Tea("Sencha", "green", "Japan", 0.12, true); +console.log(sencha.describe()); +// "Sencha (green) from Japan - 12.00 DKK/100g" + +const earlGrey = new Tea("Earl Grey", "black", "India", 0.08, false); +console.log(earlGrey.describe()); +// "Earl Grey (black) from India - 8.00 DKK/100g" +``` + +Hint: Use `(this.pricePerGram * 100).toFixed(2)` for the price. + +### Exercise 7 + +Create an `OrderItem` class that takes a Tea instance and a number of grams. Add a `lineTotal()` method. + +```js +class OrderItem { + constructor(tea, grams) { + // store the tea instance and grams + } + + lineTotal() { + // use the tea's priceFor method + } +} + +const sencha = new Tea("Sencha", "green", "Japan", 0.12, true); +const item = new OrderItem(sencha, 200); + +console.log(item.tea.name); // "Sencha" +console.log(item.grams); // 200 +console.log(item.lineTotal()); // 24 +``` + +> 💡 Notice: OrderItem *uses* a Tea instance - it doesn't extend it. This is composition: one class containing another. + +### Exercise 8 ⭐ + +Add a `describe()` method to `OrderItem` that returns a formatted line: + +```js +const item = new OrderItem(sencha, 200); +console.log(item.describe()); +// "200g Sencha - 24.00 DKK" +``` + +Then create several order items from the tea data and use `.map()` to log all descriptions: + +```js +const items = [ + new OrderItem(teaInstances[0], 100), + new OrderItem(teaInstances[1], 200), + new OrderItem(teaInstances[7], 50), +]; + +items.map(item => item.describe()).forEach(line => console.log(line)); +// "100g Sencha - 12.00 DKK" +// "200g Earl Grey - 16.00 DKK" +// "50g Matcha - 22.50 DKK" +``` + +--- + +## Part 3: this and State + +Methods can modify instance state, not just read it. `this` is how you access and change an object's data. + +
+📚 Recall: this + +Inside a class, `this` refers to the current instance. `this.name` reads the instance's name. `this.stockCount -= 10` modifies it. Each instance has its own state. +
+ +### Exercise 9 + +Create an `Inventory` class that tracks stock for a tea. It should have `sell(grams)` and `restock(grams)` methods. + +```js +class Inventory { + constructor(tea, stockCount) { + this.tea = tea; + this.stockCount = stockCount; + } + + sell(grams) { + // Subtract grams from stockCount + // Throw an error if not enough stock + } + + restock(grams) { + // Add grams to stockCount + } +} + +const sencha = new Tea("Sencha", "green", "Japan", 0.12, true); +const stock = new Inventory(sencha, 150); + +console.log(stock.stockCount); // 150 +stock.sell(50); +console.log(stock.stockCount); // 100 +stock.restock(200); +console.log(stock.stockCount); // 300 + +stock.sell(500); // Error: "Not enough stock for Sencha (have 300, need 500)" +``` + +### Exercise 10 ⭐ + +Create an `Order` class with status transitions. An order starts as "pending" and can move through: pending → confirmed → shipped → delivered. + +```js +class Order { + constructor() { + this.items = []; + this.status = "pending"; + } + + addItem(orderItem) { + // Only allow adding items when status is "pending" + // Throw error otherwise + } + + confirm() { + // Change status to "confirmed" (only from "pending") + } + + ship() { + // Change status to "shipped" (only from "confirmed") + } + + deliver() { + // Change status to "delivered" (only from "shipped") + } +} + +const order = new Order(); +const sencha = new Tea("Sencha", "green", "Japan", 0.12, true); +order.addItem(new OrderItem(sencha, 100)); +console.log(order.status); // "pending" + +order.confirm(); +console.log(order.status); // "confirmed" + +order.addItem(new OrderItem(sencha, 50)); +// Error: "Cannot add items to a confirmed order" + +order.ship(); +order.deliver(); +console.log(order.status); // "delivered" +``` + +### Exercise 11 ⭐ + +Add a `getTotal()` method to your Order class that uses `.reduce()` to sum all item totals: + +```js +const order = new Order(); +order.addItem(new OrderItem(new Tea("Sencha", "green", "Japan", 0.12, true), 100)); +order.addItem(new OrderItem(new Tea("Matcha", "green", "Japan", 0.45, true), 50)); + +console.log(order.getTotal()); // 34.5 (12 + 22.5) +``` + +Also add a `getSummary()` method that returns a formatted order summary: + +```js +console.log(order.getSummary()); +// Order (pending) - 2 items +// - 100g Sencha - 12.00 DKK +// - 50g Matcha - 22.50 DKK +// Total: 34.50 DKK +``` + +--- + +## Part 4: Classes Working Together + +Real applications have multiple classes that work together. Each class handles its own responsibility. + +
+📚 Recall: composition + +Composition means one class uses instances of another class. An Order contains OrderItems. A Catalog contains Teas. Each class handles its own job and delegates to others. +
+ +### Exercise 12 + +Create a `TeaCatalog` class that holds Tea instances and provides search/filter methods: + +```js +class TeaCatalog { + constructor(teas) { + // Store the array of Tea instances + } + + search(query) { + // Return teas where name includes query (case-insensitive) + } + + filterByType(type) { + // Return teas matching the type + } +} + +const catalog = new TeaCatalog( + teas.map(t => new Tea(t.name, t.type, t.origin, t.pricePerGram, t.organic)) +); + +console.log(catalog.search("earl")); +// [Tea { name: "Earl Grey", ... }] + +console.log(catalog.filterByType("green").map(t => t.name)); +// ["Sencha", "Dragon Well", "Matcha", "Genmaicha", "Jasmine Pearl", ...] +``` + +### Exercise 13 + +Create a `Customer` class that can place orders: + +```js +class Customer { + constructor(name, email) { + this.name = name; + this.email = email; + this.orders = []; + } + + placeOrder(order) { + // Add the order to this.orders + // Confirm the order + // Return the order + } + + totalSpent() { + // Use .reduce() to sum getTotal() across all orders + } +} + +const customer = new Customer("Alex", "alex@example.com"); + +const order1 = new Order(); +order1.addItem(new OrderItem(new Tea("Sencha", "green", "Japan", 0.12, true), 100)); +customer.placeOrder(order1); + +const order2 = new Order(); +order2.addItem(new OrderItem(new Tea("Matcha", "green", "Japan", 0.45, true), 50)); +customer.placeOrder(order2); + +console.log(customer.orders.length); // 2 +console.log(customer.totalSpent()); // 34.5 +``` + +### Exercise 14 ⭐ + +Bring it together: create a catalog, find teas, create an order, and assign it to a customer. + +```js +// 1. Create a TeaCatalog from the tea data +const catalog = new TeaCatalog( + teas.map(t => new Tea(t.name, t.type, t.origin, t.pricePerGram, t.organic)) +); + +// 2. Find all Japanese teas +const japaneseTeas = catalog.search("").filter(t => t.origin === "Japan"); + +// 3. Create an order with 100g of each Japanese tea +const order = new Order(); +japaneseTeas.forEach(tea => { + order.addItem(new OrderItem(tea, 100)); +}); + +// 4. Create a customer and place the order +const customer = new Customer("Tea Lover", "lover@tea.com"); +customer.placeOrder(order); + +// 5. Log the summary +console.log(`${customer.name} ordered ${order.items.length} Japanese teas`); +console.log(`Total: ${customer.totalSpent().toFixed(2)} DKK`); +``` + +--- + +## Part 5: Static Methods + +Static methods belong to the class itself, not to instances. Use them for factory methods and utility operations. + +
+📚 Recall: static methods + +A static method is called on the class: `Tea.fromObject(data)`. An instance method is called on an object: `sencha.priceFor(100)`. Static methods don't have access to `this` as an instance. +
+ +### Exercise 15 + +Add a static `fromObject(obj)` factory method to your Tea class that creates a Tea from a plain object: + +```js +class Tea { + // ... existing code ... + + static fromObject(obj) { + // Create and return a new Tea from a plain object + } +} + +// Convert all plain objects to Tea instances in one line: +const teaInstances = teas.map(Tea.fromObject); + +console.log(teaInstances[0].describe()); +// "Sencha (green) from Japan - 12.00 DKK/100g" +console.log(teaInstances[0].priceFor(100)); +// 12 +``` + +> 💡 This pattern is common on backends: data comes from a database or API as plain objects, and a factory method converts them to class instances. + +### Exercise 16 ⭐ + +Add these static utility methods to your Tea class: + +```js +class Tea { + // ... existing code ... + + static findCheapest(teas) { + // Return the tea with the lowest pricePerGram + } + + static findMostExpensive(teas) { + // Return the tea with the highest pricePerGram + } + + static averagePrice(teas) { + // Return the average pricePerGram across all teas + } +} + +const teaInstances = teas.map(Tea.fromObject); + +console.log(Tea.findCheapest(teaInstances).name); +// "English Breakfast" + +console.log(Tea.findMostExpensive(teaInstances).name); +// "Gyokuro" + +console.log(Tea.averagePrice(teaInstances).toFixed(2)); +// "0.22" +``` + +Hint: `findCheapest` and `findMostExpensive` can use `.reduce()`. + +--- + +## Part 6: Inheritance ⭐⭐ + +Inheritance lets one class build on another. The child class gets all parent methods and can add or override them. + +
+📚 Recall: extends and super() + +`extends` creates a child class from a parent. `super()` in the constructor calls the parent's constructor. `super.method()` calls the parent's version of an overridden method. +
+ +### Exercise 17 ⭐⭐ + +Create a `PremiumTea` class that extends `Tea`: + +```js +class PremiumTea extends Tea { + constructor(name, type, origin, pricePerGram, organic, grade) { + // Call parent constructor with super() + // Store grade ("A", "B", or "C") + } + + priceFor(grams) { + // A = 50% markup, B = 25% markup, C = 10% markup + // Use super.priceFor(grams) to get the base price + } + + describe() { + // Override to include grade + // "Gyokuro [Grade A] (green) from Japan - 84.00 DKK/100g" + } +} + +const gyokuro = new PremiumTea("Gyokuro", "green", "Japan", 0.56, false, "A"); +console.log(gyokuro.describe()); +// "Gyokuro [Grade A] (green) from Japan - 84.00 DKK/100g" + +console.log(gyokuro.priceFor(100)); +// 84 (56 * 1.5) + +// It's still a Tea: +console.log(gyokuro instanceof Tea); // true +console.log(gyokuro instanceof PremiumTea); // true +``` + +Test with different grades: + +```js +const gradeB = new PremiumTea("Silver Needle", "white", "China", 0.50, true, "B"); +console.log(gradeB.priceFor(100)); // 62.5 (50 * 1.25) + +const gradeC = new PremiumTea("Darjeeling", "black", "India", 0.18, false, "C"); +console.log(gradeC.priceFor(100)); // 19.8 (18 * 1.1) +``` + +**Bonus:** Add a static `fromTea(tea, grade)` method to PremiumTea that upgrades an existing Tea to a PremiumTea. diff --git a/courses/backend/advanced-javascript/week4/session-plan.md b/courses/backend/advanced-javascript/week4/session-plan.md new file mode 100644 index 00000000..7f8c4aaa --- /dev/null +++ b/courses/backend/advanced-javascript/week4/session-plan.md @@ -0,0 +1,156 @@ +# Session Plan (Week 4: Classes & Object-Oriented Programming) + +> This guide is for mentors. It outlines how to run the session, what to emphasize, and why we introduce certain terminology. + +--- + +## Session Goals + +By the end of this session, trainees should: + +1. Understand why classes exist (organizing data with behavior) +2. Declare a class with `constructor` and `this` +3. Create instances with `new` +4. Add and call methods on instances +5. Understand `this` as "the current object" +6. Know when to use static methods +7. Understand inheritance basics (`extends`, `super()`) + +--- + +## Glossary Goals + +| Term | Why we introduce it | +|------|---------------------| +| **class** | Fundamental concept in every backend framework. They'll see classes in Express, database ORMs, everywhere. | +| **constructor** | Understanding initialization is essential for creating properly structured objects. | +| **instance** | Distinguishing blueprint from object is crucial for understanding how classes work. | +| **this** | Required for writing any class method. Keep it simple: "this = the current object." | +| **encapsulation** | Core OOP principle. Helps them understand why methods live inside classes. | + +--- + +## Session Outline + +### 1. Introduction + +- Recap the course journey: array methods → callbacks → Promises → now organizing code +- Show the copy-paste problem: same validation, same price formula, repeated everywhere +- "What if the tea object could validate itself and calculate its own prices?" +- Introduce the class concept + +**Use:** [Slides](./slides/index.html) (introduction sections) + +### 2. Classes, Constructor, this, new + +- Start simple: a class with just a constructor +- Explain `this` as "the current object being created" +- Show how `new` creates an instance +- Live code: build a `Tea` class step by step +- Compare plain object vs class instance + +**Key point:** "A class is a blueprint. `new` builds an object from that blueprint. `this` is the object being built." + +**Exercises:** [Part 1](./session-materials/exercises.md#part-1-creating-classes) (exercises 1-4) + +### 3. Methods + +- Add `priceFor(grams)` and `describe()` methods to Tea +- Emphasize: the method lives *with* the data (encapsulation) +- Introduce `OrderItem`: a class that *uses* a Tea instance (composition) +- Live code: `OrderItem` with `lineTotal()` + +**Key point:** "Methods live inside the class, right next to the data they work with. The object knows how to do things with its own data." + +**Exercises:** [Part 2](./session-materials/exercises.md#part-2-methods) (exercises 5-8) + +### Break + +### 4. this and State + +- Show how methods can *modify* instance state, not just read it +- Live code: `Inventory` with `sell()` and `restock()` +- Live code: `Order` with status transitions +- Emphasize: `this` is how you read AND write state + +**Key point:** "Methods don't just read data - they can change it. `this.stockCount -= grams` modifies this specific instance's state." + +**Exercises:** [Part 3](./session-materials/exercises.md#part-3-this-and-state) (exercises 9-11) + +### 5. Classes Working Together + +- Show composition: classes that use other classes +- Live code: `TeaCatalog` that holds Tea instances +- Live code: `Order` that contains `OrderItem` instances +- Connect to previous weeks: use `.filter()`, `.map()`, `.reduce()` inside methods + +**Key point:** "Real programs are built from classes that work together. An Order contains OrderItems. A Catalog holds Teas. Each class does its own job." + +**Exercises:** [Part 4](./session-materials/exercises.md#part-4-classes-working-together) (exercises 12-14) + +### Break + +### 6. Static Methods + +- Some operations belong to the class, not to any instance +- Live code: `Tea.fromObject()` factory method +- Live code: `Tea.findCheapest()` utility +- Explain when to use static vs instance methods + +**Key point:** "Static methods are called on the class: `Tea.fromObject(data)`. Instance methods are called on objects: `sencha.priceFor(100)`. Use static when the operation doesn't belong to one specific instance." + +**Exercises:** [Part 5](./session-materials/exercises.md#part-5-static-methods) (exercises 15-16) + +### 7. Wrap-up + +- Brief inheritance demo: `PremiumTea extends Tea` +- Recap: the four-week journey (array methods → callbacks → Promises → classes) +- How classes connect to everything: the Tea Shop API uses models like these behind the scenes +- Part 6 is a stretch goal for fast learners +- Introduce the assignment + +--- + +## Teaching Tips + +### Managing exercises with mixed skill levels + +The exercises are intentionally numerous - students progress at different speeds. Here's how to manage this: + +1. **Let students work on Part 1** - observe how far each gets individually +2. **Solve one exercise in plenum** - pick one that weaker students struggled with but stronger students just finished. This way everyone benefits +3. **Move on to Part 2** - weaker students skip remaining Part 1 exercises (they can revisit later) +4. **Repeat** for each part + +Exercises marked with ⭐ are optional stretch goals. If your fastest students finish all exercises while others haven't solved the first one, that part needs an additional challenge - let us know, or create a PR with a creative challenging exercise! + +### Keep `this` simple + +The `this` keyword is notoriously confusing in JavaScript. For this session, keep it simple: "Inside a class, `this` means the current object." Don't get into `bind`, `call`, `apply`, or arrow function `this` behavior. Those are advanced topics for later. + +### Composition before inheritance + +Spend most of the time on classes working together (composition). Inheritance is introduced last and only briefly. Most real-world JavaScript code uses composition more than inheritance. + +### Connect to previous weeks + +This is the last week - tie it all together: +- "Remember `.filter()` and `.map()` from Week 1? You'll use them inside class methods" +- "Remember `.reduce()` from Week 2? Perfect for calculating totals in an Order" +- "The Tea Shop API from Week 3? Its backend uses classes just like these" + +### Common mistakes to watch for + +- Forgetting `new` when creating instances (get `undefined` or errors) +- Forgetting `this.` when accessing properties in methods +- Calling instance methods on the class instead of on an instance +- Confusing static methods with instance methods + +--- + +## Materials + +- [Slides](./slides/index.html) - Reveal.js presentation +- [Exercises](./session-materials/exercises.md) - In-class exercises +- [Assignment](./assignment.md) - Take-home work +- [Tea Data](../data/teas.js) - Shared dataset diff --git a/courses/backend/advanced-javascript/week4/slides/index.html b/courses/backend/advanced-javascript/week4/slides/index.html new file mode 100644 index 00000000..89946c0e --- /dev/null +++ b/courses/backend/advanced-javascript/week4/slides/index.html @@ -0,0 +1,527 @@ + + + + + + Week 4 - Classes & OOP + + + + + + + +
+
+ + +
+

Classes & OOP

+

Organizing code with blueprints

+

Week 4

+
+ + +
+
+

The Problem

+

Scattered data, scattered logic

+
+ +
+

Plain Objects Everywhere

+
const tea1 = { name: "Sencha", pricePerGram: 0.12 };
+const tea2 = { name: "Earl Grey", pricePerGram: 0.08 };
+const tea3 = { naem: "Matcha", price: 0.45 };
+//               ^^^^ typo!     ^^^^^ wrong name!
+

No structure. No validation. No consistency.

+
+ +
+

Logic Scattered Everywhere

+
// Price calculation - repeated in 5 places
+const price1 = tea1.pricePerGram * 100;
+const price2 = tea2.pricePerGram * 100;
+
+// Validation - a separate function
+function validateTea(tea) {
+  if (!tea.name) throw new Error("Name required");
+}
+
+// Formatting - another separate function
+function describeTea(tea) {
+  return `${tea.name} from ${tea.origin}`;
+}
+
+ +
+

What If...

+
+ What if the object could validate itself?

+ What if the object could calculate its own price?

+ What if the object could describe itself? +
+
+
+ + +
+
+

Classes

+

Blueprints for objects

+
+ +
+

A Class

+
class Tea {
+  constructor(name, pricePerGram, origin) {
+    this.name = name;
+    this.pricePerGram = pricePerGram;
+    this.origin = origin;
+  }
+}
+

class - declares a blueprint

+

constructor - initializes each object

+

this - the object being created

+
+ +
+

Creating Instances with new

+
const sencha = new Tea("Sencha", 0.12, "Japan");
+const earl = new Tea("Earl Grey", 0.08, "India");
+
+console.log(sencha.name);  // "Sencha"
+console.log(earl.origin);   // "India"
+

One class (blueprint) → many instances (objects)

+
+ +
+

Blueprint vs Building

+

📐 → 🏠 🏠 🏠

+

One blueprintMany buildings

+

Same structure, different details

+
+
+ + +
+
+

Methods

+

Behavior lives with data

+
+ +
+

Adding Methods

+
class Tea {
+  constructor(name, pricePerGram, origin) {
+    this.name = name;
+    this.pricePerGram = pricePerGram;
+    this.origin = origin;
+  }
+
+  priceFor(grams) {
+    return this.pricePerGram * grams;
+  }
+
+  describe() {
+    return `${this.name} from ${this.origin}`;
+  }
+}
+
+ +
+

Calling Methods

+
const sencha = new Tea("Sencha", 0.12, "Japan");
+
+sencha.priceFor(100);  // 12
+sencha.describe();     // "Sencha from Japan"
+

The tea knows how to calculate its own price

+

No separate functions needed

+
+ +
+

Before vs After

+
+
+

Without classes

+
const tea = {
+  name: "Sencha",
+  pricePerGram: 0.12
+};
+
+function priceFor(tea, g) {
+  return tea.pricePerGram * g;
+}
+
+priceFor(tea, 100);
+
+
+

With classes

+
class Tea {
+  constructor(name, ppg) {
+    this.name = name;
+    this.pricePerGram = ppg;
+  }
+  priceFor(grams) {
+    return this.pricePerGram * grams;
+  }
+}
+new Tea("Sencha", 0.12)
+  .priceFor(100);
+
+
+
+ +
+

Composition

+
class OrderItem {
+  constructor(tea, grams) {
+    this.tea = tea;      // Tea instance
+    this.grams = grams;
+  }
+
+  lineTotal() {
+    return this.tea.priceFor(this.grams);
+  }
+}
+
+const sencha = new Tea("Sencha", 0.12, "Japan");
+const item = new OrderItem(sencha, 200);
+item.lineTotal(); // 24
+

Classes using other classes

+
+
+ + +
+
+

this & State

+

Reading and changing data

+
+ +
+

this = The Current Object

+
class Inventory {
+  constructor(tea, stockCount) {
+    this.tea = tea;
+    this.stockCount = stockCount;
+  }
+
+  sell(grams) {
+    this.stockCount -= grams;  // modifies state
+  }
+
+  restock(grams) {
+    this.stockCount += grams;  // modifies state
+  }
+}
+
+ +
+

Each Instance Has Its Own State

+
const senchaStock = new Inventory(sencha, 150);
+const matchaStock = new Inventory(matcha, 30);
+
+senchaStock.sell(50);
+console.log(senchaStock.stockCount); // 100
+console.log(matchaStock.stockCount); // 30 (unchanged)
+

Selling Sencha doesn't affect Matcha

+
+ +
+

Order Status

+
class Order {
+  constructor() {
+    this.items = [];
+    this.status = "pending";
+  }
+
+  addItem(item) {
+    this.items.push(item);
+  }
+
+  confirm() {
+    this.status = "confirmed";
+  }
+
+  getTotal() {
+    return this.items.reduce(
+      (sum, item) => sum + item.lineTotal(), 0
+    );
+  }
+}
+
+
+ + +
+
+

Classes Working Together

+

Composition

+
+ +
+

Each Class Has a Job

+

Tea knows its name, price, origin

+

OrderItem knows tea + quantity

+

Order manages a list of items

+

Customer tracks orders

+
+ +
+

Building an Order

+
const sencha = new Tea("Sencha", 0.12, "Japan");
+const matcha = new Tea("Matcha", 0.45, "Japan");
+
+const order = new Order();
+order.addItem(new OrderItem(sencha, 100));
+order.addItem(new OrderItem(matcha, 50));
+
+console.log(order.getTotal()); // 34.5
+
+ +
+

Array Methods + Classes

+
// Week 1 skills inside class methods!
+class TeaCatalog {
+  constructor(teas) {
+    this.teas = teas;
+  }
+
+  filterByType(type) {
+    return this.teas.filter(t => t.type === type);
+  }
+
+  search(query) {
+    return this.teas.filter(t =>
+      t.name.toLowerCase().includes(query.toLowerCase())
+    );
+  }
+}
+

.filter() and .map() from Week 1 — inside classes!

+
+
+ + +
+
+

Static Methods

+

Methods on the class itself

+
+ +
+

Instance vs Static

+

Instance method: called on an object

+
sencha.priceFor(100);  // on the instance
+

Static method: called on the class

+
Tea.fromObject(data);  // on the class
+
+ +
+

Factory Method

+
class Tea {
+  // ...
+
+  static fromObject(obj) {
+    return new Tea(
+      obj.name, obj.pricePerGram, obj.origin
+    );
+  }
+}
+
+// Convert API/database data to class instances:
+const teaInstances = teas.map(Tea.fromObject);
+

Very common on backends

+
+ +
+

Utility Methods

+
class Tea {
+  // ...
+
+  static findCheapest(teas) {
+    return teas.reduce((min, t) =>
+      t.pricePerGram < min.pricePerGram ? t : min
+    );
+  }
+}
+
+Tea.findCheapest(teaInstances).name;
+// "English Breakfast"
+
+
+ + +
+
+

Inheritance

+

Specializing a class

+
+ +
+

extends and super()

+
class PremiumTea extends Tea {
+  constructor(name, pricePerGram, origin, grade) {
+    super(name, pricePerGram, origin);
+    this.grade = grade;
+  }
+
+  priceFor(grams) {
+    return super.priceFor(grams) * 1.5;
+  }
+}
+
+const gyokuro = new PremiumTea("Gyokuro", 0.56, "Japan", "A");
+gyokuro.priceFor(100); // 84
+
+ +
+

When to Use

+

"Is-a" relationship: PremiumTea is a Tea

+

But most of the time, prefer composition

+

An Order has OrderItems (not "is an" OrderItem)

+
+
+ + +
+
+

The Course Journey

+
+ +
+

Four Weeks

+

+ W1: Array Methods + W2: Callbacks +

+

+ W3: Promises + W4: Classes +

+

Transform data → Handle timing → Fetch from APIs → Organize code

+
+ +
+

It All Connects

+
class TeaShop {
+  async fetchAndStore() {
+    // Week 3: fetch from API
+    const response = await fetch(`${API}/teas`);
+    const data = await response.json();
+
+    // Week 4: convert to class instances
+    this.teas = data.map(Tea.fromObject);
+
+    // Week 1: filter and transform
+    const organic = this.teas
+      .filter(t => t.organic)
+      .map(t => t.describe());
+
+    // Week 2: reduce to totals
+    const total = this.teas.reduce(
+      (sum, t) => sum + t.priceFor(100), 0
+    );
+  }
+}
+
+
+ + +
+
+

Summary

+
+ +
+

Key Concepts

+

class - blueprint for objects

+

constructor - initializes instances

+

this - the current object

+

new - creates an instance

+

static - method on the class

+

extends - inherit from another class

+
+ +
+

Questions to Ask

+

Does this data need behavior? → class

+

Does this method need instance data? → instance method

+

Does it operate on the class? → static method

+

Is it a specialized version? → extends

+
+ +
+

Time to Practice

+

🍵

+
+
+ +
+
+ + + + + + +