1

Hello, Node.js!

The Lay of the Land

Learning objective: After this step you will be able to explain how Node.js uses V8 and libuv to run JavaScript outside the browser, write console.log() statements, and use if/else if/else and for...of control flow.

You already know two languages. JavaScript powers the apps you use every day — Discord, Spotify, Netflix, TikTok’s web player, Twitch, and even parts of VS Code. Here is how it fits into your mental model:

  C++ Python JavaScript (Node.js)
Typing Static Dynamic Dynamic
Memory Manual (new/delete) GC (reference counting) GC (V8 engine)
Run with Compile → ./app python script.py node script.js
I/O model Synchronous (blocks) Synchronous (blocks) Asynchronous (non-blocking)

Node.js takes JavaScript out of the browser by wrapping two engines:

  • V8 — Google’s just-in-time (JIT) compiler that turns JavaScript into machine code (like g++ for C++) right before you execute it.
  • libuv — A C library providing the Event Loop and non-blocking I/O access to the OS.

Together, they let JavaScript write backend servers, CLI tools, and scripts — just like Python or C++. Node.js powers the backend of apps you probably used today, so learning it gives you superpowers to build your own web apps and tools.

Predict Before You Code

Look at hello.js — this is our soon-to-be hello world program. In C++ your hello world would be printf("Hello from C++!\n"); In Python it would be print("Hello from Python!"). What might it be for JavaScript running in Node.js? Maybe a mix of both? Not at all. JavaScript has its own syntax for printing to the console.

Quick Syntax Reference: Control Flow

JavaScript’s control flow looks like C++ (braces required), not Python (no colons/indentation):

// if/else — braces required (unlike Python's colon + indentation)
if (score >= 90) {
    console.log("A");
} else if (score >= 60) {
    console.log("Pass");
} else {
    console.log("Fail");
}

// for loop — same structure as C++
for (let i = 0; i < 5; i++) {
    console.log(i);
}

// for...of — like Python's "for x in list"
const names = ["Alice", "Bob", "Carol"];
for (const name of names) {
    console.log(name);
}

Python students: No colons, no elif (use else if), and braces {} define blocks — not indentation. C++ students: Almost identical, but use let/const instead of type declarations in for loops.

Semicolons: Unlike Python, JavaScript statements conventionally end with ; (like C++). JavaScript can usually auto-insert them, but always using semicolons avoids subtle bugs and matches the style you will see in professional codebases.

Task: Your First Node.js Script

Open hello.js in the editor. Complete the three TODO items:

  1. Print "Hello from Node.js!" using console.log().
  2. Write an if/else block that checks the variable score: if it is >= 60, print "Pass", otherwise print "Fail".
  3. Write a for...of loop that iterates over the languages array and prints each language name.

Click ▶ Run to execute the script and see the output. This executes node hello.js in background. In this tutorial you focus just on writing Node.js. We run these commands for you.

Starter files
hello.js
// Your first Node.js script!

// TODO 1: Print "Hello from Node.js!" using console.log()


// TODO 2: If score >= 60 print "Pass", otherwise print "Fail"
const score = 85;


// TODO 3: Use a for...of loop to print each language in the array.
const languages = ["C++", "Python", "JavaScript"];
2

Variables, Types & The === Trap

Bridging Your Mental Models

Learning objective: After this step you will be able to declare variables with let/const, use template literals, and explain why === should be preferred over ==.

JavaScript’s type system looks like Python but hides a critical landmine.

let and const

Forget C++’s int x = 5. Modern JavaScript uses:

let count = 0;          // Mutable — like a regular Python variable
const MAX_SIZE = 200;   // Immutable binding — like Python's ALL_CAPS convention, but enforced

Mutable variables can be assigned different values afterwards. This is useful when the value is expected to change, e.g. a counter. However, it also masks bugs that result from incorrect assignments. Use immutable bindings (const in JS, final in Java, const in C++) when declaring constants that are not expected to change.

Avoid using var — it has “hoisting” scoping rules that violate everything you know from C++ and Python. Always use let or const.

Template Literals (like Python’s f-strings)

// Python:      f"Hello, {name}! You scored {grade}."
// JavaScript:  `Hello, ${name}! You scored ${grade}.`
//               ^backtick                  ^dollar-brace

The === Trap ⚠️

JavaScript has TWO equality operators with different semantics. To avoid surprises, always use ===:

// SURPRISE: == triggers implicit type coercion — a JS-specific danger
console.log(1 == "1");    // true  ← DANGEROUS SURPRISE
console.log(0 == false);  // true  ← DANGEROUS SURPRISE

// AS EXPECTED: === checks value AND type (behaves like == in Python and C++)
console.log(1 === "1");   // false ← correct
console.log(0 === false); // false ← correct

This is negative transfer: your existing == intuition from C++ and Python does not transfer to JavaScript. Use === and it matches your expectation.

Debugging tip: When a comparison behaves unexpectedly, use typeof to check what type a value actually is: console.log(typeof myVar) prints "string", "number", "boolean", "undefined", or "object". This is your first debugging tool for type-related surprises.

Feeling confused by == vs ===? That is completely normal — this trips up experienced developers too. The fact that you are learning the distinction now puts you ahead of most JavaScript beginners.

JavaScript’s Two “Nothings”: null vs undefined

C++ has nullptr. Python has None. JavaScript has two values meaning “nothing” — and they are not the same:

let score;                // declared but no value → undefined
console.log(score);       // undefined
console.log(typeof score); // "undefined"

let student = null;       // explicitly set to "no value"
console.log(student);     // null
console.log(typeof student); // "object" (yes, this is a known JS quirk)
  undefined null
Meaning “no value was assigned yet” “intentionally empty”
When you see it Uninitialized variables, missing function arguments, req.query.missing You (or an API) explicitly set it
typeof "undefined" "object" (a famous JS bug that can never be fixed)
Python equivalent No direct equivalent (Python raises NameError) None

Watch out: null == undefined is true (coercion!), but null === undefined is false. One more reason to always use ===.

You will encounter undefined constantly — every time you access a property that does not exist or forget a function argument. Recognizing it instantly will save you hours of debugging.

Predict Before You Run

Before clicking Run on types.js, predict: will userInput == expectedScore (where userInput is the string "42" and expectedScore is the number 42) be true or false? What would it be in Python?

Task: Fix the Fixer-Upper

Open types.js. It has three bugs:

  1. Two comparisons that produce wrong results because they do not type-check — fix them!
  2. A mutable declaration for a value that never changes — change it to be immutable.
  3. A messy string concatenation — replace it with a template literal.

