一度見始めると、Base64はどこにでも現れます — JWTトークン、データURI、メールの添付ファイル、 バイナリファイルを運ぶAPIペイロードなど。エンコード自体は RFC 4648 で定義されており、概念的にはシンプルです:任意のバイトを64個の印刷可能なASCII文字のみを使用して表現します。 人々を困らせるのは、JavaScriptでの実装です — ブラウザとNode.jsの異なるAPI、 btoa()をクラッシュさせるUnicodeの問題、そしてJWTが依存するURLセーフバリアント。 このガイドは動作するコードですべてをカバーします。

ブラウザのbtoa()とatob()

ブラウザは長い間 btoa()atob() を持っています。名前は紛らわしいですが(binary to ASCIIとその逆)、シンプルな文字列での使用は わかりやすいです:

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
Unicodeの罠:btoa()は各文字のコードポイントが≤ 255(Latin-1範囲)の 文字列のみを処理します。絵文字や非Latin文字を含む文字列を渡すと、すぐにInvalidCharacterErrorが発生します。 これはブラウザコードで最も一般的なBase64バグの一つです。
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: ...

ブラウザでのUnicodeの安全な処理

解決策は、まず文字列をUTF-8バイトにエンコードし、次にそれらのバイトをBase64エンコードすることです。 クラシックなアプローチはencodeURIComponentとパーセントデコードのトリックを使用します。 モダンなアプローチはTextEncoderを使用します。これは すべてのモダンブラウザ と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é"

これら2つのユーティリティ関数をコードベースのどこかに保持し、裸のbtoa()が存在することを 忘れてください。TextEncoder/TextDecoderペアは、純粋なASCII以外のものに対する 適切なツールです。Base64エンコーダーツールで今すぐ試すことができます。

Node.jsのBuffer.from()

Node.jsには、 Bufferクラスを通じた独自のAPIがあります。 これはエンコード/デコードをよりクリーンに処理します。入力エンコーディングを明示的に指定するため、 ここではUnicodeの問題はありません:

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"

btoa()atob()はNode.js 16+でもグローバルとして利用可能ですが (ブラウザ互換性のため)、Buffer APIはNode.jsでよりイディオマティックで、 Node.js v0.1 から存在しています。JSON固有のエンコードには、JSONからBase64ツールが クイックな手動変換に便利です。

URLセーフBase64 — JWTが実際に使うもの

標準のBase64はアルファベットに+/を使用します。これらの文字は URLで特殊です — +はクエリ文字列でスペースを意味し、/はパスセパレータです。 URLやJWTセグメントとしてBase64が必要な場合は、URLセーフバリアントを使用します: +-に、/_に置き換え、 =パディングを削除します。これは RFC 4648 §5 で標準化されており、すべてのJWTライブラリが内部で使用するものです:

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}

これがJWTでeyJhbGciOiJIUzI1NiJ9のようなBase64文字列を見る理由です — パディングなし、プラス記号の代わりにダッシュ。URLクエリパラメータとしてエンコードされたデータを送信する際は、 URLが壊れないようにいつもURLセーフバリアントを使用してください。Base64デコーダー ツールは標準とURLセーフBase64の両方を自動的に処理します。

FileReader APIでのファイルエンコード

一般的なブラウザタスク:ユーザーが画像やドキュメントを選択し、それをBase64としてAPIに送信する必要があります。 FileReader API には、まさにこれのためのreadAsDataURL()があります — MIMEタイプを含む完全なデータURIを提供します:

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

生のBase64ではなく完全なデータURI(MIMEタイプのプレフィックスを含む)が必要な場合は、 .split(',')[1]をスキップしてreader.resultを直接使用してください。 大量のファイル変換には、画像からBase64ツールがコードを書かずに 画像を処理します。

バイナリデータとUint8Arraysのエンコード

文字列やファイルから始めるのではなく、WebCrypto操作、キャンバスエクスポート、または WebAssemblyモジュールからの生のバイトがある場合があります。両方の環境でUint8Arrayから Base64へ、またはその逆への方法を以下に示します:

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

データURIとして画像を埋め込む

Web開発においてBase64の最も実用的な用途の一つは、HTTPリクエストを排除して画像を直接HTMLやCSSに 埋め込むことです。インラインSVGやメールテンプレートでデータURIを見たことがあるでしょう。パターンはこちらです:

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
サイズの警告:Base64エンコードはファイルサイズを約33%膨らませます。 100KBの画像は約133KBのBase64テキストになります。データURIは小さなアセット (アイコン、SVG、小さなスプライト)に最適です — 写真や大きな画像には不向きです。 それらの場合、HTTP/2の多重化により別々のリクエストの方がインライン化より高速です。

両方の環境向けコンパクトなユーティリティモジュール

コードベース全体にbtoa()呼び出しを散在させる代わりに、Unicode、URLセーフバリアント、 ブラウザとNode.jsの両方で動作する単一のユーティリティモジュールを持つ価値があります。 それをすべて行うものを以下に示します:

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

注意すべき一般的な落とし穴

  • btoa()は非Latin文字でエラーを発生させる — コードポイント255を超えるすべての文字がInvalidCharacterErrorを引き起こします。常にTextEncoderアプローチまたはNode.jsのBuffer.from(str, 'utf8')を使用してください。
  • デコードではパディングが重要 — Base64文字列は4の倍数の長さでなければなりません。=パディングが欠けていると、ブラウザによってはatob()がサイレントにゴミを返したりエラーを発生させたりします。URLセーフ文字列をデコードする前に常にパディングを復元してください。
  • Node.jsでのBufferと文字列エンコーディングBuffer.from(str)はデフォルトでUTF-8ですが、Buffer.from(str, 'binary')は文字列をLatin-1バイトとして扱います。デコード時に間違ったエンコーディングを使用すると、デバッグが困難なぐちゃぐちゃな出力が生成されます。
  • データURIのMIMEタイプdata:;base64,...(MIMEタイプなし)は一部のブラウザで動作しますが、他のブラウザでは動作しません。常にMIMEタイプを含めてください:data:image/png;base64,...
  • MIME Base64の改行 — RFC 4648は実装が76文字ごとに改行を挿入できることを許可しています(メールエンコーダーがそうするように)。atob()Buffer.from()はどちらもこれを処理しますが、自分でBase64を生成する場合、ターゲットシステムが期待しない限り改行を追加しないでください。

まとめ

JavaScriptのBase64は、噛みつかれるまで些細に見えるトピックの一つです。 簡単なバージョン:ユーザーが生成したものには裸のbtoa()を使用しないでください — Unicodeを正しく処理するためにTextEncoderでラップしてください。Node.jsでは Buffer.from(str, 'utf8').toString('base64')が正しいイディオムです。エンコードされた文字列が URLやJWTに入る場合はURLセーフバリアントに切り替えてください。クイックな実験や一回限りの変換には、 Base64エンコーダーBase64デコーダー、 およびJSONからBase64ツールが時間を節約します。 MDNのBase64用語集ページ も、これらの何かについて第二の意見が必要な場合に備えて、ブラウザに焦点を当てた確かなリファレンスを持っています。