Applications of Web Programming
🎯 (SE-12-03)
📌 Explore the applications of web programming
- interactive website/webpages
- e-commerce
- progressive web apps (PWAs)
Interactive Websites and Webpages
Interactive websites use client-side JavaScript to respond to user actions in real time — without requiring a full page reload. The Document Object Model (DOM) is the programming interface that allows JavaScript to read and manipulate the structure, style, and content of a live HTML page. Examples include form validation, dropdown navigation, image carousels, and live search filtering. Modern Single Page Applications (SPAs) such as those built with React or Vue extend this further, dynamically rendering entire pages client-side.
A worked example shows what "without a full page reload" means. This JavaScript listens for typing in a search box and hides the list items that do not match, so the page filters instantly and nothing is sent to the server.
const box = document.querySelector('#search');
const items = document.querySelectorAll('#game-list li');
box.addEventListener('input', () => { // fires on every keystroke
const term = box.value.toLowerCase();
items.forEach(li => {
li.hidden = !li.textContent.toLowerCase().includes(term);
});
});
If the list holds Reef Runner, Kelp Quest and Star Sailor, typing qu leaves only Kelp Quest visible, and clearing the box brings all three back. The input event is the trigger, the DOM is the thing changed, and the rest of the page is untouched.
E-Commerce
E-commerce platforms use web programming to support the entire buying lifecycle: product browsing, shopping cart management, payment processing, and order tracking. Key technical requirements include secure HTTPS connections (to protect payment data), session management (to maintain cart state across pages), database integration (to query product catalogues and stock levels), and integration with payment gateway APIs (such as Stripe or PayPal). Examples include Amazon, eBay, and Shopify-powered stores.
A checkout is a good example of front-end and back-end work being shared. The browser makes the page easy to use, but every step that involves money or stock is finished on the server.
| Stage | Where it happens | What is done |
|---|---|---|
| Browse and search | Browser and server | The page requests product data; the server queries the product tables and returns it. |
| Add to cart | Server (session) | The cart is tied to a session so it survives moving between pages. |
| Enter card details | Payment gateway | The card number goes to the gateway over HTTPS, so the shop never has to store it. |
| Confirm the order | Server and database | The server recalculates the total from its own prices, checks stock and writes the order. |
| Receipt and tracking | Server and email | The server sends a confirmation and later status updates. |
Progressive Web Apps (PWAs)
PWAs are web applications built with standard web technologies (HTML, CSS, JavaScript) that offer a native-app-like experience. They are installable on the user's home screen, can work offline via service workers, and receive push notifications. Starbucks and Pinterest are companies that have released PWAs. PWAs are significant because they eliminate the need for a separate iOS/Android codebase while still appearing as a native application to the end-user.
| Application | Main technologies | Strength | Limitation |
|---|---|---|---|
| Interactive website | HTML, CSS, JavaScript, a server | Works in any browser with nothing to install | Needs a connection for most features |
| E-commerce site | Interactive front end, sessions, database, payment API, HTTPS | Handles the whole buying process securely | Needs careful security and privacy handling |
| Progressive web app | Manifest, service worker, HTTPS | Installable, works offline, one code base | Fewer device features than a native app |
How Data Is Transferred on the Internet
🎯 (SE-12-03)
📌 Investigate and practise how data is transferred on the internet
- data packets
- internet protocol (IP) addresses, including IPv4
- domain name systems (DNS)
Data Packets
When data is sent over the internet, it is broken into small, manageable units called data packets. Each packet contains a header (addressing and control information) and a payload (the actual data fragment). Packets from the same transmission may take different routes through the network and are reassembled in the correct order at the destination. This packet-switching model means the failure of one route does not disrupt the overall transmission — other paths are used instead.
Animated diagram
Data packets: split up, sent separately, put back together
One message becomes four packets that cross the internet by different routes and are reassembled at the server.
What to notice
- Routers only read the header: the destination IP address tells them where to send each packet next.
- Packets can arrive out of order; the sequence numbers let TCP rebuild the message in the right order.
- A gap in the sequence numbers is how the receiver knows a packet was lost, so it can ask for it again.
Choose a journey with the tabs. Step with ◀ and ▶ (or the arrow keys), play or pause with Space.
Try thisWhy is it useful for packets from one message to take different routes? Give one advantage and one problem it creates.
Two layers of headers travel with each packet. The IP header carries the source and destination IP addresses, a hop limit (so a packet lost in a loop is eventually discarded) and the protocol that comes next. The TCP header carries the source and destination port numbers, the sequence number and a checksum for error detection. The payload is one slice of the message.
Worked example: one reply, three packets
GameShelf's server sends a 2,400-byte page to a browser. Suppose the network allows 1,000 bytes of payload per packet (real limits are about 1,500 bytes for the whole packet, but round numbers keep the arithmetic clear). The page needs 2,400 ÷ 1,000 = 2.4, rounded up to 3 packets.
| Packet | Sequence (byte offset) | Payload | What happens |
|---|---|---|---|
| 1 | 0 | bytes 0 to 999 | Arrives first via router A |
| 2 | 1000 | bytes 1000 to 1999 | Lost on the way (a router is overloaded) |
| 3 | 2000 | bytes 2000 to 2399 | Arrives second via router B, so it is out of order |
- The browser acknowledges packet 1 ("I have everything up to byte 999").
- Packet 3 arrives but the next expected sequence number is 1000. The gap tells TCP that packet 2 is missing, so it keeps packet 3 and does not acknowledge past byte 999.
- The server gets no acknowledgement for packet 2 in time and retransmits it.
- Packet 2 arrives. TCP now has bytes 0 to 2399 in order and hands the complete page to the browser.
This is why the web feels reliable even though the network underneath does not promise delivery. Before any of this, TCP opens the connection with a three-way handshake: the client sends SYN, the server replies SYN-ACK, and the client answers ACK.
IP Addresses and IPv4
Every device on a network is assigned an Internet Protocol (IP) address — a unique numerical label used to identify it for communication purposes. IPv4 (Internet Protocol version 4) addresses are 32-bit numbers expressed in dotted-decimal notation, divided into four octets (e.g., 192.168.1.1). IPv4 supports approximately 4.3 billion unique addresses. Due to address exhaustion, Network Address Translation (NAT) is used so that multiple devices on a private network can share a single public IP address. IPv6 was introduced to address this limitation using 128-bit addresses.
Worked example: reading an IPv4 address
An IPv4 address is 32 bits, written as four octets of 8 bits, each shown as a decimal number from 0 to 255. Converting 192.168.1.1 one octet at a time:
| Octet | Decimal | Binary | Working |
|---|---|---|---|
| 1st | 192 | 11000000 | 128 + 64 |
| 2nd | 168 | 10101000 | 128 + 32 + 8 |
| 3rd | 1 | 00000001 | 1 |
| 4th | 1 | 00000001 | 1 |
With 32 bits there are 232 = 4,294,967,296 possible addresses. IPv6 uses 128 bits, written in hexadecimal groups (for example 2001:db8::1, a reserved documentation address), which gives 2128 addresses.
| Kind | Range | Meaning |
|---|---|---|
| Private | 10.0.0.0 to 10.255.255.255; 172.16.0.0 to 172.31.255.255; 192.168.0.0 to 192.168.255.255 | Used inside a home, school or business network; not routed on the public internet. NAT lets these devices share one public address. |
| Loopback | 127.0.0.1 | "This computer". A web server run on your own machine is reached here. |
| Public | Everything else (for example 8.8.8.8) | Unique across the internet; assigned through your ISP. |
import ipaddress
for text in ["192.168.1.1", "172.20.4.9", "8.8.8.8", "127.0.0.1"]:
ip = ipaddress.ip_address(text)
print(text, "private:", ip.is_private, "loopback:", ip.is_loopback)
192.168.1.1 private: True loopback: False
172.20.4.9 private: True loopback: False
8.8.8.8 private: False loopback: False
127.0.0.1 private: True loopback: True
The second address is private because 172.16 to 172.31 is a private block, not just 172.16. This is a common trap in questions that ask which address could belong to a school network.
Try it
IPv4 address explorer
Read an IPv4 address as four octets and as 32 bits, and see its network and host parts. Enable JavaScript to use this tool. In short: an IPv4 address is four numbers from 0 to 255, which are 32 bits, and some of the first bits name the network.
Domain Name System (DNS)
The Domain Name System (DNS) translates human-readable domain names (e.g., www.google.com) into machine-readable IP addresses (e.g., 142.250.70.142). When a user types a URL, the browser queries a DNS resolver (usually provided by their ISP), which traverses a hierarchy of DNS servers (Root → TLD → Authoritative) to resolve the domain. The resolved IP address is then cached locally (for a duration defined by TTL — Time To Live) to speed up future requests.
Worked example: resolving a name
A student opens www.gameshelf.example (a made-up name, so it will not resolve on the real internet) for the first time. The steps are the same for any real site:
- The browser checks its own DNS cache, then the operating system's cache. Both miss.
- The OS asks the configured resolver (usually run by the ISP or the school network). It has no cached answer either.
- The resolver asks a root server, which does not know the address but says which servers look after the
.exampletop-level domain. - The TLD server does not know the address either but names the authoritative name server for
gameshelf.example. - The authoritative server answers with the record, for instance an A record giving an IPv4 address, and a TTL (time to live) such as 3600 seconds.
- Every cache on the way back keeps the answer for the TTL, so the next lookup within the hour is answered instantly and no root, TLD or authoritative server is asked.
| Record | Holds | Used for |
|---|---|---|
| A | An IPv4 address | Finding the web server for a name |
| AAAA | An IPv6 address | The same, for IPv6 |
| CNAME | Another name (an alias) | Pointing www at the main name, or at a CDN |
| MX | The mail server for the domain | Telling SMTP where to deliver email |
The TTL explains the "Try this" above: a changed address keeps being served from caches until each TTL runs out, so the change spreads over minutes to hours.
Hands-on: watch it happen
- Open a terminal and run
nslookup www.education.nsw.gov.au(on macOS or Linux,dig www.education.nsw.gov.auis an alternative). You should see the resolver that answered and one or more addresses. The exact numbers can change between runs and between networks; that is normal. - Run
tracert www.education.nsw.gov.auon Windows, ortraceroute www.education.nsw.gov.auon macOS and Linux. Each numbered line is one router (a hop) that the packets crossed, with its response time. Some lines may show*because a router does not answer. - Run
ping 127.0.0.1. Every reply comes from your own computer, which is the loopback address described above.
Sequence diagram
How DNS finds an IP address
The browser asks a resolver, which asks the root, TLD and authoritative servers in turn.
What to notice
- A cached answer skips every step after the first.
- Each server only knows where to ask next, until the authoritative server gives the address.
Try thisWhy does a changed website address sometimes take hours to work everywhere?
Sequence diagram
An HTTP request and response
A GET request fetches a page; a POST request sends form data and is redirected.
What to notice
- The server talks to the database; the browser never does.
- After a POST, a redirect stops the form being resubmitted on refresh.
Try thisOpen your browser's Network tab and find a GET and a POST request on a real site.
Two short videos that show how data physically travels as packets and how the browser and server talk using HTTP — ideal background before the protocols section.
Putting it together: one page request
| Step | What happens | Protocol or address involved |
|---|---|---|
| 1 | The browser turns the name in the URL into an address | DNS, port 53 |
| 2 | It opens a reliable connection to the server | TCP three-way handshake to the server's IP address |
| 3 | For https:// addresses, it agrees encryption | TLS handshake, port 443 |
| 4 | It sends the request | HTTP GET, split into packets |
| 5 | The server sends the page back | HTTP response, packets that may take different routes |
| 6 | TCP reorders the packets and the browser renders the page | TCP sequence numbers |
Web Protocols and Their Ports
🎯 (SE-12-03)
📌 Investigate and describe the function of web protocols and their ports
- HTTP, HTTPS
- TCP/IP
- DNS
- FTP, SFTP
- SSL, TLS
- SMTP, POP 3, IMAP
The protocols and their ports
| Protocol | Port(s) | Function |
|---|---|---|
| HTTP | 80 | Hypertext Transfer Protocol — transfers plain-text web content between client and server. Unencrypted; vulnerable to interception. |
| HTTPS | 443 | HTTP Secure — HTTP encrypted with TLS/SSL. Required for any site handling sensitive data (login, payments). Provides confidentiality and integrity. |
| TCP/IP | Various | Transmission Control Protocol / Internet Protocol — the foundational communication suite of the internet. TCP ensures reliable, ordered, error-checked delivery of packets; IP handles addressing and routing. |
| DNS | 53 | Domain Name System — resolves human-readable domain names to IP addresses. Uses UDP for most queries and TCP for very large responses and zone transfers. |
| FTP | 21 (control), 20 (data) | File Transfer Protocol — transfers files between a client and server. Unencrypted; largely replaced by SFTP in modern deployments. |
| SFTP | 22 | SSH File Transfer Protocol — secure, encrypted file transfer using SSH. Standard for uploading files to web servers safely. |
| SSL / TLS | Inside other protocols (443 for HTTPS) | Secure Sockets Layer / Transport Layer Security — cryptographic protocols that provide secure communication over a network. TLS is the modern successor to the deprecated SSL. |
| SMTP | 25 (server to server), 587 (submission) | Simple Mail Transfer Protocol — sends outgoing email from a client to a mail server, or between mail servers. Port 587 is used for authenticated submission. |
| POP3 | 110 (995 with TLS) | Post Office Protocol 3 — downloads email from a server to a local client, then typically deletes it from the server. Best for single-device access. |
| IMAP | 143 (993 with TLS) | Internet Message Access Protocol — synchronises email between server and multiple clients without deleting from the server. Best for multi-device access (e.g., phone and laptop). |
Ports: which program should receive the data?
An IP address finds the computer; a port is a 16-bit number (0 to 65,535) that finds the program on that computer. A web server, a mail server and an SSH server can all run on one machine because each listens on its own port. Ports 0 to 1023 are the "well-known" ports reserved for standard services such as those in the table, 1024 to 49151 are registered for particular applications, and the rest are used for short-lived connections that a client opens. When you visit https://www.gameshelf.example/games?max_cost=30 the browser fills in port 443 itself, because it is the default for https. Writing http://localhost:8000 names a different port, which is how a program under development is usually reached.
https://www.gameshelf.example:443/games?max_cost=30#top
\___/ \___________________/ \_/ \____/ \_________/ \_/
scheme host port path query fragment
(protocol) (found by DNS) (default (data sent to the server)
for https)
Where each protocol sits
Protocols work in layers, each relying on the one below. This is why a question can ask "which protocol finds the address, which delivers it reliably and which carries the page?" and expect three different answers.
| Layer | Job | Protocols in this dot point |
|---|---|---|
| Application | Defines the conversation for one kind of service | HTTP, HTTPS, FTP, SFTP, SMTP, POP3, IMAP, DNS |
| Security | Encrypts and authenticates the connection | TLS (and its predecessor SSL); SFTP uses SSH for the same purpose |
| Transport | Delivers data to the right program, in order and complete (or not) | TCP (reliable, ordered); UDP (fast, no guarantees) is used by most DNS queries |
| Internet | Addresses and routes packets between networks | IP (IPv4 and IPv6) |
TCP suits web pages, files and email, where a missing byte would corrupt the result. UDP suits things such as a DNS lookup or a live video call, where a fast answer matters more than resending every lost packet. HTTP/3, the newest version of HTTP, runs over a UDP-based transport called QUIC, but the syllabus expects the classic picture of HTTP over TCP.
HTTP in detail: one request, one response
HTTP is a text-based request and response protocol. The browser sends a request line, headers and, for some methods, a body; the server answers with a status line, headers and a body. This is the exchange behind GET /games?max_cost=30 on the GameShelf site (the Server and Date headers depend on the software and the time):
GET /games?max_cost=30 HTTP/1.1
Host: www.gameshelf.example
Accept: application/json
Cookie: session=7f3a9c
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 103
[{"Cost":20.0,"Name":"Kelp Quest"},{"Cost":25.0,"Name":"Reef Runner"},{"Cost":30.0,"Name":"Lava Lab"}]
The Content-Length of 103 is the number of bytes in the body. The Host header matters because one server can host many sites. HTTP is stateless: the server treats every request separately, which is why cookies and sessions (covered in the next section) are needed to recognise a returning user.
| Method | Meaning | Example |
|---|---|---|
| GET | Read a resource; should not change anything | GET /games/4 |
| POST | Send data to be processed, often creating something | Submitting a sign-up form |
| PUT | Replace a resource | Saving an edited profile |
| DELETE | Remove a resource | DELETE /games/4 |
| Status code | Meaning | Typical cause |
|---|---|---|
| 200 OK | The request worked | A page or JSON returned normally |
| 301 / 302 | Moved permanently / found elsewhere (redirect) | An old URL, or a redirect after a form POST |
| 400 Bad Request | The request is malformed or invalid | ?max_cost=abc where a number was needed |
| 401 Unauthorised | The user is not authenticated | No valid login or token |
| 403 Forbidden | Authenticated, but not permitted | A student requesting an admin page |
| 404 Not Found | No such resource | A mistyped path such as /games/99 |
| 500 Internal Server Error | The server's code failed | An unhandled exception in the back end |
The first digit is the family: 2xx success, 3xx redirection, 4xx the client's mistake, 5xx the server's mistake. That pattern lets you reason about an unfamiliar code.
Email: sending and collecting are different jobs
SMTP only sends and relays mail: your phone hands the message to your provider's server over SMTP, and that server hands it on to the recipient's server over SMTP. To collect the mail, the recipient's device uses IMAP, which keeps the mail on the server and synchronises every device, or POP3, which downloads it (usually removing it from the server). A person who reads email on a phone and a laptop needs IMAP, because with POP3 each message would end up on only one of them.
Insecure and secure versions
Several of the protocols above have a version that adds encryption. The insecure ones send passwords and content as readable text, so anyone on the path (a shared Wi-Fi network, for example) can read them.
| Purpose | Insecure | Secure | What changes |
|---|---|---|---|
| Web pages | HTTP, 80 | HTTPS, 443 | HTTP is wrapped in TLS |
| File transfer | FTP, 21 | SFTP, 22 | SFTP is a different protocol built on SSH, not FTP with TLS added (that variant is called FTPS) |
| Reading email | POP3 110, IMAP 143 | POP3 over TLS 995, IMAP over TLS 993 | The same protocols inside a TLS connection |
| Sending email | SMTP 25 | SMTP submission 587 (with STARTTLS), or 465 | The connection is upgraded to TLS and the user logs in |
SSL (Secure Sockets Layer) is the older protocol, and all its versions are now considered insecure and are disabled by modern browsers. TLS (Transport Layer Security) replaced it, and TLS 1.3 is the current version. People still say "SSL certificate" out of habit, which is why the syllabus uses both names.
Which protocol?
| Scenario | Protocol and port | Reason |
|---|---|---|
| A student uploads a website to the school's server and the password must not be readable on the way | SFTP, 22 | Encrypted file transfer |
| A teacher wants the same inbox on a phone and a laptop | IMAP, 143 (993 with TLS) | Mail stays on the server and devices synchronise |
| The browser needs the IP address for a domain name | DNS, 53 | Name to address |
| A shop's checkout page must protect card details | HTTPS, 443 | HTTP inside TLS |
| A mail server passes a message to another mail server | SMTP, 25 | SMTP relays between servers |
Try it
Which protocol?
Practise pairing a job with its protocol and port. Enable JavaScript to use this tool. In short: HTTP 80, HTTPS 443, FTP 21, SFTP 22, SMTP 25, POP3 110, IMAP 143 and DNS 53.
Securing the Web
🎯 (SE-12-04, SE-12-07)
📌 Explain the processes for securing the web
- Secure Sockets Layer (SSL) certificates
- encryption algorithms
- encryption keys
- plain text and cipher text
- authentication and authorisation
- hash values
- digital signatures
SSL/TLS Certificates
An SSL/TLS certificate is a digital document issued by a trusted Certificate Authority (CA) (e.g., Let's Encrypt, DigiCert) that verifies a website's identity and enables encrypted HTTPS connections. When a browser connects to a site, it checks that the certificate is: (1) issued by a trusted CA, (2) not expired, and (3) issued for the correct domain. Without a valid certificate, browsers display a "Not Secure" warning, deterring users and search engines alike.
A certificate is a small signed file with a fixed set of fields. Browsers trust it because of a chain of trust: the server's certificate is signed by an intermediate authority, whose certificate is signed by a root authority, and the root certificates are already stored in the browser or operating system. The browser never has to know the website in advance; it only has to trust the root.
| Certificate field | Example | What the browser does with it |
|---|---|---|
| Subject (the domain) | www.gameshelf.example | Compares it with the address in the URL bar; a mismatch means a warning |
| Issuer | An intermediate CA | Follows the chain up to a root it already trusts |
| Validity dates | Not before / not after | Rejects an expired (or not yet valid) certificate |
| Public key | The server's public key | Uses it in the handshake to check the server holds the matching private key |
| Signature | The CA's digital signature over all of the above | Verifies it with the CA's public key, so any tampering is detected |
A certificate proves that whoever controls the domain asked a CA for it. It does not prove that the site is honest: a scam site can have a valid certificate for its own domain. The padlock means "your connection to this domain is encrypted", not "this site is safe".
Encryption Algorithms
Encryption algorithms transform readable plaintext into unreadable ciphertext, protecting data from interception. Symmetric encryption (e.g., AES-256) uses a single shared key for both encryption and decryption — fast and efficient, but the key must be securely exchanged beforehand. Asymmetric encryption (e.g., RSA-2048) uses a mathematically linked key pair: a public key to encrypt and a private key to decrypt. TLS uses asymmetric encryption to securely exchange a session key, then switches to faster symmetric encryption for the remainder of the connection.
Encryption Keys
An encryption key is a string of bits used by an algorithm to encrypt or decrypt data. Public keys are freely shared and used to encrypt data sent to the key's owner. Private keys are kept secret by the owner and used to decrypt data or create digital signatures. Key length determines security strength: a 256-bit AES key has 2256 possible combinations, making brute-force attacks computationally infeasible.
| Symmetric (e.g. AES) | Asymmetric (e.g. RSA) | |
|---|---|---|
| Keys | One shared secret key | A public key and a private key |
| Speed | Fast; suits large amounts of data | Slow; suits small amounts of data |
| Main problem | How do two strangers share the key safely? | Public keys must be known to belong to the right owner (hence certificates) |
| Used in HTTPS for | Encrypting the page and form data | Proving identity and agreeing the session key |
Worked example: plain text to cipher text
Real algorithms such as AES are far stronger, but a Caesar cipher shows the vocabulary. The algorithm is "shift each letter along the alphabet"; the key is how far (here 3). Anyone who knows the algorithm still cannot decrypt without the key, and that is the point of having a key.
def caesar(text, key):
result = ""
for ch in text:
if ch.isalpha():
base = ord("A") if ch.isupper() else ord("a")
result += chr((ord(ch) - base + key) % 26 + base)
else:
result += ch # leave digits and symbols alone
return result
plain_text = "Pay $25 to Ava"
cipher_text = caesar(plain_text, 3) # encrypt with key 3
print(cipher_text)
print(caesar(cipher_text, -3)) # decrypt: shift back by the same key
Sdb $25 wr Dyd
Pay $25 to Ava
A Caesar cipher has only 25 useful keys, so a computer tries them all in an instant. AES-256 has 2256 keys, which is why key length matters, and why a good algorithm is one that stays secure even when everyone knows how it works.
Authentication and Authorisation
Authentication is the process of verifying who a user is (e.g., verifying a username and password match a database record). Authorisation is the process of verifying what an authenticated user is permitted to do (e.g., checking if a logged-in user has the "admin" role before allowing access to the dashboard). Modern web applications use JSON Web Tokens (JWT) or OAuth 2.0 to manage these processes securely across multiple services.
Worked example: two different questions
A school portal asks two separate questions on every protected request, and the HTTP status code tells you which one failed.
| Situation | Question that failed | Response |
|---|---|---|
| No login cookie or token, or a wrong password | Authentication: who are you? | 401 Unauthorised |
A logged-in student requests /admin/marks | Authorisation: are you allowed? | 403 Forbidden |
A logged-in teacher requests /admin/marks | Both pass | 200 OK |
ROLES = {"ava": "admin", "ben": "student"} # in real life: a database table
def can_delete_game(username):
return ROLES.get(username) == "admin" # only admins may delete
print(can_delete_game("ava")) # authenticated as ava, and ava is an admin
print(can_delete_game("ben")) # authenticated as ben, but a student
True
False
Multi-factor authentication (MFA) strengthens the first question by asking for two different kinds of proof: something you know (a password), something you have (a phone or security key) or something you are (a fingerprint). Two passwords are not two factors, because both are things you know.
Sequence diagram
Logging in with a JSON Web Token
The server checks the password once, then trusts the signed token on later requests.
What to notice
- The password is compared with a stored hash, never stored as plain text.
- Later requests are verified by the token's signature without a database lookup.
Try thisWhat should happen when the token expires?
Cookies and sessions
HTTP is stateless, so after you log in the server needs a way to recognise your next request. It does this with a session: the server creates a record (who you are, what is in your cart) and gives the browser a long random session ID in a cookie. The browser sends the cookie automatically with every later request to that site, and the server looks the session up.
HTTP/1.1 200 OK
Set-Cookie: session=7f3a9c41d2; Secure; HttpOnly; SameSite=Lax; Max-Age=1800; Path=/
| Attribute | Effect | Protects against |
|---|---|---|
Secure | Sent only over HTTPS | Interception on an unencrypted connection |
HttpOnly | JavaScript cannot read it | A cross-site scripting attack stealing the session ID |
SameSite | Limits sending the cookie on requests started by other sites | Cross-site request forgery |
Max-Age / Expires | Lifetime of the cookie (1800 seconds is 30 minutes) | A stolen or forgotten session staying valid for ever |
- The user submits the login form; the server checks the password hash.
- The server creates a session, stores it and sends
Set-Cookiewith a new random ID. - Each later request carries
Cookie: session=…; the server finds the session and knows who is asking. - On logout or timeout the server deletes the session, so the old cookie is worthless.
Good session management uses unpredictable IDs, issues a new ID at login (so an attacker cannot plant a known one), sets a timeout and deletes the session on logout. The alternative shown in the JWT diagram keeps the state in a signed token instead of on the server; each approach trades server storage against how easily a session can be cancelled.
Hash Values
A hash function is a one-way mathematical function that converts any input into a fixed-length output called a hash value (or digest). For example, the SHA-256 algorithm produces a 64-character hexadecimal hash. Hash functions are deterministic (same input always produces the same hash) but irreversible — it is computationally infeasible to reconstruct the original input from the hash. Web applications use hashing to store passwords securely: the hash of the password is stored, never the password itself. Algorithms like bcrypt add a random salt before hashing to prevent rainbow table attacks.
Digital Signatures
A digital signature is a cryptographic mechanism that proves the authenticity and integrity of a message or document. The sender hashes the content, then encrypts the hash with their private key to produce the signature. The recipient decrypts the signature using the sender's public key and compares it to their own hash of the received content. If they match, the message has not been tampered with and genuinely originated from the claimed sender. Digital signatures provide non-repudiation — the sender cannot later deny having sent the message.
Worked example: hashing a password
A hash has three properties you can see for yourself: the same input always gives the same output, the output has a fixed length, and a tiny change to the input gives a completely different output.
import hashlib
for password in ["password1", "Password1"]:
print(password, hashlib.sha256(password.encode()).hexdigest())
# A salted, deliberately slow hash (the fixed salts here only make the output repeatable;
# real systems generate a new random salt for every user)
print(hashlib.pbkdf2_hmac("sha256", b"password1", b"x9Zk", 100_000).hex()[:32])
print(hashlib.pbkdf2_hmac("sha256", b"password1", b"Q7pd", 100_000).hex()[:32])
password1 0b14d501a594442a01c6859541bcb3e8164d183d32937b851835442f69d5c94e
Password1 19513fdc9da4fb72a4a05eb66917548d3c90ff94d5419e1f2363eea89dfee1dd
46882ae63343ff32527bc80e66619bc7
31abdbf53828c47bc0f844de3a934420
- Changing one letter's case changes every character of the digest, and both digests are 64 hexadecimal characters long.
- The same password with two different salts gives two unrelated hashes, so two users with the same password have different stored values, and a table of precomputed hashes (a rainbow table) is useless.
- The 100,000 repetitions make each guess slow. That barely delays a real login but multiplies the cost of guessing billions of passwords. bcrypt, scrypt and Argon2 are designed for this, which is why plain fast SHA-256 alone is not the right choice for passwords.
- To check a login, the server hashes the submitted password with the stored salt and compares it with the stored hash. It never needs the original password and cannot recover it.
The steps of a digital signature reuse the same pieces, in a different order:
| Step | Signing (sender) | Verifying (receiver) |
|---|---|---|
| 1 | Hash the message to get a digest | Hash the received message to get its own digest |
| 2 | Encrypt the digest with the sender's private key: this is the signature | Decrypt the signature with the sender's public key to recover the sender's digest |
| 3 | Send the message and the signature | Compare the two digests: equal means unaltered and from the key's owner |
Animated diagram
Putting it together: how HTTPS secures a connection
This whole dot point in one animation: a certificate, hash values, a digital signature, public and private keys, and plain text becoming cipher text.
What to notice
- Asymmetric encryption is slow, so HTTPS uses it only to check who the site is and to deliver the session key. The data itself is encrypted with the fast symmetric session key.
- Checking the certificate is checking a digital signature: the CA signed a hash of the certificate with its private key, and the browser checks it with the CA's public key.
- Eve can copy everything that passes, but without the private key she only ever sees cipher text.
- TLS 1.3 agrees the session key with a key exchange (Diffie–Hellman) rather than sending it in a locked box, but the idea is the same: asymmetric cryptography sets up a shared symmetric key.
Compare connections with the tabs. Step with ◀ and ▶ (or the arrow keys), play or pause with Space.
Try thisSwitch to Imposter site. Which check catches the fake? What would Eve need to pass it? (Answer: the CA's private key, to make a valid signature.)
The TLS handshake step by step
The animation above shows the idea. The handshake that makes it happen has these steps, at the depth the syllabus needs:
- The browser opens a TCP connection to port 443, then sends a ClientHello listing the TLS versions and cipher suites (algorithm combinations) it supports.
- The server replies with the choice it made, and sends its certificate, together with proof that it holds the matching private key.
- The browser checks the certificate: signed by a trusted CA, still in date, and issued for exactly this domain. If any check fails, the user sees a warning.
- Both sides use a key exchange to work out the same session key. The key itself never crosses the network in a form an eavesdropper can use.
- Each side sends a Finished message, protected by the new key, to prove nothing was altered during the handshake.
- From here, the HTTP request and response are encrypted with fast symmetric encryption using the session key.
Sequence diagram
The TLS handshake
Certificate checks and a shared session key come first; only then does the HTTP request travel, as cipher text.
What to notice
- The certificate check happens before any private data is sent.
- Only after both Finished messages does data travel, and from then on an eavesdropper sees only cipher text.
- This is simplified: TLS 1.3 combines steps and encrypts the certificate as well, and TLS 1.2 uses a slightly different exchange, but the sequence of ideas is the same.
Try thisA hacker on the cafe Wi-Fi presents a certificate for a different domain. Which step fails, and what does the user see?
Cross-site scripting (XSS)
Encryption protects data on its way to the site; it does nothing if the site itself sends the attacker's code to other users. The Course Specifications describe cross-site scripting as injecting malicious code into an otherwise safe website, usually through user input that is not sufficiently sanitised before it is processed and stored on the server. Students are expected to interpret JavaScript fragments about it.
const comment = document.querySelector('#comment').value; // whatever the user typed
document.querySelector('#comments').innerHTML += '<p>' + comment + '</p>';
The fault is innerHTML combined with unchecked input: the browser treats the text as HTML, so any tags in it become real elements. If an attacker submits <img src="x" onerror="alert('XSS')"> the image fails to load, the onerror code runs, and the same thing happens for every visitor who is shown the comment. This is a stored attack when the server saves the comment and serves it to everyone. It is a reflected attack when the input comes from the URL and is echoed straight back on the page. In place of an alert, real attacks try to read the page, submit forms as the user or steal cookies.
const comment = document.querySelector('#comment').value;
const p = document.createElement('p');
p.textContent = comment; // shown as text, never run as HTML
document.querySelector('#comments').appendChild(p);
On the server, the same defence is output encoding: turning characters such as < into < before they go into a page. Template engines such as Jinja2 do it automatically for template variables, and Python's html.escape does it by hand:
import html
print(html.escape("<script>alert(1)</script>"))
<script>alert(1)</script>
The browser now displays the characters instead of running them. Defence in depth adds validation on input (reject or limit what is not expected), a Content Security Policy header that stops inline scripts running, and HttpOnly cookies so a script cannot read the session ID even if one gets through. Compare this with SQL injection, which attacks the database using unchecked input; the parameterised queries in the SQL section are its defence.
textContent or output encoding. Common mistakes are saying HTTPS stops all attacks, and mixing up the public and private keys (the public key encrypts a message to its owner or verifies a signature; the private key decrypts or signs).Big Data and Web Architecture
🎯 (SE-12-03, SE-12-05)
📌 Investigate the effect of big data on web architecture
- data mining
- metadata
- streaming service management
Big data is usually described by three properties, the "three Vs": volume (too much to store on one server), velocity (arriving continuously, such as clicks and video views) and variety (text, images, video and logs together). A web application that must store and process data like this cannot rely on a single server and one database, so its architecture changes.
| Concern | Small web app | Big-data web architecture |
|---|---|---|
| Servers | One web server | Many servers behind a load balancer, added as demand grows |
| Storage | One relational database | Data split across many machines, often with NoSQL stores and separate storage for media |
| Delivery | Everything from one place | Copies served from a CDN close to the user |
| Processing | Queries run as requests arrive | Logs and events collected continuously and analysed in batches or as streams |
| Failure | The site goes down | Other servers take over |
Data Mining
Data mining is the process of discovering patterns, correlations, and insights within large datasets using statistical and machine learning techniques. Web platforms mine user interaction data (clicks, search queries, time on page) to personalise recommendations, optimise advertising, and predict future behaviour. For example, Netflix mines viewing history to recommend shows; Google mines search patterns to rank results. Ethical concerns include user consent, data sovereignty, and the potential for discriminatory algorithmic outcomes.
Worked example: mining a pattern
GameShelf stores five customers' baskets. Data mining looks for items that appear together. Two measures are widely used: support (how many of all baskets contain both items) and confidence (of the baskets with the first item, how many also have the second).
baskets = [
{"Reef Runner", "Kelp Quest"},
{"Reef Runner", "Kelp Quest", "Lava Lab"},
{"Reef Runner", "Star Sailor"},
{"Kelp Quest", "Lava Lab"},
{"Reef Runner", "Kelp Quest"},
]
a, b = "Reef Runner", "Kelp Quest"
with_a = sum(a in basket for basket in baskets)
with_both = sum(a in basket and b in basket for basket in baskets)
print("baskets with", a + ":", with_a)
print("baskets with both:", with_both)
print("support:", with_both / len(baskets))
print("confidence:", with_both / with_a)
baskets with Reef Runner: 4
baskets with both: 3
support: 0.6
confidence: 0.75
Three of the four Reef Runner buyers also bought Kelp Quest (confidence 0.75), so the site could suggest Kelp Quest on the Reef Runner page. Five baskets are far too few to trust; the same calculation on millions of baskets is what makes the pattern reliable, and why mining depends on big data. It also shows the ethical limit: the recommendation is useful, but the same method applied to health or location data could reveal things people never chose to share.
Metadata
Metadata is "data about data" — descriptive information that provides context about other data without being the primary content itself. On the web, metadata includes HTTP headers (content type, cache directives), HTML <meta> tags (page description, author, viewport settings), and file metadata (creation date, author, geolocation embedded in photos). Metadata is critical for search engine indexing (SEO), content management, and security analysis. Importantly, metadata can reveal sensitive information even when primary content is private — for example, a photo's EXIF data can expose a user's exact GPS location.
Worked example: metadata you can inspect
Metadata is often invisible but easy to find. Every HTTP response carries some, every page can carry more in its <head>, and every photo carries its own.
| Where | Example | What it tells a system |
|---|---|---|
| HTTP header | Content-Type: image/webp, Cache-Control: max-age=3600 | How to treat the file and how long it can be cached |
HTML <meta> tag | <meta name="description" content="Browse Australian indie games"> | What a search engine can show under the page title |
| Photo file (EXIF) | Camera model, time taken, GPS latitude and longitude | Where and when the photo was taken, even though the image shows no map |
| Streaming service | What you watched, when, on what device, how long before you stopped | Your habits, without reading any private message |
HTTPS hides page content from people on the network, but the destination address, the size and timing of traffic are still visible. That is one reason metadata is sensitive: it can be collected in bulk and mined without anyone opening a single file. Under the Privacy Act 1988 and the Australian Privacy Principles, information about an identifiable person is personal information whether it is content or metadata, so a service must say what it collects and why.
Streaming Service Management
Streaming services (e.g., YouTube, Netflix, Spotify) manage enormous volumes of concurrent data delivery. Key architectural solutions include: Content Delivery Networks (CDNs) — geographically distributed server networks that cache and serve content from the nearest node to reduce latency; Adaptive Bitrate Streaming (ABR) — which dynamically adjusts video quality based on the user's current bandwidth; and load balancers that distribute incoming requests across multiple server instances to prevent any single server from being overwhelmed.
How a streaming service copes with millions of viewers
- Each video is encoded several times, at different quality levels, and cut into short segments of a few seconds. A small text file (a manifest) lists the segments for each quality.
- The segments are copied to CDN servers in many cities. When you press play, DNS or the CDN's routing sends you to a nearby one, which cuts latency and keeps traffic off the origin server.
- The player fetches segments one at a time and measures how quickly they arrive. This is adaptive bitrate streaming: if the connection slows it asks for the lower-quality version of the next segment, so the picture gets softer instead of freezing.
- A load balancer spreads requests for the site itself (search, login, recommendations) across many identical servers, and stops sending traffic to any that fail. When a popular show is released, more servers are started to cope.
- Viewing events (play, pause, stop) stream into storage where they are mined to make recommendations.
The Role of the W3C
🎯 (SE-12-05)
📌 Investigate and explain the role of the World Wide Web Consortium (W3C) in the development of applications for the web
- Web Accessibility Initiative (WAI)
- internationalisation
- web security
- privacy
- machine-readable data
The World Wide Web Consortium (W3C) is an international community, led by its member organisations and founded by Tim Berners-Lee, that develops open standards for the web. A standard starts as a draft, is reviewed and tested by implementers, and only becomes a W3C Recommendation once it has been shown to work in more than one browser. The value to developers is that code written to a standard behaves the same way in every compliant browser, and that no single company controls how the web works. HTML and the DOM are now maintained as a "living standard" by the WHATWG community, which works with the W3C; CSS, WCAG, WebAuthn and many other web standards are W3C's own.
Web Accessibility Initiative (WAI)
Develops the Web Content Accessibility Guidelines (WCAG) — the international standard for making web content accessible to people with disabilities (visual, auditory, cognitive, motor). Compliance levels: A, AA (the level Australian government guidance expects), AAA. Practices include alternative text for images, keyboard-navigable interfaces, and sufficient colour contrast ratios.
Internationalisation (i18n)
Ensures web technologies support all languages, scripts, and cultures globally. This includes Unicode character encoding (UTF-8), right-to-left (RTL) text support for Arabic/Hebrew, locale-specific date/number formatting, and tools for translating and localising web content without changing the underlying code structure.
Web Security
The W3C develops security-related standards such as Content Security Policy (CSP) headers (which restrict the sources from which scripts, styles, and images can be loaded, mitigating XSS attacks), Subresource Integrity (SRI) (verifying third-party CDN resources haven't been tampered with), and the Web Authentication API (WebAuthn) for passwordless login.
Privacy
The W3C's Privacy Interest Group (PING) helps review new web specifications for privacy, and W3C specifications include privacy considerations. The aim is that new browser features cannot be used for fingerprinting or tracking users without consent. Replacing third-party cookies has been proposed by browser makers (for example Google's Privacy Sandbox) and debated in W3C groups. The Do Not Track signal was never widely honoured by websites, and its W3C working group has closed. Australian law adds its own duties under the Privacy Act 1988.
Machine-Readable Data
The W3C develops standards for structured, semantic data that machines (search engines, AI systems) can interpret. This includes Schema.org vocabulary embedded as JSON-LD or microdata in HTML (enabling rich search results), the Resource Description Framework (RDF), and Semantic HTML5 elements (<article>, <nav>, <section>) that provide meaning beyond visual presentation.
Worked example: is this text readable? (WAI and WCAG)
WCAG requires a contrast ratio of at least 4.5:1 for normal text at level AA (3:1 for large text). The ratio compares the brightness of the text and background colours, from 1:1 (identical) to 21:1 (black on white). This program follows the WCAG formula for relative luminance:
def luminance(hex_colour):
r, g, b = [int(hex_colour[i:i + 2], 16) / 255 for i in (1, 3, 5)]
def channel(c):
return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b)
def contrast(colour_a, colour_b):
lighter, darker = sorted([luminance(colour_a), luminance(colour_b)], reverse=True)
return (lighter + 0.05) / (darker + 0.05)
for colour in ["#000000", "#767676", "#999999", "#6366F1", "#4F46E5"]:
ratio = contrast(colour, "#FFFFFF")
print(colour, round(ratio, 2), "passes AA" if ratio >= 4.5 else "fails AA")
#000000 21.0 passes AA
#767676 4.54 passes AA
#999999 2.85 fails AA
#6366F1 4.47 fails AA
#4F46E5 6.29 passes AA
The fourth line is a useful surprise: the popular indigo #6366F1 is 0.03 short of the AA threshold for normal text on white, so a slightly darker #4F46E5 (used in the CSS examples on this page) is the safer choice. Accessibility is decided by measurement, not by whether a colour "looks fine" to the person who chose it. WCAG is organised around four principles, often remembered as POUR: content must be perceivable, operable, understandable and robust.
Worked example: internationalisation and machine-readable data in the markup
<html lang="en-AU">
<head>
<meta charset="utf-8"> <!-- UTF-8 can represent every script -->
<title>Reef Runner | GameShelf</title>
<script type="application/ld+json">
{ "@context": "https://schema.org", "@type": "VideoGame",
"name": "Reef Runner", "offers": { "@type": "Offer", "price": "25.00", "priceCurrency": "AUD" } }
</script>
</head>
<body>
<p lang="ar" dir="rtl">مرحبا</p> <!-- Arabic, written right to left -->
</body>
</html>
- The
langattribute tells screen readers which pronunciation rules to use and lets browsers offer translation. - UTF-8 and
dir="rtl"let one page mix scripts correctly, which is internationalisation: designing so that translation (localisation) needs no code changes. - The JSON-LD block is machine-readable data: a search engine can read that this page is a video game costing 25.00 AUD without guessing from the layout.
| W3C area | Problem it solves | Developer action |
|---|---|---|
| WAI / WCAG | People are excluded by low contrast, missing alt text or mouse-only controls | Contrast checks, alt text, keyboard operation, labels |
| Internationalisation | Text is garbled or reads the wrong way in other languages | UTF-8, lang, dir, no hard-coded formats |
| Web security | Injected scripts and tampered third-party files | Content Security Policy, Subresource Integrity, WebAuthn |
| Privacy | Tracking and fingerprinting without consent | Minimise data collected, respect browser privacy settings and consent |
| Machine-readable data | Software cannot tell what a page is about | Semantic HTML, JSON-LD, Schema.org |
lang, a Content Security Policy header or JSON-LD. A common error is to say the W3C "runs the internet" or "makes the browsers"; it does neither. Another is to say WCAG is only about blind users, when it also covers hearing, motor and cognitive needs.Modelling a Web Development System
🎯 (SE-12-02, SE-12-06)
📌 Model elements that form a web development system
- client-side (front-end) web programming
- server-side (back-end) web programming
- interfacing with databases that are based on Structured Query Language (SQL) or non-SQL
Data flow diagram
Data flow diagram: a web application
How data moves between the user, the server-side processes and the database.
What to notice
- The user is an external entity; the database is a data store.
- Processes are numbered so they can be refined into lower-level DFDs.
Try thisAdd a process for logging in. Which data store would it read?
A web development system has four parts that pass data to each other. Keeping them separate lets each be changed, tested and secured on its own.
| Element | Runs where | Job | GameShelf example |
|---|---|---|---|
| Front end | The user's browser | Show the interface and respond to the user | HTML page, CSS layout, JavaScript that shows the price filter |
| API | The boundary between the two | A defined set of URLs and message formats the front end can call | GET /games?max_cost=30 returning JSON |
| Back end | The web server | Apply rules, check who is asking, talk to the database | A Python function that validates max_cost and runs a query |
| Database | A database server or file | Store data permanently | The Games, Publishers and Developers tables |
Worked example: the lifecycle of one request
A visitor moves a slider to "under $30" on GameShelf. Follow the data through the model, and compare it with the data flow diagram above.
| Step | Element | What happens | Data |
|---|---|---|---|
| 1 | Front end | A JavaScript event handler reads the slider value | 30 |
| 2 | Front end to API | The browser sends an HTTP request | GET /games?max_cost=30 |
| 3 | Back end | The route function checks that 30 is a number and that the user may see this data | Validated max_cost |
| 4 | Back end to database | A parameterised query is run | SELECT Name, Cost FROM Games WHERE Cost <= ? with 30 |
| 5 | Database to back end | Matching rows are returned | Kelp Quest, Reef Runner, Lava Lab |
| 6 | Back end to API | The rows are converted to JSON and sent with status 200 | [{"Name": "Kelp Quest", "Cost": 20.0}, ...] |
| 7 | Front end | JavaScript builds list items from the JSON and updates the page | Three list items in the DOM |
The database is reached only in steps 4 and 5, and only by the back end. The browser has no direct route to it, so a curious user cannot send their own SQL. In DFD terms, the visitor is the external entity, "Validate request" and "Get games" are processes, and Games is the data store.
Client-Side (Front-End) Web Programming
The client side refers to all code that executes within the user's web browser. Front-end code consists of three complementary technologies: HTML (structure and content), CSS (visual presentation and layout), and JavaScript (interactivity and dynamic behaviour). The browser downloads these files from the server, parses them, and renders the visual page. Client-side code is visible to the user (via browser dev tools) and should never contain sensitive data such as API keys or database credentials.
Server-Side (Back-End) Web Programming
The server side refers to code that executes on the web server — invisible to the client. The server receives HTTP requests, applies business logic (authentication, data processing, calculations), queries the database, and returns a response (HTML page or JSON data). Common back-end languages include Python (Django, Flask), JavaScript (Node.js/Express), Ruby (Rails), and PHP. The server environment is protected from end-users and may contain sensitive configuration, credentials, and proprietary algorithms.
Databases — SQL and Non-SQL
SQL (Relational) Databases
Store data in structured tables with rows and columns. Relationships between tables are defined by primary and foreign keys. Enforce data integrity via ACID transactions. Examples: PostgreSQL, MySQL, SQLite. Best for: structured, consistent data with complex relationships (e.g., e-commerce orders, user accounts).
Non-SQL (NoSQL) Databases
Store data in flexible formats — documents (JSON), key-value pairs, graphs, or column families. Schema-free, allowing rapid iteration. Examples: MongoDB (documents), Redis (key-value), Neo4j (graphs). Best for: unstructured or evolving data (e.g., social media posts, real-time analytics, content catalogues).
APIs: how the layers talk
An API (application programming interface) is the contract between front end and back end. Web APIs commonly follow REST conventions: each resource has a URL, and the HTTP method says what to do with it. Data usually travels as JSON.
| Action | Method and URL | Body | Success status |
|---|---|---|---|
| List games | GET /games | none | 200 |
| Read one game | GET /games/4 | none | 200 (404 if absent) |
| Create a game | POST /games | JSON for the new game | 201 Created |
| Change a game | PUT /games/4 | JSON with the changes | 200 |
| Delete a game | DELETE /games/4 | none | 204 No Content |
async function showGames(maxCost) {
const response = await fetch('/games?max_cost=' + encodeURIComponent(maxCost));
if (!response.ok) { // 400, 404, 500 ...
document.querySelector('#status').textContent = 'Could not load games';
return;
}
const games = await response.json(); // parse the JSON body
const list = document.querySelector('#game-list');
list.replaceChildren(); // clear the old items
games.forEach(game => {
const li = document.createElement('li');
li.textContent = game.Name + ' ($' + game.Cost.toFixed(2) + ')';
list.appendChild(li);
});
}
showGames(30);
With the running example's data, the list would show Kelp Quest ($20.00), Reef Runner ($25.00) and Lava Lab ($30.00). The matching back-end route is shown in the back-end request section. Note textContent again: the data is displayed as text, so a game called <b> cannot inject markup.
Worked example: the same data in SQL and non-SQL databases
The relational design splits one game across three tables and joins them when needed. A document database keeps everything about a game together in one record.
{
"id": 4,
"name": "Star Sailor",
"release_date": "2023-03-20",
"cost": 45.00,
"publisher": { "name": "Pixel Harbour" },
"developer": { "first_name": "Ava", "last_name": "Nguyen" }
}
| SQL (relational) | Non-SQL (document) | |
|---|---|---|
| Structure | Fixed columns; every row has the same fields | Flexible; each document may have different fields |
| Publisher's name changes | Update one row; every game sees the change through the foreign key | The name is copied into every game document, so every one must be updated |
| Show a game with its publisher | A join across tables | One read of one document |
| Best suited to | Related, consistent data such as orders and accounts | Varied or fast-changing data such as activity feeds and catalogues |
Neither is "better". The choice depends on the data's shape and how it is used, and many systems use both.
Web Browsers and Developer Tools
🎯 (SE-12-03, SE-12-06)
📌 Explore and explain the influence of a web browser on web development, including the use of developer (dev) tools
Your code only works if the browser runs it the way you expect. Browsers use different engines, support features at different times and handle errors differently, so developers test in more than one and use the browser's built-in developer tools to find out what is really happening.
Browser Rendering Engines
Different browsers use different rendering engines to parse HTML/CSS and render the visual page: Blink (Chrome, Edge, Opera), WebKit (Safari), and Gecko (Firefox). Differences in engine implementations can cause the same CSS to render slightly differently across browsers — a core challenge of front-end development known as cross-browser compatibility. Developers use tools like Can I Use (caniuse.com) to check which CSS/JavaScript features are supported across browser versions before using them in production.
Whatever the engine, the browser follows the same pipeline, and knowing it explains many bugs and slow pages:
- It parses the HTML into the DOM (a tree of elements) and the CSS into a matching set of style rules.
- It combines them into a render tree of the elements that will actually be shown (an element with
display: noneis left out). - It works out the layout: the size and position of every box.
- It paints the pixels to the screen.
A script that changes the DOM or CSS forces some of these steps to be done again, and a script or stylesheet in the <head> can hold up the first paint. Browsers also add new features at different times, so a developer either checks support on Can I Use or tests for the feature before relying on it:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js'); // browser supports service workers
} else {
console.log('No service worker support: the site still works online.');
}
In CSS the equivalent is the @supports rule. The principle is progressive enhancement: build a working baseline for every browser, then add newer features where they are available.
Developer (Dev) Tools
All modern browsers include built-in developer tools (opened with F12 or right-click → Inspect) that allow engineers to inspect, debug, and optimise web applications in real time:
console.log() output for debugging.Hands-on walkthrough: five minutes in the dev tools
Do this on any page, including your own school's public website. Your changes affect only your copy in your browser and disappear when you reload, so you cannot break the real site. The panel names below are Chrome's; Firefox's are close (Inspector for Elements, Debugger for Sources, Storage for Application).
- Elements. Right-click a heading and choose Inspect. Double-click its text in the tree and type something new: the page changes instantly. In the Styles pane, untick a
colorrule and watch it change back. The Computed tab shows the final value of each property after the cascade. Expected result: the page looks different, but reloading restores it. - Console. Type
document.titleand press Enter: it prints the page title. Typedocument.querySelectorAll('img').lengthfor the number of images. Broken JavaScript shows up here as a red error message with a file name and line number, which is where debugging starts. - Network. With the panel open, reload the page. Each row is one request: name, status code, type, size and time. Click a row to see the request and response headers. Filter by Fetch/XHR to see API calls only. Tick Disable cache and reload to see the cost of a first visit, then untick it and reload to see cached files served from disk. Expected result: repeat loads show fewer bytes transferred; a failed request shows a 4xx or 5xx status in red.
- Throttling and Device Mode. Choose a slow connection preset in the Network panel, then switch on Device Mode and pick a phone. Reload. Expected result: the waterfall stretches out and you can see which files delay the page.
- Performance. Click Record, reload, then stop. The timeline shows when the page first painted, when scripts ran and any long tasks that froze the page. Metrics such as Largest Contentful Paint are marked on it.
- Application. Under Cookies, find the session cookie set by a site you are logged into and read its flags:
Secure,HttpOnlyandSameSite. Under Service Workers you can see, unregister or bypass a PWA's worker. - Lighthouse. Run an audit and read the scores and the list of suggestions, such as images that are too large or missing alt text.
| Panel | Question it answers |
|---|---|
| Elements | What HTML and CSS is the browser really using for this element? |
| Console | Did my script fail, and where? |
| Sources | What is my code doing line by line? (breakpoints and stepping) |
| Network | Which requests were made, how big and how slow, and what did the server reply? |
| Performance | Where did the time go: loading, scripting, layout or painting? |
| Application | What is stored in the browser: cookies, local storage, caches, service workers? |
| Lighthouse | How does the page score on performance, accessibility, SEO and PWA checks? |
The dev tools show you what the browser received, so they are also a reminder of the security rule from the front-end section: nothing in the browser can be trusted. A user can edit a price in the Elements panel or replay a request from the Network panel with different values, which is why the server must check everything again.
Cascading Style Sheets (CSS)
🎯 (SE-12-02, SE-12-06)
📌 Investigate cascading style sheets (CSS) and its impact on the design of a web application
- consistency of appearance
- flexibility with browsers or display devices
- CSS maintenance tools
Consistency of Appearance
CSS enables a single stylesheet to control the visual appearance of every page across an entire website. CSS Custom Properties (variables) allow designers to define a value once (e.g., --brand-colour: #4F46E5) and reuse it throughout, ensuring colour, typography, and spacing remain consistent. Changing a single variable instantly updates every element that references it. Design systems (such as Google's Material Design or Apple's Human Interface Guidelines) are implemented through CSS to enforce consistent visual language across large products.
/* Define design tokens as CSS variables */
:root {
--primary-colour: #4F46E5;
--font-body: 'Inter', sans-serif;
--spacing-md: 1rem;
}
/* Apply consistently across components */
.btn-primary {
background: var(--primary-colour);
font-family: var(--font-body);
padding: var(--spacing-md) calc(var(--spacing-md) * 2);
}
/* Media query for responsive design (flexibility with devices) */
@media (max-width: 768px) {
.grid-3col { grid-template-columns: 1fr; }
}
Reading CSS and HTML together (Course Specifications)
The Course Specifications expect you to interpret code fragments in CSS and HTML. A rule is a selector that picks elements, followed by braces holding property: value; declarations, and /* … */ is a comment. Here is the Specifications' style of example, with what the browser does with it.
<html>
<head><title>My Website</title></head>
<body>
<h1>Welcome!</h1>
<p id="welcome">Welcome to my website!</p>
<p class="red-text">This text should be red</p>
<p>My website also has <span class="red-text">red text</span> here</p>
</body>
</html>
/* This selector targets an HTML element */
h1 {
font-size: 18px;
}
/* This selector targets an HTML element with a specific "id" */
#welcome {
font-style: italic;
}
/* This selector targets an HTML element with a specific "class" */
.red-text {
color: red;
}
h1matches the heading only, so "Welcome!" is drawn at 18px.#welcomematches the one element with that id, so only the first paragraph is italic. An id must be unique on a page..red-textmatches every element with that class, so the second paragraph and the word "red text" inside the third paragraph are red, but the rest of the third paragraph is not.
| Selector | Matches | Example |
|---|---|---|
| Element (type) | Every element of that name | p |
| Class | Elements with that class (reusable) | .red-text |
| ID | The one element with that id | #welcome |
| Descendant | An element anywhere inside another | body p |
| Combined | Both conditions on one element | p.red-text |
| Group | Several selectors sharing one block | h1, h2 { … } |
| Pseudo-class | An element in a particular state | a:hover |
The cascade and specificity
Several rules can set the same property on the same element, so the browser needs a rule for choosing. That rule is the cascade. It compares, in order: (1) importance (a declaration marked !important beats one that is not), (2) whether the style is inline (in the element's own style attribute), (3) specificity, and (4) source order, where the later rule wins a tie. A selector's specificity is three counts read left to right: IDs, then classes (with attributes and pseudo-classes), then element names. Compare column by column, starting at the left; a higher number in an earlier column wins no matter what is to its right.
| Selector | IDs | Classes | Elements | Specificity |
|---|---|---|---|---|
p | 0 | 0 | 1 | (0, 0, 1) |
body p | 0 | 0 | 2 | (0, 0, 2) |
.red-text | 0 | 1 | 0 | (0, 1, 0) |
p.red-text | 0 | 1 | 1 | (0, 1, 1) |
#welcome | 1 | 0 | 0 | (1, 0, 0) |
nav ul li a:hover | 0 | 1 | 4 | (0, 1, 4) |
Worked examples: which colour wins?
| Element | Competing rules | Winner and reason |
|---|---|---|
<p class="red-text"> | p { color: black; } then .red-text { color: red; } | Red: (0, 1, 0) beats (0, 0, 1). Order does not matter here. |
<p class="red-text"> | .red-text { color: red; } then p.red-text { color: blue; } | Blue: (0, 1, 1) beats (0, 1, 0). |
<p id="welcome" class="red-text"> | #welcome { color: green; } and p.red-text { color: blue; } | Green: one ID (1, 0, 0) beats any number of classes (0, 1, 1). |
<p class="red-text big"> | .red-text { color: red; } then .big { color: black; } | Black: both are (0, 1, 0), so the later rule wins. |
<p id="welcome"> | #welcome { color: green; } and p { color: black !important; } | Black: !important is compared first. Use it sparingly; it makes styles hard to override. |
<span class="red-text"> inside <p> | p { color: black; } only | Black: color is inherited, so the span takes its parent's value when no rule targets it directly. A rule aimed at the span itself always beats an inherited value. |
The box model
Every element is drawn as a box made of four layers: content at the centre, then padding (space inside the border), the border and the margin (space outside). Most layout bugs are box-model bugs, and the Elements panel draws the four layers in colour for the selected element.
.card {
width: 200px; /* content width */
padding: 10px; /* inside the border */
border: 2px solid #333;
margin: 20px; /* outside the border */
box-sizing: content-box; /* the default: width is the content only */
}
/* Total width taken on the page: 200 + 2*10 + 2*2 + 2*20 = 264px.
With box-sizing: border-box, width: 200px would include padding and border. */
Flexibility with Browsers and Display Devices
Responsive web design ensures a single codebase adapts to any screen size — from a 27-inch desktop monitor to a 375px mobile screen. CSS media queries apply different styles at defined breakpoints. Flexbox and CSS Grid provide flexible layout systems that redistribute space and reflow content automatically. The viewport meta tag instructs the browser to scale the page to the device's width, preventing the "zoomed-out desktop site" appearance on mobile.
Try it
Responsive layout explorer
Change the screen width and see which media queries apply. Enable JavaScript to use this tool. In short: base rules fit a phone and min-width media queries add columns and a sidebar as the screen gets wider.
Worked example: one layout, three screen sizes
Without the viewport meta tag, phones pretend to be about 980px wide and shrink the page. With it, the layout can respond to the real width. This code puts three cards in a row on wide screens and stacks them on narrow ones. It uses flexbox, relative units and one media query, with no separate mobile site.
<meta name="viewport" content="width=device-width, initial-scale=1">
.cards {
display: flex;
flex-wrap: wrap; /* let items drop to the next line */
gap: 1rem;
}
.card {
flex: 1 1 250px; /* grow, shrink, and start at 250px wide */
font-size: 1rem; /* rem scales with the user's font setting */
}
@media (max-width: 600px) {
.card { flex-basis: 100%; } /* one card per row on phones */
}
img { max-width: 100%; height: auto; } /* pictures never overflow their box */
- At about 1000px wide, three 250px cards fit on one row and share the leftover space.
- At 390px (a common phone width), only one card fits per line, so they stack; the media query makes this explicit for widths up to 600px.
- Percentages,
remandmax-widthmake sizes relative, so they adapt to the browser window and to users who enlarge their text. Fixed pixel widths do not.
For flexibility across browsers, developers write standard CSS, check support (Can I Use, @supports) and test in more than one engine. For flexibility across display devices, they also design for touch (tap targets of at least about 44px), for high-resolution screens (images supplied at more than one size with srcset) and for user preferences such as prefers-color-scheme and prefers-reduced-motion.
CSS Maintenance Tools
Worked example: a maintenance tool at work
Sass lets you nest rules and reuse values; a build step compiles it to plain CSS that browsers understand. If the brand colour changes, you edit one variable.
$brand: #4F46E5;
.nav {
background: $brand;
a { color: white; }
a:hover { text-decoration: underline; }
}
.nav { background: #4F46E5; }
.nav a { color: white; }
.nav a:hover { text-decoration: underline; }
Other tools work on the finished CSS: a linter (such as Stylelint) flags mistakes and inconsistent style, and a minifier removes whitespace and comments to cut file size for production. Plain CSS custom properties, shown earlier, now cover much of what Sass variables were used for, without a build step.
.red-text) and an ID selector one element (#welcome); swapping them is the most common slip.Version Control
🎯 (SE-12-09)
📌 Investigate the reasons for version control and apply it when developing web application
Web projects change constantly, often with several people editing the same files. Version control records every change so you can see what changed, undo mistakes and work on new features without breaking the live site.
| Reason | What goes wrong without it |
|---|---|
| History and rollback | A bad change reaches the live site and nobody can tell what the working version was. |
| Branching | Half-finished features are mixed into the code everyone relies on. |
| Collaboration | Two developers overwrite each other's edits to the same stylesheet. |
| Accountability | No record of who changed the login code, or why. |
Version control systems (VCS) track every change made to a codebase over time, enabling developers to: roll back to any previous working state if a new change breaks functionality; branch the codebase to develop features or fix bugs in isolation without affecting the main production code; merge contributions from multiple developers working simultaneously; and maintain a complete audit trail of who changed what and why. Git is the dominant distributed VCS; GitHub, GitLab, and Bitbucket are the popular remote hosting platforms.
# Initialise a new repository
git init
# Create a feature branch (isolates work from main; "git switch -c" is the newer equivalent)
git checkout -b feature/user-login
# Stage changed files
git add index.html styles.css app.js
# Commit with a descriptive message
git commit -m "Add user login form with client-side validation"
# Push branch to remote repository (GitHub)
git push origin feature/user-login
# Merge feature into main once reviewed
git checkout main
git merge feature/user-login
Worked example: a branch, a merge and a conflict
Ava and Ben are building the GameShelf site. This sequence uses real Git output (your commit codes will differ, because each is calculated from the content, author and time). First Ava tells Git who she is once, then creates the project:
git config --global user.name "Ava"
git config --global user.email "ava@example.com"
git init -b main # start tracking this folder; the first branch is main
git add index.html styles.css # stage the files for the next commit
git commit -m "Add home page and styles"
git log --oneline
68ffb32 Add home page and styles
Ava now works on a search box in her own branch, so that main always holds working code. Meanwhile Ben adds an about page directly on main (in a team this would normally also be a branch and pull request).
git switch -c feature/search # create and move to a new branch
# ... edit index.html ...
git add index.html
git commit -m "Add search box"
# ... edit styles.css ...
git commit -am "Style the search box" # -a stages changes to files Git already tracks
git switch main
# ... Ben adds about.html ...
git add about.html
git commit -m "Add about page"
git log --oneline --graph --all
* 0408178 Style the search box
* 6a62b66 Add search box
| * a2ef249 Add about page
|/
* 68ffb32 Add home page and styles
The graph shows the two lines of work splitting at the first commit. The search branch has two commits that main does not have, and main has one that the branch does not. Because they changed different files, Git can combine them automatically:
git merge feature/search -m "Merge feature/search"
git log --oneline --graph
git branch -d feature/search # the branch has done its job
Merge made by the 'ort' strategy.
index.html | 1 +
styles.css | 1 +
2 files changed, 2 insertions(+)
* 22ae2e4 Merge feature/search
|\
| * 0408178 Style the search box
| * 6a62b66 Add search box
* | a2ef249 Add about page
|/
* 68ffb32 Add home page and styles
Deleted branch feature/search (was 0408178).
Diagram
The branch and merge above, as a picture
Each circle is a commit; the merge commit M has two parents and joins the two lines of work.
What to notice
- Both branches start from the same commit, so nothing is lost or copied twice.
- The merge commit records that two histories were combined, which is why
git log --graphshows a diamond.
Try thisRedraw the diagram if Ben had also added a commit on main after Ava's merge. Where would C5 go?
When two people change the same line: a merge conflict
Now suppose Ava renames the heading in a branch and Ben renames the same line differently on main. Git cannot know which is right, so it stops the merge and marks the file:
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
<<<<<<< HEAD
<h1>Game Shelf</h1>
=======
<h1>GameShelf Store</h1>
>>>>>>> feature/title
<input id="search" type="search">
<p>Browse games.</p>
- The section between
<<<<<<< HEADand=======is what the current branch (main) has; the section below it is the incoming branch's version. - Ava and Ben talk, agree on "GameShelf Store", and edit the file so only that line remains, deleting the three marker lines.
- They run
git add index.html, thengit committo finish the merge.git statusshows the file as unmerged (UU) until this is done.
Conflicts are normal, and small, frequent merges keep them small. They are a human decision, not a failure of the tool, and the fix is a conversation as much as an edit.
Try it
Git sandbox
Commit, branch and merge, and see a merge conflict. Enable JavaScript to use this tool. In short: a branch is a label on a commit, a merge joins two branches, and a conflict happens when both changed the same file.
Working with a remote and other habits
| Command or practice | Purpose |
|---|---|
git clone URL | Copy a remote repository, with its whole history, to your computer |
git push origin feature/search | Send your branch's commits to the shared remote (GitHub, GitLab, Bitbucket) |
git pull | Fetch others' commits and merge them into your branch; do it before starting work each day |
| Pull request | A request on the hosting platform to merge a branch, with a place for review and comments |
git revert <commit> | Undo a commit by making a new commit that reverses it, keeping history honest |
.gitignore | Lists files never to commit, such as node_modules/, .env and *.log |
| Commit messages | Short, in the imperative ("Add search box"), one change per commit, so history reads as a story |
Never commit passwords, API keys or database credentials. Once pushed, they remain in the history even if the file is later deleted, so a leaked key must be cancelled and replaced. Git is the tool; GitHub is one website that hosts Git repositories.
feature/login, commit the form, push it, open a pull request for review, then merge into main". If asked what happens on a conflict, say that two changes touched the same lines, Git marks the file, and developers choose the final content, then commit. Do not confuse Git (the version control system) with GitHub (a hosting service).Code Libraries for Front-End Development
🎯 (SE-12-06)
📌 Explore the types and significance of code libraries for front-end web development
- frameworks that control complex web applications
- template engines
- predesigned CSS classes
A code library is pre-written code you can reuse instead of writing everything yourself. Front-end developers rely on three main kinds, which save time and make large applications more consistent.
Frameworks for Complex Web Applications
Front-end frameworks provide a structured architecture for building large, complex web applications. They enforce separation of concerns (UI components, state management, routing) and provide build tools that optimise code for production.
Why frameworks are useful
The Course Specifications say students should understand why front-end frameworks are useful; they are not expected to know or code in any particular one. The reasons apply to all of them:
- Components: the interface is built from small reusable pieces (a card, a menu, a button), each with its own markup, style and behaviour, so a change is made once.
- Automatic updates: the developer describes what the page should look like for the current data, and the framework updates the DOM when the data changes. This removes a large amount of hand-written DOM code.
- Structure: conventions for routing (which component shows for which URL), state (where shared data lives) and folders let teams work on one large code base without clashing.
- Tooling: build steps bundle, minify and split code for fast delivery, and there are testing and debugging tools and a large community.
The difference is easiest to see side by side. Both fragments show a list of games, but only the second keeps itself up to date:
function addGame(name) {
games.push(name);
const li = document.createElement('li');
li.textContent = name;
document.querySelector('#game-list').appendChild(li); // must remember to update the page
}
<ul>
<li v-for="game in games">{{ game }}</li> <!-- the framework redraws when games changes -->
</ul>
The costs are real: a learning curve, extra code to download (which slows loading), a build process, and dependence on a project that must be kept up to date.
Template Engines and CSS Frameworks
Template engines allow developers to inject dynamic data into HTML templates on the server before sending the page to the client, avoiding duplicate HTML markup. Template engines support variables, control structures (loops, conditionals), and template inheritance.
A worked template example uses Jinja2, the template engine used by Flask. The template is one HTML layout with placeholders; the Python code supplies the data.
from jinja2 import Template
page = Template("""<ul>
{% for g in games %} <li>{{ g.name }} - ${{ "%.2f"|format(g.cost) }}{% if g.cost > 40 %} (premium){% endif %}</li>
{% endfor %}</ul>""")
print(page.render(games=[{"name": "Kelp Quest", "cost": 20},
{"name": "Star Sailor", "cost": 45}]))
<ul>
<li>Kelp Quest - $20.00</li>
<li>Star Sailor - $45.00 (premium)</li>
</ul>
The {% %} blocks are control structures (a loop and a condition) and {{ }} inserts a value. Because the layout is written once, the header and navigation can be shared by every page through template inheritance. Flask switches on escaping for its HTML templates (the plain Template class above does not), so a value such as <script> is displayed rather than run, which links this section to cross-site scripting.
CSS frameworks provide pre-built components and responsive utility classes to accelerate styling and ensure consistent design across pages. They typically include grid systems, responsive breakpoints, and pre-styled UI components.
Predesigned CSS classes work the other way: you write almost no CSS and instead add ready-made class names to the HTML.
<div class="container">
<div class="row">
<div class="col-12 col-md-4"><button class="btn btn-primary">Buy</button></div>
</div>
</div>
Here container, row and col-* make a responsive grid (a full-width column on phones, one third of the row from medium screens), and btn btn-primary gives a styled button. The trade-off is that sites built with the same framework can look alike, and the whole stylesheet is downloaded even if only a few classes are used.
| Type | Examples | Why it matters |
|---|---|---|
| Frameworks | React, Vue, Angular | Organise complex interfaces into reusable components that update automatically when data changes |
| Template engines | Jinja2, Handlebars, EJS | Fill one HTML layout with different data, so pages share a structure without copy-and-paste |
| Predesigned CSS classes | Bootstrap, Tailwind CSS | Give consistent, responsive styling by adding class names such as btn btn-primary |
| Consideration | Benefit of a library | Risk or cost |
|---|---|---|
| Development time | Ready-made components and layouts | Time to learn the library first |
| Consistency | Shared classes and components look and behave the same | Sites can look generic |
| Performance | Optimised, widely tested code and build tools | More code to download and run |
| Security and upkeep | Fixes arrive as updates | Outdated versions carry known vulnerabilities; updates can break code |
| Independence | Large communities and documentation | Dependence on a third party that may stop maintaining it |
When a library is loaded from a CDN, the integrity attribute (Subresource Integrity) lets the browser check that the file is unchanged: <script src="https://cdn.example/lib.js" integrity="sha384-…" crossorigin="anonymous"></script>, where the value is a hash of the expected file. If the hash does not match, the browser refuses to run it.
Open-Source Software
🎯 (SE-12-05)
📌 Explain the use and development of open-source software in relation to web development
Much of the modern web is built on open-source software. Web servers (Apache, Nginx), programming languages (Python, Node.js), frameworks (React, Django), and databases (PostgreSQL, MySQL) are all open source. The NPM (Node Package Manager) ecosystem alone hosts millions of open-source packages that developers can import with a single command (npm install package-name).
"Open source" means the source code is published under a licence that lets others read, use, modify and share it. It is not the same as "free of charge" (many open-source products are sold as services), and it is not the same as "public domain": the author still holds the copyright, and the licence is what grants the permissions. In Australia the licence works because of the Copyright Act 1968, which gives the author exclusive rights that the licence then shares on stated conditions.
Comparing the licences
| Licence | Type | You may | You must | Typical use |
|---|---|---|---|---|
| MIT | Permissive | Use, change and sell it, including in closed products | Keep the copyright and licence notice | Many JavaScript libraries |
| Apache 2.0 | Permissive | The same, plus an explicit patent grant | Keep notices and state significant changes | Larger corporate projects |
| GPL v3 | Copyleft | Use and change it, and sell copies | Release your version, if you distribute it, under the GPL with source code | Programs whose authors want improvements to stay open |
| Proprietary | Closed | Use it as the licence allows | Pay any fees and follow the terms; no source access | Commercial software |
The GPL's requirement is triggered by distributing the combined program. Software that only runs on your own server and is used over the web is not distributed, which is why some projects use the AGPL, a variant that also covers network use. When in doubt, read the licence or ask; that is a professional habit, not a legal technicality.
Worked example: can GameShelf use these?
| Situation | Decision | Reason |
|---|---|---|
| The paid mobile app bundles an MIT-licensed date library | Allowed | Keep the library's copyright notice in an "about" or licences screen |
| The team wants to ship a closed-source desktop version containing GPL v3 code | Not without releasing that code under the GPL | Distributing a combined program triggers the copyleft requirement; the team could instead choose a permissively licensed alternative |
| A developer copies a function from a forum post with no licence stated | Do not use it | No licence means no permission: copyright still applies |
| The site uses a library that has had no update for several years | Review | The licence allows it, but the security risk of an unmaintained dependency may not |
How open-source software is developed
- A maintainer publishes the code in a public repository with a licence, a README and instructions for contributing.
- Users report bugs and suggest features as issues.
- A contributor forks (copies) the repository, creates a branch, makes a change, and opens a pull request back to the original.
- Maintainers and other contributors review the change, request edits, and merge it if it meets the project's standards and tests.
- New versions are released, and users update their dependencies.
This is the same Git workflow as the version-control section, applied across a community. The benefits are low cost, many reviewers, fast fixes and no lock-in to one supplier. The costs are uneven quality, projects that are abandoned, and the fact that a bug in a popular component affects everyone who uses it. The Heartbleed flaw of 2014, a bug in the widely used OpenSSL library, is a documented example of both: a large share of the web's servers depended on one small project.
Security dependency risk and managing dependencies
Relying on open-source libraries introduces a supply chain security risk: if a maintainer introduces a vulnerability (accidentally or maliciously), every project that depends on it is affected. Engineers should regularly check for known vulnerabilities in dependencies, update promptly, and include only libraries with active maintenance and community trust.
{
"name": "gameshelf",
"version": "1.0.0",
"dependencies": {
"express": "^4.19.0",
"dompurify": "^3.1.0"
}
}
npm installdownloads each dependency and the dependencies of those, so a handful of declared packages can mean hundreds of installed ones.- A lock file records the exact versions installed, so every developer and the live server run identical code.
npm auditcompares the installed versions against a database of known vulnerabilities; running it regularly, and updating, is the practical defence against supply-chain risk.- Prefer packages with recent releases, active maintainers and a clear licence, and remove ones you no longer use.
Managing Load Times
🎯 (SE-12-07, SE-12-08)
📌 Investigate methods to support and manage the load times of web pages/applications
Minification & Compression
Minification removes whitespace, comments, and unused code from HTML/CSS/JS files, reducing file size. GZIP/Brotli compression further compresses files before transmission — the server compresses the file, the browser decompresses it. Text files such as HTML, CSS and JavaScript compress very well, often to a fraction of their original size.
Caching
HTTP Cache-Control headers instruct the browser to store static assets (images, CSS, JS) locally for a defined period. On repeat visits, cached files load instantly without a server request. Service workers in PWAs enable advanced programmatic caching strategies.
Content Delivery Networks (CDN)
A CDN distributes static assets across servers in multiple geographic locations. Users receive files from the nearest server, reducing latency. Popular CDNs include Cloudflare, AWS CloudFront, and Fastly.
Lazy Loading
Images and heavy components are only loaded when they enter the user's viewport, rather than all at once on page load. Implemented with the loading="lazy" HTML attribute or JavaScript Intersection Observer API. Significantly reduces initial page weight.
Code Splitting
Bundlers (Webpack, Vite) split a large JavaScript application into smaller chunks that are only loaded when needed. A user visiting the home page doesn't need to download the JavaScript for the account settings page until they navigate there.
Core Web Vitals
Google's set of real-world performance metrics: LCP (Largest Contentful Paint — how fast main content loads), INP (Interaction to Next Paint — responsiveness), and CLS (Cumulative Layout Shift — visual stability). Google uses them as one of many signals in search ranking.
loading="lazy" to images below the fold and turning on caching cuts the data for a first load from about 12 MB to about 1 MB, and repeat visits reuse the cached files. The worked example below shows the arithmetic.Worked example: where do the seconds go?
Transfer time is the size of the data divided by the connection speed. Because file sizes are in megabytes (MB) and speeds in megabits per second (Mbps), multiply the size by 8 first: time in seconds = size in MB × 8 ÷ speed in Mbps. Take a school news page on an 8 Mbps mobile connection:
| Version | What is downloaded on first load | Size | Transfer time |
|---|---|---|---|
| Original | 0.5 MB of HTML, CSS and JavaScript, plus three 4 MB photos | 12.5 MB | 12.5 × 8 ÷ 8 = 12.5 s |
| Photos resized and converted to WebP (0.3 MB each); text compressed to about 0.15 MB (assumed) | 0.15 MB + 3 × 0.3 MB | 1.05 MB | 1.05 × 8 ÷ 8 = 1.05 s |
| Plus lazy loading: only the first photo is needed on screen | 0.15 MB + 0.3 MB | 0.45 MB | 0.45 s to first content; the other photos load as the reader scrolls |
| Repeat visit with caching | Cached files reused; the server is only asked if the page has changed | Close to 0 MB | Close to 0 s of transfer |
The sizes and the 30% compression figure are assumptions chosen to make the arithmetic clear. Your own numbers come from the Network panel. Transfer time is also only part of the story: every separate file also costs at least one round trip, and the browser cannot draw the page until it has processed any stylesheet or script in the head that blocks rendering.
<head>
<link rel="stylesheet" href="/css/site.9f3a1c.css"> <!-- fingerprinted file name, see caching below -->
<script src="/js/app.js" defer></script> <!-- download while parsing; run after the HTML is parsed -->
</head>
<body>
<img src="/img/hero-600.webp" width="600" height="400" alt="Students at the science fair">
<img src="/img/photo2-600.webp" width="600" height="400" alt="Robotics team"
loading="lazy"> <!-- fetched only when scrolled near -->
</body>
deferlets the browser keep drawing the page while the script downloads. A plain script in the head stops parsing until it has run.- The
widthandheightattributes reserve space, so nothing jumps when the image arrives (this helps the layout-shift measure, CLS). - The first image is not lazy-loaded, because it is the largest contentful paint (LCP); lazy loading it would delay the main content.
Cache-Control: public, max-age=31536000, immutable
Content-Encoding: br
A one-year max-age is safe here because the file name contains a fingerprint (9f3a1c). When the CSS changes, the build produces a new name, so browsers download it at once, and the old cached copy is simply never asked for again. Content-Encoding: br means the server compressed the file with Brotli and the browser decompresses it.
| Method | What it reduces |
|---|---|
| Image resizing and modern formats | Bytes (usually the biggest saving) |
| Minification and compression | Bytes of text files |
| Caching | Repeat downloads, and so bytes and requests on later visits |
| CDN | Distance, and so latency |
| Lazy loading and code splitting | Work and bytes needed before the first screen |
defer and removing render-blocking files | Waiting time before the first paint |
To find out which method to apply, measure first: run Lighthouse, read the Network waterfall for the biggest files and the longest waits, and change the largest cost first. Re-measure afterwards, because a change that does not improve the numbers is only added complexity.
Try it
Page load explorer
Turn on performance techniques and watch a waterfall of requests shrink. Enable JavaScript to use this tool. In short: compress and resize files, cache them, serve them from nearby and lazy-load what is below the fold.
Web Content Management Systems
🎯 (SE-12-06)
📌 Research, experiment with and evaluate the prevalence and use of web content management systems (CMS)
A Content Management System (CMS) is a software platform that allows users to create, edit, organise, and publish digital content via a graphical interface — without requiring direct coding knowledge. CMSs run a very large share of the websites on the internet (W3Techs publishes the current figures), and WordPress alone is used by a large share of all sites.
What a CMS is made of
- Content store: a database holding pages, posts, users and settings, separate from the design.
- Admin interface: forms and an editor so non-programmers can write, review and publish.
- Templates or themes: the code that turns stored content into HTML pages, so changing the theme restyles the whole site.
- Plugins or extensions: add-ons for features such as forms, shops and SEO, written by third parties.
- Roles and workflow: permissions (author, editor, administrator) and drafts, so an article can be reviewed before it goes live.
The separation of content from presentation is the key idea, and it explains the headless variant below: once content is stored separately, any front end can display it.
| CMS Type | Examples | Evaluation |
|---|---|---|
| Traditional (Coupled) | WordPress, Drupal, Joomla | Pros: All-in-one, large plugin ecosystem, easy for non-developers. Cons: Tightly coupled front-end limits design flexibility; common target for security exploits due to widespread use. |
| Headless CMS | Contentful, Sanity, Strapi | Pros: Content served via API to any front-end (web, mobile, IoT); maximum design flexibility; better performance. Cons: Requires developer to build the front-end separately; higher initial complexity. |
| Visual / No-Code | Webflow, Squarespace, Wix | Pros: Drag-and-drop interface; fast deployment; hosted infrastructure included. Cons: Limited customisation; vendor lock-in; less suited for complex custom functionality. |
Architecture diagram
Traditional and headless CMS
A traditional CMS joins content and presentation; a headless CMS serves content to any front end through an API.
What to notice
- Headless separates where content is stored from where it is shown.
- One headless back end can serve a website, an app and other devices.
Try thisA school wants its news on the website and in an app. Which CMS type suits it?
Evaluating a CMS: a weighted decision matrix
To evaluate is to make a judgement against criteria. A weighted matrix makes the criteria and the reasoning visible. Here a school compares the three types. Each option is scored 1 (poor) to 5 (excellent) on each criterion, and each score is multiplied by the criterion's weight, which reflects what matters to this school. The scores are the writer's judgements for illustration, not measurements.
| Criterion | Traditional | Headless | Visual / no-code | Weight: website and app | Weight: newsletter site only |
|---|---|---|---|---|---|
| Easy for non-coders | 5 | 3 | 5 | 3 | 5 |
| Low cost | 4 | 3 | 2 | 3 | 4 |
| Design flexibility | 3 | 5 | 2 | 2 | 1 |
| Low security upkeep | 2 | 4 | 4 | 4 | 3 |
| Content reuse across website and app | 2 | 5 | 1 | 5 | 1 |
| Weighted total: website and app | 51 | 69 | 46 | ||
| Weighted total: newsletter only | 52 | 49 | 48 | ||
Working for the traditional CMS in the first scenario: 5×3 + 4×3 + 3×2 + 2×4 + 2×5 = 15 + 12 + 6 + 8 + 10 = 51. The same scores give different winners when the weights change: headless wins clearly when the content must appear in an app as well, and the traditional CMS narrowly wins when the school only wants a simple newsletter site. The judgement depends on the context, which is what an evaluation should say.
Research and experiment: a plan you can carry out
- Research prevalence. W3Techs publishes the share of websites using each CMS, and it changes over time, so cite the figure with its date. WordPress alone is used by a large share of all websites, which is why it is also a common target for attackers.
- Experiment. Install a traditional CMS on your own computer (a local WordPress install is free). Create a page, a post and a menu, change the theme, and add one plugin.
- Inspect what the plugin cost. Open the Network panel before and after adding it and record the change in the number of requests and total size. Run Lighthouse and record the performance and accessibility scores both times.
- Check upkeep. Note how many updates are waiting (core, themes, plugins). Each unpatched plugin is a possible way in.
- Evaluate. Fill in a matrix like the one above with your own scores and evidence, and state a recommendation for a named client.
The Contribution of Back-End Development
🎯 (SE-12-07)
📌 Assess the contribution of back-end web development to the success of a web application
Back-end development is responsible for all logic that occurs on the server — the invisible engine that powers every user-facing feature. Its contribution to project success encompasses several critical dimensions:
Technology Selection
The choice of back-end framework determines the project's scalability, maintainability, and development speed. Django (Python) provides a "batteries-included" approach with an ORM, admin panel, and authentication built in — accelerating development of data-heavy applications. Node.js/Express enables the same language (JavaScript) on front-end and back-end, simplifying the development team's skill requirements. Flask offers a minimal, flexible microframework for APIs. The right technology choice directly impacts delivery timelines and long-term maintenance cost.
Business Logic and Data Integrity
The back end enforces all business rules that protect data integrity. For example, validating that a user's age is above 18 before processing a purchase, calculating order totals including applicable tax rates, or checking inventory levels before confirming a stock reservation. Enforcing these rules on the server (rather than only client-side) is essential because client-side code can be bypassed by malicious users using browser developer tools.
Security Enforcement
The back end is the primary security boundary of a web application. It implements authentication (password hashing with bcrypt, JWT token management), authorisation (role-based access control), input sanitisation (preventing SQL injection and XSS), and rate limiting (preventing brute-force login attacks). A poorly secured back end exposes the entire database and all user data to attackers.
To assess a contribution is to judge how much it matters and why. A useful way to organise that judgement is to test the back end against criteria that a web application is measured by, and to give evidence for each.
| Criterion | Back-end contribution | What happens if it is weak |
|---|---|---|
| Security | Password hashing, sessions, role checks, parameterised queries, output encoding, rate limiting | Accounts taken over, data stolen, site defaced |
| Correctness and integrity | Server-side validation and business rules; transactions that succeed or fail as a whole | Wrong totals, oversold stock, corrupt records |
| Performance | Efficient queries, indexes, caching, sending only the data needed | Slow pages and timeouts under load |
| Scalability | Stateless handlers, shared session store, load balancing, a database that can grow | The site fails when it becomes popular |
| Maintainability | Clear structure, tests, logging, a suitable framework | Every change is slow and risky |
Worked example: enforcing a rule on the server
The browser's form might limit the quantity box to 1 to 5, but anyone can send a request with any value. The back end applies the rule again and answers with a status code that says what was wrong:
def validate_order(quantity_text, stock):
if not quantity_text.isdigit():
return (400, "Quantity must be a whole number")
quantity = int(quantity_text)
if quantity < 1:
return (400, "Quantity must be at least 1")
if quantity > stock:
return (409, f"Only {stock} in stock")
return (200, f"Order accepted: {quantity}")
for text in ["2", "0", "abc", "-1", "9"]:
print(repr(text), validate_order(text, 5))
'2' (200, 'Order accepted: 2')
'0' (400, 'Quantity must be at least 1')
'abc' (400, 'Quantity must be a whole number')
'-1' (400, 'Quantity must be a whole number')
'9' (409, 'Only 5 in stock')
Notice that "-1" is caught by the first test, because the minus sign is not a digit. Testing with awkward inputs like this is how you find gaps. Status 409 (Conflict) tells the client the request was valid but cannot be done in the current state.
Worked example: efficiency is a back-end matter too
Slow pages are often caused by too many database queries. The first version below asks one question per publisher; the second asks once. On the running database the loop needs four queries (one for the publishers and one for each of the three), and with 1,000 publishers it would need 1,001. The join needs one however many there are.
# Slow: 1 query for the publishers, then 1 more for each publisher (the "N + 1" pattern)
for pub_id, name in db.execute("SELECT Publisher_ID, Name FROM Publishers").fetchall():
titles = db.execute("SELECT Name FROM Games WHERE Publisher_ID = ?", (pub_id,)).fetchall()
print(name, len(titles))
# Better: one query does the counting
sql = """SELECT Publishers.Name, COUNT(Games.ID)
FROM Publishers JOIN Games ON Games.Publisher_ID = Publishers.Publisher_ID
GROUP BY Publishers.Name"""
for name, total in db.execute(sql):
print(name, total)
Games Inc 4
Pixel Harbour 2
Coral Studios 1
Coral Studios 1
Games Inc 4
Pixel Harbour 2
Making the judgement
A strong assessment weighs the back end against the other parts. A back end that is secure, correct and quick gives users a site they can trust, so its contribution to success is large. But it cannot rescue a confusing interface or a slow front end (an oversized image is a front-end problem), and a well-built site still fails if the business idea does not work. So the judgement is that the back end is essential and often decisive for trust, data integrity and scalability, but it is one of several contributors, and it is most visible when it fails.
The Back-End Web Request Process
🎯 (SE-12-07)
📌 Observe and describe the back-end process used to manage a web request
- role of webserver software
- web framework
- objects
- libraries
- databases
The Request-Response Lifecycle
Sequence diagram
The back-end process for one web request
One request passes through DNS, the web server, the framework, a handler and the database.
What to notice
- The framework's router decides which handler runs.
- Queries are parameterised before they reach the database.
Try thisWhere could caching skip the database for popular pages?
Worked example: a Flask route from URL to response
Flask is a small Python web framework. This program (illustrative, using the GameShelf database created in the SQL section) shows each element of the dot point in one place: the framework, its request and response objects, a library, and the database.
import sqlite3
from flask import Flask, jsonify, request, abort, render_template_string
app = Flask(__name__) # the web framework object
def get_db():
db = sqlite3.connect("gameshelf.db") # library: Python's built-in sqlite3
db.row_factory = sqlite3.Row # rows behave like dictionaries
return db
@app.route("/games") # route: this URL pattern -> this function
def list_games():
max_cost = request.args.get("max_cost", type=float) # Request object: the ?max_cost=30 part
if max_cost is None:
abort(400, description="max_cost must be a number")
db = get_db()
rows = db.execute(
"SELECT Name, Cost FROM Games WHERE Cost <= ? ORDER BY Cost ASC",
(max_cost,), # the ? placeholder keeps data separate from the SQL
).fetchall()
db.close()
return jsonify([dict(r) for r in rows]) # Response object: JSON body, status 200
@app.route("/games/<int:game_id>") # <int:game_id> captures the number from the URL
def show_game(game_id):
db = get_db()
row = db.execute("SELECT Name, Cost FROM Games WHERE ID = ?", (game_id,)).fetchone()
db.close()
if row is None:
abort(404) # no such game: send 404 Not Found
return render_template_string("<h1>{{ name }}</h1><p>${{ '%.2f' % cost }}</p>",
name=row["Name"], cost=row["Cost"])
Save it as app.py, create the database, and run flask run (the server listens on 127.0.0.1:5000). From another terminal, curl -i prints the full response including headers. This is what four requests return (the Server and Date header lines depend on your software and clock, and are trimmed here):
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 103
[{"Cost":20.0,"Name":"Kelp Quest"},{"Cost":25.0,"Name":"Reef Runner"},{"Cost":30.0,"Name":"Lava Lab"}]
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 33
<h1>Star Sailor</h1><p>$45.00</p>
HTTP/1.1 404 NOT FOUND
HTTP/1.1 400 BAD REQUEST
...
<p>max_cost must be a number</p>
Flask's JSON output sorts the keys, which is why Cost comes before Name; the client should never depend on key order. The terminal running flask run also logs one line per request, such as "GET /games?max_cost=30 HTTP/1.1" 200 -, which is the first place to look when a request misbehaves.
Observing the process
Matching the code to the dot point:
| Element | In the code above | Role in the request |
|---|---|---|
| Web server software | Not visible: flask run starts a simple development server | In production, Nginx or Apache receives the request, handles HTTPS and static files, and passes dynamic requests to the application through a gateway such as Gunicorn (using the WSGI standard) |
| Web framework | Flask, @app.route, abort | Matches the URL and method to a function, parses the request, builds the response, converts errors to status codes |
| Objects | request, and the response made by jsonify; the row objects from the database | Wrap raw HTTP and database results as objects with properties and methods |
| Libraries | sqlite3, render_template_string (which uses Jinja2) | Reusable code for database access and page templating |
| Database | gameshelf.db and the SQL string | Stores and returns the data the handler needs |
The ? placeholder deserves attention. If the code built the query by joining strings, a value such as 0 OR 1=1 could change what the query means (SQL injection). With a placeholder the database receives the SQL and the value separately, so the value is always treated as data.
Hands-on: watch a request being handled
- Save the program, create the database and start it with
flask run. - Open
http://127.0.0.1:5000/games?max_cost=30in a browser with the Network panel open. Confirm the status 200, theapplication/jsoncontent type and the body. - Change 30 to
abc, then change the path to/games/99. Note each status code and match it to the line in the code that produced it. - Add
print(request.headers)at the top oflist_games, reload, and read the headers your browser sent (such asHostandUser-Agent) in the terminal.
A clear walkthrough of how a client sends a request and a server sends back data — exactly the back-end request/response process described above.
Web Applications and Shell Scripts
🎯 (SE-12-02)
📌 Develop a web application using an appropriate scripting language with shell scripts to make files and directories, and searching for text in a text file
A scripting language such as Python runs the web application, while shell scripts automate jobs around it: setting up folders, creating files and searching logs. Together they save time and make set-up repeatable on any computer.
Worked example: a set-up script
Before writing any Flask code, the team needs the same folder layout on every computer. A shell script records the steps once, so anyone can repeat them exactly. This one is saved as setup.sh and run with bash setup.sh:
#!/usr/bin/env bash
set -e # stop at the first error
mkdir -p gameshelf/{static/{css,js,images},templates,logs} # nested directories in one command
touch gameshelf/app.py gameshelf/requirements.txt # create empty files
echo "Flask" > gameshelf/requirements.txt # > writes text into a file
echo "Created:"
find gameshelf | sort # list everything created
Created:
gameshelf
gameshelf/app.py
gameshelf/logs
gameshelf/requirements.txt
gameshelf/static
gameshelf/static/css
gameshelf/static/images
gameshelf/static/js
gameshelf/templates
The -p flag creates parent folders as needed and does not complain if a folder already exists, so the script is safe to run twice. > replaces a file's contents and >> adds to the end. The braces expand into every combination, which saves typing. On Windows PowerShell, the equivalents are New-Item -ItemType Directory, New-Item for files and Select-String for searching; the ideas are the same.
Worked example: searching a text file
The web app writes a log file. Suppose logs/app.log contains these six lines:
2026-09-29 09:01:12 INFO GET /games 200
2026-09-29 09:01:15 ERROR GET /games/99 404 game not found
2026-09-29 09:02:03 INFO POST /login 200
2026-09-29 09:02:41 ERROR POST /login 401 bad password
2026-09-29 09:03:09 WARN GET /games?max_cost=abc 400
2026-09-29 09:04:30 ERROR GET /games/7 500 database timeout
$ grep "ERROR" logs/app.log
2026-09-29 09:01:15 ERROR GET /games/99 404 game not found
2026-09-29 09:02:41 ERROR POST /login 401 bad password
2026-09-29 09:04:30 ERROR GET /games/7 500 database timeout
$ grep -n "POST" logs/app.log
3:2026-09-29 09:02:03 INFO POST /login 200
4:2026-09-29 09:02:41 ERROR POST /login 401 bad password
$ grep -c "ERROR" logs/app.log
3
$ grep -i "bad password" logs/app.log
2026-09-29 09:02:41 ERROR POST /login 401 bad password
$ grep -v INFO logs/app.log
2026-09-29 09:01:15 ERROR GET /games/99 404 game not found
2026-09-29 09:02:41 ERROR POST /login 401 bad password
2026-09-29 09:03:09 WARN GET /games?max_cost=abc 400
2026-09-29 09:04:30 ERROR GET /games/7 500 database timeout
| Option | Meaning |
|---|---|
-n | Show line numbers |
-c | Count matching lines instead of showing them |
-i | Ignore upper and lower case |
-v | Invert: show the lines that do not match |
-r | Search every file in a folder and its sub-folders |
--include="*.log" | Only search files whose names match |
Worked example: the same search from Python, inside the web app
Shell scripts run around the application; Python runs inside it. This script makes a directory and a file (as the shell commands did) and searches the log. Then a route makes the result available to an administrator.
from pathlib import Path
def find_errors(log="logs/app.log"):
"""Return every line of the log that contains ERROR."""
return [line.rstrip() for line in open(log) if "ERROR" in line]
uploads = Path("static/uploads")
uploads.mkdir(parents=True, exist_ok=True) # like mkdir -p
(uploads / "README.txt").write_text("User uploads") # like echo ... > file
errors = find_errors()
print(len(errors), "errors found")
for line in errors:
print(line)
print(sorted(p.name for p in Path("static").iterdir()))
3 errors found
2026-09-29 09:01:15 ERROR GET /games/99 404 game not found
2026-09-29 09:02:41 ERROR POST /login 401 bad password
2026-09-29 09:04:30 ERROR GET /games/7 500 database timeout
['css', 'images', 'js', 'uploads']
@app.route("/admin/errors")
def error_count():
# a real application would check the user's role first (authorisation)
return jsonify(count=len(find_errors())) # {"count": 3} for the log above
Two more commands from the same family: find . -name "*.py" -mtime -1 lists Python files changed in the last day, and find templates/ -name "*.html" | xargs wc -l counts the lines in every template (the | pipe passes one command's output to the next).
Choose the tool by the job: shell commands are quick for one-off tasks, file searching and set-up scripts that need no program logic; Python is better when the result must feed into the application, needs conditions and loops, or must run on any operating system.
mkdir (with -p), touch, cd, ls and grep (with -n, -i, -c). Explain each command in a line and predict the output for a small sample file; a common slip is forgetting that grep is case-sensitive unless -i is used.setup.sh script in your project. Anyone who clones the repository can run one command to create the folders, install packages and start the server.Web-Based Databases and SQL
🎯 (SE-12-06, SE-12-08)
📌 Apply a web-based database and construct script that executes SQL
- selecting fields
- incorporating ‘group by’
- common SQL queries
- constraints using WHERE keyword
- table joins
Web applications keep their data in a relational database and read and change it with Structured Query Language (SQL). The Course Specifications set the syntax used in the HSC: SELECT the fields or calculated values to display, FROM the tables to use, WHERE the search criteria, GROUP BY the fields that group the rows and ORDER BY the fields that set the sequence. AS renames a field for display. Every query on this page runs on one running database, the GameShelf catalogue, whose three tables and keys are the ones in the Specifications' example.
The running database
| Table | Fields (P = primary key, F = foreign key) | Relationship |
|---|---|---|
| Publishers | Publisher_ID (P), Name | One publisher has many games |
| Developers | Developer_ID (P), First_name, Last_name | One developer has many games |
| Games | ID (P), Name, Release_date, Cost, Publisher_ID (F), Developer_ID (F) | Each game has one publisher and one developer, held as foreign keys |
The tables contain this data (dates are written year-month-day, which databases such as SQLite compare correctly as text; the Specifications' examples write them as '01/03/2022', so follow the format your exam environment gives):
Publisher_ID Name
------------ -------------
1 Games Inc
2 Pixel Harbour
3 Coral Studios
(3 rows)
Developer_ID First_name Last_name
------------ ---------- ---------
1 Ava Nguyen
2 Ben Clarke
3 Chloe Zhang
4 Dev Patel
(4 rows)
ID Name Release_date Cost Publisher_ID Developer_ID
-- --------------- ------------ ----- ------------ ------------
1 Reef Runner 2022-03-15 25.00 1 1
2 Lava Lab 2022-11-02 30.00 1 2
3 Kelp Quest 2021-08-09 20.00 1 1
4 Star Sailor 2023-03-20 45.00 2 1
5 Bush Tracker 2023-06-01 35.00 2 3
6 Quest for Coral 2023-01-12 40.00 3 3
7 Outback Odyssey 2022-03-31 50.00 1 2
(7 rows)
Notice that Dev Patel has no games and that the same developer appears many times in Games. That is why developers are in their own table: each fact is stored once and linked by a key. To recreate the database on your own computer, run this once (it also creates the gameshelf.db file that the Flask examples use):
import sqlite3
db = sqlite3.connect("gameshelf.db")
db.executescript("""
DROP TABLE IF EXISTS Games;
DROP TABLE IF EXISTS Publishers;
DROP TABLE IF EXISTS Developers;
CREATE TABLE Publishers (Publisher_ID INTEGER PRIMARY KEY, Name TEXT);
CREATE TABLE Developers (Developer_ID INTEGER PRIMARY KEY, First_name TEXT, Last_name TEXT);
CREATE TABLE Games (
ID INTEGER PRIMARY KEY, Name TEXT, Release_date TEXT, Cost REAL,
Publisher_ID INTEGER REFERENCES Publishers(Publisher_ID),
Developer_ID INTEGER REFERENCES Developers(Developer_ID));
INSERT INTO Publishers VALUES (1,'Games Inc'), (2,'Pixel Harbour'), (3,'Coral Studios');
INSERT INTO Developers VALUES (1,'Ava','Nguyen'), (2,'Ben','Clarke'), (3,'Chloe','Zhang'), (4,'Dev','Patel');
INSERT INTO Games VALUES
(1,'Reef Runner','2022-03-15',25.00,1,1), (2,'Lava Lab','2022-11-02',30.00,1,2),
(3,'Kelp Quest','2021-08-09',20.00,1,1), (4,'Star Sailor','2023-03-20',45.00,2,1),
(5,'Bush Tracker','2023-06-01',35.00,2,3), (6,'Quest for Coral','2023-01-12',40.00,3,3),
(7,'Outback Odyssey','2022-03-31',50.00,1,2);
""")
db.commit()
db.close()
print("gameshelf.db created")
Selecting fields
List the fields you want after SELECT. Use * only for exploring; naming fields sends less data and survives a new column being added. A calculated value can be given a display name with AS:
SELECT Name, Cost
FROM Games
SELECT Name, Cost - 5 AS Sale_price
FROM Games
Name Sale_price
--------------- ----------
Reef Runner 20.00
Lava Lab 25.00
Kelp Quest 15.00
Star Sailor 40.00
Bush Tracker 30.00
Quest for Coral 35.00
Outback Odyssey 45.00
(7 rows)
Rows come back in no guaranteed order unless you use ORDER BY, so never rely on the order shown by a query without it.
Constraints using WHERE
WHERE keeps only the rows for which the condition is true. The Specifications list relational operators by name; in an SQL engine you write the symbol, and CONTAINS becomes a LIKE pattern where % stands for "any characters".
| Specifications' operator | Written in SQL | Example |
|---|---|---|
| EQUALS | = | Publishers.Name = 'Games Inc' |
| NOT EQUAL TO | <> (or !=) | Publisher_ID <> 1 |
| GREATER THAN / GREATER THAN OR EQUAL TO | > / >= | Cost > 30 |
| LESS THAN / LESS THAN OR EQUAL TO | < / <= | Release_date <= '2023-03-31' |
| CONTAINS | LIKE '%text%' | Name LIKE '%Quest%' |
| DOES NOT CONTAIN | NOT LIKE '%text%' | Name NOT LIKE '%Quest%' |
| AND, OR, NOT | the same words | Cost < 25 OR Cost > 45 |
Text and date values go in single quotes; numbers do not. The first query below is the Specifications' date-range example, using this database:
SELECT Name, Release_date
FROM Games
WHERE Release_date >= '2022-03-01' AND Release_date <= '2023-03-31'
ORDER BY Name ASC
Name Release_date
--------------- ------------
Lava Lab 2022-11-02
Outback Odyssey 2022-03-31
Quest for Coral 2023-01-12
Reef Runner 2022-03-15
Star Sailor 2023-03-20
(5 rows)
SELECT Name, Cost FROM Games WHERE Name LIKE '%Quest%'
SELECT Name, Cost FROM Games
WHERE Cost >= 30 AND NOT Publisher_ID = 1
ORDER BY Cost DESC
SELECT Name, Cost FROM Games
WHERE Cost < 25 OR Cost > 45
ORDER BY Cost ASC
Name Cost
--------------- -----
Kelp Quest 20.00
Quest for Coral 40.00
(2 rows)
Name Cost
--------------- -----
Star Sailor 45.00
Quest for Coral 40.00
Bush Tracker 35.00
(3 rows)
Name Cost
--------------- -----
Kelp Quest 20.00
Outback Odyssey 50.00
(2 rows)
ORDER BY takes the field and a method: ASC (A to Z, 0 to 9) or DESC (Z to A, 9 to 0). Both Kelp Quest and Quest for Coral match '%Quest%' because the pattern can appear anywhere in the name.
Incorporating GROUP BY
GROUP BY collapses the rows that share a value into one row per group, and functions such as SUM, AVG, COUNT, MAX and MIN calculate one value for each group. Reading the query below: find each game's publisher, group the games by publisher, then for each group count the games, average the cost and find the dearest.
SELECT Publishers.Name AS Publisher, COUNT(Games.ID) AS Titles,
AVG(Games.Cost) AS AvgCost, MAX(Games.Cost) AS Dearest
FROM Games, Publishers
WHERE Publishers.Publisher_ID = Games.Publisher_ID
GROUP BY Publishers.Publisher_ID
ORDER BY Titles DESC
Publisher Titles AvgCost Dearest
------------- ------ ------- -------
Games Inc 4 31.25 50.00
Pixel Harbour 2 40.00 45.00
Coral Studios 1 40.00 40.00
(3 rows)
Check it by hand: Games Inc published Reef Runner (25), Lava Lab (30), Kelp Quest (20) and Outback Odyssey (50), so the count is 4, the total is 125 and the average is 125 ÷ 4 = 31.25. Every field in SELECT that is not a calculation should be one of the fields you group by (or be determined by them); some database engines reject a query that breaks this rule.
Table joins
A game holds only a publisher's and developer's ID numbers. To show names, the query must link the tables by matching each foreign key to its primary key. The Specifications join by listing the tables in FROM and adding the matching conditions to WHERE. This is their second example on the running database:
SELECT Developers.First_name, Developers.Last_name, SUM (Games.cost) AS Totalcost
FROM Games, Publishers, Developers
WHERE Publishers.Name = 'Games Inc'
AND Publishers.Publisher_ID = Games.Publisher_ID
AND Developers.Developer_ID = Games.Developer_ID
GROUP BY Developers.Developer_ID
ORDER BY Developers.Last_name DESC
First_name Last_name Totalcost
---------- --------- ---------
Ava Nguyen 45.00
Ben Clarke 80.00
(2 rows)
Only Games Inc's four games count. Ava's two (Reef Runner and Kelp Quest) total 45 and Ben's two (Lava Lab and Outback Odyssey) total 80; Chloe made no game for Games Inc, so she does not appear. The same idea is written with an explicit JOIN keyword in most SQL you will meet outside the exam:
SELECT Games.Name, Developers.Last_name
FROM Games INNER JOIN Developers ON Games.Developer_ID = Developers.Developer_ID
WHERE Games.Cost > 30
ORDER BY Games.Name ASC
SELECT Developers.First_name, Developers.Last_name, COUNT(Games.ID) AS Titles
FROM Developers LEFT JOIN Games ON Developers.Developer_ID = Games.Developer_ID
GROUP BY Developers.Developer_ID
ORDER BY Developers.Last_name ASC
Name Last_name
--------------- ---------
Bush Tracker Zhang
Outback Odyssey Clarke
Quest for Coral Zhang
Star Sailor Nguyen
(4 rows)
First_name Last_name Titles
---------- --------- ------
Ben Clarke 2
Ava Nguyen 3
Dev Patel 0
Chloe Zhang 2
(4 rows)
An inner join keeps only rows with a match on both sides. A left join keeps every row from the left table, filling the missing side with NULL, which is how Dev Patel appears with 0 titles. COUNT(Games.ID) counts only non-null values, so it correctly gives 0. Leave out the matching condition and the database pairs every row with every row (a Cartesian product):
SELECT COUNT(*) AS Rows_returned
FROM Games, Publishers, Developers
Rows_returned
-------------
84
(1 row)
That is 7 × 3 × 4 = 84 rows of nonsense, and a good sign to look for when a result has far too many rows.
Try it
SQL practice: games, publishers and developers
Write SELECT queries with joins, GROUP BY and aggregate functions on the running database. Enable JavaScript to use this tool. In short: SELECT chooses the fields, JOIN links tables on matching keys, WHERE filters rows, GROUP BY makes one row per group for COUNT, SUM, AVG, MAX and MIN, and ORDER BY sorts.
Common SQL queries
| Statement | Purpose | Example on the running database |
|---|---|---|
SELECT | Retrieve records | SELECT * FROM Games; |
INSERT INTO | Add a record | INSERT INTO Games (Name, Release_date, Cost, Publisher_ID, Developer_ID) VALUES ('Tide Pool', '2024-02-10', 18.00, 3, 4); |
UPDATE | Change records | UPDATE Games SET Cost = 22.00 WHERE ID = 3; |
DELETE | Remove records | DELETE FROM Games WHERE ID = 3; |
WHERE | Filter which rows are affected | WHERE Cost < 25 AND Publisher_ID = 1 |
GROUP BY | One row per group, with SUM, COUNT, AVG, MAX, MIN | GROUP BY Publisher_ID |
INNER JOIN | Rows with matching keys in both tables | JOIN Developers ON Games.Developer_ID = Developers.Developer_ID |
LEFT JOIN | All rows from the left table, NULL where no match | Developers who have made no games |
Always give UPDATE and DELETE a WHERE. DELETE FROM Games; removes every game.
A script that executes SQL
From a web application, SQL is sent as text by a script. This program (run in the folder containing gameshelf.db) executes two queries, passing the user's value as a parameter rather than joining it into the SQL string.
import sqlite3
db = sqlite3.connect("gameshelf.db")
publisher = "Games Inc" # in a web app this would come from the request
rows = db.execute(
"SELECT Games.Name, Games.Cost "
"FROM Games, Publishers "
"WHERE Publishers.Name = ? AND Publishers.Publisher_ID = Games.Publisher_ID "
"ORDER BY Games.Cost DESC",
(publisher,), # the value is sent separately from the SQL
).fetchall()
for name, cost in rows:
print(f"{name}: ${cost:.2f}")
total = db.execute("SELECT SUM(Cost) FROM Games").fetchone()[0]
print("Catalogue value:", total)
db.close()
Outback Odyssey: $50.00
Lava Lab: $30.00
Reef Runner: $25.00
Kelp Quest: $20.00
Catalogue value: 245.0
If the script had built the query as "... WHERE Publishers.Name = '" + publisher + "' ..." and a user supplied x' OR '1'='1, the condition would become true for every row and the query would return the whole table. That is SQL injection. The ? placeholder prevents it because the database never treats the value as SQL.
Common mistakes
| Symptom | Likely cause |
|---|---|
| Far too many rows | A missing join condition (Cartesian product) |
| An error about a field not being grouped | A non-calculated field in SELECT that is not in GROUP BY |
| No rows returned | Text compared with the wrong case or spelling, or quotes missing ('Games Inc' needs them) |
| Rows in a surprising order | No ORDER BY, so the order is not guaranteed |
| Everything changed or deleted | UPDATE or DELETE without WHERE |
Two different tables' Name fields confused | Field names not qualified: write Publishers.Name and Games.Name |
GROUP BY without an aggregate function, and for confusing AND with OR in a range test.Comparing ORM to SQL
🎯 (SE-12-06, SE-12-08)
📌 Compare Object-Relational Mapping (ORM) to SQL
| Method | Description | Pros & Cons |
|---|---|---|
| 🛠️ Raw SQL | Queries written directly in SQL and sent to the database engine. Provides fine-grained control over every aspect of the query. | Pros: Maximum performance; expressive power for complex JOINs, subqueries, and aggregations; universal — works with any SQL database. Cons: Vulnerable to SQL injection if inputs aren't parameterised; verbose for simple CRUD operations; query strings are hard to refactor. |
| 🛠️ ORM | Maps database tables to Python/JavaScript classes. Developers interact with objects; the ORM generates SQL automatically (e.g., Django ORM, SQLAlchemy, Sequelize). | Pros: Faster development for standard operations; built-in input sanitisation prevents injection; database-agnostic (swap PostgreSQL for MySQL with minimal changes); code is more readable and object-oriented. Cons: Can generate inefficient SQL for complex queries; adds an abstraction layer that obscures what is actually executed; requires learning the ORM API on top of SQL. |
What an ORM maps
The Course Specifications describe an ORM as a layer of abstraction between the database and the programming language: in an object-oriented language, database rows are represented as objects and their columns as properties of the object. Students are expected to interpret code fragments used in an ORM framework, so the goal here is to read ORM code fluently, not to memorise one library.
| Database idea | ORM idea | In the GameShelf example |
|---|---|---|
| Table | Class | Games table, Game class |
| Row (record) | Object (instance) | One Game object for Kelp Quest |
| Column (field) | Attribute (property) | game.Cost |
| Foreign key | Relationship between objects | game.developer gives the Developer object |
| SQL query | Method calls that build the query | select(Game).where(Game.Cost > 30) |
Worked example: the running database as classes
This code uses SQLAlchemy, a widely used Python ORM (installed with pip install sqlalchemy). It describes the same three tables that setup_db.py created. Each class is one table; each attribute line is one column; ForeignKey and relationship describe the links.
from sqlalchemy import create_engine, ForeignKey, select, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session
class Base(DeclarativeBase):
pass
class Publisher(Base):
__tablename__ = "Publishers" # the class maps to this table
Publisher_ID: Mapped[int] = mapped_column(primary_key=True)
Name: Mapped[str]
games: Mapped[list["Game"]] = relationship(back_populates="publisher")
class Developer(Base):
__tablename__ = "Developers"
Developer_ID: Mapped[int] = mapped_column(primary_key=True)
First_name: Mapped[str]
Last_name: Mapped[str]
games: Mapped[list["Game"]] = relationship(back_populates="developer")
class Game(Base):
__tablename__ = "Games"
ID: Mapped[int] = mapped_column(primary_key=True)
Name: Mapped[str] # each attribute maps to a column
Release_date: Mapped[str]
Cost: Mapped[float]
Publisher_ID: Mapped[int] = mapped_column(ForeignKey("Publishers.Publisher_ID"))
Developer_ID: Mapped[int] = mapped_column(ForeignKey("Developers.Developer_ID"))
publisher: Mapped[Publisher] = relationship(back_populates="games")
developer: Mapped[Developer] = relationship(back_populates="games")
engine = create_engine("sqlite:///gameshelf.db")
Now the same queries as the SQL section, first as SQL and then as ORM code. The first query lists games costing more than 30 in name order, and shows each one's developer:
from models import *
with Session(engine) as session:
query = select(Game).where(Game.Cost > 30).order_by(Game.Name)
for game in session.scalars(query):
print(game.Name, game.Cost, game.developer.Last_name) # follows the foreign key
Bush Tracker 35.0 Zhang
Outback Odyssey 50.0 Clarke
Quest for Coral 40.0 Zhang
Star Sailor 45.0 Nguyen
The query object is built in Python, but the ORM turns it into SQL for you. It sends something equivalent to SELECT ... FROM Games WHERE Games.Cost > ? ORDER BY Games.Name, with the value 30 passed separately, which is also why an ORM protects against SQL injection by default. Reading a game's developer makes a further query for that developer.
The Specifications' grouping example (each developer's total for 'Games Inc') is written like this. The join calls replace the matching conditions in WHERE, and group_by, order_by and func.sum correspond directly to GROUP BY, ORDER BY and SUM:
from models import *
with Session(engine) as session:
query = (
select(Developer.First_name, Developer.Last_name, func.sum(Game.Cost).label("Totalcost"))
.join(Game).join(Publisher)
.where(Publisher.Name == "Games Inc")
.group_by(Developer.Developer_ID)
.order_by(Developer.Last_name.desc())
)
for row in session.execute(query):
print(row.First_name, row.Last_name, row.Totalcost)
Ava Nguyen 45.0
Ben Clarke 80.0
Changing data is where an ORM feels most like ordinary object-oriented code: you load an object, change its attribute and commit. Add a new object and commit to insert a row.
from models import *
with Session(engine) as session:
game = session.get(Game, 3) # SELECT ... WHERE ID = 3
print(game.Name, game.Cost, game.publisher.Name)
game.Cost = 22.00 # change the attribute
session.commit() # the ORM runs UPDATE Games SET Cost = 22.0 WHERE ID = 3
session.add(Game(Name="Tide Pool", Release_date="2024-02-10", Cost=18.00, Publisher_ID=3, Developer_ID=4))
session.commit() # the ORM runs INSERT INTO Games ...
Kelp Quest 20.0 Games Inc
This changes the database, so run setup_db.py again to restore the original data before trying other examples. Other frameworks express the same ideas differently: Django writes the first query as Game.objects.filter(cost__gt=30).order_by("name"), but the mapping of tables to classes, rows to objects and columns to attributes is identical, which is what the Course Specifications ask you to recognise.
Comparing the two approaches
| Criterion | Raw SQL | ORM |
|---|---|---|
| Speed of writing routine code | Slower: text queries and manual conversion of rows | Faster: create, read, update and delete through objects |
| Readability for programmers | Good for people who know SQL; strings hide errors until run time | Reads like the rest of the program, with editor help and checks |
| Control over the query | Complete; can use every database feature | Less direct; the generated SQL may be inefficient (for example one query per object in a loop) |
| Performance tuning | Direct: you write the exact query | Needs knowledge of how the ORM builds queries, or a raw SQL escape hatch |
| Switching database engine | Some SQL differs between engines | The ORM hides most differences |
| Security | Safe only if every query is parameterised | Parameterises by default, though raw strings passed to it are still risky |
| Learning cost | Learn SQL | Learn SQL and the ORM |
In practice teams often use both: the ORM for everyday operations on single records, and raw SQL for a reporting query that joins several tables and must be fast.
filter or where is a WHERE clause. A full-mark comparison gives a similarity and a difference, and a benefit and a drawback of each, not a list of one-word labels. A common mistake is claiming an ORM is always slower or that raw SQL is always insecure.Front-End and Back-End Collaboration
🎯 (SE-12-05, SE-12-09)
📌 Describe how collaborative work practices between front-end and back-end developers improve the development of a web solution
API Contracts and Documentation
The primary collaboration mechanism between front-end and back-end developers is the API contract — an agreed-upon specification of every endpoint's URL, HTTP method, request parameters, and response format. Tools like OpenAPI/Swagger allow teams to document and even mock APIs before the back end is fully built, enabling front-end and back-end development to proceed simultaneously. This prevents integration errors at the end of the project and enables both teams to work independently with confidence.
Worked example: an API contract
Before either team writes code, they agree on the contract. This fragment (in the OpenAPI format, which is widely used to document web APIs) says that GET /games needs a numeric max_cost and returns a list of objects with a name and a cost:
openapi: 3.0.3
info: { title: GameShelf API, version: "1.0" }
paths:
/games:
get:
summary: List games costing no more than max_cost
parameters:
- { name: max_cost, in: query, required: true, schema: { type: number } }
responses:
"200":
description: A list of games
content:
application/json:
schema:
type: array
items:
type: object
properties:
Name: { type: string }
Cost: { type: number }
"400":
description: max_cost is missing or not a number
With the contract written down, the front-end developer can build against fake data that has the agreed shape (a small mock that returns [{ Name: 'Kelp Quest', Cost: 20 }] is enough), and swap in the real API later.
Meanwhile the back-end developer writes a contract test that fails if the real API drifts from the agreement:
def test_games_contract(client):
response = client.get("/games?max_cost=30")
assert response.status_code == 200
for game in response.get_json():
assert set(game) == {"Name", "Cost"} # exactly the agreed fields
assert isinstance(game["Cost"], (int, float))
Run against the GameShelf route, the test passes silently. Now suppose the back-end developer renames Cost to Price without telling anyone. Without the test, the front end fails in users' browsers with a message like Cannot read properties of undefined (reading 'toFixed'). With it, the change fails the test in seconds, before it is merged. The fix is a conversation: either revert the rename, or agree on a new version of the contract (for example /v2/games) and change both sides together.
Version Control and Branching Strategies
Teams use Git branching workflows to manage parallel development. In the Git Flow model: the main branch always contains production-ready code; develop is the integration branch for completed features; and individual feature/ branches are created for each task. Pull Requests (PRs) are submitted when a feature is complete — requiring at least one peer code review before merging. This enforces code quality and knowledge sharing across the team, and ensures no one developer is a single point of failure for any part of the system.
Open-Source and Ethical Collaboration
Collaborative web development carries ethical responsibilities. Teams must fairly attribute contributions, respect the licences of all third-party code used, ensure equitable division of workload, and maintain transparent communication with stakeholders about progress and setbacks. When using open-source code libraries, teams must monitor for security vulnerabilities and update dependencies promptly to protect users.
Practices and what they prevent
| Practice | How it helps the two teams | Problem it prevents |
|---|---|---|
| API contract and mock data | Both sides build in parallel against an agreed shape | Waiting for the other team; "it worked on my side" integration failures |
| Shared repository, branches and pull requests | Every change is reviewed by a second person, often from the other team | Unreviewed changes breaking the live site; overwriting each other's files |
| Agreed error handling | The back end returns consistent status codes and messages; the front end shows friendly text for each | Blank screens and unexplained failures |
| Issue tracker and regular stand-ups | Visible tasks, owners and blockers | Duplicated work and forgotten tasks |
Designing a Progressive Web App
🎯 (SE-12-02, SE-12-03, SE-12-06)
📌 Design, develop and implement a progressive web app (PWA)
- the application of design and user interface (UI) and user experience (UX) principles of font, colour, audio, video and navigation
- a UI that considers accessibility and inclusivity
UI and UX Principles
Typography
Select legible, web-safe or Google Fonts typefaces. Establish a clear typographic hierarchy (H1 → H2 → body → caption). Minimum body font size of 16px for readability. Sufficient line-height (1.5–1.7) for comfortable reading. Limit typeface count to 2 maximum.
Colour
Use a defined colour palette: primary, secondary, and accent colours plus semantic colours (danger red, success green). Ensure a minimum WCAG AA contrast ratio of 4.5:1 between text and background. Avoid conveying information by colour alone (for colour-blind users).
Audio and Video
Provide controls for all auto-playing media. Supply captions/subtitles for video content (WCAG requirement). Compress media for fast loading. Avoid content that flashes more than 3 times per second (seizure risk).
Navigation
Consistent navigation structure across all views. Clear visual indication of the current page (active state). Breadcrumbs for deep navigation hierarchies. Touch-friendly tap targets (minimum 44×44px). Logical tab order for keyboard navigation.
5-Point Syllabus Compliance Checklist for PWA UX
Ensure your software engineering project meets these core NESA user experience standards:
- Responsiveness: Adapts cleanly to mobile, tablet, and desktop viewports.
- Accessibility: Achieves WCAG AA contrast and full keyboard operability.
- Offline Grace: Displays a custom UI (via Service Worker) when the network drops, rather than the browser's default error.
- Installability: Provides a valid
manifest.jsonso the app can be pinned to the home screen. - Consistency: Uses a predictable interaction model and uniform colour palette across all screens.
Accessibility and Inclusivity
PWA interfaces must be designed to be usable by everyone, including people with visual, auditory, cognitive, or motor disabilities:
<button> for buttons, <nav> for navigation, <main> for content). Screen readers rely on semantic structure to describe pages to blind users.alt attribute text. Decorative images use alt="" so screen readers skip them.aria-label, aria-expanded, role) describe the state and purpose of dynamic UI components to assistive technologies.Core PWA Technical Requirements
Web App Manifest
A JSON file (manifest.json) that defines the app's name, icons, theme colour, and display mode (fullscreen/standalone). Enables the "Add to Home Screen" install prompt in mobile browsers.
Service Worker
A JavaScript file that runs in the background as a network proxy. Intercepts fetch requests to serve cached assets when offline, enable push notifications, and implement background sync for reliable data submission.
HTTPS Required
PWAs must be served over HTTPS — service workers are restricted to secure origins. HTTPS also builds user trust and is a requirement for browser installability.
const CACHE_NAME = 'pwa-cache-v1';
const ASSETS_TO_CACHE = ['/', '/index.html', '/css/styles.css', '/js/app.js'];
// Install event: cache core app shell assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS_TO_CACHE))
);
});
// Fetch event: serve from cache, fallback to network
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cached => cached || fetch(event.request))
);
});
Flowchart
A service worker serving cached files
A cache-first strategy answers from the cache and only uses the network when it must.
What to notice
- Cached files load instantly and work offline.
- The offline page means the app never shows a blank error.
Try thisNews headlines change often. Would cache-first suit them? What would you change?
Web App Manifest
The manifest.json file tells the browser how to install and present the PWA. Link it in your HTML <head>: <link rel="manifest" href="/manifest.json">
{
"name": "HSC Study Planner",
"short_name": "StudyApp",
"description": "A PWA for organising HSC study sessions",
"start_url": "/index.html",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#4f46e5",
"orientation": "portrait",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}
PWA vs Native App
| Feature | PWA | Native App |
|---|---|---|
| Installation | Add to home screen via browser | App Store / Play Store download |
| Offline capability | Yes — via service worker cache | Yes — full local storage |
| Performance | Good — limited by browser | Excellent — direct hardware access |
| Updates | Instant — update the web files | User must download update |
| Device features | Limited (camera, GPS, notifications) | Full access to all device APIs |
| Development cost | Low — one codebase for all platforms | High — separate iOS and Android teams |
The service worker lifecycle
A service worker is a script the browser runs separately from the page. It has its own lifecycle, and most PWA bugs come from misunderstanding it.
- Register. The page asks the browser to install the worker (code below). The browser downloads
sw.js. - Install. The worker's
installevent runs once for each new version. It normally caches the app shell here. - Activate. When the new worker takes over, the
activateevent runs. This is the place to delete old caches. - Fetch. From now on, every request from pages the worker controls goes through its
fetchevent, where it decides whether to answer from the cache or the network.
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(reg => console.log('Service worker registered, scope:', reg.scope))
.catch(err => console.log('Registration failed:', err));
});
}
On a first visit the console shows Service worker registered, scope: https://…/. The worker controls pages only inside its scope (the folder it is served from), so sw.js is placed at the site root. A page loaded before the worker was installed is not controlled until it is reloaded.
Caching strategies
| Strategy | How it works | Best for | Drawback |
|---|---|---|---|
| Cache first | Answer from the cache; use the network only if it is not cached | The app shell, fonts, images and other files that rarely change | Users may see stale files until the cache version changes |
| Network first | Try the network; use the cache if the network fails | Data that changes often, such as headlines or prices | Slower when the connection is poor |
| Stale while revalidate | Answer from the cache immediately, fetch a fresh copy in the background for next time | Content where a slightly old copy is acceptable, such as avatars | The first view after a change is one version behind |
The earlier example is purely cache first. A more realistic worker uses network first for the API, cache first for the shell, cleans old caches when the version changes, and shows a friendly offline page instead of the browser's error:
const CACHE = 'gameshelf-v2'; // change this to release new files
const SHELL = ['/', '/index.html', '/css/site.css', '/js/app.js', '/offline.html'];
self.addEventListener('install', event => {
event.waitUntil(caches.open(CACHE).then(cache => cache.addAll(SHELL)));
});
self.addEventListener('activate', event => { // delete caches from old versions
event.waitUntil(
caches.keys().then(names => Promise.all(
names.filter(name => name !== CACHE).map(name => caches.delete(name))
))
);
});
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
if (url.pathname.startsWith('/games')) { // API data: network first
event.respondWith(
fetch(event.request).catch(() => caches.match(event.request))
);
} else { // everything else: cache first
event.respondWith(
caches.match(event.request).then(cached =>
cached || fetch(event.request).catch(() => caches.match('/offline.html')))
);
}
});
Testing that it really works offline
- Serve the app over HTTPS (or from
localhost, which browsers treat as secure for development) and load it once. - Open the dev tools, Application panel, Service Workers. You should see
sw.jslisted as activated and running. Under Cache Storage you should seegameshelf-v2containing the shell files. - Tick Offline (or switch off the computer's Wi-Fi) and reload. Expected result: the app shell still loads instantly; the list of games comes from the network-first fallback if you have loaded it before, or the offline page appears if you have not.
- Change
CACHEtogameshelf-v3, reload twice, and check Cache Storage: the oldgameshelf-v2cache is deleted after the new worker activates. - Run Lighthouse and read the installability and PWA checks it reports; fix anything it lists, such as a missing icon size or a manifest without a
start_url.
Browsers decide for themselves when to offer installation. The usual conditions are a secure connection, a linked manifest with a name, icons and a start URL, and (in some browsers) a registered service worker, so check the current requirements for the browsers you support rather than assuming them.
Worked example: accessible, inclusive markup
Accessibility is built into the markup, not added afterwards. This fragment gives a form a visible label, a status message announced by screen readers and a landmark structure for navigation:
<header>
<nav aria-label="Main">
<a href="/" aria-current="page">Home</a>
<a href="/games">Games</a>
</nav>
</header>
<main>
<h1>Find a game</h1>
<form>
<label for="max">Maximum price (AUD)</label>
<input id="max" name="max" type="number" min="0" inputmode="decimal" required>
<button type="submit">Search</button>
</form>
<p id="status" role="status">3 games found.</p> <!-- read out when its text changes -->
</main>
- The
labelis tied to the input withforandid, so a screen reader announces "Maximum price" and a tap on the label focuses the box. - Real
button,navandmainelements give keyboard focus, key handling and navigation landmarks for free; a clickabledivwould need all of that added by hand. role="status"makes a screen reader announce a change to the message without moving focus.
| Quick accessibility test | How | Pass looks like |
|---|---|---|
| Keyboard only | Put the mouse aside; use Tab, Shift+Tab, Enter and Space | Every control is reachable, the focus outline is visible and the order makes sense |
| Zoom and text size | Zoom the browser to 200% and narrow the window | Nothing is cut off and no horizontal scrolling for text content |
| Contrast | Check text against its background (see the W3C section) | At least 4.5:1 for normal text |
| Not by colour alone | View the page in greyscale | Errors and states are still clear from icons or words |
| Media | Play any video without sound; check for controls | Captions are available and nothing autoplays with sound |
| Motion | Turn on "reduce motion" in the operating system | Large animations stop or shrink (prefers-reduced-motion) |