ファイルを選択
  1. base64hub
  2. 各言語での Base64
  3. Node.js
コード

Node.js での Base64

Node.js では Buffer で Base64 を扱い、base64 と base64url が組み込みのエンコーディングとして使えます。以下の出力はすべて Node.js 24 のものです。

文字列のエンコードとデコード

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.0
  • btoa と 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 の制約も同じです。

続けて読む