Menu

CommonJS vs ES Modules: require o import in Node

I due sistemi di moduli di JavaScript, perché esistono entrambi e come scegliere tra require e import nei progetti Node.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Due sistemi di moduli, un solo linguaggio

In origine JavaScript non aveva alcun sistema di moduli. Node ha colmato il vuoto nel 2009 con CommonJS (require, module.exports), e per anni il codice Node è stato così. Poi nel 2015 il linguaggio stesso ha introdotto un sistema di moduli standard, gli ES Modules (import, export), che oggi supportano sia i browser sia Node.

Quindi in giro li vedrai entrambi. Ecco lo stesso piccolo modulo scritto nei due modi:

Stessa funzione, due involucri diversi. Il resto della pagina spiega quando l'involucro conta e quale scegliere.

Le differenze di sintassi

Le differenze di tutti i giorni stanno su una cartolina:

// CommonJS
const fs = require("fs");
const { readFile } = require("fs/promises");

module.exports = something;
module.exports.name = value;
exports.name = value;
// ES Modules
import fs from "fs";
import { readFile } from "fs/promises";

export default something;
export const name = value;
export { name };

require è una normale chiamata di funzione. import è un'istruzione: può comparire solo al livello principale di un modulo e il percorso deve essere una stringa letterale. Questa restrizione non è arbitraria: è ciò che permette a ESM di fare cose che CommonJS non può fare.

La vera differenza: statico e dinamico

CommonJS valuta require() quando viene eseguita la riga. Puoi metterlo dentro un if, calcolare il percorso a runtime, caricare un modulo in modo condizionale:

Gli ES Modules sono statici. Il motore analizza tutte le istruzioni import prima di eseguire qualsiasi codice, costruisce un grafo delle dipendenze e risolve tutto in anticipo. Ecco perché il percorso deve essere una stringa letterale e perché import compare solo al livello principale.

Il vantaggio: gli strumenti possono vedere l'intero grafo dei moduli senza eseguire nulla. È così che i bundler fanno il tree-shaking (eliminando gli export non usati), che gli editor ti danno un completamento automatico preciso e che il browser può scaricare i moduli in parallelo.

Quando ti serve davvero un caricamento dinamico in ESM, usa import(), un'espressione simile a una funzione che restituisce una Promise:

Come Node decide quale sistema usa un file

Un singolo progetto Node può contenere entrambi i tipi di file. Node capisce quale sistema usa ciascun file guardando due cose:

  • L'estensione del file: .mjs è sempre ESM, .cjs è sempre CommonJS.
  • Il campo "type" del package.json più vicino: "module" significa che i file .js sono ESM, "commonjs" (il valore predefinito se manca) significa che sono CJS.
// package.json
{
    "name": "my-app",
    "type": "module"
}

Con "type": "module", un semplice hello.js nello stesso pacchetto usa import/export. Metti un hello.cjs accanto e quel singolo file usa require. È così che un progetto può migrare gradualmente, o che una libreria può distribuire entrambe le versioni fianco a fianco.

Tranello per chi inizia: in un file ESM, require e module.exports semplicemente non esistono. Se li usi per abitudine otterrai un ReferenceError.

Interoperabilità: mescolare i due sistemi

Capiterà spesso che un file ESM debba usare un pacchetto CommonJS, o viceversa. Le regole non sono simmetriche.

ESM che importa CommonJS funziona direttamente. L'oggetto module.exports di CJS diventa l'export predefinito:

// app.mjs
import greet from "./greet.cjs";
console.log(greet("Rosa"));

Gli import con nome da CommonJS a volte funzionano (Node prova a rilevare gli export con nome in modo statico), ma per andare sul sicuro prendi l'export predefinito e destrutturalo:

import pkg from "./utils.cjs";
const { parse, stringify } = pkg;

CommonJS che importa ESM è la direzione dolorosa. Non puoi fare require() di un ES module: genera l'errore ERR_REQUIRE_ESM. La via d'uscita è l'import() dinamico, che funziona anche in CJS e restituisce una Promise:

