文字列のエンコードとデコード
Buffer.from(string, "utf8") がテキストを UTF-8 のバイト列に変換し、.toString("base64") がそれをエンコードします。デコードは同じ 2 つの呼び出しを逆にするだけです。
const enc = Buffer.from("héllo wörld", "utf8").toString("base64");
console.log(enc); // aMOpbGxvIHfDtnJsZA==
console.log(Buffer.from(enc, "base64").toString("utf8")); // héllo wörld
どちらの呼び出しでも "utf8" がデフォルトですが、明示すると意図がはっきりします。Buffer は Uint8Array のサブクラスなので、素の Uint8Array はラップが必要です。u8.toString("base64") は引数を無視して "104,105" を返しますが、Buffer.from(u8).toString("base64") なら aGk= を返します。
URLセーフな Base64(base64url)
Node には URLセーフなアルファベット専用のエンコーディングがあります。パディングは一切付けません。
Buffer.from("<<???>>").toString("base64"); // PDw/Pz8+Pg==
Buffer.from("<<???>>").toString("base64url"); // PDw_Pz8-Pg
Buffer.from("PDw_Pz8-Pg", "base64url").toString(); // <<???>>
受け取り側がパディング付きの URLセーフ Base64 を求めるなら、s + "=".repeat((4 - s.length % 4) % 4) のように自分で付け足してください。
デコード側の Node は寛容です。"base64" と "base64url" のどちらもアルファベットを問わず受け付け、パディングも気にしません。
Buffer.from("PDw_Pz8-Pg==", "base64").toString(); // <<???>>
Buffer.from("PDw/Pz8+Pg==", "base64url").toString(); // <<???>>
JWT やトークンには便利ですが(JWT デコーダー で試せます)、後述の検証のセクションもご覧ください。2 つのアルファベットの背景については Base64 と Base64URL の違い をご覧ください。
ファイルのエンコードとデコード
fs はエンコーディングとして "base64" を受け付けるので、どちらの方向でも 1 ステップ省けます。
import { readFileSync, writeFileSync } from "node:fs";
readFileSync("hello.txt").toString("base64"); // aGVsbG8K
readFileSync("hello.txt", "base64"); // aGVsbG8K (same thing)
writeFileSync("copy.txt", Buffer.from("aGVsbG8K", "base64"));
writeFileSync("copy2.txt", "aGVsbG8K", "base64"); // decodes while writing
ファイルを HTML や CSS に埋め込むには、結果に `data:image/png;base64,${b64}` のように接頭辞を付けます。Data URI ジェネレーター なら、同じ文字列をブラウザで作れます。
大きなファイルのストリーム処理
2 GB のファイルをメモリに読み込んでからエンコードするのは悪手です。Transform ストリームなら、3 バイト単位のグループだけをエンコードし、余りを次のチャンクに持ち越すことで、チャンクごとにエンコードできます。
import { createReadStream, createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
import { Transform } from "node:stream";
class Base64Encode extends Transform {
constructor() { super(); this.rest = Buffer.alloc(0); }
_transform(chunk, _enc, cb) {
const buf = Buffer.concat([this.rest, chunk]);
const cut = buf.length - (buf.length % 3);
this.rest = buf.subarray(cut);
cb(null, buf.subarray(0, cut).toString("base64"));
}
_flush(cb) { cb(null, this.rest.toString("base64")); }
}
await pipeline(createReadStream("big.bin"), new Base64Encode(),
createWriteStream("big.b64"));
1 MB のランダムなファイルでは、出力は readFileSync("big.bin").toString("base64") と完全に一致しました。持ち越しをしないと、3 の倍数でないチャンクの境界ごとに、出力の途中に = のパディングが入ってしまいます。その理由は Base64 の仕組み で説明しています。
デコードせずにデコード後のサイズを知りたいときは、Buffer.byteLength("aGVsbG8K", "base64") を使います。結果は 6 です。
入力の検証
Buffer.from(s, "base64") は、内容が不正でも決してスローしません。どちらのアルファベットにもない文字を捨て、残ったものを返します。
Buffer.from("aGk=!!", "base64"); // <Buffer 68 69>
Buffer.from("not base64 at all", "base64"); // <Buffer 9e 8b 5b 6a c7 ba e1 ab 5a 96>
Buffer.from("aGk", "base64").toString(); // hi (missing padding is fine)
信頼できない入力では、アルファベットと長さを確認してから、再エンコードして比較してください。往復変換により、aGl= のような 0 でないパディングビットも検出できます。aGl= も hi にデコードされますが、正規形ではありません。
function isBase64(s) {
return /^[A-Za-z0-9+/]*={0,2}$/.test(s) && s.length % 4 === 0 &&
Buffer.from(s, "base64").toString("base64") === s;
}
isBase64("aGk="); // true
isBase64("aGk"); // false (no padding)
isBase64("aGl="); // false (non-canonical)
isBase64("not base64"); // false
base64url の場合は、正規表現を [A-Za-z0-9_-] に変え、長さのチェックを外してください。Base64 バリデーター では、同じチェックを対話的に確認できます。
落とし穴とエラーメッセージ
- 引数の型の誤り。
Buffer.from(123, "base64")はTypeError [ERR_INVALID_ARG_TYPE]: The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received type number (123)をスローします。たいていは JSON からundefinedや数値が紛れ込んだのが原因です。 new Buffer()。 このコンストラクターはセキュリティ上の理由で非推奨(DEP0005)です。Buffer.fromかBuffer.allocを使ってください。- バイナリをテキストとしてデコードする。 画像のバイト列に
.toString("utf8")を使うと、エラーなしで置換文字(U+FFFD)が生成され、書き戻したファイルは壊れます。バイナリデータはBufferのまま扱ってください。 - Data URI の接頭辞。
Buffer.from("data:image/png;base64,iVBOR...", "base64")は失敗せず、接頭辞をゴミとしてデコードしてしまいます。先にカンマで分割してください。 - 改行。 Node はデコード時に
\nと\rを無視するので、MIME 形式で折り返された入力も扱えますが、エンコーダーは折り返しません。MIME 形式の行 が必要ならb64.match(/.{1,76}/g).join("\r\n")を使ってください。
バージョンごとの注意
base64urlエンコーディング:Node.js 15.7.0 と 14.18.0btoaとatobのグローバル関数:Node.js 16.0、レガシー扱いUint8Array.prototype.toBase64とUint8Array.fromBase64は Node.js 24 では使えません(typeofは"undefined"を返します)。これらは新しいバージョンの V8 で使えるようになる機能で、それまではBufferを使ってください。Buffer.from(string, "base64")はどのバージョンでも空白や不正な文字を無視してきたので、検証には頼らないでください。
よくある質問
Buffer.from(str, 'base64') は不正な入力で例外をスローしますか?
スローしません。Node はアルファベット外の文字を読み飛ばし、最初の = で止まるため、ゴミのような入力でもエラーなしで何らかのバイト列にデコードされます。信頼できない入力なら、文字列を自分で検証してください。
base64url はどの Node.js バージョンで追加されましたか?
base64url エンコーディングは Node.js 15.7.0 で追加され、14.18.0 にバックポートされました。現行のバージョンなら問題なく使えます。
Node.js で btoa を使うべきですか?
Node は btoa と atob をレガシー扱いにしており、Buffer を推奨しています。これらはブラウザとの互換性のために存在し、JavaScript での Base64 で説明した Latin-1 の制約も同じです。