Devi lavorare con il formato Base64? Allora questo sito è perfetto per te! Usa il nostro praticissimo strumento online per codificare o decodificare i tuoi dati.

Codifica Base64 in JavaScript/Node.js: una guida completa

Hai dei dati che devono diventare testo. Un file che deve viaggiare dentro un campo JSON, un'immagine che vuole vivere in un file CSS, un segreto che stazionerà in una variabile d'ambiente, un token che attraverserà una stringa di query. La risposta in JavaScript e Node.js è quasi sempre la stessa: Base64. Questo articolo è il manuale di imballaggio, dal primo byte che hai in mano al momento in cui la tua stringa codificata lascia la macchina.

La home page di questo sito spiega il formato in ogni dettaglio, l'alfabeto, la matematica, il riempimento, quindi qui restiamo in una sola frase: ogni tre byte diventano quattro caratteri stampabili, ed è per questo che il tuo output sarà circa il 33 percento più grande dell'input. Tienilo in tasca, perché è la ragione per cui esiste ogni sezione di questo articolo, ed è il numero in cui è calcolato il tuo conto di archiviazione.

La parte rassicurante: non installi nulla. Ogni browser moderno distribuisce btoa() e il più recente Uint8Array.toBase64(), e ogni versione di Node.js che conti porta la classe Buffer con una modalità 'base64' e, dalla versione 15.7.0, una modalità 'base64url' di prima classe. L'arte sta nel sapere quale forma di input hai in mano, quale alfabeto esige la destinazione, e quali regole di a capo i vecchi formati impongono ancora.

Conosci il tuo input prima di codificare

Ogni domanda di codifica parte dalla stessa: cosa hai esattamente in mano? Una stringa JavaScript è testo UTF-16, un Buffer è un array di byte, e la chiamata giusta dipende da quale dei due hai:

Cosa hai in mano Chiamare questo Note
Una stringa solo ASCII (caratteri sotto 256) btoa(string) La via più veloce nei browser e in Node.js 16+, ma si ferma al primo carattere che non sta in un byte
Qualsiasi stringa Unicode TextEncoder verso i byte, poi una chiamata base64 Il ponte UTF-8; l'unica via sicura per lettere accentate ed emoji
Un Buffer o un Uint8Array buffer.toString('base64') o bytes.toBase64() Il cavallo da lavoro di Node.js, e il metodo ES2026 nei browser moderni e in Node.js 25+

Tre esempi, uno per ogni riga della tabella:

// Testo solo ASCII: la scorciatoia di eredità (browser e Node.js 16+)
console.log(btoa('hello world')); // "aGVsbG8gd29ybGQ="
// Qualsiasi testo in Node.js: il Buffer legge UTF-8 per predefinito
const { Buffer } = require('node:buffer');
console.log(Buffer.from('héllo ⛳', 'utf8').toString('base64')); // "aMOpbGxvIOKbsw=="
// Byte che possiedi già
console.log(Buffer.from([1, 2, 3, 4]).toString('base64')); // "AQIDBA=="
console.log(new Uint8Array([1, 2, 3, 4]).toBase64()); // "AQIDBA==" (runtime ES2026)

Nota il secondo esempio: lo stesso testo produce una stringa Base64 diversa a seconda della codifica dei caratteri in cui lo codifichi. Non è un bug - è tutto il gioco. Il livello Base64 codifica byte, e una stringa diventa byte solo quando hai scelto una codifica dei caratteri, quindi "codifica questo testo" significa sempre di nascosto "codifica i byte UTF-8 di questo testo" (o i byte Latin-1, se lo dici tu).

Il muro Unicode e i ponti che lo attraversano

btoa() è l'API più antica della stanza, e il suo contratto è degli anni Novanta: ogni carattere della stringa in ingresso deve stare in un singolo byte, punti di codice tra 0 e 255. Tutto ciò che supera quel limite, un emoji, una lettera cirillica accentata, un carattere cinese, lancia:

try {
  btoa('héllo ⛳');
} catch (error) {
  console.log(error.name); // "InvalidCharacterError"
  console.log(error.message); // "Invalid character" in Node; il messaggio sul range Latin1 nei browser
}

La soluzione è smettere di pensare per caratteri e iniziare a pensare per byte. TextEncoder (una globale in ogni browser e in Node.js) trasforma la stringa nella sua sequenza di byte UTF-8; sposti quei byte in una stringa Latin-1, e btoa() riceve esattamente ciò che aveva promesso di gestire:

function encodeUnicode (text) {
  const bytes = new TextEncoder().encode(text);
  let binary = '';
  for (const byte of bytes) {
    binary += String.fromCharCode(byte);
  }
  return btoa(binary);
}
console.log(encodeUnicode('héllo ⛳')); // "aMOpbGxvIOKbsw=="
console.log(encodeUnicode('héllo ⛳') === Buffer.from('héllo ⛳', 'utf8').toString('base64')); // true, gli stessi byte