Before you click Run, add a brief comment above each fix explaining why your change is correct — for example, // Fixed: === checks type + value, prevents coercion. Explaining your reasoning strengthens understanding far more than just making the code pass.

Click ▶ Run to check your output. It should no longer show any [BUG] messages.

Starter files
types.js
// FIXER-UPPER: This file has three bugs. Find and fix them all.

// Does this comparison really make sense?
let userInput = "42";
let expectedScore = 42;
if (userInput == expectedScore) {
    console.log("[BUG] String '42' should NOT equal number 42 here!");
} else {
    console.log("Score check: types are different, correctly rejected.");
}

// How about this comparison?
let isAdmin = false;
if (isAdmin == 0) {
    console.log("[BUG] false should NOT equal the number 0 here!");
} else {
    console.log("Admin check: false and 0 are different types, correctly rejected.");
}

// What if we accidently use the same name later on in the program, how could we ensure that we always find that bug?
let MAX_STUDENTS = 200;

// Bruh so many + and " characters. How could we simplify this?
// Expected output format: "Student Alex scored 95 out of 200"
let studentName = "Alex";
let studentGrade = 95;
let message = "Student " + studentName + " scored " + studentGrade + " out of " + MAX_STUDENTS;
console.log(message);
3

Arrow Functions & Callbacks

The Foundation of Everything That Follows

Learning objective: After this step you will be able to write arrow functions, pass functions as callbacks, and use .filter() to select array elements.

In C++, you’ve encountered function pointers. In Python, you’ve passed functions to sorted(key=...) or map(). JavaScript takes this further: functions are just values, exactly like numbers or strings.

This is not merely a stylistic feature — it is the entire foundation of Node.js’s asynchronous model and the Express web framework you will use starting in Step 5. Understanding it now makes everything later obvious.

Arrow Functions

// C++ equivalent: int add(int a, int b) { return a + b; }
// Python equivalent: def add(a, b): return a + b

// JavaScript (regular function):
function add(a, b) { return a + b; }

// JavaScript (arrow function — the modern preferred style):
const add = (a, b) => a + b;

// More examples:
const greet  = (name) => `Hello, ${name}!`;
const double = n => n * 2;   // Parentheses optional for a single parameter
const hi     = () => "Hi!";  // Empty parentheses for no parameters

Callbacks: Passing Functions as Arguments

A callback is a function you pass as an argument to another function. The receiving function “calls it back” at the right time.

// Python equivalent: list(filter(lambda x: x > 2, [1, 2, 3, 4, 5]))
const numbers = [1, 2, 3, 4, 5];

const bigNums = numbers.filter(n => n > 2);        // [3, 4, 5]
const evens   = numbers.filter(n => n % 2 === 0);  // [2, 4]

.filter() takes a callback — an arrow function that returns true or false for each element. Only elements where the callback returns true are kept.

Why Callbacks Matter

In the upcoming steps, you will see callbacks everywhere:

// In Express (Step 5): the route handler IS a callback
app.get('/', (req, res) => { res.send('Hello!'); });

// In setTimeout (Step 8): the Event Loop calls your function later
setTimeout(() => console.log('done'), 1000);

The mental model — pass a function, get called back later — is the single most important pattern in JavaScript.

Predict Before You Code

What does [10, 20, 30, 40, 50].filter(n => n > 25) return? Write your prediction before reading on.

Investigate (after completing the task)

  • What happens if you change >= to > in your passing filter? Which students change?
  • What does students.filter(s => s.grade >= 60).length return? (Hint: not an array.)

Task: Arrow Functions & Filtering

Open functions.js. Complete the three TODO items:

  1. Convert getLetterGrade from a function declaration to an arrow function assigned to const.
  2. Use .filter() with an arrow function to keep only passing students (grade >= 60).
  3. Use .filter() again to create an honors list (grade >= 90).

Click ▶ Run to check your output.

Starter files
functions.js
// Arrow Functions & Callbacks — complete the three TODOs below

const students = [
    { name: "Alice", grade: 95 },
    { name: "Bob",   grade: 42 },
    { name: "Carol", grade: 78 },
    { name: "Dave",  grade: 55 },
    { name: "Eve",   grade: 88 },
];

// TODO 1: Convert this to an arrow function assigned to a const
function getLetterGrade(score) {
    if (score >= 90) return "A";
    if (score >= 80) return "B";
    if (score >= 70) return "C";
    if (score >= 60) return "D";
    return "F";
}

// TODO 2: Use .filter() with an arrow function to keep only passing students (grade >= 60)
// Replace the line below — Bob (42) and Dave (55) should be excluded
const passingStudents = students;

// TODO 3: Use .filter() to create an honors list (grade >= 90)
// Only Alice (95) should be in this list
const honorsStudents = students;

console.log("=== Passing Students ===");
passingStudents.forEach(s => console.log(`${s.name}: ${s.grade} (${getLetterGrade(s.grade)})`));

console.log("\n=== Honors Students ===");
honorsStudents.forEach(s => console.log(`${s.name}: ${s.grade}`));
4

Array Transformation & Destructuring

Transforming Data with Array Methods

Learning objective: After this step you will be able to use .map() to transform arrays, .reduce() to accumulate values, and destructure objects and arrays.

In Step 3 you learned .filter() — selecting elements. Now you will learn to transform them with .map() and combine them with .reduce(). These three methods — .filter(), .map(), .reduce() — are the workhorses of data processing in JavaScript, and you will use all three inside Express route handlers starting in Step 5.

Objects and JSON — What You Have Been Using All Along

Since Step 3 you have been writing { name: "Alice", grade: 95 }. These are object literals — JavaScript’s equivalent of Python dictionaries and C++ structs:

const student = { name: "Alice", grade: 95 };

// Access properties with dot notation (most common):
console.log(student.name);      // "Alice"
console.log(student.grade);     // 95

// Or bracket notation (useful when the key is a variable):
const key = "name";
console.log(student[key]);      // "Alice"

// Add or update properties:
student.email = "alice@school.edu";
student.grade = 97;

JSON (JavaScript Object Notation) is the text format for sending objects over HTTP — every API you will build uses it:

// Object → JSON string (for sending in a response):
const jsonStr = JSON.stringify(student);  // '{"name":"Alice","grade":97}'

// JSON string → Object (for reading a request body or file):
const parsed = JSON.parse('{"name":"Bob","grade":42}');
console.log(parsed.name);  // "Bob"

