Capstone — putting the whole module together with form handling, validation, and backend anatomy
Forms are the original input mechanism of the web. When you submit a login form, a search box, a registration page — you're triggering a form submission. Let's trace what actually happens.
Every form has a few key attributes that control what happens when the user submits it.
<form action="/api/register" method="POST">
<label for="username">Username</label>
<input type="text" id="username" name="username" required minlength="3">
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
<label for="password">Password</label>
<input type="password" id="password" name="password" required minlength="8">
<button type="submit">Create Account</button>
</form>
| Attribute | What it does |
|---|---|
action="/api/register" |
The URL the form data is sent to when submitted |
method="POST" |
Sends data in the request body — not the URL. Critical for passwords. |
name="username" |
The key in the submitted data. Without name, the field isn't included. |
required |
HTML5 client-side validation — browser blocks submission if empty |
minlength="8" |
HTML5 minimum character count — browser enforces before sending |
type="email" |
Browser validates basic email format (contains @) before sending |
When you click Submit on a traditional form, the browser constructs an HTTP request that looks like this:
POST /api/register HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
username=blake&email=blake%40example.com&password=hunter2
The form fields become URL-encoded key=value pairs in the request body, joined by &. Special characters (like @) are percent-encoded (%40).
Most modern apps don't use raw form submissions. Instead, they intercept the submit event in JavaScript, collect the values, and send JSON via fetch(). This gives more control and returns a JSON response without a full page reload.
// Instead of a raw form submission, most modern apps use fetch
const formData = {
username: document.getElementById('username').value,
email: document.getElementById('email').value,
password: document.getElementById('password').value
};
const response = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
event.request.json() to get the submitted credentials. The pattern above is exactly what you already built.
Validation is checking that input meets requirements before processing it. There are two places validation can happen — and both are needed for different reasons.
Happens in JavaScript before the request is sent. Provides immediate feedback — red borders, inline error messages — without a network round-trip.
Uses HTML5 attributes (required, minlength, type="email") or custom JavaScript logic.
The catch: trivially bypassed. Open DevTools, disable JS, or use curl to send a raw HTTP request — you'll skip every HTML5 attribute and every JS check.
Happens on the server after receiving the request. The attacker cannot bypass code that runs on your machine — they have no control over the server.
Must validate everything: field presence, data types, lengths, formats, and business rules.
The tradeoff: slower (requires network round-trip) and returns HTTP error codes in the response (e.g. 400 Bad Request with error details).
If you only validate on the client side, an attacker using curl or Postman can send any data they want — bypassing every HTML5 required attribute and every JavaScript check. Server-side validation is non-negotiable. It is the only layer the attacker cannot skip.
Here's a minimal server-side validation function — the kind you'd call inside a Netlify Function or Express handler before touching the database:
function validateRegistration(data) {
const errors = [];
if (!data.username || data.username.length < 3) {
errors.push('Username must be at least 3 characters');
}
if (!data.email || !data.email.includes('@')) {
errors.push('Valid email required');
}
if (!data.password || data.password.length < 8) {
errors.push('Password must be at least 8 characters');
}
return errors; // empty array = valid
}
Return an empty array → valid. Return a non-empty array → send a 400 response with the error list.
A developer says: "We have validation in our React forms, so we don't need server validation." What's wrong with this reasoning?
React validation runs in the browser. An attacker can send HTTP requests directly to the API, completely bypassing the React app entirely. Using curl, Postman, or a short script, they can submit any data they like — they will never see a single React validation message.
Server validation is the only layer the attacker cannot skip, because it runs on a machine they don't control. Client-side validation is a UX feature. Server-side validation is a security requirement.
Luis's tutoring site uses Netlify Functions for the student login API. This is a real, working serverless backend you can look at — and the structure is worth understanding line by line.
// netlify/functions/auth.cjs
exports.handler = async function(event, context) {
// 1. Only allow POST
if (event.httpMethod !== 'POST') {
return {
statusCode: 405,
body: JSON.stringify({ error: 'Method not allowed' })
};
}
// 2. Parse the request body
let data;
try {
data = JSON.parse(event.body);
} catch (e) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Invalid JSON' })
};
}
// 3. Validate input
const { username, password } = data;
if (!username || !password) {
return {
statusCode: 400,
body: JSON.stringify({ error: 'Username and password required' })
};
}
// 4. Check credentials (simplified)
if (username === 'blake' && password === process.env.BLAKE_PASSWORD) {
return {
statusCode: 200,
body: JSON.stringify({ token: generateJWT(username) })
};
}
return {
statusCode: 401,
body: JSON.stringify({ error: 'Invalid credentials' })
};
};
| Part | What it is |
|---|---|
exports.handler |
The function Netlify calls when a request arrives at this endpoint |
event.httpMethod |
The HTTP method — GET, POST, DELETE, etc. |
event.body |
The raw request body as a string — must be JSON.parse()'d |
statusCode |
The HTTP status code to return (200 OK, 400 Bad Request, 401 Unauthorized…) |
body |
The response body as a string — must JSON.stringify() objects |
process.env.BLAKE_PASSWORD |
Environment variable — credentials live here, never hardcoded in source |
Connection to Express: If you compare this to Express.js (app.post('/auth', (req, res) => {...})), the concept is identical — just different syntax. event is the equivalent of req. The returned object is the equivalent of calling res.status(200).json(...). The mental model transfers directly.
As applications grow, mixing database code, business logic, and HTML in one file becomes unmanageable. MVC (Model-View-Controller) is the dominant pattern for organising server-side code. It gives each concern its own home.
Database access, data validation rules, business objects.
Example: User.findByUsername(username), Post.create({title, userId})
Knows about: database queries, data structure.
Doesn't know about: HTTP, HTML, user interface.
HTML templates, JSON responses — the format shown to the user or client.
Example: render('profile.html', {user}) or return JSON.stringify(user)
Knows about: display format, HTML structure.
Doesn't know about: database, business rules.
Receives HTTP request, calls the Model, returns the View. The coordinator.
Example: receives POST /login → calls User.authenticate() → returns JSON or redirects.
Knows about: HTTP, routing.
Coordinates between: Model and View.
Here's the complete picture — every component a production web application uses, from browser to database and back. This is the diagram you need to be able to describe in HSC written answers.
fetch('/api/data', { method: 'POST', body: JSON.stringify(payload) }) — outgoing HTTP requestexports.handler is called."SELECT * FROM users WHERE id=1". Cache hit → return result in microseconds. Cache miss → continue to database, then store result for next time.const data = await response.json() — parse the response, update the page without reload.| Component | Problem it solves | Example |
|---|---|---|
| CDN | Latency — users far from the origin server wait longer | Vercel Edge Network, Cloudflare |
| Load Balancer | Single server can't handle millions of concurrent requests | AWS ALB, Nginx upstream |
| Cache (Redis) | Database queries are slow — identical queries shouldn't hit disk every time | Redis, Memcached |
| Database | Data must persist beyond a single request or server restart | PostgreSQL, MySQL, MongoDB |
The HSC SE exam often asks you to explain web concepts in written answers worth 4–6 marks. Here's how the question types work, then three practice questions to try.
| Question Format | Common topics | Answer strategy |
|---|---|---|
| "Describe the purpose of X" | HTTP methods, status codes, DNS, TLS, REST APIs, SQL | Define → explain what it does → give one example |
| "Explain the steps involved in..." | HTTP request/response cycle, DNS resolution, form submission | Numbered list, one sentence per step, include component names |
| "Compare and contrast X and Y" | Client vs server validation, static vs dynamic sites, GET vs POST | Table or paragraph — similarities, differences, tradeoffs |
| "Evaluate the security of..." | Given code or architecture — identify vulnerabilities, justify fixes | Name the vulnerability → explain attack vector → state the fix |
Explain the role of DNS in accessing a website, including the steps involved in a DNS lookup.
DNS (Domain Name System) translates human-readable domain names (such as volcanic-pantheon.vercel.app) into IP addresses that computers use to route network traffic. Without DNS, users would need to remember numerical IP addresses for every site.
Steps in a DNS lookup:
.app).A web developer builds a registration form with client-side validation using JavaScript. Explain why this is insufficient and describe what additional measures are required.
Client-side validation runs in the user's browser and can be completely bypassed — an attacker can disable JavaScript, modify the code in DevTools, or send a raw HTTP request using tools like curl or Postman, entirely skipping the HTML form and any JavaScript checks. This means any data, regardless of format or content, can be submitted directly to the server API.
Server-side validation is required because it runs on the server where the attacker has no control. The server must:
Server-side validation also forms the defence against injection attacks (such as SQL injection) when combined with parameterised queries. Client-side validation should still be included for user experience, but it cannot be treated as a security control.
Explain the difference between a static website and a dynamic website, giving an example of each.
A static website delivers pre-built files (HTML, CSS, JavaScript) directly to the browser — the server has no logic and simply returns files as stored on disk. Every visitor receives the same content. Example: a personal portfolio site hosted on Vercel or GitHub Pages.
A dynamic website generates content on the server in response to each request, typically by querying a database and constructing HTML (or JSON) from the result. Different users see different content based on their account, preferences, or real-time data. Example: a social media feed, an e-commerce site showing personalised recommendations, or a news site with a database of articles.
Dynamic sites require server-side languages (Node.js, Python, PHP) and typically a database, making them more complex and costly to host than static sites. Static sites are faster, cheaper, and more secure by default — but cannot serve personalised or real-time content without additional API calls.
Six topics across four lessons. Here's what you can now explain that you couldn't before Lesson 12 — and how it all maps together.
DNS resolution chain, TCP handshake, raw HTTP request anatomy, status code families (2xx success, 3xx redirect, 4xx client error, 5xx server error), TLS certificate chain.
You can open a network tab and know exactly what every request is doing.
The trust boundary (browser = untrusted, server = trusted), why client-side auth fails, the spectrum from static → dynamic → serverless → edge and when to choose each.
You can explain why Vercel Edge Middleware was the right fix for Volcanic Pantheon.
REST conventions, idempotency, all HTTP methods (GET, POST, PUT, PATCH, DELETE), API key vs Bearer token vs OAuth — when to use each and why.
You can read any API's documentation and orient yourself immediately.
Relational tables and relationships, SQL CRUD operations, the full request → DB → response chain, the exact point SQL injection enters the picture.
You can design a simple database schema and write basic queries.
Form handling end-to-end, why server-side validation is mandatory (and client-side is just UX), Netlify Function anatomy, MVC pattern, and the full production architecture diagram.
You can read a backend codebase and orient yourself.
Every attack from the Secure Software Architecture module now has a structural home in the architecture you learned here. XSS lives in the View. SQL injection lives in the Model. CSRF lives in the Controller. Hardcoded secrets live in .env (or shouldn't).
The two modules form one complete mental model.
That's the full NESA SE-12-05 module done. Two Year 12 modules complete. Next up: Software Automation.