Upload a file
  1. base64hub
  2. Base64 in Code
  3. Node.js
Code

Base64 in Node.js

Node.js does Base64 through Buffer, with base64 and base64url as built-in encodings. Every output below comes from Node.js 24.

Encode and decode a string

Buffer.from(string, "utf8") turns text into UTF-8 bytes, and .toString("base64") encodes them. Decoding is the same two calls in reverse.

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" is the default for both calls, but spelling it out makes the intent obvious. Buffer is a subclass of Uint8Array, so a plain Uint8Array needs wrapping: u8.toString("base64") ignores the argument and returns "104,105", while Buffer.from(u8).toString("base64") returns aGk=.

URL-safe Base64 (base64url)

Node has a dedicated encoding for the URL-safe alphabet. It never writes padding:

Buffer.from("<<???>>").toString("base64");     // PDw/Pz8+Pg==
Buffer.from("<<???>>").toString("base64url");  // PDw_Pz8-Pg
Buffer.from("PDw_Pz8-Pg", "base64url").toString();  // <<???>>

If a consumer wants URL-safe Base64 with padding, append it yourself: s + "=".repeat((4 - s.length % 4) % 4).

On the decoding side Node is forgiving: both "base64" and "base64url" accept either alphabet and do not care about padding.

Buffer.from("PDw_Pz8-Pg==", "base64").toString();   // <<???>>
Buffer.from("PDw/Pz8+Pg==", "base64url").toString(); // <<???>>

That is convenient for JWTs and tokens (try the JWT decoder), but see the validation section below. Background on the two alphabets: Base64 vs Base64URL.

Encode and decode a file

fs accepts "base64" as an encoding, which saves a step in both directions:

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

To embed a file in HTML or CSS, prefix the result: `data:image/png;base64,${b64}`. The data URI generator builds the same string in the browser.

Streaming large files

Reading a 2 GB file into memory to encode it is a bad idea. A Transform stream can encode chunk by chunk, as long as it only encodes whole groups of 3 bytes and carries the remainder over to the next chunk:

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

On a 1 MB random file the output matched readFileSync("big.bin").toString("base64") exactly. Without the carry-over, every chunk boundary that is not a multiple of 3 would insert = padding in the middle of the output. The reason is covered in how Base64 works.

To know the decoded size without decoding, use Buffer.byteLength("aGVsbG8K", "base64"), which returns 6.

Validating input

Buffer.from(s, "base64") never throws for bad content. It drops characters that are not in either alphabet and returns whatever is left:

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)

For untrusted input, check the alphabet and length, then re-encode and compare. The round trip catches non-zero padding bits such as aGl=, which decodes to hi too but is not canonical.

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

Adjust the regex to [A-Za-z0-9_-] and drop the length check for base64url. The Base64 validator shows the same checks interactively.

Pitfalls and error messages

  • Wrong argument type. Buffer.from(123, "base64") throws 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). Usually an undefined or a number slipped in from JSON.
  • new Buffer(). The constructor is deprecated (DEP0005) for security reasons. Use Buffer.from or Buffer.alloc.
  • Decoding binary as text. .toString("utf8") on image bytes produces replacement characters (U+FFFD) without an error, and the file is corrupted when written back. Keep binary data as a Buffer.
  • Data URI prefix. Buffer.from("data:image/png;base64,iVBOR...", "base64") will not fail; it decodes the prefix as junk. Split at the comma first.
  • Line breaks. Node ignores \n and \r while decoding, so MIME-wrapped input works, but the encoder never wraps. Use b64.match(/.{1,76}/g).join("\r\n") when you need MIME lines.

Version notes

  • base64url encoding: Node.js 15.7.0 and 14.18.0.
  • btoa and atob globals: Node.js 16.0, marked legacy.
  • Uint8Array.prototype.toBase64 and Uint8Array.fromBase64 are not available in Node.js 24 (typeof returns "undefined"). They come with newer V8 versions; until then, Buffer is the way.
  • Buffer.from(string, "base64") has ignored whitespace and invalid characters in every version, so do not rely on it for validation.

FAQ

Does Buffer.from(str, 'base64') throw on invalid input?

No. Node skips characters outside the alphabet and stops at the first =, so garbage decodes to some bytes without any error. Validate the string yourself if the input is untrusted.

Which Node.js version added base64url?

The base64url encoding arrived in Node.js 15.7.0 and was backported to 14.18.0. On anything current you can rely on it.

Should I use btoa in Node.js?

Node marks btoa and atob as legacy and recommends Buffer. They exist for browser compatibility and share the Latin-1 limitation described in Base64 in JavaScript.

Keep reading