Incontrerai anche l'idioma più vecchio nei codebase, e sotto il cofano funziona allo stesso modo: btoa(unescape(encodeURIComponent(text))). La chiamata encodeURIComponent produce byte UTF-8 codificati con percentuali, e unescape trasforma le escape percentuali di nuovo in caratteri grezzi. Sia escape sia unescape sono funzioni di eredità, quindi il codice nuovo dovrebbe preferire il ponte TextEncoder, ma quando erediti la forma vecchia, adesso sai esattamente cosa sta facendo invece di alzare le spalle.

In Node.js il muro è quasi una non-notizia, perché Buffer.from(text) dà per scontato l'UTF-8 e fa la conversione in byte per te nella stessa chiamata. Il ponte conta di più nel browser, dove btoa() è l'opzione di eredità e il passaggio UTF-8 tocca a te farlo in modo esplicito.

Byte in entrata, lettere in uscita: Buffer, riempimento e varianti

Una volta che hai i byte, il lato codifica di Node.js è un metodo: toString('base64'). Gestisce la matematica dei gruppi, il riempimento, tutto, e produce sempre output canonico in senso RFC 4648, cioè i bit di riempimento non utilizzati dell'ultimo gruppo sono zero:

const { Buffer } = require('node:buffer');
const fox = Buffer.from('The quick brown fox jumps over the lazy dog');
console.log(fox.length); // 43 byte
console.log(fox.toString('base64')); // "VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw=="
console.log(fox.toString('base64').length); // 60 caratteri, la tassa del 33 percento in azione

Il riempimento alla fine sta facendo lavoro vero, non è decorazione. Un gruppo finale con un byte rimanente diventa due caratteri Base64 più due =, e un gruppo con due byte rimanenti diventa tre caratteri più un =. Se il tuo output può portare quel riempimento dipende dalla destinazione, ed è quella la differenza tra le due varianti di Base64 che userai ogni giorno:

const one = new Uint8Array([72]);
console.log(one.toBase64()); // "SA==" (ES2026, riempimento incluso)
console.log(one.toBase64({ omitPadding: true })); // "SA"
console.log(Buffer.from([72]).toString('base64url')); // "SA", Node elimina il riempimento in modalità base64url

Ricordati la proporzione quando dimensioni le cose: tre byte in entrata, quattro caratteri in uscita, quindi 1 MB di dati diventano circa 1,33 MB di testo, e se impacchetti il testo in righe per email o PEM, gli a capo aggiungono qualche percentuale in più.

Far attraversare i byte al cavo

Il problema più comune del cavo in JavaScript è che il JSON non ha byte. Ha stringhe, e la stringa che può viaggiare in sicurezza attraverso qualsiasi parser JSON, qualsiasi proxy HTTP e qualsiasi sistema di logging è quella Base64. Lo schema è lo stesso alle due estremità della connessione: codifica sul confine, decodifica sul confine, byte in mezzo:

const fs = require('node:fs');
const photo = fs.readFileSync('./photo.jpg', 'base64');
const payload = JSON.stringify({
  name: 'photo.jpg',
  contentType: 'image/jpeg',
  data: photo
});
console.log(payload.startsWith('{"name":"photo.jpg"')); // true, il file viaggia adesso dentro JSON ordinari

Sapere quando sfidare questo schema. Se il tuo trasporto supporta già il binario, usalo: un caricamento multipart/form-data invia il file grezzo senza tassa di dimensione, un frame WebSocket porta byte grezzi, e una colonna bytea di Postgres li conserva in modo nativo. Il Base64 in un posto dove i byte grezzi erano ammessi è puro sovraccarico, la tassa del 33 percento senza nulla da mostrare. Il Base64 ripaga quando il canale è solo testo: API JSON, corpi di email, variabili d'ambiente, stringhe di query URL, e i tanti ponti (SDK mobile, app desktop, sistemi di chat) che lasciano passare solo il testo.

Data URL: immagini che vivono nel testo

La data URL, la stringa data:image/png;base64,..., è Base64 con addosso un'etichetta MIME, ed è la ragione per cui puoi mettere un'immagine intera dentro un singolo attributo HTML. L'RFC del 1998 che ha definito lo schema dice persino che è "utile solo per valori brevi", perché le prime versioni di HTML avevano un limite di 1024 caratteri sui valori degli attributi. I browser moderni ridono di quel limite e rendono volentieri data URL di dimensioni in megabyte, il che è al tempo stesso un superpotere e una trappola.

