A confession: the Watch project still lacks a piece you may have missed. The form sends applications, the database stores them, but the frontend has never displayed the register. To see members, you opened localhost:3000/guardiani and read raw JSON. Hardly chapter 4's experience of eye, mind and heart.
Today, we finish it: a page displaying the list, welcoming recruits and updating itself after every enlistment. The complete cycle (form → API → database → interface) together for the first time.
🏗️ The register page
Start with HTML. Add a list section to the familiar form:
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8" />
5 <title>Night’s Watch register della Notte</title>
6 <link rel="stylesheet" href="guardiani.css" />
7 <script src="guardiani.js" defer></script>
8</head>
9<body>
10 <h1 id="titolo-pagina">Night’s Watch register</h1>
11
12 <!-- the list: JavaScript will populate it -->
13 <ul id="registro" class="registro"></ul>
14
15 <!-- the recruitment form -->
16 <form class="form-dark">
17 <label for="name">First name:</label>
18 <input type="text" id="name" name="name" class="campo" required />
19
20 <button class="btn-invio">Join the Watch</button>
21 <p id="errore" class="errore-form"></p>
22 </form>
23</body>
24</html>Two additions: the empty <ul id="registro"> (JavaScript fills it with real data), and an error paragraph, because last chapter was not wasted. A little CSS for these newcomers:
1.registro {
2 max-width: 500px;
3 margin: 24px auto;
4 padding: 0;
5 list-style: none;
6}
7
8.registro li {
9 padding: 10px 14px;
10 margin-bottom: 8px;
11 background-color: #2e2e2e;
12 border-radius: 6px;
13 color: #f2f2f2;
14}
15
16.errore-form {
17 color: #ff6b6b;
18}📖 Reading and displaying: the key function
The heart, in guardiani.js: request the server's list and draw it on the page. Last chapter's caricaGuardiani plus chapter 8's DOM manipulation, old friends finally working together:
1const registro = document.querySelector('#registro');
2const form = document.querySelector('.form-dark');
3const errore = document.querySelector('#errore');
4
5async function caricaRegistro() {
6 try {
7 const risposta = await fetch('http://localhost:3000/guardiani');
8 if (!risposta.ok) {
9 throw new Error(`The server responded with ${risposta.status}`);
10 }
11 const guardiani = await risposta.json();
12
13 // clear the list and redraw it
14 registro.textContent = '';
15 for (const guardiano of guardiani) {
16 const voce = document.createElement('li');
17 voce.textContent = `${guardiano.nome} - ${guardiano.ruolo}`;
18 registro.appendChild(voce);
19 }
20 } catch (err) {
21 errore.textContent = 'Unable to load the register. Is the server running?';
22 }
23}
24
25// populate the list on page load
26caricaRegistro();Notice a choice that looks minor but matters for security: data enters through textContent, never innerHTML. A watchman's name comes from the database, originally from a user. Inserting untrusted markup with innerHTML can cause code execution in visitors' browsers (XSS, SQL injection's frontend cousin). With textContent, it always stays text. Never trust the client, even when its data comes back.
✍️ Enlisting and updating
Submission now uses everything you know and calls caricaRegistro() again at the end, closing the loop:
1form.addEventListener('submit', async (event) => {
2 event.preventDefault();
3 errore.textContent = '';
4
5 const campoNome = document.querySelector('#name');
6
7 try {
8 const risposta = await fetch('http://localhost:3000/guardiani', {
9 method: 'POST',
10 headers: { 'Content-Type': 'application/json' },
11 body: JSON.stringify({ nome: campoNome.value }),
12 });
13
14 if (!risposta.ok) {
15 throw new Error(`The server responded with ${risposta.status}`);
16 }
17
18 campoNome.value = ''; // clear the field
19 await caricaRegistro(); // show the updated register
20 } catch (err) {
21 errore.textContent = 'Recruitment failed. Try again shortly.';
22 }
23});Open the page: the list fills automatically with Jon and whoever survived your experiments. Enlist someone: the field clears and their name appears immediately. Restart the server: everyone remains. Every chapter is doing its job.
🔭 The complete journey in slow motion
Chapter 2 promised to explain "what happens when you click a button". Watch frame by frame:
- The user types "Grenn" and clicks: the DOM generates
submit. - Our JavaScript intercepts it, prevents reloading and composes a
POSTusingfetch. - The HTTP request leaves: method, headers, JSON body.
- Fastify receives and routes it, validating the name (never trust the client).
- The backend executes a parameterized INSERT on SQLite: Grenn is stored persistently.
- A
201 Createdresponse returns, including the watchman'sid. - The frontend requests the register again (
GET), receiving updated JSON. - The DOM updates: Grenn appears in the list.
Eight steps, a handful of milliseconds, and you built every one. Chapter 2's journey behind a click was a promise; now it is a map you can read and follow yourself.
✅ Conclusion
The cycle closes: frontend, backend and database are three organs of one body rather than separate subjects, and you have seen them work together. Every new technology (framework, ORM, cloud service) will replace or strengthen one organ. You know the skeleton.
No farewells yet: the last section covers good habits, organizing projects, the Git promised eighteen chapters ago, and a map to continue alone. Keep the register handy: the Watch always needs recruits.