← Back to Blake's Learning Hub

Lesson 14: Client-Server Architecture & APIs

Naming what you already do — the theory behind fetch(), Vercel Edge, and API keys
March 2026 ~75 min Topics 3+4 of 6 Year 12 Module 2

1. The Client-Server Split ~12 min

SE-12-05

You've built Volcanic Pantheon. You know what runs in the browser — HTML, CSS, and your JavaScript. But what does "server-side" actually mean, and where is the line?

The web is built on a two-role model: a client that requests things, and a server that fulfils them. Every website you've ever visited follows this pattern.

Client (browser)

What runs in your browser

Renders HTML and CSS into a visual page. Executes JavaScript. Stores cookies and localStorage. Makes fetch() calls to APIs.

Has NO secrets. Anyone can open DevTools and see every byte of code, every variable, every network request.

Examples: Chrome, Firefox, Safari — and headless browsers that attackers use to automate requests.
Server

What runs behind the scenes

Runs code the client never sees. Holds API keys, database passwords, and business logic. Queries databases and builds responses.

The only place where rules can actually be enforced. The client can lie — the server shouldn't believe it.

Examples: Vercel Functions, Express.js, Django, PHP — anything that runs on a machine you don't control.

The trust boundary

Anything the client can do, an attacker can also do. JavaScript in the browser runs on the attacker's machine — they can modify it, intercept it, replay it. The server is the only place where you can enforce rules.

This is why "client-side validation only" is a security anti-pattern. Checking a form in JavaScript is helpful for user experience, but the server must check it too — because an attacker can skip the JavaScript entirely.

Volcanic Pantheon connection: Your original Volcanic Pantheon had a password check in JavaScript — meaning the attacker's browser ran that check and the attacker could just skip it. Moving to Vercel Edge Middleware moved that check to the server side, where the user can't bypass it. That's the single most important security change you made to the project.

Types of Web Architecture

Static Site

Pre-built files, no server logic

All HTML, CSS, and JS is built in advance. The server (or CDN) just delivers files — it runs no code per request. Fast, cheap, and scales effortlessly.

Limitation: can't personalise per user without client-side JS. Everyone gets the same files.

Example: Volcanic Pantheon on Vercel. Vercel serves your HTML/CSS/JS directly from a CDN.
Dynamic Site

Server generates HTML per request

When a request arrives, the server runs code — queries a database, builds an HTML page tailored to that user, and sends it. Can personalise, authenticate, and respond to state.

More powerful, but requires server infrastructure to stay running.

Examples: social media feeds, e-commerce product pages, banking dashboards. Traditional backends: PHP, Ruby on Rails, Django, Express.
Serverless Functions

On-demand server code, no persistent process

Server code that only runs when called — no persistent process sitting idle. You pay per invocation. Great for APIs and background tasks.

Cold starts can add latency on the first call after idle time.

Examples: Vercel Functions, Netlify Functions, AWS Lambda. Luis's tutoring site uses Netlify Functions for the student login API.
Edge Functions

Serverless, but geographically close

Like serverless functions, but deployed to data centres geographically near the user — not just one region. Sub-10ms response times for auth checks, redirects, and A/B tests.

Cannot run everything (e.g., no filesystem access), but perfect for lightweight logic at the network edge.

Vercel Edge Middleware is exactly this — that's why your auth check is fast whether a visitor is in Sydney or London.

2. REST APIs ~18 min

SE-12-05

REST (Representational State Transfer) is a set of conventions — not a strict protocol — for designing web APIs. It's the dominant style today, and almost every API you'll ever call follows it.

When you call fetch('https://api.example.com/users/42'), you're using a REST API. The URL design, the HTTP method, and the response format are all governed by REST conventions.

The three core REST principles

  • Stateless — every request contains all the information needed to process it. The server doesn't remember previous requests. If you need to stay authenticated, you include your token in every request — the server doesn't "remember" you from last time.
  • Resource-based — things (users, posts, products) are represented as URLs. Actions are expressed through HTTP methods, not verbs in the URL. /users/42 is a resource; you GET it, PUT it, or DELETE it.
  • Standard methods — GET reads, POST creates, PUT/PATCH updates, DELETE removes. The HTTP method tells the server what you want to do to the resource.

