Over the previous lessons, tools such as flatMap, the ... operator, nullish coalescing ?? and arrow functions have kept turning up with no formal explanation. It is time to name them. Node.js embeds a very up-to-date V8 engine, which means you can use all of modern JavaScript with no transpilers and no configuration: there are no ancient browsers to support on the server.

This lesson is not a language reference — the documentation is there for that — but a practical refresher: every section solves a real problem in the Escena Viva catalog. If you already master these concepts, read it as a checklist; if you come from an older JavaScript, here is the foundation of every line of code in the course.

One warning up front: async/await and promises are not covered here. They are the heart of Module 2 and deserve a space of their own.

Contents

  1. const, let and block scope
  2. Arrow functions and the value of this
  3. Destructuring, default parameters and shorthand properties
  4. Spread and rest: the ... operator
  5. Optional chaining ?. and nullish coalescing ??
  6. Template literals
  7. Essential array methods applied to the catalog
  8. Objects: Object.keys, values and entries
  9. Map and Set: when to use each
  10. Classes, private fields and private methods
  11. JSON: parse and stringify

  1. const, let and Block Scope

The problem: we want to calculate the available tickets for each session and store the calculating functions in an array so we can run them later.

// WRONG: var does not respect block scope
var accumulators = [];
var prices = [2500, 2200, 1800];

for (var i = 0; i < prices.length; i++) {
  accumulators.push(function () { return prices[i]; });
}

console.log(accumulators[0]());   // undefined  <- they all share the same "i", which is 3
// RIGHT: let creates a new variable on every pass of the loop
const accumulators = [];
const prices = [2500, 2200, 1800];

for (let i = 0; i < prices.length; i++) {
  accumulators.push(() => prices[i]);
}

console.log(accumulators[0]());   // 2500
Aspect var let const
Scope Function Block {} Block {}
Reassignable Yes Yes No
Redeclarable in the same scope Yes No No
Hoisted and usable before declaration Yes (it is undefined) No (temporal dead zone) No
Added to globalThis at the top level Yes No No

The course's practical rule: use const by default; switch to let only when you genuinely need to reassign; never use var.

One nuance that confuses people: const prevents reassigning the variable, not modifying the object it points at.

const session = { id: 'ses-001-1', sold: 180 };

session.sold = 181;      // Fine: we are modifying a property
// session = {};         // TypeError: Assignment to constant variable

If you want to prevent modification too, there is Object.freeze(session).

  1. Arrow Functions and the Value of this

The problem: a CapacityManager class that adds up the tickets sold across several sessions.

Arrow functions are shorter and, above all, they have no this of their own: they inherit it from the context where they were written.

// Syntax: from longest to shortest
const available1 = function (session) { return session.capacity - session.sold; };
const available2 = (session) => { return session.capacity - session.sold; };
const available3 = (session) => session.capacity - session.sold;   // Implicit return

Watch out for the implicit return of an object literal: you have to wrap it in parentheses, or JavaScript reads the braces as a block.

const summary1 = (s) => { id: s.id };        // Returns undefined
const summary2 = (s) => ({ id: s.id });      // Correct

The classic this problem:

class CapacityManager {
  constructor(sessions) {
    this.sessions = sessions;
    this.total = 0;
  }

  // WRONG: function() creates its own "this", which here is undefined
  totalWrong() {
    this.sessions.forEach(function (session) {
      this.total += session.sold;   // TypeError: Cannot read properties of undefined
    });
    return this.total;
  }

  // RIGHT: the arrow inherits the method's "this"
  totalRight() {
    this.sessions.forEach((session) => {
      this.total += session.sold;
    });
    return this.total;
  }
}
Use an arrow function when... Use function when...
It is a callback (map, filter, setTimeout) You define an object method that uses the object's this
You want to inherit the surrounding this You need the arguments object
It is a short single-expression function You need a constructor (new)

  1. Destructuring, Default Parameters and Shorthand Properties

The problem: writing a function that formats a session without repeating session. on every line.

Object destructuring

const session = {
  id: 'ses-001-1',
  dateTime: '2026-10-03T20:00:00',
  capacity: 420,
  sold: 180,
  priceCents: 2500
};