res.json(data) in Express calls JSON.stringify for you — but when reading files (Step 8–9), you will need JSON.parse() yourself.

.map() — Transform Every Element

.map() creates a new array by applying a callback to each element:

// Python equivalent: list(map(lambda x: x * 2, [1, 2, 3]))
const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2);     // [2, 4, 6]
const labels  = numbers.map(n => `#${n}`);   // ["#1", "#2", "#3"]

.map() always returns an array of the same length. .filter() can return fewer elements; .map() transforms every one.

.reduce() — Accumulate a Single Value

.reduce() combines all elements into one value:

const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((accumulator, current) => accumulator + current, 0);
// Step by step: 0+1=1, 1+2=3, 3+3=6, 6+4=10, 10+5=15 → result: 15

The second argument (0) is the initial value of the accumulator. Always provide it — without it, .reduce() throws on empty arrays.

// Python equivalent: functools.reduce(lambda acc, n: acc + n, [1,2,3,4,5], 0)
// Or simply: sum([1, 2, 3, 4, 5])

Destructuring: Unpacking Values

JavaScript has a compact syntax for extracting values from arrays and objects:

Array destructuring — assign items by position:

const coords = [40.7, -74.0];
const [lat, lng] = coords;    // lat = 40.7, lng = -74.0

// Python equivalent: lat, lng = coords  (tuple unpacking — same idea)

Object destructuring — extract properties by name:

const student = { name: "Alice", grade: 95 };
const { name, grade } = student;   // name = "Alice", grade = 95

// Works in function parameters — you will see this in every React component:
function printStudent({ name, grade }) {
    console.log(`${name}: ${grade}`);
}

Destructuring is especially useful inside .map() callbacks:

const students = [{ name: "Alice", grade: 95 }, { name: "Bob", grade: 42 }];
const names = students.map(({ name }) => name);  // ["Alice", "Bob"]

Formatting Output: .toFixed() and .padEnd()

Two small utilities you will need for formatting:

// .toFixed(n) — format a number to n decimal places (returns a string)
const avg = 87.666;
console.log(avg.toFixed(1));   // "87.7"

// .padEnd(n) — pad a string with spaces to reach length n (left-aligns text)
console.log("Alice".padEnd(7));   // "Alice  " (7 chars total)
console.log("Bob".padEnd(7));     // "Bob    " (7 chars total)

Predict Before You Code

Predict: what does [1, 2, 3].map(n => n * 10) return? What about [1, 2, 3].reduce((acc, n) => acc + n, 0)? Write your predictions, then verify in the editor.

Task: Build a Grade Report

Open transform.js. The getLetterGrade arrow function from Step 3 is provided. Complete the four TODO items — each builds on the previous one, so do them in order:

  1. Use .map() to extract just the grade numbers into a new array: students.map(s => s.grade)[95, 42, 78, 55, 88]. This is the simplest .map() — transform objects into numbers.
  2. Use .reduce() to compute the sum of the grade numbers, then divide by the count to get the class average.
  3. Use .map() again, this time with destructuring ({ name, grade }) in the arrow function parameter, to format each student as "Name | grade (Letter)". Use getLetterGrade() for the letter and .padEnd(7) to align names.
  4. Print the class average formatted to 1 decimal place using .toFixed(1).
  5. Create an array containing only the names of students who are failing (grade < 60). Which array methods should you chain? The instructions above cover everything you need — choose the right ones yourself.

Why this progression? TODOs 1–4 each introduce one new concept with the method named for you. TODO 5 is different — it describes the outcome without telling you which methods to use. Choosing the right tool is a distinct skill from knowing how to use it.

Click ▶ Run to check your result.

Starter files
transform.js
// Array Transformation — complete the four TODOs in order

const students = [
    { name: "Alice", grade: 95 },
    { name: "Bob",   grade: 42 },
    { name: "Carol", grade: 78 },
    { name: "Dave",  grade: 55 },
    { name: "Eve",   grade: 88 },
];

// Provided: arrow function from Step 3 (already learned)
const getLetterGrade = (score) => {
    if (score >= 90) return "A";
    if (score >= 80) return "B";
    if (score >= 70) return "C";
    if (score >= 60) return "D";
    return "F";
};

// TODO 1: Use .map() to extract just the grade numbers.
// Expected result: [95, 42, 78, 55, 88]
const grades = students;

// TODO 2: Use .reduce() to compute the sum of the grades array.
// Then divide by grades.length to get the class average.
// Hint: grades.reduce((acc, g) => acc + g, 0)
const classAverage = 0;

// TODO 3: Use .map() with destructuring ({ name, grade }) to format
// each student as "Name   | grade (Letter)".
// Use getLetterGrade() for the letter and .padEnd(7) to align names.
// Expected: "Alice   | 95 (A)"
const report = students;

// TODO 4: Print the report and the class average.
// Format the average to 1 decimal place using .toFixed(1).
console.log("=== Grade Numbers ===");
console.log(grades);
console.log("\n=== Student Report ===");
report.forEach(line => console.log(line));
console.log(`Class average: ${classAverage}`);

// TODO 5: Create an array of ONLY the names of failing students (grade < 60).
// Which array methods do you need? Choose and chain them yourself.
const failingNames = students;

console.log("\n=== Failing Students ===");
console.log(failingNames);
5

Your First Express Route

From Callbacks to Web Servers

Learning objective: After this step you will be able to explain how Express uses callbacks to handle HTTP requests and create a basic Express GET route.

You have been building callback skills for two steps. Now you will see why: an Express route handler is a callback. The entire Express framework is built on the pattern you already know.

What is Express?

Express is a web framework for Node.js. While Node.js has a built-in http module, almost every real project uses Express or a similar library, because it makes routing so much easier.

Express lets you say:
  "When someone visits THIS URL, call THIS function."

That is literally it. Express routing = URL → callback.

The Anatomy of an Express App

// Step 1: Import the Express module
const express = require('express');

// Step 2: Create an Express application
const app = express();

// Step 3: Define a route — THIS IS A CALLBACK!
//         (req, res) => { ... } is the same arrow function pattern from Step 3
app.get('/', (req, res) => {
    res.send('Hello from Express!');
});

// Step 4: Start the server — listen for requests on port 3000
app.listen(3000);

Look at Step 3 carefully. The second argument to app.get() is an arrow function — a callback. Express calls this function whenever someone visits the '/' URL. This is exactly how .filter() calls your function for each array element.

