← Module 2: Programming for the Web
Inquiry Question 1: How are secure web applications developed?
Implement server-side programming, including routing, handling requests, generating responses and integrating with a database
A focused answer to the HSC Software Engineering Module 2 dot point on server-side programming. Routing, handlers, response building, database integration, the worked Flask example, and the traps markers look for.
Have a quick question? Jump to the Q&A page
What this dot point is asking
NESA wants you to write a server-side endpoint that routes a request, parses its body, validates input, talks to a database, and returns a response. Use any language/framework you are comfortable with - Python with Flask is the most common HSC choice.
The answer
Routing
A server-side framework maps HTTP method + path to a handler function:
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.get("/api/notes")
def list_notes():
return jsonify([]) # placeholder
@app.post("/api/notes")
def create_note():
return jsonify(id=1), 201
@app.get("/api/notes/<int:id>")
def get_note(id):
return jsonify(id=id, title="example")
In Node with Express:
import express from "express";
const app = express();
app.use(express.json());
app.get("/api/notes", (req, res) => res.json([]));
app.post("/api/notes", (req, res) => res.status(201).json({id: 1}));
app.get("/api/notes/:id", (req, res) => res.json({id: req.params.id}));
Reading input
Three places input arrives:
- Path parameters (
/api/notes/<id>): identifiers and resources. - Query string (
/api/notes?since=2026-01-01): filters and pagination. - Request body (POST/PUT): the payload, typically JSON.
@app.get("/api/notes")
def list_notes():
since = request.args.get("since")
limit = int(request.args.get("limit", 50))
# ...
Validating input
Always validate before using:
@app.post("/api/notes")
def create_note():
data = request.get_json(silent=True) or {}
title = (data.get("title") or "").strip()
if not (1 <= len(title) <= 200):
abort(400)
# ... safe to use title ...
Use a schema library (Pydantic, Zod, Marshmallow) for anything beyond trivial validation.
Database integration
Use parameterised queries (covered in detail in databases-and-sql):
@app.get("/api/notes")
def list_notes():
user_id = current_user(request.headers)
if not user_id:
abort(401)
with sqlite3.connect("notes.db") as conn:
rows = conn.execute(
"SELECT id, title FROM notes WHERE user_id = ? ORDER BY id DESC",
(user_id,),
).fetchall()
return jsonify([{"id": r[0], "title": r[1]} for r in rows])
Building responses
A response has three parts: status code, headers, body. The framework usually handles the headers for you:
return jsonify(error="not found"), 404, {"X-Trace-Id": "abc"}
Return JSON for APIs (application/json). Return HTML for traditional web pages (text/html).
Authentication and authorisation
Most endpoints need to confirm the requester is logged in and then verify they own the resource they are touching. A simple decorator pattern works well in Flask: the decorator authenticates the request and attaches the user identifier so the handler can use it directly.
def require_login(f):
@wraps(f)
def wrapper(*args, **kwargs):
user_id = current_user(request.headers)
if not user_id:
abort(401)
request.user_id = user_id
return f(*args, **kwargs)
return wrapper
@app.delete("/api/notes/<int:id>")
@require_login
def delete_note(id):
with db() as conn:
result = conn.execute(
"DELETE FROM notes WHERE id = ? AND user_id = ?",
(id, request.user_id),
)
if result.rowcount == 0:
abort(404)
return "", 204
The user_id = ? in the WHERE clause does object-level authorisation - a logged-in user cannot delete someone else's notes by guessing their id.
Errors
Distinguish:
- 400 Bad Request: client sent malformed input.
- 401 Unauthorized: missing or invalid credentials.
- 403 Forbidden: authenticated but not allowed.
- 404 Not Found: resource does not exist (or the user is not allowed to know it exists).
- 500 Internal Server Error: a bug. Log it; do not leak the stack trace to the user.
A full slice
The complete login endpoint below combines routing, request parsing, input validation, parameterised SQL, hashed password verification, token issuance and correct status codes. This is the standard shape of a back-end endpoint.
from flask import Flask, request, jsonify, abort
import sqlite3, bcrypt
app = Flask(__name__)
@app.post("/api/login")
def login():
data = request.get_json(silent=True) or {}
username = (data.get("username") or "").strip()
password = (data.get("password") or "").encode()
if not username or not password:
abort(400)
with sqlite3.connect("app.db") as conn:
row = conn.execute(
"SELECT id, password_hash FROM users WHERE username = ?",
(username,),
).fetchone()
if not row or not bcrypt.checkpw(password, row[1]):
abort(401)
token = issue_token(row[0])
return jsonify(token=token)
This single endpoint demonstrates routing, input parsing, validation, parameterised SQL, hashed password verification, and correct status codes.
Past exam questions, worked
Real questions from past NESA papers on this dot point, with our answer explainer.
2024 HSC6 marksWrite a server-side endpoint that accepts a POST request to /api/notes containing JSON with 'title' and 'body' fields, stores the note in a database, and returns the new note's id. Identify two security controls applied in your code.Show worked answer →
from flask import Flask, request, jsonify, abort
import sqlite3
app = Flask(__name__)
def db():
conn = sqlite3.connect("notes.db")
conn.row_factory = sqlite3.Row
return conn
def current_user(headers):
token = headers.get("Authorization", "").replace("Bearer ", "")
return verify_token(token)
@app.post("/api/notes")
def create_note():
user_id = current_user(request.headers)
if not user_id:
abort(401)
data = request.get_json(silent=True) or {}
title = (data.get("title") or "").strip()
body = (data.get("body") or "").strip()
if not (1 <= len(title) <= 200) or not (1 <= len(body) <= 5000):
abort(400)
with db() as conn:
cur = conn.execute(
"INSERT INTO notes (user_id, title, body) VALUES (?, ?, ?)",
(user_id, title, body),
)
return jsonify(id=cur.lastrowid), 201
Security controls (any two):
- Authentication via Authorization header; the endpoint aborts with 401 if the token is missing or invalid.
- Input validation of title and body lengths; rejects empty or oversized input with 400.
- Parameterised query for the INSERT prevents SQL injection.
- Per-user isolation: each note is stored against
user_id, so subsequent GET endpoints can scope to the owner.
Markers reward a real route definition, a request body parse, input validation, a parameterised database call, a correct status code (201 for creation), and at least two named security controls.
Related dot points
- Describe the client-server architecture of the web, including the roles of the browser, web server, application server and database
A focused answer to the HSC Software Engineering Module 2 dot point on web architecture. Browser, web server, application server, database, the request-response cycle, the worked three-tier example, and the traps markers look for.
- Design a relational database schema and write SQL statements to create tables, insert data, query with joins, and update or delete rows
A focused answer to the HSC Software Engineering Module 2 dot point on relational databases. Schema design, primary and foreign keys, SELECT with JOIN, INSERT, UPDATE, DELETE, the worked example, and the traps markers look for.
- Apply input validation, sanitisation and output encoding to defend against injection attacks
A focused answer to the HSC Software Engineering Module 1 dot point on input validation. Allow-list vs deny-list, sanitisation, output encoding, parameterised queries, the worked SQL injection example, and the traps markers look for.