// We pull out several properties at once
const { id, capacity, sold } = session;
console.log(`${id}: ${capacity - sold} available`);   // ses-001-1: 240 available

// Renaming while extracting
const { priceCents: price } = session;

// A default value if the property does not exist
const { discountCents = 0 } = session;
console.log(discountCents);   // 0

It is especially useful in a function's parameters:

// Without destructuring: noisy
function describeSession(session) {
  return `${session.id} | capacity ${session.capacity} | sold ${session.sold}`;
}

// Destructuring in the parameter: you can see at a glance what the function needs
function describe({ id, capacity, sold }) {
  return `${id} | capacity ${capacity} | sold ${sold}`;
}

Nested and array destructuring

const event = {
  title: 'Noche de Monologos',
  venue: 'Sala Boveda',
  sessions: [{ id: 'ses-002-1', capacity: 120 }, { id: 'ses-002-2', capacity: 120 }]
};

// Nested: the first session and its capacity, in one line
const { title, sessions: [{ id: firstSession }] } = event;
console.log(title, firstSession);   // Noche de Monologos ses-002-1

// Arrays: by position
const [first, second] = event.sessions;

// Skipping positions with an empty comma
const dateParts = '2026-10-03'.split('-');
const [year, , day] = dateParts;
console.log(year, day);   // 2026 03

Default parameters

// The empty object as a default value avoids an error when nothing is passed
function findEvents(catalog, { venue = null, maxPrice = Infinity } = {}) {
  return catalog.filter((event) => {
    const venueMatches = venue === null || event.venue === venue;
    const hasCheapTicket = event.sessions.some(
      (s) => s.priceCents <= maxPrice
    );
    return venueMatches && hasCheapTicket;
  });
}

findEvents(catalog);                              // All of them
findEvents(catalog, { venue: 'Sala Boveda' });    // A single venue
findEvents(catalog, { maxPrice: 2000 });          // Cheap tickets only

This way of receiving options — an object with default values — is the dominant pattern in the Node ecosystem. You will see it in Express, in Mongoose and in almost any library.

Shorthand properties

When the key and the variable have the same name, there is no need to repeat it:

const venue = 'Teatro Almendra';
const capacity = 420;

const record = { venue, capacity };           // Instead of { venue: venue, capacity: capacity }
console.log(record);                          // { venue: 'Teatro Almendra', capacity: 420 }

// It works for methods too
const utils = {
  format(cents) {                             // Instead of format: function (cents)
    return (cents / 100).toFixed(2);
  }
};

  1. Spread and Rest: the ... Operator

The very same three dots do two different things depending on where they appear.

Spread: unfolding

The problem: creating a modified copy of a session without altering the original.

const originalSession = { id: 'ses-002-3', capacity: 120, sold: 12, priceCents: 1500 };

// A copy with one property changed (the original is untouched)
const discountedSession = { ...originalSession, priceCents: 1200 };

console.log(originalSession.priceCents);     // 1500
console.log(discountedSession.priceCents);   // 1200

Order matters: whatever comes later overwrites what came before.

const defaults = { capacity: 100, priceCents: 2000, sold: 0 };
const receivedData = { capacity: 420 };

const finalSession = { ...defaults, ...receivedData };
// { capacity: 420, priceCents: 2000, sold: 0 }

With arrays:

const octoberSessions = ['ses-001-1', 'ses-001-2'];
const aprilSessions = ['ses-003-1', 'ses-003-2'];

const all = [...octoberSessions, ...aprilSessions];
const safeCopy = [...all];                    // So we can sort without ruining "all"

An important warning: spreading makes a shallow copy. Nested objects are shared.

const eventCopy = { ...event };
eventCopy.sessions[0].capacity = 999;   // It also changes event.sessions[0]!

For a deep copy, Node offers structuredClone(event).

Rest: gathering

// In parameters: gathers an indeterminate number of arguments
function totalCents(...prices) {
  return prices.reduce((sum, p) => sum + p, 0);
}

totalCents(2500, 2200, 1800);   // 6500

// In destructuring: separates what you care about from the rest
const { id, ...sessionData } = originalSession;
console.log(id);            // 'ses-002-3'
console.log(sessionData);   // { capacity: 120, sold: 12, priceCents: 1500 }