REST endpoint design

A well-designed REST API has predictable, consistent URL patterns:

Method Endpoint Action
GET /users List all users
GET /users/42 Get user with id 42
POST /users Create a new user
PUT /users/42 Replace user 42 entirely
PATCH /users/42 Partially update user 42
DELETE /users/42 Delete user 42

Idempotency — why it matters

Idempotent means doing the same operation multiple times produces the same result as doing it once — no extra side effects.

  • GET is idempotent: GET /users/42 five times returns the same user five times. No changes on the server.
  • DELETE is idempotent: DELETE /users/42 twice — first call deletes, second returns 404. No extra harm from calling it again.
  • POST is NOT idempotent: POST /users three times creates three users. Retrying on network failure means duplicates.

This matters for retry logic. If a network glitch causes your app to retry a request, idempotent calls are safe to repeat. POST calls are not.

Volcanic Pantheon connection: When Volcanic Pantheon calls your Vercel auth endpoint, that's a POST request — it creates a session. If the network glitches and retries, you could end up creating multiple sessions. Idempotency is why login endpoints should be carefully designed — often by checking if a session already exists before creating a new one.

Quick challenge: The "like" button problem

A user double-clicks the like button on a post — should this add two likes? What HTTP method and design would make the like action idempotent?

See the answer

Ideally, liking a post should be idempotent — double-clicking shouldn't add two likes. There are two clean approaches:

  • PATCH /posts/42 with body {"liked": true} — sets a flag rather than incrementing a counter. Calling it twice has the same result: the post is liked.
  • Server-side deduplication — the server checks if this user already liked this post before adding to the count. A second POST returns 409 Conflict or 200 OK without incrementing.

Twitter/X style: the like endpoint is designed to be idempotent by design — a second call either returns 200 OK (already liked) or 409 Conflict. The count never goes up twice from one user.

3. JSON ~8 min

SE-12-05

JSON (JavaScript Object Notation) is how modern APIs exchange data. Before JSON, most APIs used XML — JSON won because it's simpler, more readable, and native to JavaScript. You don't need a special parser; JSON.parse() is built into every browser.

{
  "student": {
    "id": 42,
    "name": "Blake",
    "lessons_completed": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
    "active": true,
    "last_seen": null
  }
}

JSON data types

Type Example Notes
String "Blake" Always double quotes — single quotes are invalid JSON
Number 42, 3.14 No quotes needed — integers and floats both work
Boolean true, false Lowercase — True is invalid JSON
Null null Lowercase — represents "no value"
Array [1, 2, 3] Ordered list — can mix types (though unusual)
Object {"key": "val"} Key-value pairs — keys must be strings in double quotes
// Making an API call and parsing JSON
const response = await fetch('https://api.example.com/users/42');
const data = await response.json(); // parses JSON string into JS object
console.log(data.student.name); // "Blake"

response.json() does JSON.parse() under the hood — it converts the JSON string the server sent into a JavaScript object you can work with. JSON.stringify() does the reverse: converts a JS object to a JSON string to send in a request body. You'll use both constantly when building APIs.

4. API Authentication ~12 min

SE-12-05

Most APIs require authentication — you need to prove who you are with every request. Remember: REST is stateless, which means your token or key must travel with every single call. There are three main patterns.

Pattern 1: API Keys

A shared secret the API provider gives you. Include it in every request as a header or query parameter. Simple and widely used for server-to-server calls.

// In a header (preferred — keeps it out of the URL/logs)
fetch('https://api.example.com/data', {
  headers: {
    'X-API-Key': 'sk-abc123...' // NEVER hardcode this — use .env
  }
});

// As a query param (less secure — appears in server logs and browser history)
fetch('https://api.example.com/data?api_key=sk-abc123...');
Volcanic Pantheon connection: This is exactly the pattern from Lesson 10. API keys must live in .env files and never be committed to git. You've seen what happens when they do — the GitHub secret scanning examples, the Log4Shell supply chain incidents. Same principle applies here.

