Base64 appare ovunque una volta che inizi a cercarlo — token JWT, URI dati, allegati email, payload API che trasportano file binari. La codifica stessa è definita in RFC 4648 ed è semplicissima in teoria: prendi byte arbitrari, rappresentali usando solo 64 caratteri ASCII stampabili. Quello che mette in difficoltà è l'implementazione in JavaScript — API diverse nel browser rispetto a Node.js, l'insidia Unicode che fa lanciare eccezioni a btoa(), e la variante URL-safe su cui si basano i JWT. Questa guida copre tutto con codice funzionante.

btoa() e atob() nel Browser

Il browser ha avuto btoa() e atob() da lungo tempo. I nomi sono confusi (binary to ASCII e viceversa), ma l'utilizzo è semplice per le stringhe normali:

js
// Encode a plain ASCII string
const encoded = btoa('hello world');
console.log(encoded); // "aGVsbG8gd29ybGQ="

// Decode it back
const decoded = atob('aGVsbG8gd29ybGQ=');
console.log(decoded); // "hello world"

// A more realistic example — encoding a simple auth token
const credentials = 'apiuser:s3cr3tkey';
const basicAuth = 'Basic ' + btoa(credentials);
// "Basic YXBpdXNlcjpzM2NyM3RrZXk="
// This is exactly what HTTP Basic Authentication uses
La trappola Unicode: btoa() gestisce solo stringhe in cui ogni carattere ha un code point ≤ 255 (il range Latin-1). Passakle una stringa contenente qualsiasi emoji o carattere non-Latin e lancerà immediatamente InvalidCharacterError. Questo è uno dei bug Base64 più comuni nel codice browser.
js
// ❌ This throws — emoji is outside Latin-1
btoa('Hello 🌍');
// Uncaught DOMException: Failed to execute 'btoa' on 'Window':
// The string to be encoded contains characters outside of the Latin1 range.

// ❌ This also throws — any non-ASCII character will do it
btoa('café');
// Uncaught DOMException: ...

Gestire Unicode in Modo Sicuro nel Browser

La soluzione è prima codificare la stringa in byte UTF-8, poi codificare quei byte in Base64. L'approccio classico usa encodeURIComponent e un trucco con percent-decode. L'approccio moderno usa TextEncoder, che è disponibile in tutti i browser moderni e Node.js 11+:

js
// ✅ Unicode-safe encode using TextEncoder
function encodeBase64(str) {
  const bytes = new TextEncoder().encode(str);          // UTF-8 byte array
  const binString = Array.from(bytes, byte =>
    String.fromCodePoint(byte)
  ).join('');
  return btoa(binString);
}

// ✅ Unicode-safe decode using TextDecoder
function decodeBase64(base64Str) {
  const binString = atob(base64Str);
  const bytes = Uint8Array.from(binString, char =>
    char.codePointAt(0)
  );
  return new TextDecoder().decode(bytes);
}

// Now emojis and international text work fine
console.log(encodeBase64('Hello 🌍'));   // "SGVsbG8g8J+MjQ=="
console.log(decodeBase64('SGVsbG8g8J+MjQ==')); // "Hello 🌍"

console.log(encodeBase64('Héllo café')); // "SMOpbGxvIGNhZsOp"
console.log(decodeBase64('SMOpbGxvIGNhZsOp')); // "Héllo café"

Tieni queste due funzioni di utilità da qualche parte nel tuo codice e dimentica che esiste il btoa() nudo. La coppia TextEncoder/TextDecoder è lo strumento giusto per qualsiasi cosa oltre il puro ASCII. Puoi provarlo subito con lo strumento Base64 Encoder.

Buffer.from() in Node.js

Node.js ha la propria API tramite la classe Buffer, che gestisce la codifica/decodifica in modo più pulito. Non c'è l'insidia Unicode qui perché specifichi esplicitamente la codifica dell'input:

js
// Encode string → Base64
const encoded = Buffer.from('Hello 🌍', 'utf8').toString('base64');
console.log(encoded); // "SGVsbG8g8J+MjQ=="

// Decode Base64 → string
const decoded = Buffer.from('SGVsbG8g8J+MjQ==', 'base64').toString('utf8');
console.log(decoded); // "Hello 🌍"

// Practical example — encoding a JSON payload to embed in a config file
const config = {
  apiKey:    'sk-prod-abc123',
  projectId: 'proj_x9f2k',
  region:    'us-east-1'
};

