Base64 apparaît partout une fois que vous commencez à chercher — jetons JWT, URI de données, pièces jointes d'e-mail, charges utiles API transportant des fichiers binaires. L'encodage lui-même est défini dans RFC 4648 et est d'une simplicité conceptuelle absolue : prendre des octets arbitraires et les représenter en utilisant uniquement 64 caractères ASCII imprimables. Ce qui piège les gens, c'est l'implémentation en JavaScript — différentes API dans le navigateur et Node.js, le piège Unicode qui fait planter btoa(), et la variante sécurisée pour les URL dont dépendent les JWT. Ce guide couvre tout cela avec du code fonctionnel.

btoa() et atob() dans le navigateur

Le navigateur dispose depuis longtemps de btoa() et atob(). Les noms prêtent à confusion (binary to ASCII et retour), mais l'utilisation est simple pour les chaînes simples :

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
Le piège Unicode : btoa() ne gère que les chaînes où chaque caractère a un point de code ≤ 255 (la plage Latin-1). Passez-lui une chaîne contenant un emoji ou un caractère non-Latin et il lance immédiatement InvalidCharacterError. C'est l'un des bugs Base64 les plus courants dans le code du navigateur.
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: ...

Gérer Unicode en toute sécurité dans le navigateur

La solution consiste à d'abord encoder la chaîne en octets UTF-8, puis à encoder ces octets en Base64. L'approche classique utilise encodeURIComponent et une astuce de décodage en pourcentage. L'approche moderne utilise TextEncoder, qui est disponible dans tous les navigateurs modernes et 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é"

Gardez ces deux fonctions utilitaires quelque part dans votre base de code et oubliez que le btoa() nu existe. La paire TextEncoder/TextDecoder est le bon outil pour tout ce qui dépasse l'ASCII pur. Vous pouvez l'essayer maintenant avec l'outil Encodeur Base64.

Buffer.from() dans Node.js

Node.js dispose de sa propre API pour cela via la classe Buffer, qui gère l'encodage/décodage plus proprement. Il n'y a pas de piège Unicode ici parce que vous spécifiez explicitement l'encodage d'entrée :

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"

Notez que btoa() et atob() sont également disponibles dans Node.js 16+ en tant que globaux (pour la compatibilité navigateur), mais l'API Buffer est plus idiomatique dans Node.js et est présente depuis Node.js v0.1. Pour l'encodage spécifique à JSON, l'outil JSON vers Base64 est pratique pour les conversions manuelles rapides.

Base64 sécurisé pour les URL — Ce que les JWT utilisent réellement

Le Base64 standard utilise + et / dans son alphabet. Ces deux caractères sont spéciaux dans les URL — + signifie un espace dans les chaînes de requête, et / est un séparateur de chemin. Quand vous avez besoin de Base64 dans une URL ou comme segment JWT, vous utilisez la variante sécurisée pour les URL : remplacez + par - et / par _, puis supprimez le remplissage =. Ceci est standardisé dans RFC 4648 §5 et c'est ce que chaque bibliothèque JWT utilise en interne :

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}

C'est pourquoi vous verrez des chaînes Base64 comme eyJhbGciOiJIUzI1NiJ9 dans les JWT — pas de remplissage, des tirets à la place des signes plus. Lors de l'envoi de données encodées en tant que paramètre de requête d'URL, utilisez toujours la variante sécurisée pour les URL pour éviter les URL brisées. L'outil Décodeur Base64 gère automatiquement les deux variantes Base64 standard et sécurisée pour les URL.

Encodage d'un fichier avec l'API FileReader

Une tâche courante dans le navigateur : l'utilisateur sélectionne une image ou un document, et vous devez l'envoyer à une API en Base64. L' API FileReader dispose de readAsDataURL() exactement pour cela — elle vous donne un URI de données complet avec le type MIME inclus :

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);
  }
});