Concept Array Method Express Route
You provide A callback function A callback function
It gets called when .filter() processes each element A user visits the URL
Arguments passed to you The current array element req (request info) and res (response tools)

The req and res Objects

  • req (request): Contains information about the incoming HTTP request — the URL, headers, query parameters, body data, etc.
  • res (response): Contains methods to send a response back — res.send() sends text, res.json() sends JSON.

Predict Before You Run

Look at server.js and predict — before clicking Run:

  1. After you click Run and start the server, what text will appear in the terminal?
  2. After you click the HTTP Client’s Send button for GET /, what text will appear in the response body?

Write your predictions down, then run the code and compare. Getting it right matters less than doing the prediction.

If your server starts but the HTTP client says “Cannot GET /” or shows an error — that is completely normal. Read the error message. It tells you exactly what is wrong. Debugging a server that does not respond yet is how every Express developer learns.

Task: Modify a Working Express Server

The file server.js contains a complete, working Express server. Almost everything is done for you.

Your only task: Change the response message from "Replace me!" to "Hello from Express!" and click ▶ Run.

Then use the HTTP Client below to send a GET request to http://localhost:3000/ and see your response appear.

This step has maximum scaffolding on purpose — you are seeing the full pattern for the first time. In the next steps, you will write more and more of it yourself.

Starter files
server.js
// Your first Express server — almost everything is provided!
const express = require('express');
const app = express();

// This route handles GET requests to "/"
// The arrow function is a CALLBACK — the same pattern from Step 3
app.get('/', (req, res) => {
    // TODO: Look what happens when you change this!
    res.send("Replace me!");
});

app.listen(3000, () => {
    console.log("Express server listening on port 3000");
});
6

Dynamic Routes: Queries, Params & POST

Making Routes Dynamic

Learning objective: After this step you will be able to read query parameters with req.query, extract URL parameters with req.params, and handle POST requests with req.body.

In Step 5, your route always returned the same response. Real APIs need to respond differently based on what the user asks for. Express provides three ways to receive data from users:

1. Query Parameters (req.query)

Query parameters are key-value pairs appended to the URL after a ?:

GET /students?passing=true&sort=name
              ^^^^^^^^^^^^^^^^^^^^^^^^ query string
app.get('/students', (req, res) => {
    const passing = req.query.passing;   // "true" (always a string!)
    const sort    = req.query.sort;       // "name"
    // Use these to filter/sort your data
});

⚠️ Step 2 connection: req.query.passing is always a string — even if the URL says ?passing=true, the value is the string "true", NOT the boolean true. Use === 'true' to compare (not == true).

2. Route Parameters (req.params)

Route parameters are placeholders in the URL path:

GET /students/3        — :id is 3
GET /students/alice     — :id is alice
app.get('/students/:id', (req, res) => {
    const id = req.params.id;   // "3" (also a string!)
    // Find the student with this ID
});

The :id in the route pattern tells Express “capture whatever appears here and put it in req.params.id.”

3. POST with Request Body (req.body)

GET requests data and puts parameters in the URL (visible to everyone). POST sends data hidden inside the request “body” — used for creating/modifying data or sending sensitive information.

// Tell Express to parse incoming JSON bodies
app.use(express.json());

app.post('/students', (req, res) => {
    const newStudent = req.body;   // { name: "Frank", grade: 72 }
    // Process the data
});

What is app.use(express.json())? Express does not read request bodies by default — they arrive as raw bytes. express.json() is middleware: a function that runs before your route handler and converts the raw JSON bytes into a JavaScript object. Without it, req.body would be undefined. Think of it as a translator that runs between the incoming HTTP request and your handler callback.

  GET + Query Params GET + Route Params POST + Body
Data in URL: ?key=value URL: /path/:param Request body (hidden)
Use for Filtering, searching Identifying ONE resource Creating/modifying data
Example /students?passing=true /students/3 POST /students with JSON

New Array Method: .find()

You already know .filter() returns all matching elements. Often you need just one. That is what .find() does:

const students = [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];

// .filter() returns an array (possibly empty):
students.filter(s => s.id === 2);   // [{ id: 2, name: "Bob" }]

// .find() returns the FIRST match (or undefined if none):
students.find(s => s.id === 2);     // { id: 2, name: "Bob" }

Use .find() when you are looking for one specific item (like a student by ID). Use .filter() when you want all items matching a condition.

Task: Build a Dynamic Student API

Open server.js. The Express app and student data are provided. Implement the three route handlers (the route structure is given — you fill in the logic):

  1. GET /students — Return all students. If ?passing=true is in the URL, use .filter() to return only passing students (grade >= 60).
  2. GET /students/:id — Find and return the student matching the given id. Use === with Number(req.params.id) to compare (remember: params are strings!).
  3. POST /students — Read the new student from req.body and add them to the array with .push(). Respond with the updated students list.

Scaffolding level: The full route declarations are provided — you write the handler logic inside each callback. This is more independence than Step 5, but you still have the structure.

Predict Before You Implement

Before writing any code, look at the starter file and answer:

  1. If you send GET /students?passing=true right now (with res.json("Implement me!") unchanged), what will the HTTP client show?
  2. What is the data type of req.query.passing — a boolean or a string?
  3. Will req.params.id === 3 (comparing to the number 3) ever be true? Why not? (Hint: revisit Step 2’s lesson about types.)

Expect at least one route to return wrong results on your first attempt — that is not failure, it is the normal debugging loop. Read the response body; it usually tells you exactly what went wrong.

Note: The starter code includes app.use(express.json()) at the top. This middleware is required for POST routes — without it, req.body would be undefined.

After implementing each route, add a one-line comment above it explaining your approach — e.g., // Filter by query param, convert with Number() + ===. Articulating why your code works catches bugs before you run and deepens your understanding.

Starter files
server.js
const express = require('express');
const app = express();
app.use(express.json());

const students = [
    { id: 1, name: "Alice", grade: 95 },
    { id: 2, name: "Bob",   grade: 42 },
    { id: 3, name: "Carol", grade: 78 },
    { id: 4, name: "Dave",  grade: 55 },
    { id: 5, name: "Eve",   grade: 88 },
];

// ROUTE 1: GET /students — return all (or filter by ?passing=true)
// Scaffolding: route declaration provided. You write the handler logic.
app.get('/students', (req, res) => {
    // TODO: If req.query.passing, filter to grade >= 60
    //       Otherwise, return all students
    //       Use res.json() to send the result as JSON
    res.json("Implement me!");
});