Pattern 2: Bearer Tokens (JWT)

After login, the server issues a short-lived signed token. The client sends it with every subsequent request. The server can verify the token without a database lookup — the signature proves authenticity.

// After logging in, you receive a token
// Store it (in memory or HttpOnly cookie — not localStorage for sensitive apps)
const token = loginResponse.token;

// Send it with every authenticated request
fetch('https://api.example.com/profile', {
  headers: {
    'Authorization': `Bearer ${token}`
    // Format is always: "Bearer " + the token string
  }
});

Key difference from API keys: JWTs expire — typically 15 minutes to 24 hours. Even if a token is stolen, it stops working after expiry. API keys are long-lived and must be manually rotated if compromised. For user-facing auth, tokens are almost always the better choice.

Pattern 3: OAuth 2.0 (overview)

"Login with Google" or "Login with GitHub" is OAuth. The user grants your app permission to act on their behalf — without ever sharing their password with your app.

The flow in plain terms:

  1. Your app redirects the user to Google's login page
  2. User logs in to Google and sees "Authorize this app?" — they're granting specific permissions (e.g., read your email address)
  3. Google redirects back to your app with a temporary code
  4. Your server exchanges that code for an access token
  5. Your app uses that token to call Google APIs on behalf of the user

You've clicked "Authorize this app?" before — step 2 is the OAuth consent screen.

HSC exam cheat sheet: API keys = simple shared secret included in every request. Bearer tokens = short-lived JWT issued after login, sent in the Authorization header. OAuth = delegated third-party auth — user grants permission without sharing their password. You don't need to implement OAuth from scratch for the exam, but you need to explain what it does and why.

5. Rate Limiting & API Versioning ~5 min

Rate limiting

APIs limit the number of requests you can make per second, minute, or day to prevent abuse and protect their infrastructure. When you exceed the limit, the server returns 429 Too Many Requests.

Your app should handle this gracefully — not by hammering the API again immediately, but by waiting and retrying with exponential backoff: wait 1 second, then 2, then 4, then 8. Many APIs also return a Retry-After header telling you exactly how long to wait.

Rate limits are also why you cache API responses where possible — if you already have the data, there's no need to ask again.

API versioning

APIs evolve. Providers add fields, rename endpoints, change behaviour. To avoid breaking existing clients, they version their API:

  • /api/v1/users — original version, stable
  • /api/v2/users — new version with breaking changes

When you call an external API, pin to a specific version — use /v1/ not /latest/. That way, when the provider releases v2 with breaking changes, your code keeps working until you're ready to migrate.

6. Challenge: Anatomy of an API Call ~10 min

Read This API Exchange

Below is a full HTTP request and response pair for a login endpoint. Work through the four questions below before revealing the answers.

POST /api/v1/auth/login HTTP/1.1 Host: api.volcanoic-pantheon.app Content-Type: application/json Accept: application/json {"username": "blake", "password": "hunter2"} --- HTTP/1.1 200 OK Content-Type: application/json Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Strict { "token": "eyJhbGciOiJIUzI1NiJ9...", "expires_in": 3600, "user": { "id": 1, "username": "blake", "role": "admin" } }
Q1 — Is this REST? What makes you say so?

Yes — this is a REST API. The evidence:

  • HTTP methods used semantically: POST to create a session (a resource), not GET which should be read-only.
  • Resource-based URL: /auth/login represents the login resource — the action is the method, not a verb in the URL like /doLogin.
  • Stateless: all credentials are in the request body — the server doesn't rely on any prior context to process this.
  • JSON data exchange: both request and response bodies use JSON, with proper Content-Type headers.
  • Versioned: /api/v1/ indicates this is a versioned API, a REST best practice.
Q2 — What type of authentication will this client use for future requests?

Bearer token authentication — the server returned a JWT in the "token" field of the response body.

Future requests will include this token in the Authorization header:

Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