const encodedConfig = Buffer.from(JSON.stringify(config), 'utf8').toString('base64');
// eyJhcGlLZXkiOiJzay1wcm9kLWFiYzEyMyIsInByb2plY3RJZCI6InByb2pfeDlmMmsiLCJyZWdpb24iOiJ1cy1lYXN0LTEifQ==

// Decode and parse it back
const decodedConfig = JSON.parse(
  Buffer.from(encodedConfig, 'base64').toString('utf8')
);
console.log(decodedConfig.region); // "us-east-1"

Nota che btoa() e atob() sono disponibili anche in Node.js 16+ come globali (per compatibilità con il browser), ma l'API Buffer è più idiomatica in Node.js ed è disponibile sin da Node.js v0.1. Per la codifica specifica di JSON, lo strumento JSON to Base64 è comodo per conversioni manuali rapide.

Base64 URL-Safe — Quello che Usano Realmente i JWT

Base64 standard usa + e / nel suo alfabeto. Entrambi quei caratteri sono speciali negli URL — + significa uno spazio nelle query string, e / è un separatore di percorso. Quando hai bisogno di Base64 in un URL o come segmento JWT, usi la variante URL-safe: sostituisci + con - e / con _, poi rimuovi il padding =. Questo è standardizzato in RFC 4648 §5 ed è ciò che ogni libreria JWT usa internamente:

js
// Convert standard Base64 to URL-safe Base64
function toBase64Url(base64Str) {
  return base64Str
    .replace(/+/g, '-')
    .replace(///g, '_')
    .replace(/=+$/, '');  // strip padding
}

// Convert URL-safe Base64 back to standard Base64
function fromBase64Url(base64UrlStr) {
  // Restore padding — length must be a multiple of 4
  const padded = base64UrlStr + '==='.slice((base64UrlStr.length + 3) % 4);
  return padded
    .replace(/-/g, '+')
    .replace(/_/g, '/');
}

// Encode a string to URL-safe Base64
function encodeBase64Url(str) {
  return toBase64Url(btoa(str));
}

// Decode URL-safe Base64 to a string
function decodeBase64Url(str) {
  return atob(fromBase64Url(str));
}

// Example: manually inspect a JWT payload
const jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjQyLCJyb2xlIjoiYWRtaW4iLCJpYXQiOjE3MTM0MDAwMDB9.signature';
const [header, payload] = jwt.split('.');

console.log(decodeBase64Url(header));
// {"alg":"HS256","typ":"JWT"}

console.log(decodeBase64Url(payload));
// {"userId":42,"role":"admin","iat":1713400000}

Ecco perché vedrai stringhe Base64 come eyJhbGciOiJIUzI1NiJ9 nei JWT — nessun padding, trattini invece di segni più. Quando si inviano dati codificati come parametro query URL, usa sempre la variante URL-safe per evitare URL non funzionanti. Lo strumento Base64 Decoder gestisce sia Base64 standard che URL-safe automaticamente.

Codifica di un File con l'API FileReader

Un compito comune nel browser: l'utente seleziona un'immagine o un documento, e devi inviarlo a un'API come Base64. L' API FileReader ha readAsDataURL() esattamente per questo — ti fornisce un URI dati completo con il tipo MIME incluso:

js
// Wrap FileReader in a Promise for easier async usage
function fileToBase64(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();

    reader.onload  = () => {
      // result is "data:image/png;base64,iVBORw0KGgo..."
      // Strip the data URI prefix to get just the Base64 string
      const base64 = reader.result.split(',')[1];
      resolve(base64);
    };

    reader.onerror = () => reject(new Error('Failed to read file'));
    reader.readAsDataURL(file);
  });
}

// Hook it up to a file input
const fileInput = document.getElementById('avatarUpload');

fileInput.addEventListener('change', async (event) => {
  const file = event.target.files[0];
  if (!file) return;

  try {
    const base64 = await fileToBase64(file);
    console.log(`File size: ${file.size} bytes`);
    console.log(`Base64 length: ${base64.length} chars`);

    // Send to your API
    await fetch('/api/users/42/avatar', {
      method:  'PUT',
      headers: { 'Content-Type': 'application/json' },
      body:    JSON.stringify({ image: base64, mimeType: file.type })
    });
  } catch (err) {
    console.error('Upload failed:', err.message);
  }
});