// ROUTE 2: GET /students/:id — return one student by ID
app.get('/students/:id', (req, res) => {
    // TODO: Find the student whose id matches Number(req.params.id)
    //       Use .find() or .filter() to search the array
    //       If found, res.json(student). If not, res.json({ error: "Not found" })
    res.json("Implement me!");
});

// ROUTE 3: POST /students — add a new student
app.post('/students', (req, res) => {
    // TODO: Read the new student from req.body
    //       Push it into the students array
    //       Respond with the full students array
    res.json("Implement me!");
});

app.listen(3000, () => {
    console.log("Student API listening on port 3000");
});
7

The Express Router

Organizing Routes Like a Professional

Learning objective: After this step you will be able to create an Express Router, define routes on it, and mount it on a prefix path using app.use().

The Problem: One File Gets Messy

In Step 6, you wrote three routes in one file. Imagine a real app with 50 routes — for students, courses, professors, assignments. Having all of them in one file would be unmaintainable. This is the problem express.Router() solves.

A Router is like a mini Express app that only handles routes. You create it, define routes on it, then mount it onto your main app at a specific URL prefix.

// --- studentRoutes.js ---
const express = require('express');
const router = express.Router();

// Routes are defined relative to WHERE the router is mounted
router.get('/', (req, res) => {       // Handles GET /???/ (prefix added later)
    res.json({ message: "all students" });
});

router.get('/:id', (req, res) => {   // Handles GET /???/:id
    res.json({ message: `student ${req.params.id}` });
});

module.exports = router;              // Export so other files can use it
// --- app.js ---
const express = require('express');
const app = express();
const studentRoutes = require('./studentRoutes');

// Mount the router at /api/students
// Now:  router.get('/') handles    GET /api/students
//       router.get('/:id') handles GET /api/students/3
app.use('/api/students', studentRoutes);

app.listen(3000);

The Pattern

1. Create a Router:        const router = express.Router();
2. Define routes on it:    router.get('/'), router.post('/'), ...
3. Export it:               module.exports = router;
4. Mount it in your app:   app.use('/prefix', router);

Key insight: Routes on the router are relative. router.get('/') handles requests at whatever prefix you mount it with app.use(). If mounted at /api/students, then router.get('/') handles /api/students and router.get('/:id') handles /api/students/42.

Task: Refactor into a Router

You have two files: studentRoutes.js and app.js.

In studentRoutes.js (the router module):

  1. Create an Express Router
  2. Define a GET / route that returns all students as JSON
  3. Define a GET /:id route that finds a student by ID and returns them (use Number() + ===)
  4. Define a POST / route that adds a new student from req.body
  5. Export the router with module.exports

In app.js (the main app):

  1. Import the router from ./studentRoutes
  2. Mount it at /api/students
  3. Start the server on port 3000

Scaffolding level: The file structure is defined. In studentRoutes.js, you write everything. In app.js, you have TODO comments. This is near-independent: you know the pieces from Steps 5–6, now you assemble them yourself.

Predict Before You Run

Before writing any code in studentRoutes.js, predict:

  1. If you send GET /api/students but forget module.exports = router in studentRoutes.js, what will happen?
  2. If you define router.get('/api/students', ...) instead of router.get('/', ...), and mount at /api/students, what URL will actually match?

Two-file apps are harder to debug because errors often appear in app.js but originate in studentRoutes.js. If you see "Cannot GET /api/students", the most likely cause is a missing export or wrong mount path — not a syntax error in the route handler itself.

Growth mindset moment: This step is a significant jump — you are now writing routes and organizing them across files. If it takes multiple attempts, that is normal. Professional developers debug module import issues regularly. Each error you fix here builds a mental model that will save you hours in the capstone.

Starter files
studentRoutes.js
// Student Routes — create a Router with three routes
// This file handles: GET /, GET /:id, POST /
// (The prefix /api/students is added when mounted in app.js)
const express = require('express');

const students = [
    { id: 1, name: "Alice", grade: 95 },
    { id: 2, name: "Bob",   grade: 42 },
    { id: 3, name: "Carol", grade: 78 },
];

// TODO: Create a router, define three routes, export it
// Hint: const router = express.Router();
//       router.get('/', ...);
//       router.get('/:id', ...);
//       router.post('/', ...);
//       module.exports = router;
app.js
// Main Express app — import and mount the student router
const express = require('express');
const app = express();
app.use(express.json());

// TODO: Import the studentRoutes module
// Hint: const studentRoutes = require('./studentRoutes');

// TODO: Mount it at '/api/students'
// Hint: app.use('/api/students', studentRoutes);

app.listen(3000, () => {
    console.log("Server with Router listening on port 3000");
});
8

The Blocked Chef — The Event Loop

The Most Important Concept in Node.js

Learning objective: After this step you will be able to predict the execution order of synchronous and asynchronous code and explain how the Event Loop, Call Stack, and Task Queue interact.

This is the paradigm shift that trips up every C++ and Python developer. Read carefully — and expect to be surprised.

Before you begin: Rate your confidence: “I understand how code execution order works” — 1 (not sure) to 5 (very confident). Revisit this rating after completing the step.

Growth mindset moment: This step is the hardest concept in the entire tutorial. Professional developers with years of experience still get tripped up by the Event Loop. If you feel confused or frustrated, that is a sign your brain is building a fundamentally new mental model — not a sign that something is wrong with you. Every Node.js developer went through this exact struggle. Take your time, re-read the metaphor, and trust the process.

JavaScript is single-threaded. There is only one “chef” in the kitchen. This is how your Express server handles thousands of requests — and why a single slow route handler can block everything.

The Restaurant Metaphor

Kitchen Role Node.js Equivalent What It Does
The Chef Call Stack Executes one task at a time. If busy, everything else waits.
The Hard Drives / Network libuv / OS Do the slow work (file reads, HTTP responses, DB queries) in the background while the Chef handles other tasks.
The Waiter Task Queue When the OS finishes, the waiter places the callback on the staging table.
The Kitchen Manager Event Loop Watches the Chef. Only when the Chef’s hands are empty does the Manager hand over the next queued callback.

Node.js File I/O: Two Ways

The clearest real-world example of blocking vs. non-blocking is file reading:

const fs = require('fs');

// NON-BLOCKING — schedules a callback and moves on immediately
fs.readFile('data.json', 'utf8', (err, data) => {
    // This runs LATER, when the OS has finished reading
    console.log('File ready:', data.length, 'bytes');
});
console.log('This runs BEFORE the file is ready!'); // prints first

