Guida pratica su come le scelte di Ryan Dahl con Node.js e Deno hanno plasmato il backend JavaScript, gli strumenti, la sicurezza e i flussi di lavoro quotidiani degli sviluppatori — e come scegliere oggi.

Un runtime JavaScript è più di un modo per eseguire codice. È un insieme di decisioni sulle caratteristiche di performance, API integrate, impostazioni di sicurezza, packaging e distribuzione, e sugli strumenti quotidiani su cui gli sviluppatori fanno affidamento. Quelle decisioni determinano come il backend JavaScript si percepisce: come strutturi i servizi, come fai debugging in produzione e con quale fiducia puoi pubblicare modifiche.
La performance è la parte più ovvia: quanto efficientemente un server gestisce I/O, concorrenza e task intensivi di CPU. Ma i runtime decidono anche cosa ottieni “di default”. Hai un modo standard per effettuare richieste HTTP, leggere file, avviare server, eseguire test, fare linting o bundle di un'app? Oppure devi assemblare tu questi pezzi?
Anche quando due runtime possono eseguire JavaScript simile, l'esperienza sviluppatore può essere molto diversa. Conta anche il packaging: i sistemi di moduli, la risoluzione delle dipendenze, i lockfile e il modo in cui le librerie sono pubblicate influenzano l'affidabilità della build e il rischio di sicurezza. Le scelte sugli strumenti impattano il tempo di onboarding e il costo di mantenere molti servizi nel tempo.
Questa storia viene spesso raccontata attorno alle persone, ma è più utile concentrarsi sui vincoli e sui compromessi. Node.js e Deno rappresentano risposte diverse alle stesse domande pratiche: come eseguire JavaScript fuori dal browser, come gestire le dipendenze e come bilanciare flessibilità con sicurezza e coerenza.
Vedrai perché alcune scelte iniziali di Node.js hanno sbloccato un enorme ecosistema — e cosa quell'ecosistema ha richiesto in cambio. Vedrai anche cosa Deno ha cercato di cambiare e quali nuovi vincoli arrivano con quelle modifiche.
Questo articolo percorre:
È scritto per sviluppatori, tech lead e team che scelgono un runtime per nuovi servizi — o che mantengono codice Node.js esistente e valutano se Deno si adatta a parti dello stack.
Ryan Dahl è noto soprattutto per aver creato Node.js (rilasciato per la prima volta nel 2009) e, più tardi, per aver avviato Deno (annunciato nel 2018). Presi insieme, i due progetti leggono come un resoconto pubblico di come il backend JavaScript si è evoluto — e di come le priorità cambino una volta che l'uso reale mette in luce i compromessi.
Quando Node.js è apparso, lo sviluppo server era dominato da modelli thread-per-request che faticavano con molte connessioni concorrenti. L'obiettivo iniziale di Dahl era semplice: rendere pratico costruire server di rete I/O-heavy in JavaScript associando il motore V8 di Google a un approccio event-driven e a I/O non bloccante.
Gli obiettivi di Node erano pragmatici: rilasciare qualcosa rapidamente, mantenere il runtime piccolo e lasciare alla comunità il compito di colmare le lacune. Questo ha aiutato Node a diffondersi rapidamente, ma ha anche imposto pattern che sono diventati difficili da cambiare in seguito — specialmente riguardo alla cultura delle dipendenze e ai default.
Quasi dieci anni dopo, Dahl presentò “10 Things I Regret About Node.js”, delineando problemi che riteneva fossero radicati nel design originale. Deno è la “seconda bozza” plasmata da quei rimpianti, con default più chiari e un'esperienza sviluppatore più opinionata.
Invece di massimizzare prima la flessibilità, gli obiettivi di Deno puntano verso un'esecuzione più sicura, un supporto moderno per TypeScript e strumenti integrati così che i team abbiano bisogno di meno pezzi di terze parti per iniziare.
Il tema comune ai due runtime non è che uno sia “giusto” — è che vincoli, adozione e retrospettiva possono spingere la stessa persona a ottimizzare per risultati molto diversi.
Node.js esegue JavaScript su un server, ma l'idea centrale riguarda meno il “JavaScript ovunque” e più il modo in cui gestisce l'attesa.
La maggior parte del lavoro backend è attesa: una query al database, la lettura di un file, una chiamata di rete a un altro servizio. In Node.js, l'event loop è come un coordinatore che tiene traccia di questi task. Quando il tuo codice avvia un'operazione che richiederà tempo (come una richiesta HTTP), Node affida quel lavoro di attesa al sistema e passa subito oltre.
Quando il risultato è pronto, l'event loop mette in coda un callback (o risolve una Promise) così il tuo JavaScript può continuare con la risposta.
Il JavaScript di Node gira in un thread principale singolo, il che significa che un pezzo di JS viene eseguito alla volta. Questo sembra limitante finché non capisci che il design evita di fare “attesa” dentro quel thread.
L'I/O non bloccante significa che il server può accettare nuove richieste mentre quelle precedenti sono ancora in attesa del database o della rete. La concorrenza si ottiene:
Per questo Node può sembrare “veloce” sotto molte connessioni simultanee, anche se il tuo JS non corre in parallelo nel thread principale.
Node eccelle quando la maggior parte del tempo è attesa. Fa più fatica quando l'app passa molto tempo a calcolare (elaborazione immagini, cifratura su larga scala, grandi trasformazioni JSON), perché il lavoro intensivo di CPU blocca il thread singolo e rallenta tutto.
Opzioni tipiche:
Node tende a brillare per API e backend-for-frontend, proxy e gateway, app real-time (WebSocket) e CLI orientate agli sviluppatori dove avvio rapido ed ecosistema ricco contano.
Node.js è stato costruito per rendere JavaScript un linguaggio server pratico, specialmente per app che passano molto tempo in attesa di rete: richieste HTTP, database, letture file e API. La sua scommessa centrale era che throughput e reattività contano più del “thread per richiesta”.
Node associa il motore V8 di Google (esecuzione JavaScript veloce) a libuv, una libreria C che gestisce l'event loop e l'I/O non bloccante tra sistemi operativi. Questa combinazione ha permesso a Node di restare single-process e event-driven pur performando bene con molte connessioni concorrenti.
Node ha anche fornito moduli core pragmatici — in particolare http, fs, net, crypto e stream — così potevi costruire server reali senza aspettare pacchetti di terze parti.
Compromesso: una libreria standard ridotta ha mantenuto Node snello, ma ha anche spinto gli sviluppatori verso dipendenze esterne prima che in altri ecosistemi.
Node inizialmente si affidava molto ai callback per esprimere “fai questo quando l'I/O finisce”. Era adatto all'I/O non bloccante, ma generava codice annidato e pattern di gestione degli errori complicati.
Col tempo, l'ecosistema è passato a Promises e poi a async/await, che hanno reso il codice più leggibile pur mantenendo lo stesso comportamento non bloccante.
Compromesso: la piattaforma ha dovuto supportare più generazioni di pattern, e tutorial, librerie e codebase spesso miscelano stili.
L'impegno di Node per la backward compatibility lo ha reso sicuro per le aziende: gli upgrade raramente rompono tutto da un giorno all'altro, e le API core tendono a rimanere stabili.
Compromesso: quella stabilità può ritardare o complicare miglioramenti che richiederebbero rotture nette. Alcune incoerenze e API legacy restano perché rimuoverle danneggerebbe app esistenti.
La capacità di Node di chiamare binding in C/C++ ha abilitato librerie critiche per le performance e l'accesso a funzionalità di sistema tramite addon nativi.
Compromesso: gli addon nativi possono introdurre step di build specifici per piattaforma, errori di installazione difficili e oneri di sicurezza/aggiornamento — specialmente quando le dipendenze devono essere compilate in ambienti diversi.
Complessivamente, Node ha ottimizzato per rilasciare servizi di rete rapidamente e gestire tanto I/O in modo efficiente — accettando complessità in compatibilità, cultura delle dipendenze e evoluzione delle API a lungo termine.
npm è una grande ragione per cui Node.js si è diffuso così in fretta. Ha trasformato “mi serve un server + logging + driver DB” in pochi comandi, con milioni di pacchetti pronti da integrare. Per i team, questo significava prototipi più rapidi, soluzioni condivise e una lingua comune per il riuso.
npm ha abbassato il costo di costruire backend standardizzando come installare e pubblicare codice. Hai bisogno di validazione JSON, di un helper per le date o di un client HTTP? Probabilmente c'è un pacchetto — con esempi, issue e conoscenza comunitaria. Questo accelera la delivery, specialmente quando si assemblano molte piccole feature sotto scadenza.
Il compromesso è che una dipendenza diretta può tirare dentro dozzine (o centinaia) di dipendenze indirette. Col tempo, i team spesso incontrano:
Semantic Versioning (SemVer) sembra rassicurante: patch sicure, minor che aggiungono senza rompere, major che possono rompere. In pratica, grandi grafi di dipendenze mettono alla prova questa promessa.
I manutentori a volte pubblicano cambiamenti breaking in minor, i pacchetti vengono abbandonati o un aggiornamento “sicuro” cambia il comportamento tramite una dipendenza transitiva. Quando aggiorni una cosa, ne puoi aggiornare molte.
Alcune abitudini riducono il rischio senza rallentare lo sviluppo:
package-lock.json, npm-shrinkwrap.json o yarn.lock) e committali.npm audit è una base; considera revisioni pianificate delle dipendenze.npm è al contempo un acceleratore e una responsabilità: rende lo sviluppo veloce e rende l'igiene delle dipendenze una parte reale del lavoro backend.
Node.js è famoso per la sua mancanza di opinioni. È una forza — i team possono assemblare esattamente il workflow che vogliono — ma significa anche che un progetto Node “tipico” è più una convenzione costruita dalle abitudini della comunità.
La maggior parte dei repository Node ruota attorno a un file package.json con script che fungono da pannello di controllo:
dev / start per eseguire l'appbuild per compilare o fare il bundle (quando serve)test per eseguire il test runnerlint e format per applicare lo stile del codicetypecheck quando è coinvolto TypeScriptQuesto pattern funziona bene perché ogni strumento può essere collegato agli script e i sistemi CI/CD possono eseguire gli stessi comandi.
Un workflow Node tipico diventa un insieme di strumenti separati, ognuno risolve una parte:
Nessuno di questi è “sbagliato” — sono potenti, e i team possono scegliere il meglio. Il costo è che stai integrando una toolchain, non solo scrivendo applicazione.
Perché gli strumenti evolvono indipendentemente, i progetti Node possono incontrare ostacoli pratici:
Col tempo, questi punti dolenti hanno influenzato runtime più nuovi — soprattutto Deno — a fornire default (formatter, linter, test runner, supporto TypeScript) in modo che i team possano partire con meno parti mobili e aggiungere complessità solo quando serve.
Deno è nato come un secondo tentativo di runtime JavaScript/TypeScript server — uno che riconsidera alcune scelte iniziali di Node.js dopo anni di uso reale.
Ryan Dahl ha riflettuto pubblicamente su cosa cambierebbe se ricominciasse: l'attrito causato da alberi di dipendenze complessi, la mancanza di un modello di sicurezza di prima classe e la natura “a innesto” delle comodità sviluppatore che col tempo sono diventate essenziali. Gli obiettivi di Deno si possono riassumere in: semplificare il workflow di default, rendere la sicurezza parte esplicita del runtime e modernizzare la piattaforma attorno a standard e TypeScript.
In Node.js, uno script tipicamente può accedere a rete, filesystem e variabili d'ambiente senza chiedere. Deno ribalta quel default. Per default, un programma Deno gira senza alcun accesso a capacità sensibili.
Nella pratica quotidiana significa concedere i permessi intenzionalmente a runtime:
--allow-read=./data--allow-net=api.example.com--allow-envQuesto cambia le abitudini: pensi a cosa il programma dovrebbe poter fare, puoi mantenere i permessi stretti in produzione e hai un segnale più chiaro quando il codice tenta qualcosa di inaspettato. Non è una soluzione di sicurezza completa da sola (servono review del codice e igiene della supply-chain), ma rende il principio del minimo privilegio la via predefinita.
Deno supporta l'importazione di moduli via URL, il che cambia il modo di pensare alle dipendenze. Invece di installare pacchetti in un albero node_modules, puoi riferire il codice direttamente:
import { serve } from "https://deno.land/std/http/server.ts";
Questo spinge i team a essere più espliciti su da dove arriva il codice e quale versione stanno usando (spesso pinando le URL). Deno inoltre esegue la cache dei moduli remoti, quindi non riscarichi ad ogni esecuzione — ma serve comunque una strategia chiara per versioning e aggiornamenti, come con gli upgrade dei pacchetti npm.
Deno non è “Node.js ma migliore per ogni progetto.” È un runtime con default diversi. Node rimane una scelta solida quando dipendi dall'ecosistema npm, da infrastrutture esistenti o da pattern consolidati.
Deno è interessante quando valorizzi strumenti integrati, un modello di permessi e un approccio ESM/URL-first — specialmente per nuovi servizi dove queste assunzioni sono valide fin dall'inizio.
Una differenza chiave tra Deno e Node.js è cosa può fare un programma “per default”. Node assume che se puoi eseguire lo script, può accedere a tutto ciò che il tuo account utente può: rete, file, variabili d'ambiente e altro. Deno ribalta questa assunzione: gli script partono con nessun permesso e devono richiederlo esplicitamente.
Deno tratta le capacità sensibili come funzionalità con gate. Le concedi al runtime nella riga di comando (e puoi limitarle):
--allow-net): se il codice può fare richieste HTTP o aprire socket. Puoi limitarlo a host specifici (per esempio, solo api.example.com).--allow-read, --allow-write): se il codice può leggere o scrivere file. Puoi limitarlo a cartelle (es. ./data).--allow-env): se il codice può leggere segreti e configurazione dalle variabili d'ambiente.Questo riduce il “raggio d'azione” di una dipendenza o di uno snippet copiato, perché non può automaticamente raggiungere posti in cui non dovrebbe.
Per script monouso, i default di Deno riducono l'esposizione accidentale. Uno script che analizza CSV può girare con --allow-read=./input e nient'altro — quindi anche se una dipendenza è compromessa, non può telefonare all'esterno senza --allow-net.
Per piccoli servizi puoi essere esplicito su ciò che serve. Un listener webhook potrebbe avere --allow-net=:8080,api.payment.com e --allow-env=PAYMENT_TOKEN, ma nessun accesso al filesystem, rendendo più difficile l'esfiltrazione di dati in caso di problema.
L'approccio di Node è comodo: meno flag, meno momenti di “perché fallisce?”. L'approccio di Deno aggiunge attrito — specialmente all'inizio — perché devi decidere e dichiarare cosa il programma può fare.
Quell'attrito può essere una caratteristica: costringe i team a documentare l'intento. Ma significa anche più setup e debugging occasionale quando un permesso mancante blocca una richiesta o una lettura di file.
I team possono trattare i permessi come contratto dell'app:
--allow-env o amplia --allow-read, chiedi perché.Usati in modo coerente, i permessi di Deno diventano una checklist di sicurezza leggera che vive vicino al modo in cui esegui il codice.
Deno tratta TypeScript come cittadino di prima classe. Puoi eseguire direttamente un file .ts e Deno si occupa della compilazione dietro le quinte. Per molti team questo cambia la “forma” di un progetto: meno decisioni di setup, meno parti mobili e un percorso più chiaro da “nuovo repo” a “codice funzionante”.
Con Deno, TypeScript non è un optional che richiede una build chain separata dal giorno uno. Di solito non inizi scegliendo un bundler, collegando tsc e configurando script multipli solo per eseguire il codice in locale.
Non significa che i tipi spariscano — i tipi contano ancora. Significa che il runtime si assume la responsabilità per i punti di frizione comuni di TypeScript (esecuzione, cache dell'output compilato e allineamento del comportamento runtime con il type-checking) così i progetti possono standardizzarsi più rapidamente.
Deno include una serie di strumenti che coprono le basi che la maggior parte dei team usa subito:
deno fmt) per stile del codice coerentedeno lint) per controlli di qualità e correttezzadeno test) per test unitari e di integrazionePoiché sono integrati, un team può adottare convenzioni condivise senza dibattere su “Prettier vs X” o “Jest vs Y” all'inizio. La configurazione è tipicamente centralizzata in deno.json, che aiuta a mantenere i progetti prevedibili.
I progetti Node possono assolutamente supportare TypeScript e ottimo tooling — ma di solito devi assemblare il workflow da solo: typescript, ts-node o step di build, ESLint, Prettier e un framework di test. Quella flessibilità è preziosa, ma può portare a setup incoerenti tra repository.
Il language server e le integrazioni editor di Deno mirano a rendere formattazione, linting e feedback TypeScript uniformi tra le macchine. Quando tutti eseguono stessi comandi integrati, i problemi di “funziona sulla mia macchina” spesso si riducono — specialmente attorno a formattazione e regole di lint.
Come importi il codice influenza tutto il resto: struttura delle cartelle, tooling, pubblicazione e anche quanto velocemente un team può rivedere le modifiche.
Node è cresciuto con CommonJS (require, module.exports). È semplice e ha funzionato bene con i pacchetti npm iniziali, ma non è lo stesso sistema di moduli che i browser hanno standardizzato.
Node ora supporta ES modules (ESM) (import/export), ma molti progetti reali vivono in un mondo misto: alcuni pacchetti sono solo CJS, altri solo ESM, e le app a volte hanno bisogno di adattatori. Questo può manifestarsi con flag di build, estensioni dei file (.mjs/.cjs) o impostazioni in "type": "module" in package.json.
Il modello di dipendenza è tipicamente l'importazione per nome di pacchetto risolta tramite node_modules, con versioning controllato da un lockfile. È potente, ma significa anche che il passo di install può diventare parte del debugging quotidiano.
Deno è partito dall'assunto che ESM è il default. Gli import sono espliciti e spesso assomigliano a URL o percorsi assoluti, il che rende più chiaro da dove viene il codice e riduce la “risoluzione magica”.
Per i team, il cambio più grande è che le decisioni sulle dipendenze sono più visibili nelle review: una riga di import spesso dice la fonte esatta e la versione.
Le import maps permettono di definire alias come @lib/ o pinning di una lunga URL a un nome corto. I team le usano per:
Sono utili quando un codebase ha molti moduli condivisi o vuoi nomi coerenti tra app e script.
In Node, librerie si pubblicano su npm; app vengono distribuite con il loro node_modules (o bundle); script spesso si appoggiano a install locali.
Deno rende script e piccoli strumenti più leggeri (esegui direttamente con import), mentre librerie tendono a enfatizzare compatibilità ESM e entry point chiari.
Se mantieni un codice legacy Node, resta su Node e adotta ESM gradualmente dove riduce attrito.
Per un nuovo codice, scegli Deno se vuoi una struttura ESM-first e controllo via import-map dal giorno uno; scegli Node se dipendi pesantemente da pacchetti npm e tooling maturo specifico per Node.
Scegliere un runtime riguarda più la compatibilità che il “meglio”. Il modo più veloce per decidere è allinearsi su cosa il team deve consegnare nei prossimi 3–12 mesi: dove gira, da quali librerie dipende e quanto cambiamento operativo può assorbire.
Fai queste domande in ordine:
Se stai valutando runtime e vuoi comprimere il time‑to‑delivery, può aiutare separare la scelta del runtime dall'effort di implementazione. Per esempio, piattaforme come Koder.ai permettono ai team di prototipare e spedire web, backend e mobile tramite un workflow chat‑driven (con export del codice quando serve). Questo aiuta a eseguire un piccolo pilot “Node vs Deno” senza impegnare settimane in scaffolding.
Node tende a prevalere quando hai servizi Node esistenti, hai bisogno di librerie e integrazioni mature o devi seguire un playbook di produzione collaudato. È anche una scelta forte quando conto su onboarding rapido, perché molti sviluppatori hanno esperienza pregressa.
Deno è spesso adatto per script di automazione sicuri, tool interni e nuovi servizi dove vuoi sviluppo TypeScript-first e una toolchain built-in più unificata con meno decisioni di terze parti.
Invece di riscrivere tutto, scegli un caso d'uso contenuto (un worker, un handler webhook, un job schedulato). Definisci i criteri di successo in anticipo — tempo di build, tasso di errore, performance di cold‑start, sforzo di security review — e limita il pilot nel tempo. Se va bene, avrai un template ripetibile per l'adozione più ampia.
La migrazione raramente è un rewrite totale. La maggior parte dei team adotta Deno a fette — dove il payoff è chiaro e il raggio d'azione è piccolo.
I punti di partenza comuni sono tooling interno (script di release, automazioni repo), utility CLI e edge services (API leggere vicine agli utenti). Queste aree tendono ad avere meno dipendenze, confini più chiari e profili di performance più semplici.
Per i sistemi di produzione, l'adozione parziale è normale: mantenere l'API core su Node.js mentre si introduce Deno per un nuovo servizio, un handler webhook o un job schedulato. Col tempo impari cosa funziona senza obbligare l'intera organizzazione a cambiare in una volta.
Prima di impegnarti, valida alcune realtà:
Inizia con uno di questi percorsi:
Le scelte di runtime non cambiano solo la sintassi — modellano le abitudini di sicurezza, le aspettative sugli strumenti, i profili di hiring e il modo in cui il team mantiene i sistemi negli anni a venire. Considera l'adozione come un'evoluzione del workflow, non come un progetto di riscrittura.
Un runtime è l'ambiente di esecuzione più le API integrate, le aspettative sugli strumenti, le impostazioni di sicurezza e il modello di distribuzione. Queste scelte influenzano come strutturi i servizi, gestisci le dipendenze, risolvi i problemi in produzione e standardizzi i flussi di lavoro tra repository — non solo le prestazioni brute.
Node ha reso popolare un modello event-driven con I/O non bloccante che gestisce efficacemente molte connessioni concorrenti. Questo ha reso JavaScript pratico per server I/O-heavy (API, gateway, app real-time), ma ha anche spinto i team a considerare con attenzione i carichi CPU-bound che possono bloccare il thread principale.
Il thread principale di JavaScript in Node esegue un pezzo di codice alla volta. Se fai calcoli intensi in quel thread, tutto il resto resta in attesa.
Mitigazioni pratiche:
Una libreria standard piccola mantiene il runtime leggero e stabile, ma spinge gli sviluppatori a usare pacchetti di terze parti per necessità quotidiane. Col tempo questo può tradursi in più gestione delle dipendenze, più revisione della sicurezza e più lavoro di integrazione del toolchain.
npm accelera lo sviluppo rendendo il riuso banale, ma crea grandi alberi di dipendenze transitive.
Pratiche consigliate:
npm audit e revisioni periodicheNella realtà dei grafi di dipendenze, gli aggiornamenti possono trascinare molte modifiche transitive e non tutti i progetti rispettano perfettamente SemVer.
Per ridurre le sorprese:
Lo sprawl degli strumenti nasce dall'assemblare strumenti separati per formattazione, linting, testing, TypeScript e bundling. È potente, ma può creare proliferazione di config, mismatch di versioni e drift tra ambienti.
Approccio pratico: standardizza gli script in package.json, fissa le versioni degli strumenti e imposta una versione unica di Node in locale e in CI.
Deno è stato costruito come una “seconda bozza” che rivede decisioni fatte all’epoca di Node: è TypeScript-first, include strumenti built-in (fmt/lint/test), usa moduli ESM e enfatizza un modello di sicurezza basato sui permessi.
Va trattato come un’alternativa con default diversi, non come una sostituzione universale di Node.
Node di solito permette l'accesso completo a rete, filesystem e variabili d'ambiente dell'utente che esegue lo script. Deno nega quelle capacità per default e richiede flag espliciti (es. --allow-net, --allow-read).
Nella pratica, questo incoraggia il principio del privilegio minimo e rende le modifiche ai permessi verificabili insieme al codice.
Inizia con un pilot piccolo e definisci criteri di successo (deployabilità, performance, osservabilità, sforzo di manutenzione).
Controlli iniziali: