Organizzare un backend Express con TypeScript
Nel tutorial precedente ho creato il primo server Express utilizzando Node.js e TypeScript. Il server è in grado di avviarsi, ricevere richieste HTTP e restituire una risposta JSON. È una base funzionante, ma il codice è ancora concentrato in un unico file.
In un'applicazione reale, il numero di funzionalità cresce rapidamente. Oltre alle rotte, dovrò gestire la configurazione del server, l'accesso al database, la validazione dei dati, gli errori e, in seguito, l'autenticazione degli utenti. Inserire tutto nello stesso file renderebbe il progetto difficile da leggere e da mantenere.
Per questo motivo, prima di aggiungere nuove funzionalità, voglio organizzare meglio il codice. Separerò la creazione dell'applicazione Express dal suo avvio e sposterò le rotte in un modulo dedicato. In questo modo ogni file avrà una responsabilità più precisa e sarà più semplice individuare dove intervenire quando dovrò modificare il comportamento del backend.
Continuerò a utilizzare TypeScript, gli ES Modules e tsx durante lo sviluppo. Non introdurrò ancora MongoDB: prima voglio definire una struttura di base comprensibile, sulla quale potrò aggiungere il database senza dover riscrivere l'intera applicazione.
Separare le responsabilità
Il primo passo consiste nel distinguere tre operazioni: configurare l'applicazione Express, definire le rotte HTTP e avviare il server.
La configurazione dell'applicazione comprende la creazione dell'istanza Express e la registrazione dei middleware. Le rotte stabiliscono invece come il server deve rispondere ai diversi percorsi e metodi HTTP. Infine, il punto di ingresso si occupa di avviare il server e metterlo in ascolto sulla porta configurata.
Questa separazione non è un requisito obbligatorio di Express, ma una scelta architetturale utile. Un progetto piccolo potrebbe funzionare anche con un solo file, tuttavia separare le responsabilità fin dall'inizio evita di dover riorganizzare tutto quando le funzionalità aumentano.
Creare la struttura delle directory
Dalla directory principale del progetto, creo le directory necessarie:
cd ~/dev/node-study
mkdir -p src/routesLa struttura iniziale sarà questa:
node-study/
├── node_modules/
├── src/
│ ├── routes/
│ └── index.ts
├── dist/
├── package.json
├── package-lock.json
└── tsconfig.jsonLa directory src continuerà a contenere il codice TypeScript. Al suo interno, routes ospiterà i moduli che definiscono gli endpoint HTTP. Per ora ci sarà una sola rotta, ma la stessa organizzazione potrà essere estesa in seguito a utenti, prodotti o altre risorse.
La directory dist contiene invece i file JavaScript generati dalla compilazione. Non devo modificarli manualmente: ogni nuova compilazione può rigenerarli a partire dai sorgenti.
Creare il modulo delle rotte
Creo il file src/routes/index.ts:
import { Router } from 'express';
const router = Router();
router.get('/', (_req, res) => {
res.json({
message: 'Backend Node.js funzionante',
});
});
export default router;In questo file utilizzo Router, una funzionalità di Express che permette di raggruppare le rotte in un modulo separato dall'applicazione principale.
La chiamata Router() crea un router sul quale posso registrare gli endpoint. La rotta GET / restituisce lo stesso messaggio del tutorial precedente. Ho mantenuto il comportamento originale per verificare che la riorganizzazione non cambi il risultato visibile al client.
La parte finale è particolarmente importante:
export default router;Con export default rendo disponibile il router agli altri moduli del progetto. Potrò quindi importarlo nel file che crea l'applicazione Express e collegarlo al percorso desiderato.
Questo è il vantaggio degli ES Modules: posso suddividere il codice in file indipendenti e utilizzare import ed export per collegarli.
Creare l'applicazione Express
Adesso creo il file src/app.ts, che si occuperà della configurazione dell'applicazione:
import express from 'express';
import router from './routes/index.js';
const app = express();
app.use(express.json());
app.use('/', router);
export default app;La prima istruzione importa Express, mentre la seconda importa il router creato nel passaggio precedente.
C'è un dettaglio che merita attenzione: nell'import ho scritto ./routes/index.js, anche se il file sorgente si chiama index.ts. Questa scelta è coerente con la configurazione NodeNext e con gli ES Modules di Node.js. TypeScript risolve l'import rispetto al sorgente durante la compilazione, mentre nel JavaScript generato il riferimento .js corrisponde al file effettivamente eseguibile da Node.js.
Creo quindi l'istanza Express e registro il middleware JSON. Infine, collego il router alla radice dell'applicazione:
app.use('/', router);Questo significa che le rotte definite nel router verranno esposte a partire dal percorso /. Poiché nel router ho definito una GET su /, l'indirizzo finale continuerà a essere http://localhost:3000/.
Concludo esportando l'applicazione:
export default app;In questo modo il file si occupa di configurare Express, ma non di avviare il server. Questa distinzione sarà utile anche quando inizierò a scrivere test automatici per l'applicazione.
Avviare il server dal punto di ingresso
Resta da modificare src/index.ts. Questo file dovrà importare l'applicazione configurata e avviarla sulla porta 3000.
Sostituisco il contenuto precedente con:
import app from './app.js';
const port = 3000;
app.listen(port, () => {
console.log(`Server in ascolto sulla porta ${port}`);
});Il file ora contiene soltanto le istruzioni necessarie per avviare il server. Non definisce più le rotte e non configura direttamente i middleware: queste responsabilità sono state spostate negli altri moduli.
Questa organizzazione è utile perché mantiene distinto il codice che configura Express da quello che apre la porta di ascolto. In futuro potrò importare app anche in un ambiente di test senza dover necessariamente avviare un server HTTP reale.
Avviare il backend durante lo sviluppo
Per eseguire il server utilizzo ancora tsx:
npx tsx src/index.tsSe tutto è configurato correttamente, il terminale mostrerà:
Server in ascolto sulla porta 3000Lascio il processo in esecuzione e apro un secondo terminale per provare la rotta:
curl -i http://localhost:3000/La risposta attesa contiene lo stato HTTP 200 OK e il messaggio JSON:
{
"message": "Backend Node.js funzionante"
}Se ricevo la risposta prevista, significa che Express ha caricato correttamente il router e lo ha collegato al percorso principale. La struttura interna è cambiata, ma il comportamento esterno del backend è rimasto lo stesso.
Verificare i tipi e compilare il progetto
Dopo aver suddiviso il codice in più file, verifico che TypeScript riesca ad analizzare tutti i moduli:
npx tsc --noEmitIl comando controlla il progetto secondo le impostazioni definite in tsconfig.json, senza generare file JavaScript. Se non restituisce errori, posso procedere con la compilazione:
npx tscTypeScript genererà la struttura corrispondente all'interno di dist. Posso controllare i file prodotti con:
find dist -maxdepth 3 -type fDovrei trovare dist/index.js, dist/app.js e dist/routes/index.js. Per avviare la versione compilata utilizzo:
node dist/index.jsAnche in questo caso il server deve rispondere alla richiesta GET sulla porta 3000. È importante verificare sia l'esecuzione tramite tsx sia quella del JavaScript compilato, perché il progetto dovrà funzionare correttamente in entrambe le modalità.
Conclusione
In questo tutorial ho riorganizzato il backend Express separando le rotte, la configurazione dell'applicazione e l'avvio del server. Ho utilizzato Router() per raggruppare gli endpoint, app.ts per configurare Express e index.ts come punto di ingresso.
Il risultato è un progetto più ordinato, nel quale ogni file ha una responsabilità specifica. Questa struttura non aggiunge complessità inutile, ma prepara il codice alle funzionalità che arriveranno nei prossimi tutorial.
Il passo successivo sarà configurare le variabili d'ambiente e preparare la connessione a MongoDB. In questo modo potrò separare i dati di configurazione dal codice sorgente e collegare il backend a un database senza inserire credenziali direttamente nei file TypeScript.