// BLOCKING — the Chef stares at the disk. Nothing else can run.
const data = fs.readFileSync('data.json', 'utf8');
console.log('File ready (sync):', data.length, 'bytes'); // prints after the read

fs.readFile leaves the Chef free. fs.readFileSync pins the Chef to the disk until the read is complete — and blocks your entire Express server in the meantime.

Why This Matters for Your Express Server

// BAD: readFileSync blocks every other request while reading!
app.get('/students', (req, res) => {
    const data = fs.readFileSync('students.json', 'utf8'); // Chef is STUCK
    res.json(JSON.parse(data));
});

// GOOD: readFile frees the Chef while the OS reads the file
app.get('/students', (req, res) => {
    fs.readFile('students.json', 'utf8', (err, data) => {
        res.json(JSON.parse(data));
    });
});

In Step 9 you will replace this callback-style file read with elegant async/await.

A Complete Example — With Output

The clearest way to see the Event Loop in action is setTimeout(..., 0). Even with zero delay, the callback fires after all synchronous code completes:

// Schedule a callback — should run "right away" with 0ms delay, right?
setTimeout(() => {
    console.log("[3] setTimeout fired — the chef is finally free!");
}, 0);

// Synchronous code: this runs first, blocking everything else
console.log("[1] Starting synchronous work...");

// Simulates a slow synchronous operation
let total = 0;
for (let i = 0; i < 5000000; i++) {
    total += i;
}
console.log(`[2] Synchronous work done. total = ${total}`);

// Second setTimeout added at the end
setTimeout(() => {
    console.log("Event loop is free again!");
}, 0);

Actual output:

[1] Starting synchronous work...
[2] Synchronous work done. total = 12499997500000
[3] setTimeout fired — the chef is finally free!
Event loop is free again!

Both setTimeout callbacks fire only after all synchronous code finishes — the loop must complete before the Event Loop can hand off any queued callbacks to the Chef.

Predict Before You Code

Look at event_loop.js. It reads students.json twice:

  • Once with fs.readFile (async callback)
  • Once with a direct console.log

Before clicking Run, write down the order you expect to see [1], [2], and [3] in the output. Most people from C++/Python predict [1] → [2] → [3]. Are you right?

If your prediction was wrong, that is exactly the point. The event loop violates the top-to-bottom ordering intuition from every other language you know.

Investigate (try these after your first Run)

  • Change 'utf8' to 'utf-8' in the first fs.readFile — does it still work?
  • What happens if you change 'students.json' to 'missing.json'?

Task: Add a Second File Read

  1. Click ▶ Run and note the actual output order.
  2. Your task: At the END of the file, add a second fs.readFile call that logs "[4] Second read complete!".

Click ▶ Run again. Predict the order of [3] and [4] before you look.

Reflect

Re-rate your confidence: “I understand how code execution order works” — 1 to 5. Did your rating change from the start of this step? If so, write one sentence about what shifted in your understanding.

Before You Move On

Stop here and take a break. The Event Loop is the most important concept in this tutorial — and cognitive science shows that your brain consolidates new mental models during rest, not during continuous study. Come back to Step 9 after at least 30 minutes (a day is even better). The async/await syntax you will learn next builds directly on this mental model, and it will click faster if the Event Loop has time to settle.

Starter files
students.json
[
  { "name": "Alice", "grade": 95 },
  { "name": "Bob",   "grade": 42 },
  { "name": "Carol", "grade": 78 },
  { "name": "Dave",  "grade": 55 },
  { "name": "Eve",   "grade": 88 }
]
event_loop.js
// The Blocked Chef Demo — reading a real file
// PREDICT the console.log order BEFORE you run!

const fs = require('fs');

// fs.readFile is ASYNCHRONOUS — it schedules a callback and moves on.
// The OS reads the file in the background; the Chef keeps working.
fs.readFile('students.json', 'utf8', (err, data) => {
    if (err) throw err;
    const students = JSON.parse(data);
    console.log(`[3] File read finished — ${students.length} students loaded`);
});

// These run synchronously — BEFORE the file is ready
console.log('[1] File read has been requested (but not finished yet)');
console.log('[2] Chef is free — doing other work while OS reads the file');

// TODO: Add a second fs.readFile here that logs "[4] Second read complete!"
// Will [4] arrive before or after [3]? Predict first, then run!
9

From Callbacks to async/await

From Callbacks to Clean Code

You just conquered the Event Loop — the single hardest concept in Node.js. If it clicked, you are ahead of most JavaScript beginners. If it is still fuzzy, revisit the Restaurant Metaphor whenever async code surprises you — that is completely normal and the metaphor becomes clearer with practice.

Learning objective: After this step you will be able to refactor fs.readFile callback-style code to use async/await with fs.promises.readFile, explain what a Promise represents, and use try/catch for async error handling.

Quick Retrieval: Event Loop Check

Before learning new syntax, verify that the Event Loop model is solid. Without looking back at Step 8, answer these two questions on paper or in your head:

  1. fs.readFile('data.json', 'utf8', callback) — does this line block the Chef, or does the Chef move on immediately?
  2. If you write console.log('A') immediately after an fs.readFile call, and the callback logs 'B' — which prints first?

Answers: (1) The Chef moves on immediately — fs.readFile delegates to the OS and returns. (2) 'A' prints first — it is synchronous. 'B' prints later when the Event Loop delivers the callback. If you got both right without looking, the model has stuck. If not, re-read the Restaurant Metaphor in Step 8 before continuing.

The Problem with Callbacks

In Step 8 you used fs.readFile with a callback. That works — but imagine reading a file, then parsing it, then reading another file based on the first result:

// Generation 1: Callback Hell
fs.readFile('roster.json', 'utf8', (err, rosterData) => {
    if (err) throw err;
    const roster = JSON.parse(rosterData);
    fs.readFile('grades.json', 'utf8', (err2, gradesData) => {
        if (err2) throw err2;
        // Level 3... "Pyramid of Doom"
    });
});

Every nested file read adds another level of indentation. This is “Callback Hell.”

What is a Promise?

A Promise is an object representing a value that does not exist yet — like a receipt for food you ordered. The food is not ready, but the receipt guarantees you will get it (or be told if something went wrong).

A Promise has three possible states:

  • Pending — the operation is still in progress (your food is cooking)
  • Fulfilled — the operation succeeded and the result is available (food is ready)
  • Rejected — the operation failed (the kitchen is out of that dish)