This second use is common for stripping out sensitive fields before sending data, something we will apply to users in Module 8:

const user = { id: 'usr-01', email: '[email protected]', passwordHash: '$2b$10$...' };
const { passwordHash, ...publicUser } = user;   // publicUser no longer carries the hash

  1. Optional Chaining ?. and Nullish Coalescing ??

The problem: displaying an event's organizer name, knowing that some older events do not have that data.

const legacyEvent = { id: 'evt-000', title: 'Gala de Reapertura' };

// Without ?. : an error
console.log(legacyEvent.organizer.name);
// TypeError: Cannot read properties of undefined (reading 'name')

// With ?. : it returns undefined instead of blowing up
console.log(legacyEvent.organizer?.name);   // undefined

?. stops the evaluation as soon as it hits null or undefined. It comes in three forms:

event.organizer?.name             // Property
event.sessions?.[0]               // Array index
event.calculateCapacity?.()       // Call to a method that may not exist

?? (nullish coalescing) returns the right-hand side only if the left-hand side is null or undefined. Do not confuse it with ||:

const session = { sold: 0, discount: null };

console.log(session.sold || 'no data');   // 'no data'  <- WRONG: 0 is falsy
console.log(session.sold ?? 'no data');   // 0          <- RIGHT
Left-hand value || returns ?? returns
0 the right side 0
'' the right side ''
false the right side false
null the right side the right side
undefined the right side the right side

In a domain like ours this is critical: 0 tickets sold and 0 euros of discount are legitimate values. Using || would wipe them out.

Combined, they solve the problem elegantly:

function organizerName(event) {
  return event.organizer?.name ?? 'Escena Viva';
}

There is also an assignment operator, ??=:

const counts = {};
counts['Sala Boveda'] ??= 0;    // Only assigns if it is null or undefined
counts['Sala Boveda'] += 1;

  1. Template Literals

The problem: generating the line of a ticket to be printed.

const ticket = {
  code: 'EV-2026-000123',
  event: 'Concierto de Otono',
  venue: 'Teatro Almendra',
  priceCents: 2500
};

// Classic concatenation: unreadable
const line1 = 'Ticket ' + ticket.code + ' - ' + ticket.event +
              ' (' + ticket.venue + ') - ' + (ticket.priceCents / 100) + ' EUR';

// Template literal: it reads straight through
const line2 = `Ticket ${ticket.code} - ${ticket.event} (${ticket.venue}) - ${(ticket.priceCents / 100).toFixed(2)} EUR`;

Additional advantages:

// 1. Real line breaks, no \n
const receipt = `
ESCENA VIVA
Code   : ${ticket.code}
Event  : ${ticket.event}
Venue  : ${ticket.venue}
Amount : ${(ticket.priceCents / 100).toFixed(2)} EUR
`;

// 2. Any expression inside ${}
const session = { capacity: 420, sold: 180 };
console.log(`Occupancy: ${Math.round((session.sold / session.capacity) * 100)}%`);

// 3. Inline conditionals
const soldOut = session.sold >= session.capacity;
console.log(`Status: ${soldOut ? 'SOLD OUT' : 'AVAILABLE'}`);

  1. Essential Array Methods Applied to the Catalog

This is the section you will use most. All of these methods return a new array or a value without modifying the original (except sort, which does modify it).

Method Returns The question it answers
map An array of the same size "Transform every element"
filter An array of the same size or smaller "Keep the ones that match"
find One element or undefined "Give me the first one that matches"
findIndex A number or -1 "Which position is it in?"
reduce Anything at all "Accumulate everything into one value"
some true / false "Is there any that matches?"
every true / false "Do they all match?"
sort The same array, sorted "Sort them" (it mutates!)
flatMap An array flattened one level "Transform and flatten"
includes true / false "Is this value in there?"

Let's work with the Escena Viva catalog:

// map: a list of titles for a menu
const titles = catalog.map((event) => event.title);
// [ 'Concierto de Otono', 'Noche de Monologos', 'Festival de Jazz de Primavera' ]

// filter: the events at one venue
const atBoveda = catalog.filter((event) => event.venue === 'Sala Boveda');

// find: look one up by identifier (it returns the object, not an array)
const event = catalog.find((e) => e.id === 'evt-003');
console.log(event.title);   // Festival de Jazz de Primavera

