Il tuo backend Node.js funziona, ma ogni volta che aggiungi una rotta o modifichi un modello, ti ritrovi a cacciare errori che potevano essere evitati. Un campo scritto male, un tipo sbagliato, una risposta API che cambia forma senza preavviso. Se stai leggendo questa guida, probabilmente conosci già la sensazione.
Noi, di Meteora Web, lavoriamo con Laravel e TypeScript ogni giorno. E sappiamo che la differenza tra un backend che regge e uno che si rompe sta quasi sempre nella tipizzazione. Non è una questione di moda: è una questione di costi. Ogni bug che arriva in produzione è tempo perso, clienti insoddisfatti e fatturato che scende.
In questa guida ti mostriamo come costruire un backend con TypeScript, Node.js ed Express che non si rompe. Partiamo dai concetti, poi passiamo al codice operativo. Alla fine, avrai una base solida che puoi usare subito nei tuoi progetti.
Perché TypeScript con Node.js ed Express è la scelta giusta per il tuo backend?
Quando lavori con JavaScript puro, il runtime non ti avvisa se passi una stringa dove serve un numero. Il codice parte, magari funziona per mesi, poi un dato inatteso fa saltare tutto. TypeScript aggiunge un livello di controllo statico che intercetta questi errori prima che arrivino in produzione.
Pensala così: JavaScript è come guidare senza cruscotto. Puoi andare, ma non sai mai se stai finendo il carburante. TypeScript è il cruscotto che ti dice esattamente cosa sta succedendo, prima che il motore si spenga.
Con Express, poi, il vantaggio è doppio. Express è minimalista e flessibile, ma proprio per questo lascia spazio a errori di tipo. TypeScript lo rende prevedibile, senza togliergli la potenza.
Errori comuni che TypeScript previene
- Passare un id come stringa dove il database si aspetta un numero
- Dimenticare un campo in una risposta API, causando errori lato client
- Modificare una funzione e rompere tutte le chiamate che la usano
Questi non sono scenari teorici. Li vediamo ogni giorno nei progetti che ci arrivano. E ogni volta, la soluzione è la stessa: tipizzare prima, debuggare dopo.
Sponsored Protocol
Come configurare un progetto Node.js con TypeScript ed Express?
Partiamo da zero. Se hai già un progetto, puoi saltare al prossimo paragrafo. Ma se stai iniziando, questa è la base che ti serve.
Step 1: Inizializza il progetto
mkdir backend-ts && cd backend-ts
npm init -y
npm install express
npm install -D typescript @types/node @types/express ts-node nodemonQui installiamo Express per il server, e TypeScript con i tipi per Node ed Express come dipendenze di sviluppo. ts-node ci permette di eseguire TypeScript direttamente, mentre nodemon riavvia il server a ogni modifica.
Step 2: Configura TypeScript
Crea un file tsconfig.json nella radice del progetto:
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}La opzione strict: true è fondamentale. Attiva tutti i controlli di tipo più severi, che è esattamente quello che vogliamo. Senza strict, TypeScript perde gran parte del suo valore.
Step 3: Crea il server base
Ora creiamo un file src/index.ts con un server Express minimale:
import express, { Request, Response } from 'express';
const app = express();
const port = 3000;
app.get('/', (req: Request, res: Response) => {
res.send('Backend TypeScript funzionante');
});
app.listen(port, () => {
console.log(`Server in ascolto sulla porta ${port}`);
});Nota come abbiamo tipizzato req e res. Questo è il primo passo: ogni handler deve avere i tipi espliciti. Non lasciare che TypeScript li inferisca, perché in Express spesso l'inferenza non basta.
Per avviare il server in sviluppo, aggiungi al package.json:
"scripts": {
"dev": "nodemon --exec ts-node src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
}Ora puoi lanciare npm run dev e vedere il tuo server in azione.
Come tipizzare le richieste e le risposte in Express con TypeScript?
Il vero potere di TypeScript emerge quando definisci i tipi per i dati che entrano ed escono dal tuo server. Una rotta che accetta un id e restituisce un utente deve avere tipi chiari per entrambi.
Sponsored Protocol
Definire un modello condiviso
Creiamo un file src/types.ts con i tipi che useremo in tutto il backend:
export interface User {
id: number;
name: string;
email: string;
createdAt: Date;
}
export interface CreateUserRequest {
name: string;
email: string;
}Questi tipi sono la fonte di verità. Quando il database restituisce un utente, sappiamo esattamente che forma ha. Quando riceviamo una richiesta per creare un utente, sappiamo quali campi aspettarci.
Tipizzare i parametri della richiesta
import { Request, Response } from 'express';
import { User, CreateUserRequest } from './types';
app.get('/users/:id', (req: Request<{ id: string }>, res: Response) => {
const userId = parseInt(req.params.id, 10);
// Simuliamo una ricerca nel database
const user: User = {
id: userId,
name: 'Mario Rossi',
email: 'mario@example.com',
createdAt: new Date()
};
res.json(user);
});
app.post('/users', (req: Request<{}, {}, CreateUserRequest>, res: Response) => {
const { name, email } = req.body;
// Qui creeremmo l'utente nel database
const newUser: User = {
id: Date.now(),
name,
email,
createdAt: new Date()
};
res.status(201).json(newUser);
}); Con Request<{ id: string }> diciamo a TypeScript che il parametro id è una stringa. Con Request<{}, {}, CreateUserRequest> diciamo che il body deve avere la forma di CreateUserRequest. Se qualcuno prova a inviare un campo extra o mancante, TypeScript lo segnala in fase di sviluppo.
Questo approccio elimina un'intera classe di bug. Non devi più ricordarti a memoria la forma dei dati: il compilatore lo fa per te.
Come gestire gli errori in modo tipizzato con Express e TypeScript?
Gli errori sono inevitabili. Ma la differenza tra un backend professionale e uno improvvisato sta in come li gestisci. Con TypeScript, puoi creare un sistema di errori che sia prevedibile e tipizzato.
Sponsored Protocol
Creare una classe di errore personalizzata
export class AppError extends Error {
statusCode: number;
details?: unknown;
constructor(statusCode: number, message: string, details?: unknown) {
super(message);
this.statusCode = statusCode;
this.details = details;
}
}Ora ogni errore che lanciamo ha un codice HTTP e un messaggio chiaro. Nelle rotte, possiamo lanciare errori specifici:
app.get('/users/:id', (req: Request<{ id: string }>, res: Response) => {
const userId = parseInt(req.params.id, 10);
const user = findUserById(userId);
if (!user) {
throw new AppError(404, 'Utente non trovato');
}
res.json(user);
});Middleware di gestione errori
In Express, aggiungiamo un middleware finale che cattura tutti gli errori e risponde in modo uniforme:
import { NextFunction, Request, Response } from 'express';
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
if (err instanceof AppError) {
res.status(err.statusCode).json({
error: err.message,
details: err.details
});
return;
}
console.error(err);
res.status(500).json({ error: 'Errore interno del server' });
});Con instanceof AppError distinguiamo gli errori che conosciamo da quelli imprevisti. Il client riceve sempre una risposta JSON con una struttura chiara, mai un crash silenzioso.
Questo middleware va aggiunto dopo tutte le rotte. In questo modo, Express lo usa come ultima spiaggia per qualsiasi errore non gestito.
Come integrare TypeScript con un database in un backend Express?
Il database è il cuore del backend. E tipizzare le query è il modo migliore per evitare errori che costano cari. Con un ORM come Prisma o TypeORM, TypeScript brilla davvero.
Esempio con Prisma
Prisma genera tipi automaticamente dal tuo schema. Se hai un modello User, ottieni un tipo User con tutti i campi. Ecco come si usa in una rotta:
import { PrismaClient } from '@prisma/client';
import { Request, Response } from 'express';
const prisma = new PrismaClient();
app.get('/users/:id', async (req: Request<{ id: string }>, res: Response) => {
const userId = parseInt(req.params.id, 10);
try {
const user = await prisma.user.findUnique({
where: { id: userId }
});
if (!user) {
res.status(404).json({ error: 'Utente non trovato' });
return;
}
res.json(user);
} catch (error) {
res.status(500).json({ error: 'Errore del database' });
}
});Il tipo di user è inferito automaticamente da Prisma. Se il modello cambia, TypeScript ti avvisa in ogni punto del codice che usa quel tipo. Zero sorprese.
Sponsored Protocol
Se preferisci un approccio più SQL puro, puoi usare un query builder come Kysely, che è tipizzato ma non ti nasconde il SQL. La scelta dipende dal tuo progetto, ma il principio è lo stesso: i dati devono avere una forma nota.
Quali sono le best practice per un backend TypeScript con Express in produzione?
Abbiamo visto le basi. Ora passiamo a ciò che distingue un backend che regge da uno che si rompe. Queste sono le regole che applichiamo noi, di Meteora Web, in ogni progetto.
1. Usa DTO per i dati in ingresso
Non passare mai il body della richiesta direttamente al database. Crea un Data Transfer Object che definisce esattamente quali campi sono accettati. Questo previene attacchi di mass assignment e mantiene il codice pulito.
// dto/create-user.dto.ts
export interface CreateUserDTO {
name: string;
email: string;
password: string;
}2. Valida i dati in ingresso
TypeScript controlla i tipi a compile-time, ma a runtime il body arriva come JSON non validato. Usa una libreria come zod per validare i dati prima di usarli.
import { z } from 'zod';
const createUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
password: z.string().min(8)
});
app.post('/users', (req: Request, res: Response) => {
const result = createUserSchema.safeParse(req.body);
if (!result.success) {
res.status(400).json({ error: result.error.errors });
return;
}
// result.data è tipizzato e validato
res.status(201).json(result.data);
});Zod si integra perfettamente con TypeScript. Puoi anche inferire i tipi direttamente dallo schema, evitando duplicazioni.
Sponsored Protocol
3. Non usare any
La tentazione di usare any è forte, specialmente quando lavori con librerie non tipizzate. Ma any disattiva tutti i controlli e ti riporta al problema iniziale. Se una libreria non ha tipi, cerca un wrapper tipizzato o definisci i tipi tu stesso.
4. Separa le rotte dai controller
Non scrivere tutta la logica nelle funzioni di rotta. Separa le rotte (che gestiscono la richiesta) dai controller (che contengono la logica di business). Questo rende il codice testabile e manutenibile.
// routes/user.routes.ts
import { Router } from 'express';
import { getUsers, createUser } from '../controllers/user.controller';
const router = Router();
router.get('/', getUsers);
router.post('/', createUser);
export default router;Cosa fare adesso per un backend TypeScript che non si rompe?
Hai gli strumenti, ora mettili in pratica. Non serve rifare tutto da zero: inizia dal progetto che hai già e applica queste modifiche una alla volta.
- Configura TypeScript con strict: true e risolvi tutti gli errori che emergono. Sarà noioso, ma è il primo passo.
- Tipizza le rotte esistenti: aggiungi i tipi a Request e Response in ogni handler. Poi definisci le interfacce per i dati principali.
- Aggiungi un middleware di gestione errori con una classe AppError personalizzata. Uniforma le risposte di errore.
- Introduci un sistema di validazione come zod per i dati in ingresso. Non fidarti mai del body della richiesta.
- Separa rotte e controller se non l'hai già fatto. Il codice diventa più leggibile e testabile.
Noi, di Meteora Web, sappiamo che la tipizzazione non è un lusso: è una necessità. Un backend tipizzato costa meno da mantenere, ha meno bug e ti fa dormire tranquillo. Se vuoi approfondire, parti dalla nostra guida completa su TypeScript dove trovi tutti i concetti collegati.
E se hai un progetto che continua a rompersi, parlane con noi. Ti aiutiamo a mettere ordine.