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
const,letand block scope- Arrow functions and the value of
this - Destructuring, default parameters and shorthand properties
- Spread and rest: the
...operator - Optional chaining
?.and nullish coalescing?? - Template literals
- Essential array methods applied to the catalog
- Objects:
Object.keys,valuesandentries MapandSet: when to use each- Classes, private fields and private methods
- JSON:
parseandstringify
const, let and Block Scope
const, let and Block ScopeThe 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 variableIf you want to prevent modification too, there is Object.freeze(session).
- Arrow Functions and the Value of
this
thisThe 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 returnWatch 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 }); // CorrectThe 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) |
- 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); // 0It 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 03Default 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 onlyThis 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);
}
};
- Spread and Rest: the
... Operator
... OperatorThe 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); // 1200Order 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
- Optional Chaining
?. and Nullish Coalescing ??
?. 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:
There is also an assignment operator, ??=:
const counts = {};
counts['Sala Boveda'] ??= 0; // Only assigns if it is null or undefined
counts['Sala Boveda'] += 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'}`);
- 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); // 7reduce 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); // 3000Step 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 thereturn accinside the function. The result will beundefinedon 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); // falseBoth 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.
- Objects:
Object.keys, values and entries
Object.keys, values and entriesThe 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`);
}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
Map and Set: When to Use Each
Map and Set: When to Use EachThe 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).
- 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 timeAbout # 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 inJSON.stringify(), nor when you inspect the object. - You can test them with
#field in objectto find out whether an instance belongs to that class.
- JSON:
parse and stringify
parse and stringifyJSON 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:
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) andpriceEuros(a string with two decimals). - A method
sell(quantity = 1)that throws anErrorif there is not enough capacity and returns the number of tickets sold after the operation. - A method
toJSON()that returns a plain object ready forJSON.stringify(includingsold, 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:
- Accept
options = { venue, maxPriceCents, availableOnly = true }, all optional, using destructuring and default values. - Filter by venue if one is given.
- Discard sold-out sessions if
availableOnlyistrue. - Discard sessions above
maxPriceCentsif it is given. - Remove from the result any event left with no sessions at all.
- Return the events without the
categoryfield and with the sessions transformed into{ id, dateTime, available, priceEuros }. - 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": 120Without 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
- What Is Node.js?
- Installing and Setting Up the Environment
- Your First Node.js Program
- The Node.js REPL
- Modern JavaScript for Node.js
- The Course Project: the Escena Viva Platform
Module 2: Core Concepts
- Node.js Architecture
- The Event Loop
- Callbacks and Asynchronous Programming
- Promises and async/await
- Events and EventEmitter
- CommonJS Modules and require()
- ES Modules and Interoperability
Module 3: File System and I/O
- Reading and Writing Files
- The fs Module in Depth
- Cross-Platform Paths with the path Module
- Working with Streams
- Transform Streams and pipeline
- Buffers and Binary Data
Module 4: HTTP and Web Servers
- Creating a Simple HTTP Server
- Handling Requests and Responses
- Manual Routing
- Serving Static Files
- Receiving Data: Request Bodies and JSON
- Consuming External APIs from Node.js
Module 5: NPM and Package Management
- Introduction to NPM and package.json
- Installing and Using Packages
- Semantic Versioning and package-lock
- npm Scripts and Project Automation
- Creating and Publishing Packages
- Dependency Security and Maintenance
Module 6: The Express.js Framework
- Introduction to Express.js
- Setting Up an Express Application
- Routing in Express
- Middleware
- Essential Third-Party Middleware
- Input Data Validation
- Error Handling
Module 7: Databases and ORMs
- Introduction to Databases
- Using MongoDB with Mongoose
- CRUD Operations
- Relationships, Population and Advanced Queries
- Using SQL Databases with Sequelize
- Migrations, Transactions and Seed Data
Module 8: Authentication and Authorization
- Introduction to Authentication
- User Registration and Password Hashing
- Sessions and Cookies with Passport.js
- Authentication with JWT
- Role-Based Access Control
- API Security Best Practices
Module 9: Testing and Debugging
- Introduction to Testing
- Unit Testing with Mocha and Chai
- Test Doubles with Sinon
- Integration Testing
- Coverage and Test Automation
- Debugging Node.js Applications
Module 10: Advanced Topics
- The Cluster Module
- Worker Threads
- Caching and Job Queues with Redis
- Performance Optimization
- Building RESTful APIs
- GraphQL with Node.js
Module 11: Deployment and DevOps
- Configuration and Environment Variables
- Logging and Monitoring in Production
- Using PM2 for Process Management
- Packaging with Docker
- Deploying to Heroku and Other PaaS
- Continuous Integration and Deployment
