Writing an Application
By the end of this you will have built QuestLog: you type in a thing you have to do, it appears on screen, ticking it off awards points, and when you come back later it is all still there. Nothing is hidden from you. Every line is one you wrote and can watch running.
You write code in the page and press Run. When something goes wrong, and it will, the error says which line and what it was expecting. You fix it and run it again. Several steps let you scrub a program backwards and forwards one line at a time, watching each variable change as it happens. You need no setup and no previous programming.
The steps
What an app is made of
Every app you have used, from a bank site to a game, is three separate things stacked on top of each other. Not three parts of one thing: three separate things, written in three different languages, each of which can be removed on its own.
There is the structure, which says what exists: a button here, a heading there, a box for typing in. There is the style, which says what it looks like: this colour, that size, rounded corners. And there is the behaviour, which says what happens when you touch it.
What "a language" means here, and why there are three
A language is a set of words a computer has been taught to obey. Structure is written in HTML, style in CSS, behaviour in JavaScript. They are separate because they answer separate questions, and because keeping them separate means you can change how something looks without any risk of breaking what it does.
This course is almost entirely about the third one. Behaviour is where the thinking lives, and it is the part that transfers: the ideas in the next thirteen steps are the same ideas in Python, in Java, in the language running inside a washing machine.
With the style off and the behaviour on, you get grey unformatted text that works perfectly. With the style on and the behaviour off, you get a polished button that does nothing at all. A computer has no opinion about which of those is closer to done.
Your first line that does something
A program is a list of instructions, carried out in order, exactly as written. That last part is the hard bit. A computer will not work out what you meant, will not fill in an obvious gap, and will not quietly fix a spelling mistake. It does the thing you actually wrote.
Here is your first instruction. say is the word for "put this on the screen", and the round
brackets hold the thing you want said.
Is say a real word, or one this page made up?
It is this page's word. JavaScript itself has no say. What a browser actually gives you
is console.log("hello"), which writes a line into a panel called the console that you only
see if you go looking for it. Everything else about the line is exactly real: the name of the job, the
round brackets holding the material, the quotes, and the fact that it is carried out precisely as
written.
The page uses say because output should be visible without a detour, and because the
name says what it does. When you open a real browser console later, write console.log
instead and everything you learned here still holds. A few more page words turn up as you go:
ask() for reading the box on the page in Step 4, and load() and
save() for the cupboard in Step 12. Each one stands for something a browser really does,
and each is pointed out when it arrives.
What the brackets and quotes are actually for
The round brackets mean "do this, using what is inside". say("hi") is one instruction:
the name of the job, then the material to do it with. Without brackets, say is just the
word, in the same way that "kettle" is not the same as boiling one.
The quotes mean "the letters inside here are text, not an instruction". "hello" is
five letters. hello with no quotes is a name, and the computer will go looking for
something called hello and complain when there is nothing there. You will meet that error in a moment,
on purpose.
hello appears in the output panel under the editor.say(2 + 2) prints 4, because without quotes those are numbers and a computer
can add numbers. say("2 + 2") prints 2 + 2, because with quotes it is five
characters of text and there is nothing to work out. Same keys on the keyboard, different meaning.
Two keyboard notes while we are here: a computer writes multiply as * and divide as
/, because there is no x or division key that means only that. So 60 * 60 * 24
is sixty times sixty times twenty-four.
I do not understand what an error message is telling me
An error is not the computer telling you off. It is the computer stopping at the first thing it could not carry out and telling you where it stopped. It always includes a line number, and the line number is almost always right, even when the actual mistake is a missing bracket on the line above.
Read it in this order: which line, then what word it is complaining about, then what it says it expected. Three quarters of all errors you will ever see are a misspelled name, a missing bracket, or missing quotes.
hello on the screen. You write
say(hello) and get an error saying there is no variable called "hello". What did the
computer think you were asking for?Boxes that remember
So far every program has forgotten everything the instant it finished. To build anything at all you need somewhere to put a value and a name to get it back by. That is a variable: a labelled box with something in it.
let score = 0 makes a box labelled score and puts 0 in it. After
that, writing score anywhere means "whatever is in that box right now".
Why the = sign here does not mean "equals"
In maths, x = 5 is a statement about the world that is either true or false. In a
program it is an instruction: put 5 into x. It is an arrow pointing right to left, and it is
always an order, never an observation.
That is why score = score + 10 makes sense here and would be nonsense in maths. Work
out the right-hand side first, using the current contents of the box, then put the answer back in the
same box. If score held 30, the right side works out to 40, and 40 goes in.
Why do some programs say let and others say const?
Both make a labelled box with something in it. let makes one you are allowed to refill
later, which is why let score = 0 followed by score = 100 is fine.
const makes one that keeps whatever you first put in, and trying to refill it stops the
program with the message "was made with const, so its value cannot be changed".
Use let for a box whose contents change, like a score. Use const for one
that should not, like a list of quests that stays the same list all the way through. One catch worth
knowing before Step 7: a const list can still have things added to it and taken out of it.
What cannot change is which list the name refers to, not what is inside that list.
score = score + 10 line and read the boxes: score still holds 0, because the
highlighted line has not run yet. Take one step forward and it holds 10. In between, the right-hand
side was worked out using the old contents, and only then was the answer put back into the same box.let b = a does not tie the two boxes together. It reads what is in a right
now and puts a copy in b. Change a afterwards and b does not
care. This is obvious once you have seen it and surprising until then, so the quiz below is about
exactly that.
One thing in the editor below is not an instruction. Anything on a line after two slashes,
//, is a comment: a note written for a person, which the computer skips over completely. The
tasks in this course use comments to tell you what to do, so a comment line never counts as one of your
lines of program.
let score = has nothing after the equals sign yet.score holding 100 will pass, and there are several. It checks what the program actually
did, not whether you wrote it the way anyone expected.What makes a name a good one
A name may use letters, numbers and underscores, and may not start with a number. Beyond that the
computer does not care in the slightest: x, score and
currentPointsThisRound are all equally valid.
The people who care are you, next week. A name is a note to your future self about why this box
exists. Two words joined with a capital in the middle, like totalPoints, is the usual
habit in JavaScript. None of this affects whether the program runs, which is exactly why it gets
forgotten.
let a = 1, then let b = a, then
a = 5. What is in b at the end?a, nothing went looking for
other boxes to update. Scrub Lab 5 through those three lines and watch it happen.Talking to whoever is using it
An app nobody can type into is a demonstration. The moment a program can take something in and answer differently because of it, it stops being a list of instructions and starts being a tool.
The box on the page is structure, from Step 1. It quietly holds whatever has been typed into it, and the
behaviour layer can ask for that at any moment. Here the request is spelled ask(), so
let name = ask() puts whatever is in the box into a variable called name. What
comes back is always text, even when what was typed looks like a number and the second lab in this step is
what that costs you.
How a typed letter gets from the keyboard into a variable
Nothing happens at the moment you type. A key press changes what the box is holding and stops there. No code of yours runs, and no variable moves. The box keeps the characters the way a piece of paper keeps what was written on it.
The value only travels when something asks for it. ask() takes a copy of what is in
the box at that instant and hands the copy over. Type something else afterwards and the variable does
not notice, for the same reason b did not notice in Step 3: it was given a copy, not a
link.
Always text, which matters more than it sounds. Type 42 into a box and you get the two characters
"42", not the number. Adding 1 to that gives you "421", because with text,
plus means "stick together". The second lab in this step is that bug, on purpose.
Total: 205.Number(...) turns text into an actual number.With two numbers, + adds. With any text involved, + joins. So
"5" + 1 is "51" and 5 + 1 is 6. Nothing warns you,
because both are perfectly reasonable things to want.
20. The code says
let total = typed + 5 and prints 205. Why?"20" + 5 makes "205", and it is not an error, so nothing complains.
Number(typed) + 5 gives 25. This single confusion is behind a remarkable number of real
bugs in real software.Making a decision
Until now every program has done the same thing every time. A decision is where a program stops being a recipe and starts being able to handle a situation: check something, and take one path or the other.
The shape is always the same. if, then a question in brackets, then what to do when the
answer is yes. Optionally else, and what to do when it is no.
if (age >= 18) {
say("come in")
} else {
say("go home")
}
The curly brackets hold the instructions that belong to each side. Anything inside the first pair happens
only when the answer is yes, anything inside the second only when it is no, and whatever is written
underneath the whole thing happens either way. The comparison signs work as they do in maths:
>= is greater than or equal to, < is less than.
What counts as a question a computer can answer
Only questions with a yes or no answer. age >= 18 is one: it works out to either
true or false, nothing else. > is greater than, >= is greater than or
equal to, === is "exactly the same as", !== is "not the same as". Two
questions can be joined into one with ||, two vertical bars, which means "or". The whole
thing is true if either side is true, and the second side is not even looked at once the first one
has said yes.
True and false are real values, as real as 7 or "hello", and you can put one in a box:
let allowed = age >= 18 stores true or false. That turns out to be one of the more
useful things in programming. It is why the comparison and the decision are two separate ideas
rather than one.
>= to > and stop on 18 again. One character,
and one eighteen-year-old is now standing outside. Almost every off-by-one bug in the world is that
character.= puts a value in a box. === asks whether two things are the same. Writing
if (age = 18) does not compare anything: it puts 18 into age, throws away whatever was
there, and then treats 18 as a yes. It runs. It is wrong, which is the worst combination.
age already holding 12, and prints what label holds, which so far is
nothing yet.age box is filled in
for you before each run, so your job is only to put the right word in label. Use the
Try age buttons as often as you like, then press Check against hidden ages. That runs
your rule against a set of ages chosen to include every edge: both boundaries, one either side, zero,
and something absurd. If a case fails it tells you which age and what your rule said.Say "else if" another way
Think of a list of rules, checked from the top, where the first one that matches wins and the rest
are never looked at. That is exactly what a chain of else if is.
Which is why order matters enormously. If you check "over 10" before "over 100", then 500 matches the first rule and stops, and your "over 100" branch can never run at all. Put the narrowest rule first. This is a real bug that is hard to spot, because every individual line looks correct.
if (s > 50) say("pass"), then
else if (s > 90) say("top marks"). Someone scores 95. What gets printed?Doing it again without saying it again
You could draw ten stars with ten lines of code. A hundred stars is a hundred lines, and now you have a real problem: not the typing, but that changing the star means changing it in a hundred places, and you will miss one.
A loop says "do this again while something is still true". Three parts: where to start, when to stop, and what to change each time round.
Reading the three parts of a for loop
for (let i = 0; i < 10; i++) reads as three separate instructions separated by
semicolons. Start with i at 0. Keep going while i is under 10. After each
pass, add one to i. That last part is what i++ means.
i is short for index, and counting from 0 rather than 1 is a convention you will meet
everywhere in Step 7. With i < 10 and a start of 0, the body runs for 0 through 9:
ten times, which is what was wanted, and never with i equal to 10.
What is the dot in drawn.length?
The dot means "reach inside this thing and use the part called that". drawn holds text,
and every piece of text knows its own length, so drawn.length is the number of characters
in it. If drawn holds "***" then drawn.length is 3. Read it left
to right, as "the length of drawn".
The same dot turns up everywhere from here on and always means the same thing:
quests.length in Step 7 is how many quests there are, and quest.title in
Step 9 is the title of one quest. It is not doing anything clever. It is the difference between naming
a box and naming one particular thing inside it.
i is nine.If the stopping test can never become false, the program runs until something stops it. In a browser that means the page freezing. Here it means a message telling you what happened, because every loop in this page has a step budget. The lab below lets you cause one safely.
for (let i = 1; i <= 5; i++) run?<= lets 5 through, so 1, 2, 3, 4, 5.
Compare that with i = 0; i < 5, which also runs five times but with different values in
i. Both are used constantly. Which you want depends entirely on whether you care about the
count or the numbers.Holding many things at once
QuestLog needs to hold your quests, and there is no way to know in advance how many there will be. One box per quest cannot work. You need one box that holds a list, that can grow, and that you can walk through.
Square brackets make a list: let quests = ["Feed the cat","Do maths"]. Square brackets
also get one item back out, by its position. Four things you will do to a list, written out:
say(quests[0]) // the item at position 0
say(quests.length) // how many items there are
quests.push("Walk the dog") // add one to the end
quests.splice(1, 1) // take out 1 item, starting at position 1
The dot is the same dot as in drawn.length in Step 6: reach inside the list and use the
part called that. push and splice are the two you will use in QuestLog, and
Lab 14 below lets you press them as buttons before you have to write them.
Why the first item is number 0
Positions count from zero, so the first item is quests[0] and the second is
quests[1]. This trips up everyone at first and there is a real reason for it: the position
is a distance from the start, not an ordinal. The first item is zero steps along.
The consequence you will actually use: a list of 5 items has positions 0 to 4, so the last item is
always at length - 1. Asking for quests[5] in a five-item list is not an
error, which is worse than if it were. You get undefined, and the trouble surfaces later,
somewhere else.
total 75. Then press The off-by-one and run that one.for ... of. Both are common.
The second is easier to read and the first is the one you need when the position matters.I need this said another way: length versus last position
Think of a row of five houses, numbered from 0. The number of houses is 5. The number on the last house is 4. Both numbers are useful and they are never the same number.
So list.length is how many, and list[list.length - 1] is the last one. If
you ever write list[list.length] you get nothing back, because you have walked one house
past the end of the street.
const, so you cannot swap in a
whole new list, and list = ["b","c","d","e"] stops with an error. Changing what is
inside the list is allowed, and that is the exercise.say(quests[4]) expecting the last one. What
happens?quests[3]. Asking for 4 is asking for a fifth item that does not exist. No error is raised,
which means the wrong value travels onwards into your program and breaks something later, a long way
from the actual mistake.Naming a recipe
Awarding points in QuestLog means three things: add to the total, save it, and update the screen. You will need that in four different places. Copying those three lines four times means that when the rule changes you have to find all four, and you will find three.
A function gives a name to a set of steps, so the four places all say the same short thing and there is exactly one copy of the actual work.
What goes in, and what comes back out
A function can take material in and hand a result back. The names in the brackets when you write it are parameters: empty slots. The values you put in when you call it are arguments, and they fill those slots for that one run.
return is how a result comes back, and it also ends the function on the spot. A
function with no return still does its work and hands back undefined. That is
the right choice when the point is the doing, like drawing on screen and the wrong choice when the
point is the answer. A bare return with nothing after it is allowed too: it hands back
nothing and simply stops there, which is how a function leaves early when there is no work to do.
say(award(5)) and press
Record a run again. One box in the panel below is worth a look: award holds the
function itself, because a function is a value kept in a box like any other.These are not alternatives. say puts something on screen and the program cannot use it
afterwards. return hands a value back to whoever asked, to store or add or test. A function
that says its answer instead of returning it looks like it works and cannot be built on.
30 points, 3 messages, and your job is to get that
same line out of one copy of the work.undefined, because the body is empty and nothing is handed back. Then press
Check against hidden ages, which names the first age it tried and what came back for it.function stage(age) { ... } and hand the answer back with return, and any
program can ask about any age it likes, as often as it likes. The checker calls your function with the
same eight ages, boundaries included.function double(n) { say(n * 2) }. Someone writes
let x = double(5) and finds that x holds undefined, even though
10 appeared on screen. Why?say(n * 2) to return n * 2 and x holds 10, though now nothing
appears on screen unless the caller prints it. That division of labour is the point.Describing one whole thing
A quest is not one value. It has a title, whether it is done, and how many points it is worth. You could try to store one with the tools you already have: the titles in one list, the done marks in a second, the points in a third. On top of that, a promise to yourself that the third item of each list always describes the same quest. It works until the day you remove a quest from one list and forget the other two. From then on every quest after it is wearing somebody else's points, and nothing has gone wrong loudly enough to tell you.
An object holds named values together, so one variable is one whole thing: { title:"Feed the
cat", done: false, points: 10 }.
Getting a value back out uses a dot and the name of the field: quest.title is the title,
quest.points is the points. Setting one works the same way round: quest.done = true
marks it finished. The dot is the same one as in quests.length; it reaches inside a thing and
names one part of it. One shorthand shows up in the labs below: done already holds true or
false, so it can be the whole question by itself, and if (q.done) means "if q.done is true".
Writing if (q.done === true) says exactly the same thing and is not wrong, only longer.
How an object differs from a list
A list is things in an order, reached by number, where the order is the meaning: first, second, third. An object is things with names, reached by name, where the order does not matter at all.
Use a list when you have many of the same kind of thing and might need to walk through them. Use an object when you have one thing with several different aspects. QuestLog uses both together, and so does almost every real app: a list of objects.
level: 3 to another number
and press Run. The card on the right is drawn again with what you wrote.hp line and run
it again, and see what the card does about a value that is not there.earned 45 of 3 quests. Add up the points of the ones marked done: true
yourself, and you get 25.What is the question mark in q.done ?"[x]" :"[ ]"?
It is an if and an else squeezed into one value. Read the question mark as "then" and the colon as
"otherwise": if q.done is true the whole thing is "[x]", and if it is false
the whole thing is "[ ]". It answers with a value instead of doing something, which is why
it can sit in the middle of a line that is joining text together.
Written the long way it is four lines: let mark ="[ ]", then
if (q.done) { mark ="[x]" }, then use mark. Both are correct and every
checker in this course accepts either. The short form is worth being able to read even if you never
write one, because other people's code is full of them.
Dot or square brackets?
quest.title and quest["title"] do the same thing. The dot is shorter and
is what you will use nearly always.
The brackets earn their place when the name is in a variable: if field holds
"title", then quest[field] reaches the title, while quest.field
goes looking for something literally called field and finds nothing. That distinction matters the
moment you write code that works on a field chosen at run time.
const q = { title:"Do maths", done: false } and write
q.points. There is no points field. What do you get?NaN, which stands for not a number, and that is usually the first symptom you actually
notice, several lines from the cause.Making it react
Every program so far ran top to bottom and stopped. A real app mostly sits there doing nothing, waiting. Someone clicks, and a piece of your code runs. Then it waits again.
That is the behaviour layer from Step 1, finally connected up: you hand the browser a function and say "run this when that is clicked". The browser keeps it and calls it later, possibly never. A function handed over like that has a name of its own: a handler. It handles one kind of thing that can happen, and an app is mostly a pile of them, one for each thing a person can do. Handing one over is called registering it: you give it to the browser, the browser writes it down, and your part is finished until something happens.
Handing over a function without calling it
There is a real difference between award and award(). The first is the
function itself, as a value you can hand to someone. The second is an instruction to run it right now.
So onClick(award) means "here is a function, keep it for later" and
onClick(award()) runs it immediately and hands over whatever it returned, which is
usually nothing. The button then does nothing forever. The brackets are the entire difference, and this
catches everyone at least once.
There are two ways to hand a function over. If it already has a name, pass the name:
onClick(award). If it is only ever needed here, you can write it on the spot with no name at
all:
onClick(function () {
points = points + 1
})
The words function () { ... } in the middle of a call mean "here is a small function, made
right now, for you to keep". It is the same kind of thing as award, just never given a name
because nothing else needs to call it. That is the form the two labs below use, and the form every handler
in QuestLog is written in. onClick and onKey are this page's names for handing a
handler over, in the same way say was in Step 2. A real browser spells the same act
button.addEventListener("click", ...). Everything about how it behaves is the same.
onClick(award()). What happens?award returned, which is not a function. Drop the brackets. The symptom,
something happening once on load and never on click, is a useful one to recognise.Drawing the screen from data
Here is the idea that turns a pile of code into an app, and it is a smaller idea than it sounds. Keep the truth in one place, as data. Have one function whose only job is to draw the screen from that data. When the data changes, call it again.
The alternative, which everyone tries first, is to change the data and also reach onto the screen and patch the bit that showed it. That works until there are two places showing the same thing, and then you have two truths and no way to tell which is right.
What "the screen is out of date" actually means
Screen contents are just structure, from Step 1, and it stays exactly as it was put there. Change a variable and nothing on screen moves, because nothing is watching. The words on screen are a photograph of the data taken at the moment you drew it.
So a wrong screen has two possible causes, and telling them apart is most of debugging: either the data is wrong, or the data is right and nobody redrew. Printing the data is how you find out which, and it is why the lab below shows you both at once.
lines list comes straight back, and the message prints
what it wanted instead.done: true, but on screen it still
looks unticked. Where is the bug?Remembering after you close it
Everything so far lives for as long as the page is open. Close it and every quest is gone. A to-do app that forgets your to-dos is a curiosity.
Browsers give every app a small named cupboard to keep things in, which survives the page closing. It holds text and nothing else, so a list of quests has to be turned into text on the way in and back into a list on the way out.
Turning a list into text, and back
JSON.stringify(quests) takes your list of objects and produces one long line of text
describing it exactly. JSON.parse(text) reads that text and builds the list again. Text
goes in the cupboard; the pair of them get you there and back. JSON is just the name of that way of
writing data down as text, and it is short for JavaScript Object Notation.
What comes back is a rebuilt copy, not the original. That matters the first time you save, reload, and find your changes going into a list nobody is looking at any more. The lab below lets you cause that.
What is null, and how is it different from undefined?
null means "deliberately nothing". load() hands back null
when the cupboard has never had anything put in it, which is what happens the very first time anybody
opens your app. So if (quests === null) reads as "if nothing was saved", and the line after
it starts you off with an empty list instead of crashing on a value that is not a list.
undefined, from Step 7, is the other kind of nothing: nobody ever set this, the way
asking a four-item list for position 4 gives you undefined. A rough rule that will serve
you for years: undefined is nothing by accident, null is nothing on purpose. Both are ordinary values,
and both can be compared with ===.
The couple of lines at the top of a program that refuse to trust what came back have a name: the guard.
Checking for null catches most of the rubbish, and one case needs something new. If what was
saved is a single quest where a whole list was expected, for ... of cannot walk it. Asking
whether something really is a list is spelled Array.isArray(quests). It answers true or
false like any other question. So the guard can be:
let quests = load()
if (quests === null || Array.isArray(quests) === false) {
quests = []
}
The || from Step 5 means "or": either problem on its own is enough to start fresh. Two
separate if statements, one for each problem, would do exactly the same job.
QuestLog, all of it
Everything in the previous twelve steps was a piece of this. A list holding objects. A drawing function. Events for the buttons. A save after each change. Nothing new is needed, and the whole app is smaller than you would expect.
Two of the names in the lab are worth separating before you start. render() is yours: you
write it, and it is what Step 11 called redrawing. draw(quests, points) is the page's. It
is the thing that actually paints the screen, so it wants the list and the total. Everything the lab hands
you is listed above the editor, with the shape of each one.
Reading a whole program instead of a line
Find the data first. One variable holds everything the app knows, and everything else exists to change it or show it. Then find the drawing function, which is the only thing that touches the screen. Then the handlers, one per thing a person can do.
Read in that order and any app makes sense, including ones far larger than this. Read top to bottom instead and it looks like a wall.
done to true, saves,
and redraws. Someone suggests removing the redraw to make it faster. What breaks?Move from the course language to the browser
The small language in this course kept the important ideas visible. A browser uses JavaScript, and the
same pieces have different names. say(text) can become
output.textContent = text. onClick(fn) can become
button.addEventListener("click", fn). The browser's document object model, or DOM, is
the set of objects that represents the page.
JavaScript normally runs one piece of your code at a time on the main thread. Clicks, timers and completed network requests wait in queues. The event loop takes a ready task when the current call stack is empty. The browser can then update layout and paint the changed screen.
What happens to the page during a long loop?
A long calculation keeps the main thread busy. Click handlers cannot run and the browser cannot paint intermediate DOM changes, so the page appears frozen even though the loop is still executing.
Break large work into chunks, move suitable computation to a Web Worker, or use a server. Do not add a timer merely to hide a calculation that still blocks for the same total time.
Is the DOM the same thing as the HTML file?
The HTML file is input. The browser parses it into DOM objects, and JavaScript can add, remove or change those objects. Saving the original file does not automatically save later DOM changes.
Browser developer tools show the current DOM, the console, network requests and storage. They are the first place to inspect when the screen differs from the data you expected.
Ask another program for data
An application programming interface, or API, is an agreed set of requests and responses. A browser
might send GET /api/quests to read quests or POST /api/quests with data for a new
one. The path, method, fields and possible errors form a contract between client and server.
A network answer arrives later. JavaScript represents that future result with a Promise. The words
async and await let a function pause without blocking the event loop. The screen
still needs explicit loading, success, empty and error states while the request changes.
async function loadQuests() {
show("Loading…")
const response = await fetch("/api/quests")
if (response.ok === false) throw new Error("Request failed")
const quests = await response.json()
draw(quests)
}
Does a completed fetch mean the request succeeded?
It means an HTTP response arrived. A server can respond with 404 for a missing resource, 401 when no valid identity was supplied, or 500 when it failed internally. Check the status before trusting the response body.
A network failure, timeout and server rejection need different messages. Preserve enough information for a retry, but do not keep an endless spinner on screen.
When is retrying safe?
Reading can usually be retried. Repeating a write may create two orders, payments or quests unless the operation is idempotent. An idempotency key lets the server recognise a repeated submission and return the first result.
Wait longer between repeated attempts and add a little random variation when many clients may retry at once. The reliability course develops backoff, timeouts and retry budgets in detail.
Put shared rules on a server
Browser storage belongs to one browser profile. A server gives several devices and people a shared place to apply rules and store data. The server must treat every request as untrusted, even when your own client sent it, because anyone can construct a request without using your screen.
A database keeps records after the server process restarts. A schema describes fields, types and relationships. A transaction groups changes that must succeed or fail together. If two requests update the same quest, the server also needs a rule for concurrency, such as a version number checked on every write.
Why not put the database password in browser JavaScript?
Every browser receives that file and can read it. A secret sent to a client is no longer secret. Keep database credentials and private service keys on a server, then expose only the operations clients are permitted to request.
Public identifiers, such as an API base URL, are not secrets. Classify each configuration value instead of hiding every setting or publishing every credential.
What is a database migration?
Applications change their stored shape over time. A migration is a numbered, reviewed change that adds or transforms schema in a repeatable way. Production data may be older than the current program.
Plan how old and new application versions overlap during deployment. The Inside a Database course follows indexes, transactions, logs and recovery below the API layer.
Separate identity from permission
Authentication answers “who is making this request?” Authorization answers “may that identity do this operation on this resource?” Logging in does not grant access to every account. The server checks authorization for every protected read and write.
After login, a server may place an unpredictable session identifier in a secure cookie. The server maps it to an account and expiry time. Tokens are another way to carry signed claims, but a server receiving one must verify its signature, issuer, audience and time limits before using those claims.
How should passwords be stored?
Do not store the password or a fast ordinary hash. Use a password-hashing function designed to be slow and memory costly, with a fresh salt for each account. At login, hash the candidate using the stored parameters and compare the result.
Multi-factor authentication reduces the damage from a stolen password. Account recovery deserves the same review as login because an easy recovery path can bypass stronger sign-in checks.
Is hiding a button an authorization check?
No. Hiding controls can make the interface clearer, but the request can still be sent directly. The server must decide using the authenticated identity, requested action and resource ownership or role.
Deny by default. Grant the smallest permission needed, record sensitive changes, and make expired or revoked sessions stop working.
Treat input as data, not instructions
A quest title may contain punctuation, emoji or text that resembles HTML. If the app assigns it with
textContent, the browser displays those characters. If it builds an HTML string and assigns
innerHTML, tags can become page structure and scripts may run. That is cross-site scripting,
usually shortened to XSS.
Use the output operation that matches the context: text nodes for text, parameterized queries for database values, and carefully checked URLs for navigation. Input validation enforces the application's data rules; output encoding prevents data from becoming syntax. One does not replace the other.
What do HTTPS and browser security each protect?
HTTPS encrypts traffic in transit and authenticates the server name. It does not repair an XSS bug, stop an authorized server from collecting too much data, or make a stolen session harmless.
Cookies can be restricted with Secure, HttpOnly and SameSite attributes. A content security policy can limit which scripts and other resources a page may load. These are layers, not substitutes for safe code.
What should the app collect and log?
Collect the least personal data needed for the feature and keep it only as long as required. Access controls, deletion rules and backups must agree. Do not put passwords, session tokens or private quest text into ordinary logs.
Threat modelling asks what is valuable, who might misuse it, where trust changes, and how the design prevents or detects that misuse. Revisit the model when a new data flow is added.
What happens when someone opens it
You have an app. Someone types its address, or taps a link, and a second later it is running on their machine, which may be on the other side of the world. That last part is worth understanding, because it is simpler than most people assume.
What a web address actually asks for
An address has a machine name and a path to a file on it. The browser finds the machine, asks it for that file, and the machine sends the file back. That is the whole conversation. It is the same conversation for a photograph or a font.
Then the browser reads what came back and finds it mentions other files: a stylesheet, some JavaScript. So it asks for those too, one request each. Your app arrives as a handful of separate files and is assembled where it will be used, not where it was written.
Three words the lab below uses. The machine that keeps the files and hands them out when asked is a
server, which is all the word means: it serves files. Each of the three layers from Step 1 lives in
its own file, and the ending says which: .html for structure, .css for style,
.js for behaviour. A .css file is usually called a stylesheet, which is
the same thing under a longer name.
Using a coding model without handing it the decision
A coding model can draft a function, explain an error or suggest a test. Treat the result as a patch from someone who has not seen the whole program: state the requested behavior, inspect the changed lines, run executable tests, and reject unrelated changes.
The model should receive only the files and permissions needed for the task. Generated code is not evidence that the program works; a passing check against the specification is. The Software Construction review lesson turns those rules into an interactive patch gate.
What you can do now
- Store data in variables, lists and objects, and choose sensibly between them.
- Make decisions and repeat work, and recognise the off-by-one at the boundary.
- Write a function that takes something in and hands something back.
- Keep one copy of the truth, draw the screen from it, and redraw after every change.
- React to what someone does, and keep their data after they leave.
- Read an error message and work out where to look.
Where this goes
- Data Structures. You used a list and reached in by position. There are other shapes, and choosing the wrong one is how a fast app becomes a slow one. Starts with why removing from the middle of a list costs what it does.
- Software Construction. Your code works. This is about code that still works after someone else changes it: specifications, tests, and hiding a representation so it cannot be misused.
- Build a Microprocessor. Every line you wrote became instructions for a processor. That course builds one from a light switch upwards, and the two ends of the same story meet.
Make the app work for different people and screens
Accessibility starts with ordinary HTML that carries meaning. Use a button for an action, a label for an
input and headings in a sensible order. Those elements already support keyboards and expose useful roles
and names to assistive technology. A clickable div has none of that behaviour unless you rebuild it.
A visible focus indicator shows where keyboard input will go. Instructions and errors must not depend only on colour, position or sound. When content changes without moving focus, a suitable live region can announce the update. Test with a keyboard and a screen reader instead of guessing from the markup.
What makes a layout responsive?
Let text wrap, let flexible columns stack, and keep controls large enough to operate. Test narrow and wide viewports, enlarged text, long translations, zoom and both pointer and keyboard input.
A media query changes rules when space or user preferences change. Respect reduced-motion and colour scheme preferences, but do not remove information when an animation is disabled.
Where do language and culture affect the program?
Keep interface text outside business logic and mark the document language. Use locale-aware formatting for dates, numbers and plural forms. Do not join translated sentence fragments in a fixed English order.
Right-to-left scripts can change layout direction. Names, addresses and calendars also vary. Store the underlying value separately from its formatted display.
Test behavior and keep changes reversible
A useful test supplies an input, observes a result and explains the rule that would be broken by a failure. Unit tests isolate a function. Integration tests join parts such as an API route and database. End-to-end tests operate the application through a browser. Use the smallest level that can catch the failure honestly.
Debugging begins with a reproducible case. Read the first relevant error, inspect the actual state, reduce the input and change one assumption at a time. Logs and breakpoints provide evidence; random edits make the experiment harder to interpret.
What does version control add?
Git records snapshots and the parent relationship between them. A focused commit states one reason for change, includes its tests and can be reviewed or reverted without discarding unrelated work.
Branches let changes develop separately. A merge combines histories and may need a person to resolve places where both sides edited the same lines. Commit before a risky experiment.
What should be tested after a coding model writes a patch?
Start from the requested behavior and likely failure modes, not from the patch's own explanation. Inspect every changed line, run the existing suite, add a regression test and check permissions, data handling, dependencies and unrelated edits.
A model can propose tests, but a test that repeats the same mistaken assumption proves little. Include boundary cases and, when possible, compare against an independent reference or property.
Cache files and keep useful work offline
A cache keeps a reusable response near the browser. HTTP cache rules can reuse versioned scripts and images without asking the origin for every visit. Files that change at the same URL need validation or short lifetimes; files named with a content hash can be cached for much longer.
A service worker can intercept requests and choose cached or network responses. That makes an offline shell possible, but it also introduces another stored version of the app. Update logic must avoid serving old code beside a new data format.
What happens to edits made while offline?
Store each pending operation locally with a unique ID, creation time and sync state. When connectivity returns, submit it idempotently. Mark it complete only after the server confirms the result.
If the remote record changed too, use an explicit conflict rule. Some fields can merge; others need the person to choose. “Last writer wins” is simple but can silently discard work.
How do caches become incorrect?
A cache key that omits the signed-in user or language can return one person's data to another. Sensitive responses often need private or no-store rules. Shared caches must vary on every request property that changes the response.
Purging every cache after every change is safe but loses the performance benefit. Version immutable files and give changing documents deliberate validation rules.
app.4f2a.js be cached longer than app.js?Deploy carefully and diagnose failures
A deployment should build the same reviewed revision that passed automated checks. Configuration selects environment-specific addresses and secrets; it should not require editing source code. Database changes need an order that lets old and new application versions overlap during rollout.
Observability supplies evidence after release. Logs record discrete events, metrics summarize values over time, and traces connect work across services for one request. Include request IDs and useful context, but exclude secrets and unnecessary personal data.
What should happen during a bad release?
Stop the rollout, limit further damage and restore a known working version when that is safe. A rollback may not reverse data changes, so migrations need forward and recovery plans.
After service returns, write a timeline based on evidence, identify contributing system conditions and add changes that improve prevention, detection or recovery. Do not stop at the last person who touched it.
How do rate limits and circuit breakers help?
A rate limit protects finite capacity and can slow abuse. It should define a scope, time window and useful response. A circuit breaker temporarily stops calls to a repeatedly failing dependency so work does not pile up behind it.
Retries consume capacity too. Limit attempts, use deadlines and make writes idempotent. Continue with Reliable Systems and Fault Tolerance for deeper failure models.
Plan the next application from evidence
Start with one person, one problem and one observable outcome. Write a few acceptance examples before choosing tools. Draw the data and trust boundaries, then build a thin route through interface, server and storage. Put it in front of a real user before widening the feature list.
For each release, keep a checklist that covers behavior, accessibility, security, privacy, performance, failure recovery and deployment. Record what was measured and what remains an assumption. A small app with clear limits is a better engineering result than a large one nobody can verify.
Where can AI help in a 2026 development workflow?
Use a coding model for bounded work: explain unfamiliar code, draft tests, propose a small patch, compare alternatives or search a codebase. Give it the relevant contract and minimum access needed. Keep generated changes in a reviewable diff.
Run the program and independent checks. Review security boundaries, dependencies, data migration and accessibility yourself. For uncertain behavior, make the model produce a test or source you can verify instead of accepting a confident explanation.
Which course should come next?
Software Construction develops specifications, invariants, test design, abstraction and review. Build a Language explains how source becomes tokens, syntax and machine instructions.
Use Inside a Database for storage, Performance Engineering for measurement, and Memory Exploits for low-level security boundaries.