The server will verify the token's signature on each request to confirm the caller is authenticated, without needing to look up a session in a database.

Q3 — What does HttpOnly on the Set-Cookie do?

HttpOnly prevents JavaScript from reading the cookie via document.cookie. The cookie is sent automatically by the browser with every request to the same domain, but JS cannot access it directly.

Why this matters: an XSS attack injects malicious JavaScript into the page. If the session cookie were readable by JS, the attacker's script could steal it with document.cookie and send it to their own server — full session hijack.

With HttpOnly, even if XSS succeeds, the injected script cannot read the cookie. This is one of the XSS mitigations you covered in Lesson 9 — HttpOnly cookies are a defence-in-depth layer specifically against cookie theft via XSS.

Q4 — The token expires_in 3600 — what is that in hours, and why does expiry matter?

3600 seconds = 1 hour.

Token expiry limits the damage window if a token is stolen. Compare:

  • API key (no expiry): if stolen, works forever until manually revoked. The attacker has indefinite access.
  • JWT (1 hour expiry): if stolen, the attacker has at most one hour before the token stops working. After expiry, they'd need to authenticate again — which they can't do without the actual credentials.

For sensitive applications, tokens are made even shorter-lived (15 minutes) and paired with a long-lived refresh token that can issue new short-lived access tokens without requiring re-login.

7. Knowledge Check ~5 min

HSC exam style. Try writing your answer before expanding — even a rough answer is better practice than going straight to the solution.

Q1 — Explain, using specific examples, the difference between a static website and a server-side dynamic website.

A static website delivers pre-built files (HTML, CSS, JS) directly to the browser without any server-side processing per request. Example: a portfolio site like Volcanic Pantheon — Vercel simply serves the files it received at build time. Every visitor gets the same HTML. Static sites are fast and cheap because the server does no computation per request, but they can't personalise content without client-side JavaScript making API calls after the page loads.

A dynamic website generates HTML on the server for each request, typically by querying a database. Example: a social media feed — the server runs code like SELECT * FROM posts WHERE user_id = 42 ORDER BY created_at DESC, constructs an HTML page with that user's specific posts, and sends it. The server builds a different page for each user. Dynamic sites are more flexible and can personalise extensively, but they require server infrastructure that stays running and adds latency per request.

Q2 — A developer wants to allow users to "save" items to a favourites list. Should this endpoint be GET, POST, or PUT? Justify your answer.

POST or PUT — not GET.

GET should never cause side effects. It is a read-only operation. Using GET to save a favourite would mean that a browser pre-fetching links, a search engine crawling the site, or a user accidentally hovering a link could trigger saves — violating the principle that GET is safe and harmless.

POST /favourites creates a new favourite resource — appropriate if you're adding an item to a list.

PUT /favourites/{item_id} is more appropriate if you want the operation to be idempotent — saving the same item twice should not create two entries. PUT sets a state (this item is favourited), while POST creates a new record each call. For a favourites toggle, PUT is the cleaner design.

The key principle: GET is read-only and safe; any operation that changes server state belongs in POST, PUT, PATCH, or DELETE.

Q3 — Why must API keys be stored as environment variables rather than hardcoded in source code?

Source code is frequently committed to git, shared with teammates, and potentially published to public GitHub repositories. A hardcoded API key in source code is exposed to anyone with read access to the repository — including strangers if the repo is public.

Environment variables are loaded at runtime from a .env file that is excluded from git via .gitignore, or from the deployment platform's environment settings (e.g., Vercel's Environment Variables panel, Netlify's Site Settings). The key never appears in the codebase itself — so even if the repository is made public or a collaborator's machine is compromised, the secret remains private.

The code accesses the key as process.env.API_KEY (Node.js) or equivalent — a reference to a name, not the secret value itself. This also makes rotating a key simple: update the environment variable without touching any code.

Cross-reference: Lesson 10 covered the specific case of accidental key exposure and examples like GitHub secret scanning and supply chain attacks where leaked secrets caused major incidents.

← Back to Learning Hub