// flatMap: every session in the catalog in a single array
const sessions = catalog.flatMap((event) => event.sessions);
console.log(sessions.length);   // 7

reduce in depth

reduce is intimidating at first, but it only has two parts: an accumulating function and an initial value.

// Total capacity of the platform
const totalCapacity = sessions.reduce((acc, session) => acc + session.capacity, 0);
console.log(totalCapacity);   // 3000

Step by step: acc starts out as 0 (the second argument). On every pass it receives the previous result and the current session, and returns the new accumulated value.

reduce is not only for adding up; it can build objects:

// Tickets sold grouped by venue
const soldByVenue = catalog.reduce((acc, event) => {
  const eventSold = event.sessions.reduce((s, ses) => s + ses.sold, 0);
  acc[event.venue] = (acc[event.venue] ?? 0) + eventSold;
  return acc;   // Do not forget to return the accumulator!
}, {});

console.log(soldByVenue);
// { 'Teatro Almendra': 276, 'Sala Boveda': 175, 'Auditorio Ribera': 1360 }

The number one mistake with reduce: forgetting the return acc inside the function. The result will be undefined on the second pass.

some and every

// Does any event have tickets below 20 EUR?
const hasCheapTickets = catalog.some((e) => e.sessions.some((s) => s.priceCents < 2000));
console.log(hasCheapTickets);   // true

// Are all the sessions above 50% occupancy?
const allFull = sessions.every((s) => s.sold / s.capacity > 0.5);
console.log(allFull);   // false

Both short-circuit: some stops at the first true, every at the first false. On large catalogs, that matters.

sort and its trap

// 1. sort MUTATES the array. Copy it first.
const byPrice = [...sessions].sort((a, b) => a.priceCents - b.priceCents);

// 2. With no comparison function, sort sorts as TEXT
console.log([10, 9, 100, 20].sort());              // [ 10, 100, 20, 9 ]  <- wrong!
console.log([10, 9, 100, 20].sort((a, b) => a - b)); // [ 9, 10, 20, 100 ]

// 3. Sorting text with accents and capitals: localeCompare
const sorted = [...catalog].sort((a, b) => a.title.localeCompare(b.title, 'es'));

// 4. Sorting by date (ISO strings sort correctly alphabetically)
const byDate = [...sessions].sort((a, b) => a.dateTime.localeCompare(b.dateTime));

The fact that dates in ISO format YYYY-MM-DDTHH:mm:ss sort correctly as text is one of the reasons Escena Viva adopts that format as a convention.

Chaining methods

The real power lies in combining them:

// The three most expensive sessions with tickets left, formatted
const highlights = catalog
  .flatMap((event) => event.sessions.map((s) => ({ ...s, event: event.title })))
  .filter((session) => session.sold < session.capacity)
  .sort((a, b) => b.priceCents - a.priceCents)
  .slice(0, 3)
  .map((s) => `${s.event}: ${(s.priceCents / 100).toFixed(2)} EUR`);

console.log(highlights);
// [ 'Festival de Jazz de Primavera: 42.00 EUR',
//   'Festival de Jazz de Primavera: 38.00 EUR',
//   'Concierto de Otono: 25.00 EUR' ]

Here the sort is safe because it operates on the new array that filter produced.

  1. Objects: Object.keys, values and entries

The problem: iterating over the sales-by-venue object we have just built with reduce.

const soldByVenue = {
  'Teatro Almendra': 276,
  'Sala Boveda': 175,
  'Auditorio Ribera': 1360
};

Object.keys(soldByVenue);     // [ 'Teatro Almendra', 'Sala Boveda', 'Auditorio Ribera' ]
Object.values(soldByVenue);   // [ 276, 175, 1360 ]
Object.entries(soldByVenue);  // [ [ 'Teatro Almendra', 276 ], ... ]

Object.entries combined with destructuring is the cleanest idiom for walking an object:

for (const [venue, sold] of Object.entries(soldByVenue)) {
  console.log(`${venue.padEnd(20)} ${String(sold).padStart(5)} tickets`);
}
Teatro Almendra        276 tickets
Sala Boveda            175 tickets
Auditorio Ribera      1360 tickets