Se hai bisogno dell'URI dati completo (incluso il prefisso del tipo MIME) invece che solo del Base64 grezzo, salta il .split(',')[1] e usa direttamente reader.result. Per la conversione di file in blocco, lo strumento Image to Base64 gestisce le immagini senza scrivere alcun codice.

Codifica di Dati Binari e Uint8Array

A volte non stai partendo da una stringa o un File — hai byte grezzi da un'operazione WebCrypto, un export canvas, o un modulo WebAssembly. Ecco come passare da un Uint8Array a Base64 e viceversa in entrambi gli ambienti:

js
// --- Browser ---

// Uint8Array → Base64 (browser)
function uint8ToBase64(bytes) {
  const binString = Array.from(bytes, byte =>
    String.fromCodePoint(byte)
  ).join('');
  return btoa(binString);
}

// Base64 → Uint8Array (browser)
function base64ToUint8(base64Str) {
  const binString = atob(base64Str);
  return Uint8Array.from(binString, char => char.codePointAt(0));
}

// Example: export a canvas as raw PNG bytes → Base64
const canvas  = document.getElementById('myCanvas');
canvas.toBlob(blob => {
  blob.arrayBuffer().then(buffer => {
    const bytes   = new Uint8Array(buffer);
    const encoded = uint8ToBase64(bytes);
    console.log('PNG as Base64:', encoded.slice(0, 40) + '...');
  });
}, 'image/png');


// --- Node.js ---

// Uint8Array / Buffer → Base64 (Node.js)
function uint8ToBase64Node(bytes) {
  return Buffer.from(bytes).toString('base64');
}

// Base64 → Buffer (Node.js)
function base64ToBufferNode(base64Str) {
  return Buffer.from(base64Str, 'base64');
}

// Example: hash a password and encode the result
const crypto = require('crypto');
const hash   = crypto.createHash('sha256').update('mySecretPassword').digest();
// hash is a Buffer (which extends Uint8Array)
console.log(hash.toString('base64'));
// "XohImNooBHFR0OVvjcYpJ3NgxxxxxxxxxxxxxA=="

Incorporare Immagini come URI Dati

Uno degli usi più pratici di Base64 nello sviluppo web è incorporare immagini direttamente in HTML o CSS, eliminando una richiesta HTTP. Probabilmente hai già visto URI dati in SVG inline o template email. Ecco lo schema:

html
<!-- Inline image in HTML — no separate network request -->
<img
  src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
  alt="1x1 transparent pixel"
  width="1"
  height="1"
/>
css
/* Inline background image in CSS — commonly used for small icons and loading spinners */
.spinner {
  width:  32px;
  height: 32px;
  background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTEyIDJhMTAgMTAgMCAxIDAgMCAyMCAxMCAxMCAwIDAgMCAwLTIweiIvPjwvc3ZnPg==");
  background-repeat:   no-repeat;
  background-position: center;
  background-size:     contain;
}
js
// Generate a data URI from a fetched image (Node.js)
const fs     = require('fs');
const path   = require('path');

function imageFileToDataUri(filePath) {
  const ext      = path.extname(filePath).slice(1).toLowerCase();
  const mimeMap  = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg',
                     gif: 'image/gif', svg: 'image/svg+xml', webp: 'image/webp' };
  const mimeType = mimeMap[ext] ?? 'application/octet-stream';
  const fileData = fs.readFileSync(filePath);
  const base64   = fileData.toString('base64');
  return `data:${mimeType};base64,${base64}`;
}

const dataUri = imageFileToDataUri('./logo.png');
// "data:image/png;base64,iVBORw0KGgo..."
// Drop this into an <img src> or CSS background-image
Avviso sulle dimensioni: la codifica Base64 aumenta le dimensioni del file di circa il 33%. Un'immagine da 100 KB diventa ~133 KB di testo Base64. Gli URI dati sono meglio per asset piccoli (icone, SVG, sprite minuscoli) — non per foto o immagini grandi. Per queste, il multiplexing HTTP/2 rende le richieste separate più veloci dell'inline.

Un Modulo di Utilità Compatto per Entrambi gli Ambienti

Invece di disperdere chiamate btoa() per tutto il codice, vale la pena avere un singolo modulo di utilità che gestisce Unicode, varianti URL-safe, e funziona sia nel browser che in Node.js. Eccone uno che fa tutto questo:

js
// base64.js — drop into any project
const isNode = typeof process !== 'undefined' && process.versions?.node;

export function encode(str) {
  if (isNode) {
    return Buffer.from(str, 'utf8').toString('base64');
  }
  // Browser: encode to UTF-8 bytes first, then Base64
  const bytes = new TextEncoder().encode(str);
  const binString = Array.from(bytes, b => String.fromCodePoint(b)).join('');
  return btoa(binString);
}

export function decode(base64Str) {
  if (isNode) {
    return Buffer.from(base64Str, 'base64').toString('utf8');
  }
  // Browser: Base64 → bytes → UTF-8 string
  const binString = atob(base64Str);
  const bytes = Uint8Array.from(binString, c => c.codePointAt(0));
  return new TextDecoder().decode(bytes);
}

export function encodeUrlSafe(str) {
  return encode(str)
    .replace(/+/g, '-')
    .replace(///g, '_')
    .replace(/=+$/, '');
}

export function decodeUrlSafe(str) {
  const padded = str + '==='.slice((str.length + 3) % 4);
  return decode(padded.replace(/-/g, '+').replace(/_/g, '/'));
}

export function encodeBytes(bytes) {
  if (isNode) return Buffer.from(bytes).toString('base64');
  const binString = Array.from(bytes, b => String.fromCodePoint(b)).join('');
  return btoa(binString);
}

export function decodeToBytes(base64Str) {
  if (isNode) return Buffer.from(base64Str, 'base64');
  const binString = atob(base64Str);
  return Uint8Array.from(binString, c => c.codePointAt(0));
}
js
// Usage examples
import { encode, decode, encodeUrlSafe, decodeUrlSafe } from './base64.js';

encode('Hello 🌍');           // "SGVsbG8g8J+MjQ=="
decode('SGVsbG8g8J+MjQ==');   // "Hello 🌍"

encodeUrlSafe('[email protected]'); // "dXNlckBleGFtcGxlLmNvbQ" (no +, /, or =)
decodeUrlSafe('dXNlckBleGFtcGxlLmNvbQ'); // "[email protected]"

Insidie Comuni da Tenere Presenti

  • btoa() lancia eccezioni su caratteri non-Latin — qualsiasi carattere oltre il code point 255 causa InvalidCharacterError. Usa sempre l'approccio TextEncoder o Buffer.from(str, 'utf8') in Node.js.
  • Il padding è importante per la decodifica — le stringhe Base64 devono avere una lunghezza che sia un multiplo di 4. Il padding = mancante fa sì che atob() restituisca silenziosamente dati errati o lanci un'eccezione, a seconda del browser. Ripristina sempre il padding prima di decodificare stringhe URL-safe.
  • Buffer vs codifica stringa in Node.jsBuffer.from(str) usa UTF-8 per default, ma Buffer.from(str, 'binary') tratta la stringa come byte Latin-1. Usare la codifica sbagliata durante la decodifica produce output incomprensibili che possono essere difficili da debuggare.
  • Tipo MIME negli URI datidata:;base64,... (senza tipo MIME) funziona in alcuni browser ma non in altri. Includi sempre il tipo MIME: data:image/png;base64,....
  • Interruzioni di riga in MIME Base64 — RFC 4648 consente alle implementazioni di inserire interruzioni di riga ogni 76 caratteri (come fanno i codificatori email). atob() e Buffer.from() le gestiscono entrambe, ma se stai generando Base64 da solo, non aggiungere interruzioni di riga a meno che il sistema di destinazione non le preveda.

Conclusione

Base64 in JavaScript è uno di quei temi che sembra banale finché non ti morde. La versione breve: non usare mai il nudo btoa() per nulla generato dagli utenti — wrappalo con TextEncoder per gestire correttamente Unicode. In Node.js, Buffer.from(str, 'utf8').toString('base64') è l'idioma corretto. Quando la stringa codificata finisce in un URL o JWT, passa alla variante URL-safe. Per esperimenti rapidi o conversioni una tantum, gli strumenti Base64 Encoder, Base64 Decoder, e JSON to Base64 fanno risparmiare tempo. La pagina del glossario Base64 di MDN ha anche un buon riferimento centrato sul browser se hai bisogno di una seconda opinione su qualcuno di questi argomenti.