Data persistence in web applications
Seoul National University of Science and Technology
Information Technology Management
Lecture slides index
May 9, 2026
| origin | destination | duration |
|---|---|---|
| New York | London | 415 |
| Shanghai | Paris | 760 |
| Istanbul | Tokyo | 700 |
| New York | Paris | 435 |
| Moscow | Paris | 245 |
| Lima | New York | 455 |
CREATE TABLE — define the structure of a tableINSERT INTO — add new rowsSELECT — retrieve rowsUPDATE — modify existing rowsDELETE — remove rows




WHERE clause filters rows based on column values=, >, >=, <, <=, <> (not equal)



AND requires all conditions to be true; OR requires at least one

WHERE clauseWHERE clause, all rows would be updatedWHERE clauseWarning
Without a WHERE clause, the statement deletes every row in the table.
| id | origin | destination | duration |
|---|---|---|---|
| 1 | New York | London | 415 |
| 2 | Shanghai | Paris | 760 |
| 3 | Istanbul | Tokyo | 700 |
| id | first | last | flight_id |
|---|---|---|---|
| 1 | Harry | Potter | 1 |
| 2 | Ron | Weasley | 1 |
| 3 | Hermione | Granger | 2 |
| 4 | Draco | Malfoy | 4 |
| 5 | Luna | Lovegood | 6 |
| 6 | Ginny | Weasley | 6 |
flight_id references flights.id — the flight a passenger belongs toJOIN clause matches rows in both tables based on the relationship| first | origin | destination |
|---|---|---|
| Harry | New York | London |
| Ron | New York | London |
| Hermione | Shanghai | Paris |
For this course we use INNER JOIN. SQLite does not currently support RIGHT and FULL OUTER JOINs.

sqlite3 is the official SQLite driver for Node.js, callback-basedsqlite is a thin promise-based wrapper built on top of sqlite3sqlite allows queries to be written with async/await, matching the controller pattern from the previous lecture
sqlite.open(...) returns a Promise that resolves to the database objectdb object is reused for all queries during the application’s lifetimedb object exposes methods to execute SQL queries:| Method | Use |
|---|---|
db.all(sql, params) |
Returns all matching rows as an array of objects |
db.get(sql, params) |
Returns the first matching row as an object |
db.run(sql, params) |
Executes a statement (INSERT / UPDATE / DELETE) without returning rows |
async functionsdb.js module opens the connection, creates tables if missing, and seeds initial data onceasync function initDb() {
const db = await getDBConnection(DB_NAME);
await db.exec(`
CREATE TABLE IF NOT EXISTS flights (
id INTEGER PRIMARY KEY AUTOINCREMENT,
origin TEXT NOT NULL,
destination TEXT NOT NULL,
duration INTEGER NOT NULL
);
`);
const row = await db.get("SELECT COUNT(*) AS count FROM flights");
if (row.count === 0) {
await db.run(
"INSERT INTO flights (origin, destination, duration) VALUES (?, ?, ?)",
["New York", "London", 415]
);
}
return db;
}db.all returns the result as an array of plain JavaScript objectsdb.run executes a statement that does not return rowslastID (the autoincremented id) and changes (the number of rows affected)db.run; result.changes indicates whether any row was removedGET /flights/3 becomes a SELECT ... WHERE id = ? query// controller
async function getById(req, res) {
const id = Number(req.params.id);
const flight = await model.findById(id);
if (!flight) return res.status(404).json({ error: "Flight not found" });
res.status(200).json(flight);
}
// model
async function findById(id) {
return db.get("SELECT * FROM flights WHERE id = ?", [id]);
}GET /flights?origin=Seoul becomes SELECT ... WHERE origin = ?// controller
async function getAll(req, res) {
const flights = await model.findByFilters(req.query);
res.status(200).json(flights);
}
// model
async function findByFilters({ origin }) {
if (origin) {
return db.all("SELECT * FROM flights WHERE origin = ?", [origin]);
}
return db.all("SELECT * FROM flights");
}? placeholders in the SQL string are filled in by values from the arrayImportant
Never build SQL by concatenating strings with user input. Always use placeholders. The reason this matters will be examined next week.
WHERE clause and the parameter list togetherasync function findByFilters({ origin, destination }) {
const conditions = [];
const params = [];
if (origin) {
conditions.push("origin = ?");
params.push(origin);
}
if (destination) {
conditions.push("destination = ?");
params.push(destination);
}
let sql = "SELECT * FROM flights";
if (conditions.length > 0) {
sql += " WHERE " + conditions.join(" AND ");
}
return db.all(sql, params);
}await model calls
Objective:
Same controller, same routes; only the model and db.js are added
New capabilities:
GET /flights, GET /flights/:id, POST /flights, DELETE /flights/:id/api-docsdb.jsdb instance// db.js
const sqlite3 = require("sqlite3");
const sqlite = require("sqlite");
let db;
async function initDb() {
db = await sqlite.open({ filename: "./flights.db", driver: sqlite3.Database });
await db.exec(`
CREATE TABLE IF NOT EXISTS flights (
id INTEGER PRIMARY KEY AUTOINCREMENT,
origin TEXT NOT NULL,
destination TEXT NOT NULL,
duration INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS passengers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
first TEXT NOT NULL,
last TEXT NOT NULL,
flight_id INTEGER NOT NULL,
FOREIGN KEY (flight_id) REFERENCES flights(id)
);
`);
// seeding omitted for brevity
}
function getDb() { return db; }
module.exports = { initDb, getDb };db connection// models/flightsModel.js
const { getDb } = require("../db");
async function findAll() {
return getDb().all("SELECT * FROM flights");
}
async function findById(id) {
return getDb().get("SELECT * FROM flights WHERE id = ?", [id]);
}
async function create({ origin, destination, duration }) {
const result = await getDb().run(
"INSERT INTO flights (origin, destination, duration) VALUES (?, ?, ?)",
[origin, destination, duration]
);
return { id: result.lastID, origin, destination, duration };
}
module.exports = { findAll, findById, create };async and await model calls// controllers/flightsController.js
async function getAll(req, res) {
const flights = await model.findAll();
res.status(200).json(flights);
}
async function getById(req, res) {
const id = Number(req.params.id);
const flight = await model.findById(id);
if (!flight) return res.status(404).json({ error: "Flight not found" });
res.status(200).json(flight);
}GET /flights?origin=Seoul&destination=Tokyo// model
async function findByFilters({ origin, destination }) {
const conditions = [];
const params = [];
if (origin) { conditions.push("origin = ?"); params.push(origin); }
if (destination) { conditions.push("destination = ?"); params.push(destination); }
let sql = "SELECT * FROM flights";
if (conditions.length) sql += " WHERE " + conditions.join(" AND ");
return getDb().all(sql, params);
}db.run returns result.changes, the number of rows affected204 and 404GET /flights/:id/passengers — list passengers of a specific flight// models/passengersModel.js
const { getDb } = require("../db");
async function findByFlightId(flightId) {
const sql = `
SELECT passengers.id, passengers.first, passengers.last,
flights.origin, flights.destination
FROM passengers
JOIN flights ON passengers.flight_id = flights.id
WHERE flights.id = ?
`;
return getDb().all(sql, [flightId]);
}
module.exports = { findByFlightId };A backend can serve plain files (HTML, CSS, JS, images) directly to the browser, in addition to JSON endpoints
Express provides built-in middleware for this:
public/ folder becomes accessible by its namepublic/index.html → served at /public/styles.css → served at /styles.csspublic/manage.html → served at /manage.htmlfetch to call the API endpointsFour small HTML pages live in public/, each illustrating one concept from the lecture
/ — Search page
GET /flights?origin=...&destination=.../flight.html?id=N — Detail page
GET /flights/:id and GET /flights/:id/passengers/add.html — Add page
POST /flights/manage.html — Manage page
GET /flights and DELETE /flights/:idAll four pages share the same backend
GET /flights?origin=New York — filtered listGET /flights/1 — single flightGET /flights/1/passengers — passengers of flight 1POST /flights — create a new flight (data survives restart)
Web Programming