Si vous avez besoin de l'URI de données complète (y compris le préfixe du type MIME) plutôt que le Base64 brut, ignorez le .split(',')[1] et utilisez directement reader.result. Pour la conversion en masse de fichiers, l'outil Image vers Base64 gère les images sans écrire de code.

Encodage de données binaires et Uint8Arrays

Parfois vous ne partez pas d'une chaîne ou d'un fichier — vous avez des octets bruts d'une opération WebCrypto, d'un export de canvas, ou d'un module WebAssembly. Voici comment passer d'un Uint8Array à Base64 et inversement dans les deux environnements :

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=="

Intégration d'images comme URI de données

L'une des utilisations les plus pratiques de Base64 dans le développement web est l'intégration d'images directement dans HTML ou CSS, éliminant une requête HTTP. Vous avez probablement vu des URI de données dans des SVG inline ou des modèles d'e-mail. Voici le modèle :

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
Avertissement de taille : L'encodage Base64 gonfle la taille du fichier d'environ 33%. Une image de 100 Ko devient environ 133 Ko de texte Base64. Les URI de données sont plus adaptés aux petits actifs (icônes, SVG, petits sprites) — pas pour les photos ou les grandes images. Pour ceux-là, le multiplexage HTTP/2 rend les requêtes séparées plus rapides que l'inlining.

Un module utilitaire compact pour les deux environnements

Plutôt que de disperser des appels btoa() dans votre base de code, il vaut la peine d'avoir un seul module utilitaire qui couvre Unicode, les variantes sécurisées pour les URL, et fonctionne dans les deux navigateurs et Node.js. En voici un qui fait tout cela :

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]"

Pièges courants à surveiller

  • btoa() plante sur les caractères non-Latin — tout caractère au-dessus du point de code 255 provoque InvalidCharacterError. Utilisez toujours l'approche TextEncoder ou Buffer.from(str, 'utf8') dans Node.js.
  • Le remplissage est important pour le décodage — les chaînes Base64 doivent avoir une longueur qui est un multiple de 4. Le manque de remplissage = fait que atob() retourne silencieusement des données incorrectes ou plante, selon le navigateur. Restaurez toujours le remplissage avant de décoder les chaînes sécurisées pour les URL.
  • Encodage Buffer vs chaîne dans Node.jsBuffer.from(str) utilise par défaut UTF-8, mais Buffer.from(str, 'binary') traite la chaîne comme des octets Latin-1. L'utilisation du mauvais encodage lors du décodage produit une sortie brouillée qui peut être difficile à déboguer.
  • Type MIME de l'URI de donnéesdata:;base64,... (sans type MIME) fonctionnera dans certains navigateurs mais pas d'autres. Incluez toujours le type MIME : data:image/png;base64,....
  • Sauts de ligne dans MIME Base64 — RFC 4648 permet aux implémentations d'insérer des sauts de ligne tous les 76 caractères (comme le font les encodeurs d'e-mail). atob() et Buffer.from() gèrent tous les deux cela, mais si vous générez vous-même du Base64, n'ajoutez pas de sauts de ligne à moins que le système cible ne les attende.

Conclusion

Base64 en JavaScript est l'un de ces sujets qui semble trivial jusqu'à ce qu'il vous morde. La version courte : n'utilisez jamais btoa() nu pour quoi que ce soit généré par l'utilisateur — enveloppez-le avec TextEncoder pour gérer Unicode correctement. Dans Node.js, Buffer.from(str, 'utf8').toString('base64') est l'idiome correct. Quand la chaîne encodée se retrouve dans une URL ou un JWT, passez à la variante sécurisée pour les URL. Pour des expériences rapides ou des conversions ponctuelles, les outils Encodeur Base64, Décodeur Base64, et JSON vers Base64 font gagner du temps. La page du glossaire Base64 de MDN offre également une bonne référence centrée sur le navigateur si vous avez besoin d'un second avis sur l'un de ces points.