Nel browser l'API canvas fa tutto il lavoro per te, pixel in entrata, data URL in uscita:

const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const context = canvas.getContext('2d');
context.fillStyle = '#ff0000';
context.fillRect(0, 0, 1, 1);
const dataUrl = canvas.toDataURL('image/png'); // "data:image/png;base64,iVBOR..."
console.log(dataUrl.slice(0, 24)); // "data:image/png;base64,iV"

E in ogni runtime, incluso Node.js, costruirne una è solo concatenazione di stringhe con i metadati nel posto giusto: un prefisso data:, il tipo media, il marcatore opzionale ;base64, una virgola, e il carico utile. Senza il marcatore ;base64 il carico utile è atteso come testo codificato con percentuali, ed è per questo che il marcatore esiste:

const { Buffer } = require('node:buffer');
const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB', 'base64');
const dataUrl = 'data:image/png;base64,' + png.toString('base64');
console.log(dataUrl.startsWith('data:image/png;base64,')); // true

I compromessi onesti: una data URL è parte del documento, quindi non è memorizzabile nella cache come risorsa propria, aggiunge peso all'HTML o al CSS in cui sta, e il DOM deve analizzarla e tenerla. Per un'icona da 20 kilobyte è un affare. Per un'immagine principale da 4 megabyte, spedisci il file via HTTP dove cache e compressione funzionano entrambe, e conserva la data URL per le cose piccole.

File che viaggiano come stringhe

Da file a Base64 è una danza in due tempi che entrambi i runtime comprimono in una singola chiamata. In Node.js il filesystem accetta 'base64' come codifica di lettura, e il lato scrittura lo accetta pure:

const fs = require('node:fs');
const { Buffer } = require('node:buffer');
const base64 = fs.readFileSync('./report.pdf', 'base64');
console.log(base64.length); // il file, circa il 33 percento più pesante
fs.writeFileSync('./report.pdf.b64', base64, 'utf8');
const copy = Buffer.from(base64, 'base64');
fs.writeFileSync('./report.copy.pdf', copy);

Nel browser il FileReader fa lo stesso lavoro, con una sorpresa: la sua modalità di lettura dei dati ti consegna una data URL, quindi tagli via il prefisso per ottenere il carico utile Base64 nudo:

const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', () => {
  const reader = new FileReader();
  reader.onload = () => {
    const dataUrl = reader.result; // "data:application/pdf;base64,..."
    const payload = {
      name: fileInput.files[0].name,
      data: dataUrl.slice(dataUrl.indexOf(',') + 1)
    };
    console.log(payload.data.length); // il file, pronto per una richiesta JSON
  };
  reader.readAsDataURL(fileInput.files[0]);
});

