Build with Wild Willows

Ten chapters that take you from “what is HTML” to a webpage you built yourself, filled with live data from a real API. Every code sample below is editable, and you press Run when you want to see what it does. You cannot get to the bottom of this page without having written something.

No account Nothing to install Real API, real data

This page works on a phone, but the code editors are much easier on a laptop or a tablet with a keyboard.

The destination

By the end, you will be able to

Ten chapters, and this is what you have at the end of them. A teacher can read this as the objectives; a student can read it as where this is going.

  • Build a webpage out of HTML elements, and say what each one is for.
  • Style it with CSS: select the thing you mean, and change how it looks.
  • Change a page with JavaScript: find an element, and rewrite what it says.
  • Fetch JSON from a public API with fetch, and explain what a GET request is.
  • Read your way through objects and arrays to the value you actually wanted.
  • Ask questions of data with if, comparisons, and && / ||.
  • Filter, sort and transform a list with .filter(), .sort(), .map(), .find() and .reduce().
  • Handle the times it fails: try / catch, a status code, and an empty result that is not an error.
  • Render the response into HTML, and know when that is safe to do.
  • Look things up when you need something this page never showed you.
About the data

/GameData exposes the data the Wild Willows game runs on. It is here as a programming dataset for this lesson: real species, cited sources, and a shape worth practicing on.

It is not an academic or scientific ecology API. Diets are simplified to what a game can act on, food webs are a handful of links rather than hundreds, and the requirements that decide when an animal returns are a game designer’s model of succession rather than a published one. Use it to learn to write code. Cite something else in a biology report.

Before you start

Looking things up

Where the answers are, and the one question worth asking about code you did not write.

Nobody who writes web pages for a living has this memorized. Not the property names, not the arguments a method takes, not the order of the two things inside .reduce(). They look it up, several times a day, and then they look the same thing up again next month. Learning where to look is not a fallback for when this lesson lets you down. It is the part of the job that lasts.

The search that works is plain words plus mdn: css center a div mdn, or javascript sort array of objects mdn. That one habit answers most of what you will want to know between here and your first real project.

WhereWhat it is for
The error panel on this pageFirst stop, every time. It names the line and usually the mistake, which answers about half the questions you were going to type into a search box.
MDN Web Docs (opens in a new tab)The reference, written by the people who build browsers. Every property, every method, with examples. This is the one professionals have open.
javascript.info (opens in a new tab)For when MDN assumes something you do not have yet. Same accuracy, taught rather than listed.
caniuse.com (opens in a new tab)“Does this actually work in real browsers.” One search, a color-coded table, done.
Stack Overflow (opens in a new tab)Somebody hit your exact error in 2019 and asked about it. Read the comments under the accepted answer as well as the answer: that is usually where the correction is.
DevDocs (opens in a new tab)Every reference in one fast search box, and it works offline once loaded. Useful on a school laptop that blocks half the internet.
Your browser’s developer toolsRight-click anything on any page and choose Inspect. It is the page you are looking at, opened up, and it is the same panel you have been using here.
Eloquent JavaScript (opens in a new tab)The long version, free online, in book form. For when you want to know why rather than how.

Code you did not write

There is one question worth asking about any code you did not write yourself, whether it came from a forum, a friend, or a chatbot: could you tell if it was wrong? If yes, use it. If no, that is not a reason to avoid it. It is the thing to go and learn next.

About AI

A lot of professional software engineers use it, and there is no version of this advice where you never touch it. But there is a difference between using a tool to go faster at something you understand and using it to skip understanding. For the next ten chapters, the code is small on purpose, and typing it yourself is not busywork: it is the only thing that builds the judgment you will need later, when the AI hands you 200 lines and asks you to decide whether they are right.

Your teacher may have a rule about this, and their rule wins.

Chapter 1

Three languages, one webpage

Every webpage you have ever used is made of three things working together. You will meet each one on its own here, building up the same small page: first the words, then how they look, then what happens when you click.

HTML: what is here?

HTML is labeled text. A <h1> is a heading, a <p> is a paragraph, and the labels are what tell the browser which is which. Change the words and press Run: the panel beside the code is your page.

<h1>Wild Willows Animals</h1>
<p>Animals return when their habitat is healthy.</p>

That is a webpage. Plain, unstyled, and completely real. You could put that file on the internet right now.

CSS: what does it look like?

Same HTML. CSS says how it should look, and it only ever needs three ideas: which thing (the selector), what about it (the property), and set to what (the value).

h1 { color: forestgreen; }
p  { font-size: 18px; }

Your first challenge, ten seconds in: change forestgreen to your favorite color, press Run, and watch the heading change. Try rebeccapurple, tomato, or #4a7c59.

JavaScript: what does it do?

JavaScript makes a page respond. Also three ideas: find the element, listen for something to happen, change it. Click the heading in the preview.

const heading = document.querySelector("h1");

heading.addEventListener("click", () => {
  heading.textContent = "You found an animal!";
});

HTML is the content. CSS is the appearance. JavaScript is the behavior. That is the whole map, and the rest of this lesson is one of those three getting more interesting.

Going Deeper Optional. A few more HTML elements, and what the labels are actually for.

There are more labels than these three

You have used <h1> and <p>. There are about a hundred elements and you will use perhaps fifteen of them. These are the ones that come up first.

<h1> to <h6>   headings, in order of importance
<p>             a paragraph
<ul> <li>       a list, and one item in it
<a href="...">  a link
<img src="..." alt="...">   an image
<button>        something to press
<div>           a box with no meaning of its own
<span>          a piece of text with no meaning of its own

Why the label matters, and not just the look

You could make any of these look like any other with CSS. The label is not about the look: it is what tells a screen reader that a heading is a heading, what lets a search engine understand the page, and what makes <button> respond to the keyboard without you writing a line of code.

A <div> you have made clickable does none of that. Use the element that means what you mean, and reach for <div> and <span> when nothing means it.

Attributes

The extra information inside the opening tag. href is where a link goes, src is which image to load, and alt is what the image says for anyone who cannot see it.

<a href="https://wildwillows.app">Play the game</a>
<img src="fox.jpg" alt="A red fox in long grass">

alt is not optional and it is not a caption. It is the sentence a blind reader hears in place of the picture, and it is also what shows up if the image fails to load on school wifi.

Chapter 2

Make it look like yours

You changed one color in chapter 1. CSS has maybe a dozen ideas that matter, and they are all in this chapter.

Three ways to say which thing

Every CSS rule starts by naming what it applies to. There are three ways to name something, and they answer three different questions.

WrittenMeansUse it for
pevery paragraph on the pagethe overall look
.cardanything with class="card"a kind of thing you have several of
#totalthe one thing with id="total"a single specific element

A class is the one you will reach for most. An element can carry several, separated by spaces, and any number of elements can share one.

p       { color: #3b4232; }
.fact   { font-style: italic; }
.rare   { color: #8e4a12; font-weight: 700; }
#total  { font-size: 22px; }

The second paragraph has both classes, so it gets both rules. When two rules set the same property, the more specific one wins, and an id beats a class which beats an element.

Color, and the four ways to write one

They all mean a color. Use whichever you can read.

color: forestgreen;              /* a name, about 140 of them exist */
color: #4a7c59;                  /* hex: red, green, blue */
color: rgb(74 124 89);           /* the same numbers, in decimal */
color: rgb(74 124 89 / 40%);     /* the same, 40% see-through */

color is the text. background is behind it.

Space: padding pushes in, margin pushes away

Two properties, and the difference is exactly this. Padding is space inside the box, between its edge and its content. Margin is space outside the box, between it and everything else.

.card {
  background: #eaf3dd;
  padding: 16px;   /* inside */
  margin: 8px;     /* outside */
}

Set padding to 0, press Run, and watch the words hit the edge. Set margin to 0 and watch the two cards touch. The background color makes the difference visible, which is why it is there.

One value applies to all four sides. Two means vertical then horizontal, which is the form you will write most:

padding: 16px;              /* all four sides */
padding: 8px 16px;          /* top and bottom, then left and right */
padding: 8px 16px 24px 4px; /* top, right, bottom, left, clockwise */
padding-left: 16px;         /* or just the one you mean */

Type

Four properties do almost all of the work, and line-height is the one people forget. Text set solid is hard to read; a line height of about 1.5 is the single easiest improvement you can make to any page.

.role {
  font-size: 16px;
  line-height: 1.6;
  max-width: 34em;
}

Change line-height to 1 and read the paragraph again. Then take out max-width and widen the preview: a line of text longer than about 75 characters is measurably harder to read, because your eye loses its place coming back to the start.

Boxes: borders, corners and shadow

.card {
  background: white;
  border: 2px solid #4a7c59;
  border-radius: 14px;
  padding: 16px 20px;
  box-shadow: 0 2px 10px rgb(0 0 0 / 12%);
}

Try border-radius: 999px. A radius larger than the box gives you a pill, which is how every rounded button on this page is made.

Putting things in a row

By default a <div> stacks: one per line, full width. Three properties change that, and between them they handle most layouts you will want.

.row {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
}

Add justify-content: space-between to .row and the tags spread to the edges. gap is the property worth remembering: it spaces children without giving any of them a margin, so nothing collapses or doubles up.

Reacting to the mouse

:hover is a rule that only applies while the pointer is over the element. transition tells the browser to move between the two states over time instead of snapping.

.btn        { transition: background 0.2s ease; }
.btn:hover  { background: #39604a; }

Both buttons end up the same color. Only one of them gets there smoothly. Going Deeper, just below, takes this further into transform and real animation.

Where to look things up

There are hundreds of CSS properties. Search for what you want in plain words plus "css mdn", for example "css center a div mdn", and you will land on the reference professionals use.

Going Deeper Optional. Formatting text, custom properties, media queries, and making things move.

Formatting text

Half of this belongs to CSS and half to JavaScript, and the rule for choosing is worth stating: if it is about appearance, use CSS. Uppercasing a heading in CSS leaves the real words in the HTML, so search, screen readers and copy-and-paste all still get what you actually wrote.

/* CSS: how it looks */
text-transform: uppercase;    /* the underlying text is unchanged */
letter-spacing: 0.08em;       /* small caps and labels need this */
text-align: center;
font-variant-numeric: tabular-nums;   /* digits line up in a column */
white-space: nowrap;          /* stop this one from wrapping */

/* JavaScript: what the text IS */
name.toUpperCase();
name.trim();                  /* strip stray spaces from input */
name.split(" ");              /* "Red Fox" -> ["Red", "Fox"] */
list.join(", ");              /* back the other way */

The tabular-nums one is a real fix, not a nicety. In most fonts a 1 is narrower than a 7, so a column of numbers looks ragged until you ask for the fixed-width digits the font already contains.

One value, used in ten places

A custom property is a name you invent, set once, and use anywhere below it. Change the value at the top and every use of it follows, which is how a whole site changes color in one line.

:root {
  --green: #4a7c59;
  --ink: #3b4232;
  --radius: 14px;
}

.card   { border: 2px solid var(--green); border-radius: var(--radius); }
.btn    { background: var(--green); }
h1, h2  { color: var(--ink); }

Dark mode on this page is exactly this and nothing else: the same rules throughout, with about twenty of these values pointed somewhere darker when you press the moon.

The same page on a phone

A @media block applies its rules only when the condition holds. Write the phone layout first and add the wider one on top, because a narrow single column is the thing that always works.

.layout { display: grid; gap: 16px; }          /* one column, always */

@media (min-width: 700px) {
  .layout { grid-template-columns: 1fr 2fr; }  /* two, when there is room */
}

Transitions: from one state to the other

The hover above used a transition. The property is more general than that: name what should animate, how long it should take, and how it should ease.

.a { transition: background 0.3s ease; }
.box:hover { background: #d8eec2; }

transition: all 0.3s works and is a bad habit: it animates properties you did not mean, including ones that force the browser to redo the whole layout. Name what you want.

transform: move it without moving anything else

transform shifts, scales or rotates an element for display only. Everything around it stays exactly where it was, which is why a button can grow on hover without shoving the rest of the row sideways.

.lift:hover { transform: translateY(-4px); }
.grow:hover { transform: scale(1.12); }

Animating transform and opacity is cheap, because the browser can do it without recalculating where anything is. Animating width, height, top or margin makes it redo the layout on every frame, which is what a janky page usually is.

Keyframes: an animation that runs on its own

A transition needs something to change. When you want motion that just happens, describe the stages with @keyframes and attach them with animation.

@keyframes rise {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}
.card { animation: rise 0.45s ease both; }

The pulse is the honest use of animation: it says the page is working rather than broken. Chapter 5 fetches real data over a real network, and on school wifi that wait is long enough to need saying something about.

Some people need it to hold still

Motion on a screen makes some people dizzy or sick, and their operating system already knows they have asked for less of it. One CSS block passes that on, and every animation you write should sit behind it.

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

This page does it too. The arrow that turned when you opened this panel, and the diagram in chapter 9 that lights up one step at a time, both check that setting first and stop moving if it is on.

Chapter 3

JavaScript can change HTML

Two ideas everything else is built on: giving a value a name, and reaching into the page to change it. Worth five minutes.

First, a name for a thing

Every JavaScript example so far has started the same way, and the lesson has not said what that line does. const heading = document.querySelector("h1") makes a variable: it takes a value and gives it a name, so that every line after it can say the name instead of doing the work again.

There are three parts. const announces that a name is being made. heading is the name, and you choose it. The = attaches one to the other, and it is not the equals sign of arithmetic: it does not ask whether two things are the same, it works out whatever is on its right and points the name at the answer.

Three words that make a name, and the one to use

WordWhat it doesUse it
constMakes a name and refuses to let it be pointed at something else later.Almost always. Every example on this page uses it unless there is a reason not to.
letMakes a name that can be pointed at something else later.When the thing genuinely changes: a running total, a counter, a value that gets replaced.
varThe old way. Makes a name that leaks out of the block it was written in.Not in new code. You will meet it in older examples online, and it is worth recognizing rather than copying.

Use const until something has to change, then let. Starting with const is not caution for its own sake: a name that cannot be repointed is one fewer thing that can quietly become something else halfway down a file you are still reading.

One thing const does not do, which catches people out: it protects the name, not what the name holds. A list can still gain and lose items.

const animals = ["Red Fox"];
animals.push("Barn Owl");   /* fine: the list changed, the name did not */
animals = ["Barn Owl"];     /* TypeError: assignment to constant variable */
const animal = "Red Fox";
const legs = 4;
const nocturnal = true;

console.log(animal);
console.log(animal + " has " + legs + " legs.");
console.log("Comes out at night?", nocturnal);
What a name is allowed to hold

Anything. Text goes in quotes ("Red Fox"), numbers do not (4), and true and false are the two yes-or-no values. Further down this page a name will hold an element taken out of the page, and then the game’s entire catalog of 150 animals. The line looks the same every time; only the right-hand side gets more interesting.

The name itself is for whoever reads the code next, which within a week is you. a runs exactly as well as animal and tells nobody anything. Names cannot contain spaces, and Animal and animal are two different names.

Change "Red Fox" to another animal and press Run. Then add console.log(color) at the bottom, without ever making a name called color, and read the error panel: color is not defined is JavaScript saying it has never heard that name before.

Now find something on the page

An HTML element can be given an id, which is a name. JavaScript can then go and find the element with that name, and change it.

<p id="animal-count">Loading...</p>

const element = document.querySelector("#animal-count");
element.textContent = "150 animals";
The whole idea, in two steps

1. document.querySelector("#animal-count") found the HTML element whose id is animal-count. The # means “the thing with this id”.

2. element.textContent = "150 animals" changed its text.

The page said Loading... for a moment and then said something else, because JavaScript reached in and rewrote it. Chapters 5 to 9 are that same move, except that the new text comes from the internet instead of from the line above it.

Change "150 animals" to anything you like. Then change the id in the HTML to total without changing the JavaScript, run it again, and watch the error panel tell you exactly what broke.

Going Deeper Optional. What the page actually is in memory, and where a name lives.

The page is a tree, and that is why querySelector works

When the browser reads your HTML it builds a tree in memory called the DOM. Your <li> elements are inside a <ul>, which is inside <body>, which is inside <html>.

document.querySelector("#animal-count") searches that tree, and changing an element changes the tree. The page you see is a drawing of it, which is why the text changed without anything reloading.

const el = document.querySelector("#animal-count");
el.parentElement;        /* upwards */
el.children;             /* downwards */
el.closest("section");   /* upwards, to the first match */
document.querySelectorAll("li");   /* every match, not just the first */

textContent and innerHTML

textContent puts text on the page and never runs it. innerHTML parses what you give it as HTML, which is powerful and is why chapter 9 has a warning attached to it.

el.textContent = "<b>hello</b>";   /* shows the angle brackets */
el.innerHTML   = "<b>hello</b>";   /* shows hello, in bold */

Where a name lives

The three words are compared at the top of this chapter. The part left over is scope: not whether a name can be repointed, but which lines of the file can see it at all.

Can you point it at something else?Where does the name exist?
constNoInside the nearest { }
letYesInside the nearest { }
varYesThe whole function, wherever you wrote it

That last row is the reason var is left alone now. A name declared inside an if block with var is still there after the block ends, and still there at the bottom of the function, which makes it possible to use a name you thought you had finished with and get no complaint from anyone.

function check(animal) {
  if (animal.rarity === "rare") {
    var note = "worth seeing";
  }
  return note;   /* var: reachable here, and undefined if the if never ran */
}                /* const or let: an error, which is the more useful answer */

A name that stops existing at the closing brace is a name you cannot accidentally read afterwards. That is the whole argument, and it is why the two newer words behave the same way as each other and differently from the old one.

Chapter 4

Programs can talk to other programs

An API is how one program asks another program a question.

API stands for application programming interface. The word doing the work is interface: a fixed, published way for one program to ask another program for something, so that neither has to know anything else about how the other is built.

The Wild Willows game knows about animals, plants, biomes, recipes and everything else in it. The Wild Willows API is a web address that hands that information to any program that asks, including the webpage you are about to write.

Your webpage asks the API API sends back data

Not a diagram of something happening elsewhere. Press the button and it happens here, on this machine, on this network.

GET https://wildwillows.app/GameData

Those numbers are yours. The time is how long the request took from where you are sitting, and the size is how much data came back. The same request from a phone on mobile data gives a different answer.

Open animals, then 0, and you are looking at one animal from the game as the program sees it.

What the data is made of

That tree is colored by type. Six types is the whole vocabulary, and you have just been looking at all of them.

In the dataCalledFrom /GameData
words in quotesstring"name": "Red Fox"
plain numbersnumber"minHealth": 35
true / falseboolean"explorable": true
[ … ]array, a list"eatsOther": ["berries", "carrion"]
{ … }object, a thing with named partsone whole animal
nullnothing, on purpose"unlock": null on the meadow
What the colors in the tree mean
"text" string 35 number true boolean null null Array(150) a list you can open { 17 } an object you can open
Quotes mean text

35 is a number you can do arithmetic with. "35" is text that happens to look like a number. JavaScript will sometimes convert between the two for you, and that is exactly why it is worth being deliberate: do not rely on it. If you mean a number, use a number. That distinction comes back in chapter 7, the moment you compare minHealth > 50.

Going Deeper Optional. What JSON is, and what it is not.

JSON is text

The response you just looked at arrived as a string of characters. Turning that text into real JavaScript objects and arrays is a separate step, and it is the only reason data.animals[0].name works at all.

const text = '{"name": "Red Fox", "rarity": "rare"}';

const animal = JSON.parse(text);    /* text   -> object */
animal.name;                        /* "Red Fox" */

JSON.stringify(animal);             /* object -> text, to send or store */

Chapter 5 calls response.json(), which does the fetching and the parsing in one move. This is what it is doing underneath.

It looks like JavaScript and it is not

JSON is deliberately smaller than JavaScript, because it has to be readable by programs written in any language. The rules it does not share:

  • keys must be in double quotes: "name", never name or 'name'
  • no comments
  • no trailing comma after the last item
  • no functions, no undefined, no dates: only strings, numbers, booleans, null, arrays and objects

Those are the six types from the table above, and that is not a coincidence. The type list is the format.

Where the data actually comes from

/GameData is one address that hands back everything. Many APIs instead give you an address per thing, and ask you to say which one you want:

/GameData                     everything, in one response
/Animals/red-fox               one animal        (a common alternative)
/Animals?biome=meadow          a filtered list   (a query string)

One big response is simpler to learn against and simpler to cache, which is why this one is built that way. Once you are choosing for yourself, the question is whether the page needs most of the data or a little of it.

Chapter 5

Fetch your first API

Ask for the data, wait for the answer, and handle the times it does not arrive.

A web API is one of those interfaces reachable at a web address. Wild Willows publishes its at https://wildwillows.app/GameData: ask that address, and it answers with every animal, biome and recipe in the game, written out as text. Your page never touches the game's own database. It only ever sees what the interface hands back, which is why the game can be rebuilt underneath and your code keeps working. That holds as long as the interface itself keeps its promises: an API can change what it sends, and when it does, code that depended on the old shape breaks. Good ones say in advance what will not change. Ours does, on the API page (opens in a new tab).

fetch is the browser's way of sending that request. On its own it does exactly that and nothing else: ask, and hold on to the answer.

fetch("https://wildwillows.app/GameData");
console.log("asked!");

Nothing visible happened, and that is correct. The request went out; nobody did anything with the answer.

Every request has a verb, and this one is GET

You just sent a request, and every request carries a word saying what kind of thing it is. They are called HTTP methods, and only a handful matter.

MethodWhat it asks forWhere you have met it
GETGive me this. Changes nothing on the server.Every page you open, every image on it, and the request you just made.
POSTHere is something new. Take it and do something with it.Signing up, posting a comment, saving your game.
PUT / PATCHReplace this / change part of this. It already exists.Editing a profile. PUT sends the whole thing, PATCH sends only what changed.
DELETERemove it.Deleting a save, a post, an account.

fetch(url) with nothing after the address is a GET. That is the default, which is why the line you just ran did not have to say so. Anything else has to be spelled out in a second argument:

fetch(url);                               /* GET, because nothing said otherwise */
fetch(url, { method: "POST", body: ... }); /* anything else, spelled out */

This whole lesson is GET, and that is a choice rather than a simplification. The Wild Willows catalog is public, read-only data: there is nothing in it to create, edit or delete, so the endpoint answers GET and refuses everything else. Which means every request in the next five chapters is safe to send twice, or a hundred times, and none of them can break anything for anybody. That is a good property to learn on.

Why the difference matters more than it looks

A GET is meant to be a question, not an action. Browsers, caches and search-engine crawlers all lean on that: they will repeat one, keep the answer, or send it before you have asked for anything. That rule is what makes the web cacheable at all.

So a GET that quietly changes something is a real bug rather than a style preference. The classic version is a delete link: a crawler follows every link on the page, and by morning the page is empty.

.then() means and after that. The answer arrives as a Response, an envelope. .json() opens the envelope.

fetch("https://wildwillows.app/GameData")
  .then(response => response.json())
  .then(data => {
    console.log(data);
  });

The chain reads in the order it happens: ask, then convert, then use. That is why we start here rather than with the shorter version below.

The same thing, written the modern way

await means “wait here until this arrives, then carry on”. It does exactly what the chain above does, and now that you have watched the chain, await has something to mean.

async function loadGameData() {
  const response = await fetch("https://wildwillows.app/GameData");
  const data = await response.json();
  console.log(data);
}
loadGameData();

await only works inside a function marked async. If you forget the async, the error panel will say so. Try deleting it.

What if it fails?

Networks go down and school wifi blocks things. A fetch that fails and says nothing is a blank page and a lost class period, so handle it from the start.

try {
  const response = await fetch("https://wildwillows.app/NotARealAddress/");
  const data = await response.json();
} catch (error) {
  console.log("Could not load the game data.");
}

try means “attempt this”; catch means “and if it goes wrong, do this instead”. Change the address back to /GameData and the catch block never runs.

Going Deeper Optional. Status codes, why a 404 is not an error, and what the browser does while it waits.

What the API told you besides the data

The probe in chapter 4 reported a status of 200. Every HTTP response carries one, and the first digit is the whole summary.

StatusMeansYou will meet it as
200Here it isthe normal case
304You already have ita reload that costs nothing
404No such addressa typo in the URL
429You are asking too oftena fetch inside a loop
500The server brokenot your fault

A 404 is not an error, and this catches people out

fetch only rejects when the request could not be made at all: no network, no such host, blocked. A 404 is a request that succeeded and came back with bad news, so it lands in your then, not your catch. That is what response.ok is for.

const response = await fetch(url);

if (!response.ok) {
  message.textContent = "Could not load the game data.";
  return;
}

const data = await response.json();

Without that check, a 404 returns an HTML error page, .json() tries to parse it, and the message a student gets is Unexpected token '<'. That error is in the runner's list on this page, and this is where it comes from.

Waiting is not stopping

JavaScript runs one thing at a time. When you await a fetch, your function is set aside and the browser goes back to handling clicks, scrolling and drawing. When the response arrives, your function is put back in the queue and picks up where it left off.

That is why a slow API does not freeze the page, and why a long for loop does: the loop never lets go.

console.log("first");
setTimeout(() => console.log("third"), 0);   /* queued, even at zero delay */
console.log("second");

Zero milliseconds still means "after the current work finishes". The queue is the point, not the delay.

Chapter 6

Look inside the data

You have the whole catalog. Now reach into it and pull out one thing.

Three steps, from the biggest thing to the smallest. Run it and read the console.

console.log(data.animals);         // an array of 150 things
console.log(data.animals[0]);      // one animal
console.log(data.animals[0].name); // one value

A dot reaches into an object by name. Square brackets reach into an array by position, and positions start at 0, not 1.

Try a path yourself

Type a path and see what comes back: both the value and, more usefully, what type it is.

Press Look it up, or pick one of the paths above.

Two things worth meeting now

data.animals.length is a number: 150. "150 animals" is a string. Writing data.animals.length + " animals" glues them together: + means add for numbers and join for text, and it decides based on what you gave it.

Ask for something that is not there (try data.animals[999] above) and you get undefined. Not an error, not zero, not empty. It is JavaScript saying “there is nothing here”, and it is behind about half of the confusing errors you will meet later.

Going Deeper Optional. Reading an object whose shape you do not know, and the function that calls itself.

Object.keys(), .values() and .entries()

You reached into the data by naming the parts you wanted. This is how you read an object whose field names you do not already know.

Object.keys(animal);
Object.entries(animal.requirements);   /* [["minHealth", 35], ...] */

This is how the collapsible tree above is built. It has never seen the Wild Willows data: it asks every object what its keys are and draws whatever it finds.

Recursion, which this page is already doing

The tree cannot know how deep the data goes. An animal contains requirements, which contains objects, which could contain anything. So it does not try. It handles one value, and if that value has parts inside it, it calls itself on each one.

function countAll(value) {
  if (value === null || typeof value !== "object") return 1;
  return Object.values(value).reduce((total, part) => total + countAll(part), 0);
}

Two parts, always. A base case that answers without calling itself (a plain value counts as one), and a step that makes the problem smaller. Leave out the base case and it calls itself until the browser stops it, which is the error Maximum call stack size exceeded.

Chapter 7

Make decisions

Asking a question with a yes-or-no answer. This is the piece that makes chapter 8 easy.

Run this one before reading the next paragraph.

const animal = data.animals[0];

if (animal.rarity === "rare") {
  console.log(animal.name + " is a rare find!");
}
Nothing happened, and nothing is broken

data.animals[0] is the Banana Slug, and its rarity is "common". So the question was asked, the answer was no, and the block was skipped. The code ran perfectly.

Nothing happening and nothing being wrong look identical from the outside, so it is worth seeing once on purpose. Now change "rare" to "common" and run it again.

else: always do one or the other

if (animal.rarity === "rare") {
  message.textContent = animal.name + " is a rare find!";
} else {
  message.textContent = animal.name + " is fairly common.";
}

One of the two always happens. Change data.animals[0] to data.animals[8] and see which branch you get.

else if: more than two outcomes

There are exactly three rarities in the game: common, uncommon and rare. Three real values, three outcomes.

if (animal.rarity === "rare")           label = "Rare find";
else if (animal.rarity === "uncommon")  label = "Uncommon";
else                                    label = "Common";

Move the else branch to the top and see what happens to every animal. The first true branch wins and the rest are skipped. That is a rule far easier to see than to be told.

Comparisons

MeansUse on
===is exactly the same astext or numbers
!==is not the same astext or numbers
> < >= <=bigger / smaller thannumbers, in this lesson
console.log(animal.biome === "forest");
console.log(animal.requirements.minHealth > 50);
One equals sets, three equals asks

= puts a value into something. === asks whether two things are the same. Typing one when you meant three does not produce an error that mentions = anywhere, which is what makes it worth knowing in advance.

&& and ||: asking two things at once

animal.rarity === "rare" && animal.biome === "meadow"

Swap the && for || and run it again. && means both, || means either, and the count tells you the difference better than a definition does.

The empty-state guard

This is the one you will reuse in everything you build. Without it, a search that finds nothing renders a blank page, and every student in the room assumes their code is broken.

if (matches.length === 0) {
  result.textContent = "No animals matched. Try another biome.";
} else {
  result.textContent = matches.length + " animals found.";
}

Change "tundra" to a real biome (meadow, forest, wetland, desert, alpine or coastal) and the other branch runs.

Optional: the ? : shorthand

const label = animal.rarity === "rare" ? "Rare find" : "";

If the question is true use the first thing, otherwise the second. It does exactly what an if/else does, in one line, and it is what makes chapter 9's list code readable. Skip it if it looks like noise. Nothing later depends on it.

Where this is going

animal.rarity === "rare" is a question with a yes-or-no answer, and you just used it in an if. In the next chapter you hand that same question to .filter() and let it ask about all 150 animals at once.

Going Deeper Optional. Truthiness, the safe dot, and choosing between many values.

Everything is either truthy or falsy

An if does not need a comparison. Give it any value at all and JavaScript decides whether that value counts as yes. Six things count as no, and everything else counts as yes.

/* the falsy six */
false   0   ""   null   undefined   NaN

/* so this works, and reads well */
if (matches.length) { ... }        /* any number but 0 */
if (animal.fact) { ... }           /* any string but "" */
The trap in that

0 is falsy, so if (animal.requirements.minHealth) is false for an animal that needs no health at all, which is not what you meant. When zero is a real answer, compare explicitly: if (minHealth !== undefined).

?. and ??

Reading through something that might not be there throws Cannot read properties of undefined. The optional dot stops at the gap instead.

animal.requirements.water.tiles      /* throws if there is no water */
animal.requirements.water?.tiles     /* undefined if there is no water */

/* and a default for when it IS missing */
const tiles = animal.requirements.water?.tiles ?? 0;

?? only steps in for null and undefined, which is what makes it different from ||. With ||, a real 0 would be replaced by the default, because zero is falsy.

Many outcomes: switch

A long else if chain comparing one value against a list is what switch is for. It is not more powerful, it is easier to read.

switch (animal.rarity) {
  case "rare":
    label = "Rare find";
    break;
  case "uncommon":
    label = "Uncommon";
    break;
  default:
    label = "Common";
}

The break is not optional. Without it execution falls through into the next case and keeps going, which is occasionally useful and much more often a bug.

Chapter 8

Loop through the data

One animal was a demo. 150 is a webpage.

for...of: do this for each one

The honest starting point, and the one that reads like English. For a list, this is the default worth reaching for. It walks anything that comes in order, which covers every piece of game data on this page. A plain object is not one of those, and chapter 6 shows what to use instead. Everything after it is a shorter way of saying one particular kind of loop, which is worth knowing up front, so the methods below feel like conveniences rather than magic words.

for (const animal of data.animals) {
  console.log(animal.name);
}

.forEach(): the same thing, shorter

Here is the piece the rest of the chapter rests on. animal => means: for each one, call it animal, and here is what to do with it. That is an arrow function: a piece of code you hand to something else to run.

data.animals.forEach(animal => {
  console.log(animal.name);
});

The name animal is yours to choose. Rename it to creature in both places and it works exactly the same. It is just a label for “the one we are on”.

.map(): turn each one into something else

150 objects go in. 150 strings come out. Same length, different contents.

const names = data.animals.map(animal => animal.name);

.map() is the most useful thing in this chapter, because it is how a list of data becomes a list of HTML. Chapter 9 cashes that in.

.filter(): keep only some of them

Give it the yes-or-no question from chapter 7 and it asks that question about every animal, keeping the ones that answer yes.

const meadow = data.animals.filter(animal => animal.biome === "meadow");

150 in, 25 out. Change "meadow" to forest, wetland, desert, alpine or coastal. Then filter on something else entirely: animal.rarity === "rare", or animal.trophic === "apex-predator".

Use the real values, or you get an empty list

A filter for a value that does not exist returns nothing, and an empty list looks exactly like broken code. These are the actual values in the data:

biome: meadow forest wetland desert alpine coastal
rarity: common uncommon rare
kind: mammal bird fish insect reptile amphibian invertebrate
trophic: herbivore insectivore omnivore mesopredator apex-predator detritivore decomposer scavenger filter-feeder

And the type rule from chapter 4, at the moment it matters: .filter(a => a.biome === "meadow") compares text, so it needs quotes. .filter(a => a.requirements.minHealth > 50) compares a number, so it does not. And "50" in quotes is text. JavaScript may convert it for you here and refuse to somewhere else, so write the number when you mean the number.

.find(): get exactly one

const fox = data.animals.find(animal => animal.name === "Red Fox");
console.log(fox.diet);
The difference between them

.filter() gives you back an array, even if only one thing matched. .find() gives you back the thing itself. That is why the code above says fox.diet and not fox[0].diet.

Change .find to .filter and run it. Read what the error panel says, and then you will remember this permanently.

.sort(): put them in order

const alphabetical = [...data.animals]
  .sort((a, b) => a.name.localeCompare(b.name));

.sort() reorders the original array rather than making a new one, which is why we copy it first with [...]. That is a real bug that is very hard to find later. Something you sorted once stays sorted everywhere else in your program.

.reduce(): combine them into one value Stretch goal

Think of it as a running total. You give it something to start with, and a rule for folding the next item into what you have so far. Everything after this works fine without it, so come back to it if it does not land the first time.

const perBiome = data.animals.reduce((counts, animal) => {
  counts[animal.biome] = (counts[animal.biome] || 0) + 1;
  return counts;
}, {});

Chain them

Each of these hands its answer to the next one. This is the payoff.

data.animals
  .filter(animal => animal.biome === "meadow")
  .sort((a, b) => a.name.localeCompare(b.name))
  .map(animal => animal.name)
  .join(", ");

Now move .map() above .filter() and run it. It breaks, and the error tells you why: after .map() you no longer have animals, you have names, and a name has no .biome. Order matters, and watching it fail takes one press.

The one block worth keeping

MethodGives you backUse it when
for...ofnothingyou just want to do something with each one
.forEach()nothingthe same, in a shorter form
.map()a new array, same lengthyou want to turn each one into something else
.filter()a new array, shorteryou only want some of them
.find()one item, or undefinedyou want one specific one
.reduce()one value of any kindyou want to combine them all
.sort()the array, reorderedyou want them in a particular order
Going Deeper Optional. Five more array methods, why a function can be handed to another function, and what makes code slow.

.some() and .every()

.filter() hands back a list. Sometimes you only want a yes or a no about the whole list, and these give you one straight away.

meadow.some(animal => animal.rarity === "rare");    /* true */
meadow.every(animal => animal.rarity === "rare");   /* false */

Same question you gave .filter(). Different shape of answer: a boolean instead of an array.

new Set(), for the values you did not know were there

This chapter listed the real values of biome, rarity, kind and trophic so your filters would match something. Here is how that list was made, rather than looked up.

const distinct = [...new Set(data.animals.map(a => a.trophic))].sort();

Change trophic to any other field and you have asked the data a question nobody wrote down the answer to. That is most of what data work actually is.

Three more, briefly

.includes() asks whether a value is in a list. .slice() takes a section out without changing the original. .indexOf() says where something is, or -1 if it is not there at all.

fox.eats.includes("prairie-vole");   /* true */
data.animals.slice(0, 5);            /* the first five */
data.animals.slice(-3);              /* the last three */
distinct.indexOf("herbivore");       /* a position, or -1 */

The -1 is worth remembering. It is the same idea as the undefined in chapter 6: a real answer meaning "not here", which will quietly behave like a number if you forget to check it.

A function is a value

You have been handing functions to other functions throughout this chapter without anyone saying so out loud. That is the trick that makes .filter() possible: a function can be stored in a variable and passed around exactly like a number or a string.

const isRare = animal => animal.rarity === "rare";
data.animals.filter(isRare);

Naming the question is often clearer than writing it inline, and you can reuse it in a .some(), an .every() and a .find() without repeating yourself.

Changing something, versus copying it

This chapter copied the array with [...] before sorting it. Here is what that actually means, and why it is the kind of bug that takes an afternoon to find.

const sorted = original.sort();  /* `original` is now sorted too */

Methods that build something new (.map, .filter, .slice) are safe to use anywhere. Methods that rearrange in place (.sort, .reverse, .push) change something somebody else might still be holding.

How long does this take?

.filter() over 150 animals looks at all 150. That is instant, so it never comes up. Over 150 million rows it would look at 150 million, and the same line of code would take minutes.

The useful idea is not the speed, it is the shape: the work grows in step with the data. Chaining .filter().sort().map() walks the list three times, which is still that same shape. But a filter nested inside a filter checks every animal against every animal, which is 150 times 150, and at a million rows that is a trillion comparisons and a page that never loads.

/* one pass per method: fine at any size you will meet */
data.animals.filter(isRare).sort(byName).map(toName);

/* every animal against every animal: 150 x 150 here, and it does not scale */
data.animals.filter(a => data.animals.some(b => b.biome === a.biome));

Computer scientists write these as O(n) and O(n squared) and call it big-O notation. The notation is a name for the question you can now ask, which is: if the data got a thousand times bigger, what happens to this line?

Chapter 9

Put API data on your webpage

Every chapter so far, in one file.

Chapter 3 changed some text. Chapter 5 fetched some data. Here they meet.

document.querySelector("#animal-name").textContent = data.animals[0].name;
API JavaScript HTML the page you see

Now do it twenty-five times, in fewer lines

This is the moment the whole lesson pays off.

const items = data.animals
  .filter(animal => animal.biome === "meadow")
  .map(animal => `<li>${animal.name}</li>`)
  .join("");

document.querySelector("#animal-list").innerHTML = items;
Read those three lines again

.filter() chose 25 animals. .map() turned each one into a piece of HTML. .join("") glued them into one string. Then one line put it on the page.

Change "meadow" to "forest". One word, a different twenty-five, a whole new page.

One honest note about innerHTML

innerHTML takes whatever you hand it and parses it as HTML, which is what makes it useful: your string of <li> tags becomes real elements. It also means the string decides what ends up on the page, tags and all. Building it out of data you trust, from your own API, is fine. Building it out of something a stranger typed is how a page ends up running somebody else's script, an attack with a name: cross-site scripting, or XSS. When the text came from a person rather than from you, use textContent, which puts it on the page as text and never as markup.

Going Deeper Optional. Making numbers readable, and building elements without innerHTML.

Formatting a number so a person can read it

Putting a number on a page is a different job from calculating one. 0.3333333333333333 and 1250000 are both correct and neither is readable. The browser will format them for you, in the reader's own language and conventions.

share.toFixed(1);
(1250000).toLocaleString("en-US");
new Intl.NumberFormat("en-US", { style: "percent" }).format(share);

.toFixed() hands back a string, not a number, which matters the moment you try to sort by it. Round for display, keep the real number for the arithmetic.

Building the list without innerHTML

The warning above is real, and this is the other way to do it. It is longer, and it cannot run anything it is given, because at no point is there any HTML to parse.

const list = document.querySelector("#animal-list");

for (const animal of data.animals) {
  const item = document.createElement("li");
  item.textContent = animal.name;   /* text, never markup */
  list.appendChild(item);
}

Use innerHTML with data you trust, which is what you have here. Use this the moment any part of the text came from a person: a search box, a comment, a name someone typed.

Emptying it first

Run your render twice and you get the list twice. innerHTML = items happens to replace, but appendChild adds, so anything that can re-render needs to start from empty.

list.innerHTML = "";   /* then build it */

This is the bug behind "why do I have fifty animals" in every project that adds a filter button later.

Chapter 10

Your turn

Five changes to the code below. Tick them off as you go.

const items = data.animals
  .filter(animal => animal.biome === "meadow")
  .map(animal => `<li>${animal.name}</li>`)
  .join("");
  • Change the heading. It is in index.html. Call the page whatever you like.
  • Change its color. That one is in styles.css, chapter 2.
  • Show a different value. Instead of animal.name, try animal.diet or animal.fact. Chapter 6 has the paths.
  • Show a different biome. One word in the .filter(), chapter 8.
  • Show only the apex predators, in alphabetical order. Filter on animal.trophic, then .sort(), chapters 7 and 8 together. There are 17 of them.

If the last two gave you trouble, that is useful information rather than a problem: number 4 is chapter 8 and number 5 is chapters 7 and 8 together, so you know exactly where to scroll back to.

Ready to build something of your own?

The Code Builder gives you the same three files with room to work, thirty project ideas, and everything you just learned still in your hands. Your code from the editor above comes with you.

Open the Code Builder
Going Deeper Optional. Where to go from here.

The reference professionals actually use

MDN Web Docs is free, accurate and written by the people who build browsers. Search for any method or property on this page plus “mdn” and the first result will be the real documentation. It assumes you already know some of it, and reading it anyway is most of how the rest gets learned. The rest of the places worth looking, and what to do about code you did not write, are at the top of this page.

Three things worth building next

  • A search box: an <input>, an input event listener, and the .filter() from chapter 8 run against whatever they typed.
  • A picker: buttons for the six biomes, each one re-running your render with a different value.
  • A comparison: two animals side by side, built from the same data with the same code twice.

All three are the code you already have, with one thing that changes. That is what most of web development turns out to be.

When you get stuck

Read the error panel first: it names the line and often the mistake. Then console.log the value just before the line that broke. The error marks where things stopped working, not where they went wrong, and those are usually a step apart.

The Code Builder has the same editor, the same error explanations and thirty project ideas to pick from. Your code from this chapter comes with you.