And in the other direction, Object.fromEntries rebuilds an object from pairs:

// Sales per venue expressed in euros
const inEuros = Object.fromEntries(
  Object.entries(soldByVenue).map(([venue, n]) => [venue, n * 25])
);

Object.assign merges objects (equivalent to spreading, but it mutates the target):

const base = { capacity: 100, priceCents: 2000 };
const merged = Object.assign({}, base, { capacity: 420 });   // Empty {} target: safe

  1. Map and Set: When to Use Each

The problem: getting the list of venues with no repeats, and indexing the events by identifier so we can look them up instantly.

Set: a collection with no duplicates

const venues = new Set(catalog.map((event) => event.venue));

console.log(venues.size);                   // 3
console.log(venues.has('Sala Boveda'));     // true
console.log([...venues]);                   // Convert to an array to use map/sort

// Removing duplicates from any array in one line
const categories = [...new Set(catalog.map((e) => e.category))];

Map: a dictionary with keys of any type

// An index of events by id: lookups become instant
const index = new Map(catalog.map((event) => [event.id, event]));

console.log(index.get('evt-002').title);   // Noche de Monologos
console.log(index.has('evt-999'));         // false
console.log(index.size);                   // 3

// Walking it with destructuring
for (const [id, event] of index) {
  console.log(`${id} -> ${event.title}`);
}
Aspect Object {} Map
Key types Only text and Symbol Anything (objects, numbers, dates)
Guaranteed insertion order Partially Yes
Size Object.keys(o).length map.size
Clashes with inherited properties Yes (toString, constructor) No
Serializable to JSON directly Yes No (it has to be converted)
Performance with frequent adds and removes Worse Better

A practical rule: use an object when the keys are fixed and known (a record, a configuration, something headed for JSON) and a Map when the keys are dynamic, numerous or not strings (an index, a cache, a counter).

  1. Classes, Private Fields and Private Methods

The problem: encapsulating the logic of a session — calculating what is available, checking whether it is sold out, reserving tickets — so we do not repeat it all over the project.

class Event {
  // Private field: only reachable from inside the class
  #sessions = [];

  constructor({ id, title, venue, category, durationMinutes, sessions = [] }) {
    this.id = id;
    this.title = title;
    this.venue = venue;
    this.category = category;
    this.durationMinutes = durationMinutes;
    this.#sessions = sessions;
  }

  // Getter: used like a property, with no parentheses
  get sessionCount() {
    return this.#sessions.length;
  }

  get totalCapacity() {
    return this.#sessions.reduce((total, s) => total + s.capacity, 0);
  }

  get ticketsSold() {
    return this.#sessions.reduce((total, s) => total + s.sold, 0);
  }

  get occupancy() {
    if (this.totalCapacity === 0) return 0;
    return Math.round((this.ticketsSold / this.totalCapacity) * 100);
  }

  // Private method: an internal detail that is not part of the public API
  #findSession(sessionId) {
    return this.#sessions.find((s) => s.id === sessionId);
  }

  availableTickets(sessionId) {
    const session = this.#findSession(sessionId);
    if (!session) return 0;
    return session.capacity - session.sold;
  }

  reserve(sessionId, quantity = 1) {
    const session = this.#findSession(sessionId);

    if (!session) {
      throw new Error(`Session ${sessionId} does not exist in ${this.id}`);
    }
    if (session.capacity - session.sold < quantity) {
      throw new Error(`Insufficient capacity in ${sessionId}`);
    }

    session.sold += quantity;
    return session.sold;
  }

  // toString is called automatically when interpolated into a template
  toString() {
    return `${this.title} (${this.venue}) - ${this.occupancy}% full`;
  }

  // Static method: it belongs to the class, not to each instance
  static fromJSON(object) {
    return new Event(object);
  }
}

Usage:

const concert = Event.fromJSON(catalog[0]);

console.log(concert.sessionCount);                  // 2   (a getter, no parentheses)
console.log(concert.occupancy);                     // 33
console.log(concert.availableTickets('ses-001-1')); // 240
console.log(`${concert}`);                          // Concierto de Otono (Teatro Almendra) - 33% full

concert.reserve('ses-001-1', 4);
console.log(concert.availableTickets('ses-001-1')); // 236