Il Node moderno (22+) ha aggiunto un require() sincrono per ESM in certe condizioni, ma l'import() dinamico resta la risposta portabile.

Altre differenze di comportamento da conoscere

Oltre alla sintassi, i due sistemi non concordano su alcuni dettagli che ogni tanto fanno male:

Qualche altra:

  • this al livello principale. In CJS, this è module.exports. In ESM è undefined. ESM è sempre in strict mode.
  • __dirname e __filename. CJS te li dà gratis. In ESM li ricavi da import.meta.url:
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
  • Estensioni dei file negli import. ESM richiede l'estensione ("./utils.js", non "./utils") per i percorsi relativi. CJS è più tollerante.
  • Binding vivi o copie. Gli import ESM sono riferimenti vivi alle variabili del modulo che le esporta. CJS ti dà una copia di ciò che era assegnato a module.exports al momento del caricamento. La maggior parte del codice non se ne accorge mai, ma conta con le dipendenze circolari.

Quale dei due usare?

Per un nuovo progetto: ES Modules. Imposta "type": "module" in package.json e non voltarti indietro. ESM è lo standard del linguaggio, funziona allo stesso modo nei browser e in Node, supporta il top-level await e gli strumenti sono costruiti pensando a lui.

Resta su CommonJS quando:

  • Mantieni una codebase CJS esistente e la migrazione non vale ancora la pena.
  • Pubblichi una libreria che deve supportare versioni di Node molto vecchie o utenti che non possono usare ESM.
  • Una dipendenza fondamentale distribuisce solo CJS e la sua interoperabilità è complicata. (Oggi è raro, ma succede ancora.)

Anche in quei casi leggerai continuamente codice ESM: tutto ciò che è stato pubblicato su npm negli ultimi anni va in quella direzione. Saperli leggere entrambi non è facoltativo; conoscere bene gli idiomi di quello che stai davvero scrivendo, sì, è la parte su cui concentrarti.

Una checklist mentale veloce

Quando apri un nuovo file, chiediti:

  • Questo file usa import/export oppure require/module.exports? Non mescolarli.
  • Cosa dice il package.json più vicino riguardo a "type"?
  • Se importi un pacchetto, controlla il suo package.json: distribuisce ESM, CJS o entrambi?
  • Se incontri ERR_REQUIRE_ESM, sei in CJS e stai cercando di caricare ESM. Passa all'import() dinamico o sposta il chiamante su ESM.

Il novanta per cento della confusione sui moduli in Node rientra in uno di questi quattro casi.

Prossimo passo: le basi di npm

I moduli servono a suddividere il tuo codice in più file. Il passo successivo è usare codice scritto da altri: è a questo che serve npm. Vedremo come installare i pacchetti, gli intervalli semver e le parti del flusso di lavoro di npm che usi davvero ogni giorno.

Domande frequenti

Che differenza c'è tra require e import in JavaScript?

require è il modo di CommonJS per caricare i moduli: è sincrono, viene eseguito nel punto in cui lo chiami e restituisce ciò che il modulo ha assegnato a module.exports. import è la sintassi degli ES Modules: è statico, viene portato in cima al file e analizzato prima che il codice venga eseguito. I due sistemi non concordano nemmeno su cosa sia this, su come si risolvono le dipendenze circolari e sul fatto che sia ammesso il top-level await.

In un nuovo progetto Node conviene usare CommonJS o ES Modules?

Usa gli ES Modules. Imposta "type": "module" in package.json e scrivi import/export. ESM è lo standard ufficiale, funziona nei browser e in Node e supporta il top-level await. CommonJS compare ancora nei pacchetti e negli strumenti più vecchi, quindi lo leggerai anche se non lo scrivi.

Posso mescolare require e import nello stesso progetto?

Sì, con delle regole. Un file .mjs o un pacchetto con "type": "module" usa ESM; un .cjs o "type": "commonjs" usa CJS. ESM può fare import di un modulo CommonJS (il module.exports diventa l'export predefinito). CommonJS invece non può fare require() di un modulo ESM direttamente: devi usare l'import() dinamico, che restituisce una Promise.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA