Lektion 17 — Repository og databasen
Christian Budtz — chbu@dtu.dk
1 / 35
F fuld skærm · B sort · O overblik
Christian Budtz — chbu@dtu.dk
pg, $1 og .envserver.jsGETPOST, fejl og formularenPOST, statuskoder og $1POST og testskemaetPauser lægges ind undervejs.
Efter lektionen skal du kunne:
server.js til gruppens Neon-database med connection-stringen i .env, uden for git$1, $2) og forklare, hvorfor værdierne ikke klistres ind i SQL-teksten201, 400 og 500 og teste dem fra api.httpLektion 13: listen bor i et array i server.js. Lektion 15: de samme
patienter bor i en tabel i Neon. I dag flytter routen sine data fra arrayet
til tabellen.
flowchart LR
subgraph laptop [Jeres bærbare]
browser[Browser: mockup] -->|HTTP| route[Express-route]
route -->|funktionskald| repo[Repository: patients.js]
end
subgraph neon [Neon]
db[(Tabel patients)]
end
repo -->|SQL| db
Routen taler HTTP. Repositoryet taler SQL. Det er de tre lag fra lektion 11 — nu som tre steder i koden.
pg og Poolpg er pakken, der sender SQL fra Node til PostgreSQL. En Pool holder et
par forbindelser åbne, så hvert request ikke skal logge ind forfra.
const { Pool } = require("pg");
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const result = await pool.query("SELECT cpr, navn FROM patients ORDER BY id");
result.rows; // [{ cpr: "2512489996", navn: "Nancy Ann Test Berggren" }, …]
pool.query returnerer et objekt. Rækkerne ligger i .rows — et array af
objekter, samme form som listen i lektion 13.
$1 og et arraySQL-teksten har pladsholdere. Værdierne sendes ved siden af:
await pool.query("SELECT navn FROM patients WHERE cpr = $1", [cpr]);
PostgreSQL får sætningen og værdien hver for sig. Værdien bliver aldrig en del af SQL'en, uanset hvad den indeholder. En CPR, der ikke findes, giver ikke en fejl — den giver et tomt array.
.env og --env-fileConnection-stringen indeholder passwordet. Den står i .env, ikke i koden:
DATABASE_URL=postgresql://neondb_owner:…@…neon.tech/neondb?sslmode=verify-full&channel_binding=require
node --env-file=.env server.js
--env-file lægger linjen i process.env, før koden kører. .env står i
.gitignore. Koden kan ligge på GitHub — passwordet kan ikke.
sslmode=verify-full i stedet for Neons require fjerner en lang
sikkerhedsadvarsel fra pg. Forbindelsen er krypteret i begge tilfælde.
Gå til /quiz og indtast koden fra tavlen.
Lektion 17: forberedelse — route og repository, pg, $1 og .env.
server.jsI gruppens projekt:
npm install pg
.env med gruppens connection-string fra lektion 15, øvelse 3. .env
skrives ind i .gitignore ved siden af node_modules/. Tjek med
git status, at .env ikke står på listen.
Arrayet patients i server.js slettes. Den gamle POST-route, der lavede
patients.push, slettes også — den skriver I igen efter pausen.
patients.jsEn ny fil ved siden af server.js. Det er repositoryet.
const { Pool } = require("pg");
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
pool.on("error", (err) => {
console.error("Databasen lukkede forbindelsen:", err.message);
});
async function hentPatienter() {
const result = await pool.query("SELECT cpr, navn FROM patients ORDER BY id");
return result.rows;
}
module.exports = { hentPatienter };
module.exports er det, andre filer kan hente med require("./patients").
pool.on("error")Poolen holder forbindelser åbne til Neon. Mister den en af dem — netværket
falder ud, eller Neon lukker den — sender poolen en error-event. Uden en
lytter crasher hele Node-processen, og serveren dør, mens I kigger
væk.
Med lytteren skrives fejlen i terminalen, og poolen åbner en ny forbindelse ved næste request.
asyncconst { hentPatienter } = require("./patients");
app.get("/api/patients", async (req, res) => {
try {
const patients = await hentPatienter();
res.json(patients);
} catch (err) {
console.error(err);
res.status(500).json({ error: "serverfejl" });
}
});
Routen kender ingen SQL. Går databasen galt, svarer den 500 med JSON — og
skriver selve fejlen i terminalen, hvor I kan læse den. Brugeren skal ikke se
en stacktrace.
GET gennem tre lagsequenceDiagram
participant Browser
participant Route as Express-route
participant Repo as patients.js
participant Neon
Browser->>Route: GET /api/patients
Route->>Repo: hentPatienter()
Repo->>Neon: SELECT cpr, navn FROM patients
Neon-->>Repo: rækker
Repo-->>Route: array af patienter
Route-->>Browser: 200 og JSON
Tabellen i mockuppet er uændret. Den henter stadig fra
http://localhost:3000/api/patients — den ved ikke, at listen nu kommer fra
Neon.
GETGå til /quiz og indtast koden fra tavlen.
Lektion 17: repository — patients.js, async, 500 og .env.
Forbind server.js til gruppens database. GET /api/patients henter fra
tabellen. Tabellen i mockuppet viser de fem testpatienter. Genstart serveren —
de er der stadig.
Detaljerne står i øvelsesarket.
POST, fejl og formularenopretPatientI patients.js:
async function opretPatient(cpr, navn) {
const result = await pool.query(
"INSERT INTO patients (cpr, navn) VALUES ($1, $2) RETURNING cpr, navn",
[cpr, navn]
);
return result.rows[0];
}
module.exports = { hentPatienter, opretPatient };
RETURNING svarer med den række, der blev indsat. Routen kan sende den
tilbage uden et ekstra SELECT.
POST-routenapp.post("/api/patients", async (req, res) => {
const { cpr, navn } = req.body ?? {};
if (!cpr || !navn) {
return res.status(400).json({ error: "cpr og navn skal med" });
}
try {
const patient = await opretPatient(cpr, navn);
res.status(201).json(patient);
} catch (err) {
console.error(err);
res.status(500).json({ error: "serverfejl" });
}
});
400 tjekkes før databasen: klienten sendte for lidt. 500 er, når
databasen fejler: serveren kunne ikke. 201 er, når rækken er gemt.
$1 — SQL injectionForestil jer, at navnet blev sat ind med +:
// GØR IKKE DETTE
pool.query("INSERT INTO patients (cpr, navn) VALUES ('" + cpr + "', '" + navn + "')");
Navnet x'); DELETE FROM patients; -- gør SQL'en til to sætninger. Den
anden sletter hele tabellen. Det er Little Bobby Tables.
Med $1 og $2 er navnet bare tekst — også hvis det indeholder
anførselstegn og SQL. Alle værdier fra et request går gennem pladsholdere.
| # | Request | Forventet |
|---|---|---|
| 1 | GET /api/patients |
200 og listen |
| 2 | POST /api/patients med body |
201 og patienten |
| 3 | POST /api/patients uden body |
400 |
| 4 | GET /api/patients efter genstart |
200, patienten fra 2 er der |
| 5 | GET /api/patients med forkert password i .env |
500 og serverfejl |
| 6 | GET /findesikke |
404 |
Samme api.http som i lektion 13 — statuskoden står øverst i svaret. Skemaet kommer i projektloggen med en kolonne for, hvad I
faktisk fik.
corsFormularen i mockuppet sender POST med JSON. Før det sender browseren et
OPTIONS-request (preflight) og spørger, om det er tilladt. Den hjemmelavede
header fra lektion 13 svarer ikke på det.
npm install cors
const cors = require("cors");
app.use(cors());
cors() erstatter jeres egen Access-Control-Allow-Origin-middleware og
svarer på preflight. Den skal stå før jeres routes. Det er samme pakke som
underviserens API bruger.
Samme mønster som login i lektion 7:
form.addEventListener("submit", async (event) => {
event.preventDefault();
fejl.hidden = true;
try {
const res = await fetch("http://localhost:3000/api/patients", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
cpr: document.getElementById("cpr").value,
navn: document.getElementById("navn").value
})
});
if (!res.ok) {
fejl.hidden = false;
return;
}
form.reset();
} catch (err) {
fejl.hidden = false;
}
});
!res.ok fanger 400 og 500. catch fanger, at serveren slet ikke
svarer. Network viser OPTIONS med 204 og derefter POST med 201. Hele
uddraget med HTML står i øvelsesarket.
POST, statuskoder og $1POST og gruppens handlingPOST og testskemaetopretPatient i repositoryet og POST i routen. Kør de seks rækker i
testskemaet fra api.http, og skriv resultatet i projektloggen.
Detaljerne står i øvelsesarket.
Gruppens egen tabel fra lektion 15 får sit eget repository og en
POST-route. En formular i mockuppet sender POST, og rækken lander i
tabellen i Neon. Det er den centrale brugerhandling til D2.
Detaljerne står i øvelsesarket.
D2 afleveres tirsdag 10/11: den centrale brugerhandling skal virke hele vejen fra formular til tabel — på en URL på Vercel, ikke kun på jeres maskine.
Deploy er det samme Vercel-projekt som i lektion 9, nu med en server:
public/, og fetch bruger /api/…DATABASE_URLTrin for trin: Deploy backend til Vercel. Opgaveoplægget til D2 ligger på DTU Learn.