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.
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.
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.
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.
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.
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.
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.
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.
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.
/users/42 is a resource; you GET it, PUT it, or DELETE it.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 |
Idempotent means doing the same operation multiple times produces the same result as doing it once — no extra side effects.
GET /users/42 five times returns the same user five times. No changes on the server.DELETE /users/42 twice — first call deletes, second returns 404. No extra harm from calling it again.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.
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?
Ideally, liking a post should be idempotent — double-clicking shouldn't add two likes. There are two clean approaches:
{"liked": true} — sets a flag rather than incrementing a counter. Calling it twice has the same result: the post is liked.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.
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
}
}
| 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.
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.
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...');
.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.
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.
"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:
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.
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.
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 changesWhen 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.
Below is a full HTTP request and response pair for a login endpoint. Work through the four questions below before revealing the answers.
Yes — this is a REST API. The evidence:
/auth/login represents the login resource — the action is the method, not a verb in the URL like /doLogin./api/v1/ indicates this is a versioned API, a REST best practice.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.
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.
3600 seconds = 1 hour.
Token expiry limits the damage window if a token is stolen. Compare:
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.
HSC exam style. Try writing your answer before expanding — even a rough answer is better practice than going straight to the solution.
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.
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.
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.