BACK TO THE SERIESSviluppo web da zero · ゼロからのウェブ開発

Collegare un'app al database

19 - Collegare un'app al database


Sei capitoli fa, Samwell spariva a ogni riavvio. Oggi si chiude il cerchio: colleghiamo il server Fastify a un database vero e diamo alla Guardia il registro permanente che le abbiamo promesso.


🔌 Come parlano, in generale


Prima la panoramica, perché vale per qualsiasi stack. Il database, come sappiamo, è di solito un server a sé: il backend gli si collega attraverso un driver: una libreria che apre la connessione, spedisce le query SQL e traduce i risultati in oggetti del tuo linguaggio. Il flusso completo di una richiesta diventa:

TEXT · コード
browser ──HTTP──▶ backend ──SQL──▶ database
browser ◀──JSON── backend ◀──righe── database

Il frontend non parla mai direttamente col database: passa sempre dal backend, che valida, controlla i permessi e decide quali query eseguire. Ora capisci fino in fondo il capitolo 10: il backend è il guardiano anche dei dati.


🗃️ Il nostro database: SQLite


Per il primo collegamento scegliamo SQLite, il database perfetto per imparare: niente server da installare e configurare, perché l'intero database vive in un singolo file accanto al tuo codice. Non è un giocattolo: è il database più diffuso del pianeta (è dentro ogni telefono che possiedi), e parla lo stesso SQL del capitolo scorso.

BASH · コード
npm install better-sqlite3

Ora apriamo server.js e mandiamo in pensione l'array in memoria:

JAVASCRIPT · コード
1import Fastify from 'fastify';
2import cors from '@fastify/cors';
3import Database from 'better-sqlite3';
4
5const app = Fastify();
6await app.register(cors);
7
8// il registro ora vive in un file: guardia.db
9const db = new Database('guardia.db');
10
11// se la tabella non esiste, creala (SQL del capitolo 17!)
12db.exec(`
13  CREATE TABLE IF NOT EXISTS guardiani (
14    id INTEGER PRIMARY KEY AUTOINCREMENT,
15    nome TEXT NOT NULL,
16    ruolo TEXT NOT NULL DEFAULT 'recluta'
17  )
18`);
19
20// Read: l'elenco, dritto dal database
21app.get('/guardiani', () => {
22  return db.prepare('SELECT * FROM guardiani').all();
23});
24
25// Create: arruolamento persistente
26app.post('/guardiani', (request, reply) => {
27  const { nome } = request.body;
28
29  if (typeof nome !== 'string' || !nome.trim() || nome.length > 100) {
30    return reply.code(400).send({ errore: 'Nome non valido' });
31  }
32
33  const result = db
34    .prepare('INSERT INTO guardiani (nome) VALUES (?)')
35    .run(nome.trim());
36
37  return reply.code(201).send({
38    id: result.lastInsertRowid,
39    nome: nome.trim(),
40    ruolo: 'recluta',
41  });
42});
43
44await app.listen({ port: 3000 });
45console.log('Il registro permanente è in ascolto su http://localhost:3000');

Guarda le query: sono il SELECT e l'INSERT del capitolo scorso, consegnate al database dal driver. E ora, il momento che aspettiamo da sei capitoli. Avvia il server, arruola Samwell dal form (o con curl), poi fermalo e riavvialo. Rileggi l'elenco:


Samwell è ancora lì. 🎉


I dati ora vivono nel file guardia.db, e sopravvivono a riavvii, crash e aggiornamenti. La promessa è mantenuta: la Guardia ha un registro che non dimentica i suoi nomi.


💉 Il punto interrogativo che ti salva la vita


Prima di festeggiare troppo, torna sull'INSERT e guarda quel ?:

JAVASCRIPT · コード
db.prepare('INSERT INTO guardiani (nome) VALUES (?)').run(nome.trim());

Quella è una query parametrizzata, la difesa promessa contro la SQL injection. Il comando SQL e i dati viaggiano separati: il database riceve prima la struttura della query (con i segnaposto ?) e poi i valori, che vengono trattati sempre e solo come dati. L'input malevolo del capitolo scorso, ' OR '1'='1, qui finirebbe salvato com'è, come il nome bizzarro di una recluta, senza mai diventare parte del comando.

Tutta la difesa dalla vulnerabilità più sfruttata del web sta in un'abitudine: mai concatenare input nelle query, sempre i parametri. Ogni driver e ogni linguaggio ha la sua sintassi per farlo; d'ora in poi, quando vedi una query costruita con il +, ti deve suonare un allarme.

🧙 Gli ORM: parlare al database a oggetti


Scrivere SQL a mano va benissimo, e sapere farlo è irrinunciabile. Ma nei progetti grandi, tradurre all'infinito righe-in-oggetti e oggetti-in-query diventa manovalanza. Per questo esistono gli ORM (Object-Relational Mapping): librerie che mappano le tabelle su oggetti del tuo linguaggio, e generano l'SQL per te.


Nel mondo Node il più diffuso è Prisma. Gli descrivi lo schema:

PRISMA · コード
1model Guardiano {
2  id    Int    @id @default(autoincrement())
3  nome  String
4  ruolo String @default("recluta")
5}

E da lì in poi interroghi il database in puro JavaScript, tipizzato e senza SQL a vista:

JAVASCRIPT · コード
1// Read
2const guardiani = await prisma.guardiano.findMany({
3  where: { ruolo: 'recluta' },
4  orderBy: { nome: 'asc' },
5});
6
7// Create
8await prisma.guardiano.create({
9  data: { nome: 'Samwell Tarly' },
10});

Riconosci where e orderBy? È il SELECT del capitolo scorso con un altro vestito. E le query che Prisma genera sono parametrizzate di serie, quindi l'injection è disinnescata by design.


Driver e SQL a mano, oppure ORM? La solita risposta onesta: dipende. L'ORM regala produttività, tipi e sicurezza di default; l'SQL diretto dà controllo totale e nessuna magia di mezzo. Molti progetti reali usano entrambi: ORM per il quotidiano, SQL puro per le query complesse. (E per la cronaca: Fastify più Prisma è esattamente lo stack con cui lavoro davvero: questa coppia la ritroverai nei progetti di questo sito.)


✅ Conclusione


Guarda l'architettura completa che hai attraversato, capitolo dopo capitolo: un frontend che raccoglie dati e li spedisce via HTTP, un backend che valida, protegge e decide, un database che custodisce tutto per sempre. Questa è una web app, per intero: la stessa anatomia di Gmail, di Amazon, del gestionale che userai al lavoro. Cambia la scala, non lo scheletro.


Ogni pezzo, però, l'abbiamo studiato per conto suo. Nella prossima parte della guida, l'ultima di questo arco, ripercorriamo il ciclo intero senza soluzione di continuità: come frontend e backend si parlano davvero, e il flusso completo dal click dell'utente al pixel aggiornato. Ci manca solo di unire i puntini.