Generation 2: Promises with .then()

fs.promises.readFile returns a Promise instead of taking a callback:

const fs = require('fs');

// Returns a Promise — the file content arrives later
const promise = fs.promises.readFile('students.json', 'utf8');
// 'promise' is a Promise object right now — the data isn't here yet

// .then() registers what to do when the Promise fulfills
promise.then(data => console.log('Got data:', data.length, 'bytes'));

// .catch() handles errors (similar to except in Python)
promise.catch(err => console.error('Failed:', err.message));

This is already better than callbacks — no nesting! But async/await makes it even cleaner.

Generation 3: async/await — Looks like Python/C++

async function readStudents() {
    try {
        // 'await' suspends THIS function (non-blocking!) until the Promise resolves
        const data = await fs.promises.readFile('students.json', 'utf8');
        const students = JSON.parse(data);
        console.log('Loaded:', students.length, 'students');
    } catch (err) {
        // File not found, permission denied, etc.
        console.error('Read failed:', err.message);
    }
}

This reads like synchronous Python — but does not block the Event Loop. When await suspends the function, the Chef is free to handle other requests.

async/await in Express Route Handlers

This is the production pattern you will use in the capstone:

// An async Express route handler that reads a file
app.get('/students', async (req, res) => {
    try {
        const data = await fs.promises.readFile('students.json', 'utf8');
        res.json(JSON.parse(data));
    } catch (err) {
        res.status(500).json({ error: err.message });
    }
});

⚠️ Critical Caveat — Sequential vs Parallel reads:

// SLOWER: waits for roster, then starts grades
const rosterData  = await fs.promises.readFile('roster.json',  'utf8');
const gradesData  = await fs.promises.readFile('grades.json',  'utf8');

// FASTER: both reads start simultaneously
const [rosterData, gradesData] = await Promise.all([
    fs.promises.readFile('roster.json',  'utf8'),
    fs.promises.readFile('grades.json',  'utf8'),
]);

If two file reads are independent, always prefer Promise.all().

Predict Before You Refactor

Look at the existing readStudentsCallback() function in async.js. Before writing your async version, predict:

  1. If you define async function displayStudents() but forget to call it at the bottom, what will the output be?
  2. What is the output order: does console.log('Loading...') (if you add one after the function call) print before or after === Student Roster ===?

The second prediction tests whether you have internalized the Event Loop from Step 8. An async function that awaits is still non-blocking — code after the function call runs synchronously before the await resolves.

Task: Refactor to async/await

Open async.js. It reads students.json using the old callback style — the same fs.readFile pattern from Step 8.

Your job: Delete the callback-style function at the bottom and replace it with a clean async function that:

  • Uses await fs.promises.readFile('students.json', 'utf8') to read the file
  • Parses the JSON with JSON.parse()
  • Logs each student’s name and grade
  • Handles errors with try/catch
  • Is called at the bottom of the file
  • Includes a comment above the await line explaining: does await block the entire program or just this function? (Use your Event Loop knowledge from Step 8.)

Click ▶ Run to check your output.

Bonus — Test error handling: Temporarily change 'students.json' to 'missing.json' and verify your catch block fires.

Starter files
students.json
[
  { "name": "Alice", "grade": 95 },
  { "name": "Bob",   "grade": 42 },
  { "name": "Carol", "grade": 78 },
  { "name": "Dave",  "grade": 55 },
  { "name": "Eve",   "grade": 88 }
]
async.js
const fs = require('fs');

// OLD: Callback-style file read (Generation 1 — from Step 8)
// This works, but nesting these quickly becomes "Callback Hell".
// Your job: delete this function and the call below, then replace
// it with an async function using fs.promises.readFile.
function readStudentsCallback() {
    fs.readFile('students.json', 'utf8', (err, data) => {
        if (err) { console.error('Error:', err.message); return; }
        const students = JSON.parse(data);
        console.log('=== Student Roster ===');
        students.forEach(s => console.log(`  ${s.name}: ${s.grade}`));
    });
}

readStudentsCallback();

// TODO: Replace readStudentsCallback with an async function that:
//   1. Uses:  const data = await fs.promises.readFile('students.json', 'utf8')
//   2. Parses the JSON and logs each student
//   3. Wraps everything in try/catch
//   4. Calls the function at the bottom
10

Capstone: Deploy the Student Grade API

Learning objective: After this step you will be able to design and implement a complete Express API using the Router pattern, async data fetching, and all JavaScript skills from this tutorial.

Ship It — Your API Goes Live

You have unlocked every component skill: arrow functions, .filter(), .map(), .reduce(), destructuring, Express routes, the Router, query parameters, route parameters, POST, the Event Loop, and async/await. Now you are building a real API and deploying it to CS35L-nodejs.edu — with no scaffolding. You decide how to structure the code.

Growth mindset moment: This capstone has no scaffolding — and that is intentional. If you feel stuck, it does not mean you are missing something fundamental. It means you are doing the hard work of integrating skills that you practiced in isolation. Go back to the specific step that covers the concept you are stuck on. Every professional developer references prior work when building something new.

Design Before You Code

Before opening routes.js, sketch your design on paper (or mentally):

  1. What is the file structure? What goes in routes.js vs app.js?
  2. Write the app.use() call you’ll need in app.js before you type it.
  3. For GET /api/dashboard: what is the order of operations? List the steps (fetch, merge, compute, respond) before coding.
  4. Which tests will be hardest to pass? Which component skill from Steps 3–9 does each test exercise?

Designing before coding is a professional habit. It surfaces structural decisions (like forgetting module.exports) before you’ve written 50 lines. If you skip this and get stuck, come back to this list and check each step.

The Scenario

You are building a Student Grade API backed by two JSON files (roster.json and grades.json). Two async helper functions are provided at the top of routes.js that read these files using fs.promises.readFile — the same pattern from Step 9:

  • fetchRoster() — reads roster.json and resolves with [{ name, id }]
  • fetchGrades() — reads grades.json and resolves with [{ studentId, course, grade }]

Requirements

