MongoDB senza Mongoose
5 marzo 2026

Se cerchi un tutorial Node.js + MongoDB, troverai Mongoose. Quasi ogni guida, ogni boilerplate, ogni articolo "getting started" lo usa automaticamente. È diventato così standard che usare il driver nativo sembra una scelta controcorrente che richiede spiegazioni.
Ho smesso di usare Mongoose. Non per contrarietà — ma per una graduale realizzazione che la maggior parte di ciò che offre, o non mi serve o riesco a gestirlo meglio senza.
Cosa fa davvero Mongoose
Per essere onesti con Mongoose, fa cose reali. Offre schemi con validazione, middleware hooks (pre/post save), virtual fields, un query builder con sintassi concatenabile, e populate() per unire documenti tra collezioni.
È un set di funzionalità ragionevole. La domanda è se hai bisogno di quelle funzionalità, e cosa paghi per averle.
L'argomento TypeScript
Il principale punto di forza degli schemi Mongoose — struttura imposta sui documenti — è diventato significativamente meno convincente dal momento in cui ho iniziato a scrivere TypeScript.
Con interfacce o tipi TypeScript, ho già garanzie a compile-time sulla forma dei miei dati. Aggiungere uno schema Mongoose sopra duplica quello sforzo: definisco la stessa struttura due volte, in due sintassi diverse, e le mantengo sincronizzate manualmente.
In un progetto TypeScript, la validazione degli schemi Mongoose sta risolvendo un problema che TypeScript risolve già a livello di linguaggio. Stai pagando per il middleware, il query builder e la validazione a runtime — e l'ultimo è il motivo più debole dei tre.
Il problema delle aggregazioni
Qui l'astrazione si rompe davvero. Il query builder di Mongoose è comodo per CRUD semplice:
await Invoice.find({ status: "delivered" }).sort({ date: -1 }).limit(10);
Pulito. Leggibile. Va bene.
Ma le applicazioni reali non rimangono a CRUD semplice. Nel momento in cui hai bisogno di una pipeline di aggregazione — raggruppamenti, proiezioni, lookup, campi calcolati — stai scrivendo MongoDB grezzo comunque:
await Invoice.aggregate([
{ $match: { status: "delivered" } },
{ $group: { _id: "$clientId", total: { $sum: "$amount" } } },
{ $sort: { total: -1 } },
]);
A quel punto sei dentro il linguaggio di query di MongoDB direttamente, e Mongoose è solo un wrapper che stai attraversando. L'astrazione è trapelata. Sei tornato ad aver bisogno di capire MongoDB in profondità — cosa che avresti dovuto fare dall'inizio.
Come appare il driver nativo
Con il driver MongoDB nativo, le stesse query appaiono così:
const db = client.db("mydb");
const invoices = db.collection<Invoice>("invoices");
await invoices.find({ status: "delivered" }).sort({ date: -1 }).limit(10).toArray();
await invoices.aggregate([
{ $match: { status: "delivered" } },
{ $group: { _id: "$clientId", total: { $sum: "$amount" } } },
{ $sort: { total: -1 } },
]).toArray();
Non c'è magia qui. La collezione è tipizzata con la tua interfaccia TypeScript. Le query sono identiche a quello che scriveresti nella chiamata aggregate di Mongoose. La differenza è: nulla è nascosto.
Cosa perdi davvero
Voglio essere onesto sui compromessi, perché questo non è un pasto gratis.
populate() — il join a livello di documento di Mongoose è genuinamente comodo. Con il driver nativo, esegui più query e unisci nel codice applicativo, oppure scrivi uno stage $lookup in una pipeline di aggregazione. Nessuno dei due è ergonomico come populate(). Per applicazioni che fanno molte query relazionali tra collezioni, questo è un costo reale.
Middleware hooks — gli hook pre('save') e post('save') sono utili per logiche trasversali come aggiornare timestamp, innescare side effect, o fare hashing delle password prima della persistenza. Senza di essi, gestisci questo nel tuo service layer esplicitamente. È più codice, ma anche codice più visibile — la logica non sparisce dentro una definizione di schema.
Validazione a livello di schema — Mongoose può rifiutare documenti malformati a livello ORM. Con il driver nativo la validazione avviene nella tua logica applicativa (o usando la validazione JSON Schema built-in di MongoDB a livello di collezione, sottoutilizzata ma potente). La validazione avviene ancora — è solo tua responsabilità gestirla.
Come lo gestisco in pratica
Nel gestionale che ho costruito per Honeyside, ho interfacce TypeScript che definiscono la forma di ogni documento. L'accesso alle collezioni passa attraverso un singolo modulo db.ts che inizializza la connessione e crea gli indici all'avvio. La validazione avviene in un layer dedicato di validatori prima che qualcosa raggiunga il database.
È più codice di quanto sarebbe l'equivalente Mongoose. È anche codice che capisco pienamente, codice che non ha comportamenti nascosti, e codice su cui posso ragionare senza leggere la documentazione di Mongoose.
Quando userei ancora Mongoose
Mongoose è una buona scelta quando:
- Stai lavorando in JavaScript senza TypeScript e hai bisogno di schema enforcement a runtime
- Il tuo team è junior e beneficia dei guardrail e delle convenzioni che offre
- Il tuo modello dati è fortemente relazionale e useresti
populate()costantemente - Stai prototipando velocemente e vuoi l'impalcatura
Nessuna di queste si applica al modo in cui lavoro. Il tuo contesto potrebbe essere diverso.
Il punto vero
Il default è Mongoose perché Mongoose è arrivato prima, i tutorial lo usano, e l'inerzia è potente. È un motivo valido per usare una libreria — fino a quando non hai capito cosa fa, e puoi decidere se l'astrazione ti serve o aggiunge solo peso.
Il driver nativo non è più difficile. È più diretto. E in un progetto TypeScript, più diretto di solito significa più manutenibile.