Se nel browser ti serve il Base64 grezzo senza il prefisso data URL, file.arrayBuffer() seguito da Uint8Array.toBase64() (sui runtime che ce l'hanno) salta il prefisso del tutto ed è la via più pulita per le pipeline di caricamento.

base64url: l'alfabeto che sopravvive agli URL

Il Base64 classico porta con sé due caratteri a cui gli URL sono allergici. Il + diventa uno spazio ogni volta che una stringa di query viene decodificata da modulo, la / è un separatore di percorso, e il riempimento = sembra un'assegnazione. La variante sicura per URL e nomi di file della sezione 5 dell'RFC 4648, base64url, scambia i due speciali con - e _ e elimina il riempimento ogni volta che la lunghezza è nota dal contesto. È l'alfabeto dei JWT, dei token OAuth e dei deep link, e merita un posto di riguardo nel tuo arsenale mentale.

Il Buffer di Node parla questo dialetto dalla versione 15.7.0, e il lato codifica è un argomento:

const { Buffer } = require('node:buffer');
const classic = 'k+XS/B4=';
console.log(Buffer.from(classic, 'base64').toString('base64url')); // "k-XS_B4"

Due cose da notare. Il + è diventato un -, la / è diventata un _, e il riempimento è svanito, perché la modalità base64url lo omette per costruzione. E l'IETF è esplicito nel dire che si tratta di una codifica diversa, non la stessa con un costume, quindi quando una specifica dice "base64url", dovresti produrre base64url, non Base64 classico con un cerca-e-sostituisci. Il metodo ES2026 rende le stesse scelte opzioni esplicite, e il suo flag omitPadding ti ridà il riempimento quando il contesto lo esige:

console.log(new Uint8Array([0xab, 0xff]).toBase64({ alphabet: 'base64url', omitPadding: true })); // "q_8"
console.log(new Uint8Array([0xab, 0xff]).toBase64({ alphabet: 'base64url' })); // "q_8=", due byte richiedono un carattere di riempimento

Sui runtime senza nessuno dei due, la conversione è uno scambio di due caratteri più una potatura del riempimento, ed è uno degli snippet più copiati e incollati del mondo JavaScript:

const toUrlSafe = (value) => value.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
console.log(toUrlSafe('k+XS/B4=')); // "k-XS_B4"

Usa base64url per qualsiasi cosa che vivrà in un URL, in una stringa di query, in un nome di file, o in uno standard di token. Usa il Base64 classico per corpi MIME, data URL e qualsiasi cosa che non incontrerà mai un decodificatore di percentuali. Confonderli è il bug di interoperabilità più comune dell'intero formato.

Sigillare le credenziali: Basic Auth, JWT e PKCE

Tre pilastri dell'autenticazione del web sono costruiti sul Base64, e tutti e tre sono economici da costruire a mano una volta, il che è una buona cosa, perché sapere cosa succede sotto la libreria è ciò che ti mantiene calmo quando la libreria ti sorprende.

Prima, l'autenticazione HTTP Basic (RFC 7617): il client invia la parola di schema Basic più il Base64 di user-id:password. Una riga, e un'avvertenza seria allegata:

const { Buffer } = require('node:buffer');
console.log('Basic ' + Buffer.from('octo:cat').toString('base64')); // "Basic b2N0bzpjYXQ="

Qui il Base64 è oscuramento, non sicurezza. Chiunque possa leggere l'intestazione può leggere la password, quindi questo schema è accettabile solo su HTTPS, e persino allora è uno schema di eredità: preferisci i token. Secondo, il JWT: le prime due parti separate da punti sono base64url di JSON grezzo, e la terza è la firma. Costruire a mano un token HMAC-SHA256 è una manciata di righe del modulo crypto integrato:

const crypto = require('node:crypto');
const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');
const payload = Buffer.from(JSON.stringify({ sub: 'octocat', exp: 1893456000 })).toString('base64url');
const signature = crypto.createHmac('sha256', 'topsecret').update(header + '.' + payload).digest('base64url');
const token = header + '.' + payload + '.' + signature;
console.log(token.split('.').length); // 3 parti, base64url senza riempimento dappertutto

Nota i dettagli che fanno o disfano un token: nessun riempimento da nessuna parte (l'RFC 7515 lo omette), la firma è calcolata sulla stringa letterale header + '.' + payload, non sugli oggetti analizzati, e l'intera faccenda è segreta solo quanto la chiave. In produzione userai una libreria, jose (zero dipendenze, browser e Node.js) o jsonwebtoken (Node.js), ma sotto il cofano eseguono proprio queste chiamate. Terzo, PKCE (RFC 7636), l'estensione che lascia che i client pubblici come SPA e app mobile si autenticino in sicurezza: il client genera un code_verifier ad alta entropia, pubblica BASE64URL(SHA256(verifier)) come sfida, e dimostra il possesso del verifier allo scambio del token. La casualità conta, quindi il verifier viene dal modulo crypto, mai da Math.random():

const verifier = crypto.randomBytes(32).toString('base64url');
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
console.log(verifier.length, challenge.length); // 43 43, entrambi dentro l'intervallo consentito 43-128

La posta vecchia va avvolta: armatura MIME e PEM

Due dei formati Base64 più vecchi del mondo impongono ancora lunghezze di riga, e entrambi hanno circa 30 anni. Il MIME, lo standard di email dell'RFC 2045, avvolge il suo Base64 a 76 caratteri per riga e richiede che le righe finiscano con CRLF, un relitto dei giorni di SMTP a 8 bit puliti, quando righe lunghissime rompevano i server di posta veri. L'RFC 7468, che mette per iscritto le regole PEM per certificati e chiavi, è ancora più severo: i generatori devono avvolgere a esattamente 64 caratteri per riga, l'ultima riga più corta, con il tutto incorniciato da righe di armatura -----BEGIN e -----END che nominano il contenuto.

L'avvolgimento in sé è una riga sola di codice, e l'armatura è un modello:

const wrap = (base64, width) => base64.match(new RegExp('.{1,' + width + '}', 'g')).join('\r\n');
const certBase64 = Buffer.from('x'.repeat(150), 'utf8').toString('base64'); // 200 caratteri
console.log(wrap(certBase64, 76).split('\r\n').map((line) => line.length).join(', ')); // "76, 76, 48"
console.log(wrap(certBase64, 64).split('\r\n').map((line) => line.length).join(', ')); // "64, 64, 64, 8"
const armor = (label, body) => '-----BEGIN ' + label + '-----\r\n' + wrap(body, 64) + '\r\n-----END ' + label + '-----\r\n';
console.log(armor('CERTIFICATE', 'QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo='));
// -----BEGIN CERTIFICATE-----
// QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVo=
// -----END CERTIFICATE-----

Due note pratiche. Quando produci MIME o PEM, avvolgilo, perché i consumatori rigorosi (gateway di posta, strumenti dell'era OpenSSL, archivi di chiavi Java) rifiuteranno un blob Base64 di 4000 caratteri su una riga sola. Quando lo consumi, di solito non serve, perché il decodificatore di Node salta gli spazi bianchi, quindi Buffer.from gestisce gli a capo per te - ma le righe di armatura in sé non sono Base64, quindi rimuovi le righe -----BEGIN/-----END (o fai corrispondere solo il corpo) prima di decodificare: Buffer.from(pem.replace(/-----[A-Z ]+-----/g, ''), 'base64'). Quella asimmetria è un dono, ma non significa che puoi saltare il passaggio di rimozione dell'armatura quando il Base64 va in un posto che non salta nulla, come un parser DER.

Dove vivono i dati codificati: variabili d'ambiente, configurazioni e database

Il Base64 è anche un formato di archiviazione, il che è al tempo stesso comodo e pericolosamente facile da scambiare per sicurezza. Le variabili d'ambiente sono la casa classica: diversi gestori di segreti e sistemi CI ti consegnano valori codificati in Base64, e la decodifica è una riga sola:

const { Buffer } = require('node:buffer');
const stored = process.env.API_KEY_B64; // "c3VwZXItc2VjcmV0"
console.log(Buffer.from(stored, 'base64').toString('utf8')); // "super-secret"

Di' ad alta voce la frase importante: codificare non è cifrare. Un "segreto" Base64 in una variabile d'ambiente, in un file .env o in un segreto Kubernetes (k8s conserva i suoi segreti in Base64 nell'API e in etcd, e i documenti lo ripetono in continuazione) è leggibile da chiunque possa leggere l'ambiente del processo, il file, o il cluster. Usa Base64 lì perché il trasporto (shell, YAML, JSON) è solo testo, mai perché credi che nasconda qualcosa.

Nei database, il Base64 è il ponte standard per il binario dentro gli archivi di documenti JSON, perché una colonna jsonb o un documento MongoDB non ha un tipo byte proprio:

const document = {
  name: 'logo',
  mime: 'image/png',
  data: Buffer.from([0x89, 0x50, 0x4e, 0x47]).toString('base64')
};
console.log(JSON.stringify(document)); // {"name":"logo","mime":"image/png","data":"iVBORw=="}

Conserva il tipo media accanto al carico utile, come fa l'esempio, e ti ringrazierai tra un anno quando qualcuno chiederà cosa sono quei byte. Se il tuo database ha un tipo binario nativo (Postgres bytea è l'esempio di riferimento), preferiscilo: i byte non costano nulla in più, e salti la tassa del 33 percento per sempre.

Codificare flussi senza spezzare i gruppi

Il Base64 lavora in gruppi di tre byte, quindi un codificatore che riceve frammenti arbitrari deve trascinare il resto: uno o due byte che non possono formare un gruppo devono aspettare il frammento successivo prima di poter essere codificati. Fai i conti per ogni frammento ed emetti solo gruppi completi, e l'output è identico byte per byte alla codifica dell'intero flusso in un colpo solo:

const { Transform } = require('node:stream');
const { Buffer } = require('node:buffer');
function base64Encoder () {
  let pending = Buffer.alloc(0);
  return new Transform({
    transform (chunk, _encoding, done) {
      pending = Buffer.concat([pending, chunk]);
      const whole = Math.floor(pending.length / 3) * 3;
      this.push(pending.subarray(0, whole).toString('base64'));
      pending = pending.subarray(whole);
      done();
    },
    flush (done) {
      if (pending.length > 0) {
        this.push(pending.toString('base64'));
      }
      done();
    }
  });
}
let output = '';
const encoder = base64Encoder();
encoder.on('data', (part) => { output += part; });
encoder.on('end', () => {
  console.log(output); // "aGVsbG8gd29ybGQsIHRoaXMgaXMgYSBzdHJlYW0h"; identico a un unico grande toString('base64')
});
encoder.end(Buffer.from('hello world, this is a stream!'));

La callback flush è il dettaglio che tutti dimenticano: gli ultimi uno o due byte, quelli che non hanno mai trovato un compagno in un frammento regolare, ricevono il loro riempimento e vengono spinti fuori alla fine. La stessa logica di trascinamento è ciò che ricalcherai sul lato decodifica, tranne che lì l'API ES2026 te la regala: setFromBase64() con "stop-before-partial" si ferma esattamente sui confini dei gruppi e ti dice quanti caratteri ha consumato.

File grossi e il conto della memoria

Il Base64 è generoso con lo spazio, quindi i file grossi hanno bisogno di una strategia. Un file da 1 GB diventa circa 1,33 GB di testo Base64, e una stringa JavaScript conserva UTF-16, due byte di heap per carattere, quindi quel testo da solo vuole circa 2,7 GB di memoria prima che arrivi il tuo Buffer. Il tetto è esplicito in Node: buffer.constants.MAX_STRING_LENGTH è 536870888 caratteri, poco meno di 512 MiB di testo, che decodificati diventano circa 400 MB di byte. Oltre, una singola stringa non è un'opzione, e lo streaming è l'unico gioco in città:

const fs = require('node:fs');
const { Buffer } = require('node:buffer');
let carried = Buffer.alloc(0);
const source = fs.createReadStream('./video.mp4', { highWaterMark: 64 * 1024 });
source.on('data', (chunk) => {
  const joined = Buffer.concat([carried, chunk]);
  const whole = Math.floor(joined.length / 3) * 3;
  process.stdout.write(joined.subarray(0, whole).toString('base64'));
  carried = joined.subarray(whole);
});
source.on('end', () => {
  if (carried.length > 0) {
    process.stdout.write(carried.toString('base64'));
  }
  process.stdout.write('\n');
});

Lo schema è il codificatore di flusso della sezione precedente, appiattito: leggi in frammenti da 64 KiB, trascina il resto di 1 o 2 byte, emetti gruppi completi, svuota la coda. L'impronta di memoria resta attorno a un frammento più un resto, qualunque sia il peso del file. E se l'estremità ricevente può accettare il binario, chiediti perché stai pagando la tassa in primo luogo.

Comandi in una riga per il terminale

Node fa anche da codificatore Base64 da riga di comando, il che è comodo quando stai imballando un valore di configurazione, facendo debug di un'API, o spostando un piccolo file tra macchine attraverso un messaggio di chat:

# Codifica un file in Base64 classico su stdout
node -e 'const fs=require("node:fs");process.stdout.write(fs.readFileSync(process.argv[1],"base64"))' notes.txt
# La variante URL-safe, riempimento eliminato
node -e 'const fs=require("node:fs");process.stdout.write(fs.readFileSync(process.argv[1],"base64url"))' notes.txt
# Leggi da stdin, a questo servono le pipe
echo -n "hello world" | node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>process.stdout.write(Buffer.from(d,"utf8").toString("base64")))'

Nessuno dei tre aggiunge un a capo finale suo, il che tiene l'output pulito per il copia-incolla e per la sostituzione $(...) negli script shell. Se vuoi un file formattato con righe avvolte, passa il risultato nel tuo editor preferito, o aggiungi un \n alla fine del comando in una riga.

Trappole che sono costate ore agli sviluppatori

Ognuna di queste ha costato un pomeriggio vero in un codebase vero:

  • Il muro Unicode: btoa('héllo ⛳') lancia InvalidCharacterError perché la bandierina da golf non sta in un byte. La soluzione è il ponte UTF-8: TextEncoder verso i byte prima, poi codifica. In Node.js salti il problema del tutto con Buffer.from(text), che dà per scontato l'UTF-8.
  • L'idioma di eredità: il vecchio codice pieno di btoa(unescape(encodeURIComponent(x))) funziona, ma escape e unescape sono funzioni di eredità deprecate. Quando rifattori quel codice, sostituiscilo con il ponte TextEncoder e il comportamento resta identico.
  • L'argomento codifica mancante, al contrario: la trappola sul lato decodifica è Buffer.from(str) senza la modalità 'base64'; la gemella sul lato codifica è dare per scontato che Buffer.from(someString) faccia qualcosa di speciale con il Base64. Non lo fa. Senza una codifica esplicita costruisce un Buffer dai byte UTF-8 della stringa, e il tuo output "codificato" è il Base64 dei byte delle lettere della stringa, quasi mai ciò che si voleva. Sii esplicito in entrambe le direzioni.
  • Il disallineamento del riempimento: i JWT e la maggior parte degli standard di token vogliono base64url senza riempimento, il MIME vuole Base64 classico con riempimento, e i due sono facili da confondere. Un = di riempimento dentro una parte JWT rompe i verificatori rigorosi; un riempimento mancante dove la lunghezza è sconosciuta rompe i decodificatori pigri. Allineati allo standard, non alla tua abitudine.
  • La coda non canonica: l'RFC 4648 richiede che i bit di riempimento non utilizzati dell'ultimo gruppo siano zero. I codificatori integrati producono tutti output canonico, ma un codificatore fatto a mano che sposta i bit a mano può lasciare spazzatura in quei bit, e un decodificatore rigoroso rifiuterà il tuo carico utile per nessuna ragione apparente. Se scrivi il tuo codificatore, testalo contro i vettori di test dell'RFC 4648, non solo contro i tuoi dati.
  • L'avvolgimento dimenticato: il MIME vuole righe da 76 caratteri e il PEM ne vuole da 64, e i consumatori rigorosi (gateway di posta, strumenti per le chiavi Java) rifiutano un blob su una riga sola. Il contrario è più raro ma reale: alcuni parser sono orientati alle righe, e un CRLF mancante alla fine di un file PEM ha rotto più build di qualsiasi bug nel Base64 stesso.
  • L'illusione di sicurezza: il Base64 in una variabile d'ambiente, in un file .env o in un segreto Kubernetes non è cifratura. Si decodifica con una riga di codice, in qualsiasi linguaggio, da chiunque possa leggere il file o il cluster. Trattalo come un costume da trasporto, e lascia che siano i controlli veri (permessi, TLS, rotazione delle chiavi) a proteggere.
  • Il gonfiore JSON: il Base64 dentro il JSON costa 33 percento più escape, e un caricamento da 5 MB diventa una stringa da 6,7 MB che il tuo parser JSON deve copiare in memoria. Per qualsiasi cosa di dimensioni di file via HTTP, multipart/form-data o un corpo binario grezzo è il trasporto migliore, e il Base64 è per quando il canale è solo testo.
  • Il conto dell'heap: una stringa codificata è UTF-16 nell'heap JavaScript, due byte per carattere, e il Buffer decodificato o di origine è una seconda copia dei dati. Un file da 100 MB significa brevemente circa 270 MB di stringa più 100 MB di Buffer. Fai andare in streaming i grossi, e tieni la forma codificata riferita per il tempo più breve che il codice consente.
  • Le globali di eredità in Node: la stessa documentazione di Node marca btoa() e atob() come Stability 3, Legacy, e ti dice di usare Buffer invece. In un browser btoa() è uno strumento perfettamente valido per il testo ASCII; in Node.js, punta al Buffer e lascia le globali al codice a forma di polyfill che ne ha bisogno.

Come JavaScript ha imparato a impacchettare i byte

Il lato browser è una storia lunga e tranquilla. btoa() è stato specificato nella bozza HTML5 all'inizio del 2011, e sta in ogni browser importante dalla metà dei 2000, senza cambiare comportamento, con il suo contratto di un byte per carattere e il suo output sempre riempito. Quel contratto precede gli array tipizzati - le stringhe binarie erano l'unico modo per portare i byte prima del 2009 - ed è per questo che btoa() pensa ancora in "stringhe binarie". La metà moderna della storia è recentissima: la proposta TC39 che ha aggiunto il Base64 nativo agli array tipizzati (insieme all'esadecimale) è stata standardizzata come parte di ES2026, ed è atterrata in Firefox 133 e Safari 18.2 nel 2024, in Chrome 140 il 2 settembre 2025, ed è poi stata dichiarata Baseline Newly available. Bun ha distribuito gli stessi metodi nella versione 1.1.22 nell'agosto 2024.

Node.js ha impacchettato i byte su un altro orologio. La classe Buffer è diventata una globale nella versione 0.1.103, nell'estate del 2010, quasi cinque anni prima del Node 1.0, e toString('base64') è stato il codificatore di scelta per oltre un decennio, con le stranezze dell'alfabeto di quell'epoca (già accettava i caratteri URL-safe in decodifica, un'abitudine bilingue che la specifica non ha mai chiesto). La versione 15.7.0 di gennaio 2021 ha aggiunto la modalità 'base64url' come nome di codifica di prima classe, Node 16 nello stesso anno ha aggiunto le globali btoa()/atob() del browser (marcate Legacy immediatamente), e Node 22 nel 2024 ha distribuito ulteriore lavoro di prestazioni V8 e base64. Poi Node 25, rilasciato il 15 ottobre 2025, ha aggiornato V8 a 14.1 e ha portato nel runtime i metodi ES2026, toBase64() con la sua opzione omitPadding e setFromBase64() per la direzione opposta. Per i runtime che non tengono il passo, core-js distribuisce polyfill (features/typed-array/to-base64 / from-base64), e il piccolo pacchetto base64-js (tre funzioni, zero dipendenze) ha sostenuto in silenzio l'ecosistema per anni come dipendenza transitiva.

Il formato che servono è più vecchio di tutto questo. L'alfabeto è stato standardizzato per la prima volta per la Privacy-Enhanced Mail nel 1987 (RFC 989), la revisione del 1993 (RFC 1421) lo ha mantenuto, e il MIME l'ha adottato mesi dopo, sempre quello stesso anno, con il suo avvolgimento a 76 caratteri; l'RFC 3548 ha consolidato la famiglia Base-N nel 2003 e ha aggiunto la variante URL-safe, che l'RFC 4648 ha ripubblicato nel 2006. Un decennio dopo, l'RFC 7515 e 7519 hanno reso il base64url senza riempimento la spina dorsale di ogni JWT, e l'RFC 7636 lo ha messo nel flusso PKCE di OAuth. I codificatori di questo articolo sono l'ultimo miglio di un formato che ha trent'anni e continua a prendere passeggeri.

Da sapere a una festa

  • btoa('GIF89a') restituisce "R0lGODlh", l'intera intestazione magica di un GIF in otto caratteri. È il più piccolo "ciao" che un file binario possa dire in Base64, ed è il primo esempio Web API nell'articolo di Wikipedia per una ragione.
  • toBase64() ha un'opzione omitPadding che btoa() non avrebbe mai potuto avere, perché il contratto Web API riempie in modo incondizionato. Due decenni dello stesso alfabeto, e l'API più recente sa fare una cosa che a quella più vecchia non era mai consentita.
  • Un alfabeto, due lunghezze ufficiali di riga: il MIME avvolge a 76, il PEM a 64. Stessi 64 caratteri, stesso riempimento, due opinioni diverse di 30 anni fa su quanto larga possa essere una riga di testo.
  • Il numero del 33 percento è esatto: quattro caratteri per tre byte è un rapporto 4/3, e la mail dell'era RFC aggiungeva circa un altro 3,5 percento per gli a capo. La tua "piccola" stringa di configurazione è gonfia del 37 percento per nulla.
  • Il piccolo pacchetto base64-js incassa oltre 100 milioni di download a settimana su npm, quasi tutti nascosti negli alberi delle dipendenze di altri pacchetti. Il Base64 è il codice più contrabbandato dell'ecosistema JavaScript.
  • I piccoli Buffer non sono allocati uno alla volta: Node li ricava da un pool condiviso da 65536 byte (Buffer.poolSize), ed è per questo che la creazione di Buffer è veloce, e per questo che esistono le varianti di allocazione "unsafe" per i casi in cui i dati dell'inquilino precedente non contano.
  • L'RFC che ha definito le data URL nel 1998 avvisa che sono "utili solo per valori brevi", citando un limite di 1024 caratteri sugli attributi HTML. I browser moderni incorporano immagini di dimensioni in megabyte come data URL negli stessi attributi, il che è progresso o superbia, a seconda della tua immagine principale.
  • Gli hash delle password Unix usano i propri alfabeti dal sapore Base64, senza riempimento, e in modo confuso l'ordine cambia a seconda dello schema: gli hash classici crypt(3) usano ./0-9A-Za-z, mentre le stringhe bcrypt $2b$ che i progetti JavaScript conservano per le password degli utenti mescolano gli stessi 64 caratteri in ./A-Za-z0-9, in ordine diverso. È un buon promemoria che "Base64" in un contesto di sicurezza è una famiglia, non un singolo formato.
  • Il decodificatore di Node accetta -, _, + e / in entrambe le modalità 'base64' e 'base64url', quattro caratteri, una tabella. Il codificatore, naturalmente, parla solo il dialetto che gli hai chiesto.

La metà di un viaggio di andata e ritorno

Codificare Base64 in JavaScript e Node.js si riduce a tre decisioni: quali byte hai in mano (una stringa ha bisogno di una codifica dei caratteri, un Buffer non ne ha bisogno), quale alfabeto esige la destinazione (classico per MIME e data URL, base64url per token e URL, riempimento opzionale a seconda del contesto), e quali regole di riga il formato impone ancora (76 per email, 64 per PEM, nessuna per JSON). Rispondi a quelle e gli integrati fanno il resto: Buffer.toString() in Node, btoa() più il ponte UTF-8 nel browser, e Uint8Array.toBase64() nei runtime moderni che finalmente ne hanno avuto uno.

E ogni pacco che sigilli qui, qualcuno lo aprirà un giorno. Il lato decodifica ha il suo set di trappole: il decodificatore tollerante che inghiotte spazzatura senza un suono, il costume da stringa binaria che atob() ti consegna, le decisioni sulla codifica dei caratteri che accadono sul lato del lettore del muro, e la logica di streaming che ricalca lo schema di trascinamento che hai appena imparato. Quella storia, con esempi di codice per ogni passo, è trattata in profondità nell'articolo correlato sulla decodifica Base64 sul nostro sito sorella. Leggilo per secondo, perché le trappole su quel lato dell'alfabeto sono più silenziose, ed è proprio il silenzio il modo in cui vincono.

Ultimo aggiornamento: 2026-09-08

Articolo correlato: Decodifica Base64 in JavaScript/Node.js: una guida completa