Build an Express API with an Express Router mounted at /api. The router must have these routes:

  1. GET /api/dashboard — The main endpoint.
    • Fetch both data sources concurrently with Promise.all
    • Merge each student with their grades (match by id/studentId)
    • Compute each student’s average grade
    • Return JSON: { students: [{ name, avg, status }], passing: count, total: count }
    • status is "PASS" if average >= 60, else "FAIL"
    • avg formatted to 1 decimal place (as a string, e.g., "87.7")
  2. GET /api/students/:id — Get one student’s details.
    • Fetch both data sources
    • Find the student matching :id (use Number() + ===)
    • Return: { name, courses: [{ course, grade }], avg }
    • If not found, return { error: "Not found" }
  3. POST /api/students — Add a student to the roster.
    • Read the new student from req.body
    • Respond with { message: "Added", student: ... }
  4. Error handling: Wrap all route handlers in try/catch

Put routes in routes.js (the Router), and mount them in app.js. When your code looks complete, switch to the app.js tab and press ▶ Run to deploy your API to CS35L-nodejs.edu — then use the HTTP Client to hit your live endpoints. (routes.js is a module that only exports a router; running it directly does nothing.)

Suggested Order (if you are unsure where to start)

  1. Start with the skeleton: In routes.js, add const express = require('express'), create a router, and export it. In app.js, import and mount it at /api. Run — you should see no errors.
  2. Add the POST route first — it is the simplest (just read req.body and respond).
  3. Add GET /api/students/:id — fetch data, find one student, respond.
  4. Add GET /api/dashboard last — it is the most complex (merge, compute, format).

Hints (only if you’re stuck)

  • Use const [roster, grades] = await Promise.all([...]) for concurrent fetching
  • Use grades.filter(g => g.studentId === student.id) to get a student’s grades
  • Use .map(g => g.grade) then .reduce() for averages
  • Use express.Router() and module.exports
Starter files
roster.json
[
  { "name": "Alice", "id": 1 },
  { "name": "Bob",   "id": 2 },
  { "name": "Clara", "id": 3 }
]
grades.json
[
  { "studentId": 1, "course": "Math",    "grade": 92 },
  { "studentId": 1, "course": "English", "grade": 88 },
  { "studentId": 1, "course": "Science", "grade": 83 },
  { "studentId": 2, "course": "Math",    "grade": 45 },
  { "studentId": 2, "course": "English", "grade": 61 },
  { "studentId": 2, "course": "Science", "grade": 57 },
  { "studentId": 3, "course": "Math",    "grade": 95 },
  { "studentId": 3, "course": "English", "grade": 89 },
  { "studentId": 3, "course": "Science", "grade": 89 }
]
routes.js
// === Data helpers — read JSON files with fs.promises.readFile (do not modify) ===
const fs = require('fs');

async function fetchRoster() {
    const data = await fs.promises.readFile('roster.json', 'utf8');
    return JSON.parse(data);
}

async function fetchGrades() {
    const data = await fs.promises.readFile('grades.json', 'utf8');
    return JSON.parse(data);
}

// === Your Router code below — no scaffolding! ===
app.js
// Main app — mount your router here
const express = require('express');
const app = express();
app.use(express.json());

// Your code here

app.listen(3000, () => console.log("Grade API deployed to CS35L-nodejs.edu"));
11

You Made It!

You Built a Backend From Scratch

Take a moment to appreciate what you just did. You walked into this tutorial knowing C++ and Python. You are walking out with a working knowledge of JavaScript and Node.js backend development.

Here is everything you learned:

JavaScript Fundamentals (Steps 1–2)

  • How Node.js uses V8 and libuv to run JavaScript outside the browser
  • let vs const — and why var is banished
  • Template literals — JavaScript’s answer to Python’s f-strings
  • The === trap — why JavaScript’s == is a landmine and strict equality is your friend

Functions & Data Processing (Steps 3–4)

  • Arrow functions — the modern way to write functions in JavaScript
  • Callbacks — the single most important pattern in JavaScript: pass a function, get called back later
  • .filter(), .map(), .reduce() — the three array methods that power everything
  • Destructuring — unpacking objects and arrays in one clean line

Express & Backend Development (Steps 5–7)

  • How Express turns URLs into function calls (routes are just callbacks!)
  • req.query, req.params, req.body — three ways to receive data from users
  • GET for reading, POST for creating — the HTTP verbs
  • express.Router() — organizing routes into professional, modular code
  • module.exports and require() — sharing code between files

Async JavaScript (Steps 8–9)

  • The Event Loop — the single-threaded Chef that makes Node.js powerful
  • Why blocking the Event Loop is catastrophic for a server
  • Promises — objects representing future values
  • async/await — writing non-blocking code that reads like Python
  • Promise.all() — running multiple async operations concurrently
  • try/catch — handling errors gracefully in async code

Full Integration (Step 10)

  • Designing and building a complete Express API with zero scaffolding
  • Combining every skill: Router + async file reads + array processing + error handling

What Comes Next

You now have the foundation to:

  • Add a database — replace JSON files with MongoDB or PostgreSQL
  • Build a frontend — connect a React or Next.js app to your Express API
  • Add authentication — protect routes with JWT tokens or OAuth
  • Build real-time features — add WebSockets for live chat or notifications
  • Deploy — put your API on the internet with services like Railway, Vercel, or Render

The patterns you learned — callbacks, async/await, the Event Loop, modular code — are the exact same patterns running behind Discord’s real-time messaging, Spotify’s playlist API, Netflix’s content delivery, and Twitch’s stream management.

One Last Thing

Remember that moment in Step 8 when the Event Loop broke your mental model? Or when Step 10 asked you to build an entire API with no scaffolding? Those moments of struggle were not setbacks — they were the moments your brain was building new neural pathways. Every professional developer went through the same learning curve. The difference is that you pushed through it.

You are ready.

Strengthen Your Memory

Tomorrow, take the Node.js Concepts Quiz. It covers async reasoning, type traps, and technique selection across all 10 steps. Taking it after a gap — not immediately — is deliberate: the spacing effect means your brain consolidates knowledge between sessions, making retrieval stronger and more durable.

Starter files
done.js
// You completed the Node.js Essentials tutorial!
// No tasks here — just celebration.

const skills = [
    "JavaScript fundamentals",
    "Arrow functions & callbacks",
    "Array methods: .filter(), .map(), .reduce()",
    "Destructuring",
    "Express routing",
    "Query params, route params, POST bodies",
    "Express Router & modular code",
    "The Event Loop",
    "async/await & Promises",
    "Promise.all() for concurrency",
    "Error handling with try/catch",
    "Full API design & integration",
];

console.log("Skills unlocked:");
skills.forEach((skill, i) => console.log(`  ${i + 1}. ${skill}`));
console.log(`\nTotal: ${skills.length} skills. You are ready.`);