// Private fields really are unreachable from outside
// console.log(concert.#sessions);  // SyntaxError at compile time

About # private fields:

  • They are real privacy, enforced by the engine, not a convention like the _ prefix.
  • They can only be accessed inside the body of the class.
  • They do not show up in Object.keys(), nor in JSON.stringify(), nor when you inspect the object.
  • You can test them with #field in object to find out whether an instance belongs to that class.

  1. JSON: parse and stringify

JSON is the interchange format for the entire course: the seed file data/events.json, the API responses, the configuration. JavaScript handles it natively.

// From object to text
const text = JSON.stringify(catalog[0]);
// {"id":"evt-001","title":"Concierto de Otono",...}   all on one line

// In a readable format: the third argument is the indentation
const readableText = JSON.stringify(catalog[0], null, 2);
console.log(readableText);
{
  "id": "evt-001",
  "title": "Concierto de Otono",
  "venue": "Teatro Almendra",
  "sessions": [
    {
      "id": "ses-001-1",
      "capacity": 420
    }
  ]
}

Always use null, 2 for files a person is going to read (such as data/events.json): version control will then show differences line by line instead of one giant modified line.

The second argument (null above) is a filter, very handy for selecting fields:

// Only these fields in the output
JSON.stringify(catalog[0], ['id', 'title', 'venue'], 2);

// Or a function, to transform or exclude values
JSON.stringify(user, (key, value) => (key === 'passwordHash' ? undefined : value), 2);

From text to object:

const restored = JSON.parse(readableText);
console.log(restored.title);   // Concierto de Otono

JSON.parse throws an exception if the text is not valid JSON. Always wrap it in try/catch when the source is not trusted:

function readCatalogSafely(text) {
  try {
    return JSON.parse(text);
  } catch (error) {
    console.error(`Invalid JSON: ${error.message}`);
    return [];
  }
}

Limitations you should know about:

JavaScript type After going through JSON.stringify
Date Becomes an ISO string; on the way back it is text, not a Date
undefined Disappears from objects; becomes null inside arrays
Function Disappears
Map, Set Become {} — you have to convert them by hand
NaN, Infinity Become null
BigInt Throws TypeError
Circular reference Throws TypeError

The fact that dates turn into text is another reason for our convention: we store dates as ISO strings from the outset, so the round trip through JSON loses nothing.

Common Mistakes and Tips

Mistake 1: using || for numeric default values. session.sold || 100 returns 100 when 0 have been sold. Use ??.

Mistake 2: believing that const freezes the object. const protects the variable, not its contents. Use Object.freeze if you need immutability.

Mistake 3: forgetting the return in reduce. It is the most frequent slip. If the result is undefined, check this first.

Mistake 4: calling sort without copying. It mutates the original array and causes bugs that are hard to trace. [...array].sort(...).

Mistake 5: sort() on numbers with no comparator. It sorts as text: [10, 9] stays [10, 9]. Always pass (a, b) => a - b.

Mistake 6: the implicit return of an object from an arrow. (s) => { id: s.id } returns undefined. Wrap it in parentheses: (s) => ({ id: s.id }).

Mistake 7: believing that spreading copies deeply. { ...event } shares the sessions array with the original. Use structuredClone.

Tip 1: chain, but in moderation. Three or four chained methods read very well; ten do not. Extract steps into named variables.

Tip 2: destructure in the parameters. It makes clear what each function needs and documents the signature without writing a single comment.

Tip 3: prototype in the REPL. Every transformation in this lesson can be tried out in seconds with .load src/catalog-data.js.

Tip 4: prefer methods that do not mutate. map, filter and spreading all return new things; the resulting code is far easier to reason about.

Exercises

Exercise 1: a venue report

Write a function venueReport(catalog) that returns an array of objects with this shape, sorted from highest to lowest occupancy:

[
  { venue: 'Auditorio Ribera', events: 1, sessions: 2, capacity: 1800, sold: 1360, occupancy: 76 },
  ...
]

Requirements: use reduce to group, Object.entries to walk the result, and sort on a copy. Display the result with console.table.

Exercise 2: the Session class

Create a Session class with:

  • A constructor that receives a destructured object (id, dateTime, capacity, sold, priceCents).
  • A private field #sold.
  • Getters available, occupancy (an integer from 0 to 100), soldOut (a boolean) and priceEuros (a string with two decimals).
  • A method sell(quantity = 1) that throws an Error if there is not enough capacity and returns the number of tickets sold after the operation.
  • A method toJSON() that returns a plain object ready for JSON.stringify (including sold, which is private).

Try it out with ses-002-1 (capacity 120, 118 sold): sell 2 tickets and check that it is now sold out; then try to sell one more and catch the error.

Exercise 3: a public catalog

Write publicCatalog(catalog, options) that prepares the data to be sent to a web client. It must:

  1. Accept options = { venue, maxPriceCents, availableOnly = true }, all optional, using destructuring and default values.
  2. Filter by venue if one is given.
  3. Discard sold-out sessions if availableOnly is true.
  4. Discard sessions above maxPriceCents if it is given.
  5. Remove from the result any event left with no sessions at all.
  6. Return the events without the category field and with the sessions transformed into { id, dateTime, available, priceEuros }.
  7. Print the result with JSON.stringify(result, null, 2).

Solutions

Solution 1

function venueReport(catalog) {
  // 1. We group by venue with reduce.
  const grouped = catalog.reduce((acc, event) => {
    // ??= creates the entry the first time the venue shows up.
    acc[event.venue] ??= { events: 0, sessions: 0, capacity: 0, sold: 0 };

    const record = acc[event.venue];
    record.events += 1;
    record.sessions += event.sessions.length;

    for (const session of event.sessions) {
      record.capacity += session.capacity;
      record.sold += session.sold;
    }

    return acc;   // Essential
  }, {});

  // 2. From object to array, adding the venue name and the occupancy.
  const rows = Object.entries(grouped).map(([venue, data]) => ({
    venue,
    ...data,
    occupancy: Math.round((data.sold / data.capacity) * 100)
  }));

  // 3. We sort on a copy (rows is already a new array, but we keep the habit).
  return [...rows].sort((a, b) => b.occupancy - a.occupancy);
}

console.table(venueReport(catalog));
┌─────────┬────────────────────┬────────┬──────────┬──────────┬──────┬───────────┐
│ (index) │ venue              │ events │ sessions │ capacity │ sold │ occupancy │
├─────────┼────────────────────┼────────┼──────────┼──────────┼──────┼───────────┤
│ 0       │ 'Auditorio Ribera' │ 1      │ 2        │ 1800     │ 1360 │ 76        │
│ 1       │ 'Sala Boveda'      │ 1      │ 3        │ 360      │ 175  │ 49        │
│ 2       │ 'Teatro Almendra'  │ 1      │ 2        │ 840      │ 276  │ 33        │
└─────────┴────────────────────┴────────┴──────────┴──────────┴──────┴───────────┘

Solution 2

class Session {
  #sold = 0;

  constructor({ id, dateTime, capacity, sold = 0, priceCents }) {
    this.id = id;
    this.dateTime = dateTime;
    this.capacity = capacity;
    this.priceCents = priceCents;
    this.#sold = sold;
  }

  get sold() {
    return this.#sold;
  }

  get available() {
    return this.capacity - this.#sold;
  }

  get occupancy() {
    return Math.round((this.#sold / this.capacity) * 100);
  }

  get soldOut() {
    return this.available === 0;
  }

  get priceEuros() {
    return (this.priceCents / 100).toFixed(2);
  }

  sell(quantity = 1) {
    if (!Number.isInteger(quantity) || quantity < 1) {
      throw new Error('The quantity must be a positive integer');
    }
    if (quantity > this.available) {
      throw new Error(`Insufficient capacity in ${this.id}: ${this.available} left`);
    }

    this.#sold += quantity;
    return this.#sold;
  }

  // JSON.stringify calls toJSON automatically if it exists.
  toJSON() {
    return {
      id: this.id,
      dateTime: this.dateTime,
      capacity: this.capacity,
      sold: this.#sold,
      priceCents: this.priceCents
    };
  }
}

// Test
const session = new Session({
  id: 'ses-002-1',
  dateTime: '2026-10-10T21:30:00',
  capacity: 120,
  sold: 118,
  priceCents: 1800
});

console.log(session.available);    // 2
session.sell(2);
console.log(session.soldOut);      // true
console.log(session.occupancy);    // 100
console.log(session.priceEuros);   // 18.00

try {
  session.sell();
} catch (error) {
  console.error(`Could not sell: ${error.message}`);
  // Could not sell: Insufficient capacity in ses-002-1: 0 left
}

console.log(JSON.stringify(session, null, 2));   // Includes "sold": 120

Without toJSON(), the private field #sold would not appear in the serialization: that is exactly the use case the method exists for.

Solution 3

function publicCatalog(
  catalog,
  { venue = null, maxPriceCents = Infinity, availableOnly = true } = {}
) {
  return catalog
    // 2. Filter by venue (if none is given, they all pass).
    .filter((event) => venue === null || event.venue === venue)

    // 3, 4 and 6. We transform each event and its sessions.
    .map((event) => {
      // We drop "category" with destructuring + rest.
      const { category, sessions, ...publicData } = event;

      const filteredSessions = sessions
        .filter((s) => !availableOnly || s.sold < s.capacity)
        .filter((s) => s.priceCents <= maxPriceCents)
        .map((s) => ({
          id: s.id,
          dateTime: s.dateTime,
          available: s.capacity - s.sold,
          priceEuros: (s.priceCents / 100).toFixed(2)
        }));

      return { ...publicData, sessions: filteredSessions };
    })

    // 5. Out go the events left with no sessions.
    .filter((event) => event.sessions.length > 0);
}

// Test: tickets of up to 20 EUR at the Sala Boveda
const result = publicCatalog(catalog, {
  venue: 'Sala Boveda',
  maxPriceCents: 2000
});

console.log(JSON.stringify(result, null, 2));
[
  {
    "id": "evt-002",
    "title": "Noche de Monologos",
    "venue": "Sala Boveda",
    "durationMinutes": 80,
    "sessions": [
      { "id": "ses-002-1", "dateTime": "2026-10-10T21:30:00", "available": 2, "priceEuros": "18.00" },
      { "id": "ses-002-2", "dateTime": "2026-10-11T21:30:00", "available": 75, "priceEuros": "18.00" },
      { "id": "ses-002-3", "dateTime": "2026-10-17T21:30:00", "available": 108, "priceEuros": "15.00" }
    ]
  }
]

Notice how const { category, sessions, ...publicData } = event does two jobs at once: it pulls out what we need to handle separately and removes category from the result without writing a single delete.

Conclusion

This refresher has walked through the modern JavaScript you will use on every line of the rest of the course, always while solving real catalog problems: const/let and block scope, arrow functions and the behavior of this, destructuring and default parameters as a way of receiving options, the dual use of the ... operator for copying and for gathering, ?. and ?? for handling incomplete data without blowing up and without erasing legitimate zeros, template literals, the arsenal of array methods — with particular attention to reduce and to the traps of sort —, Object.entries for walking records, Map and Set for indexes and unique collections, classes with getters and # private fields, and JSON with its mandatory null, 2 and its type limitations.

Along the way several project decisions have been settled for good: prices as whole cents, dates as ISO strings (which, as a bonus, sort correctly as text) and option objects with default values as the usual signature of our functions.

The other half of server-side JavaScript is still missing: asynchrony. Promises, async/await and the real order in which things run are the backbone of Module 2, and that is where the model from the first lesson — the waiter who never waits — turns into code.

Before that, we close the module with the lesson that ties everything together: The Course Project: the Escena Viva Platform. We will formalize the domain model, write the seed file data/events.json that will replace the data hard-coded in our scripts, settle the project conventions, and you will see the complete road map: what you will build in each of the twelve modules and how every piece fits into the final platform.

Node.js Course: From Beginner to Advanced

Module 1: Introduction to Node.js

Module 2: Core Concepts

Module 3: File System and I/O

Module 4: HTTP and Web Servers

Module 5: NPM and Package Management

Module 6: The Express.js Framework

Module 7: Databases and ORMs

Module 8: Authentication and Authorization

Module 9: Testing and Debugging

Module 10: Advanced Topics

Module 11: Deployment and DevOps

Module 12: Real-World Projects

© Copyright 2026. All rights reserved