Subir un archivo
  1. base64hub
  2. Base64 en código
  3. Node.js
Código

Base64 en Node.js

Node.js trabaja con Base64 a través de Buffer, con base64 y base64url como codificaciones integradas. Todas las salidas de esta página son de Node.js 24.

Codificar y decodificar una cadena

Buffer.from(string, "utf8") convierte el texto en bytes UTF-8, y .toString("base64") los codifica. Para decodificar se usan las mismas dos llamadas en orden inverso.

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" es el valor por defecto en las dos llamadas, pero escribirlo deja clara la intención. Buffer es una subclase de Uint8Array, así que un Uint8Array normal hay que envolverlo: u8.toString("base64") ignora el argumento y devuelve "104,105", mientras que Buffer.from(u8).toString("base64") devuelve aGk=.

Base64 URL-safe (base64url)

Node tiene una codificación propia para el alfabeto URL-safe. Nunca escribe relleno:

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

Si quien consume los datos quiere Base64 URL-safe con relleno, añádelo tú: s + "=".repeat((4 - s.length % 4) % 4).

Al decodificar, Node es permisivo: tanto "base64" como "base64url" aceptan cualquiera de los dos alfabetos y les da igual el relleno.

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

Es cómodo para JWT y tokens (prueba el decodificador JWT), pero lee la sección de validación de más abajo. Contexto sobre los dos alfabetos: Base64 frente a Base64URL.

Codificar y decodificar un archivo

fs acepta "base64" como codificación, lo que ahorra un paso en ambos sentidos:

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

Para incrustar un archivo en HTML o CSS, añade un prefijo al resultado: `data:image/png;base64,${b64}`. El generador de data URI construye la misma cadena en el navegador.

Archivos grandes en flujo

Cargar en memoria un archivo de 2 GB para codificarlo es mala idea. Un stream Transform puede codificar bloque a bloque, siempre que solo codifique grupos completos de 3 bytes y pase el resto al bloque siguiente:

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

Con un archivo aleatorio de 1 MB, la salida coincidió exactamente con readFileSync("big.bin").toString("base64"). Sin ese arrastre del resto, cada límite de bloque que no fuera múltiplo de 3 insertaría relleno = en mitad de la salida. El motivo se explica en cómo funciona Base64.

Para saber el tamaño decodificado sin decodificar, usa Buffer.byteLength("aGVsbG8K", "base64"), que devuelve 6.

Validar la entrada

Buffer.from(s, "base64") nunca lanza un error por un contenido incorrecto. Descarta los caracteres que no pertenecen a ninguno de los dos alfabetos y devuelve lo que quede:

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)

Con entradas que no son de confianza, comprueba el alfabeto y la longitud, y después vuelve a codificar y compara. La ida y vuelta detecta los bits de relleno distintos de cero, como en aGl=, que también se decodifica como hi pero no es canónico.

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

Para base64url, cambia la expresión regular a [A-Za-z0-9_-] y quita la comprobación de longitud. El validador Base64 hace las mismas comprobaciones de forma interactiva.

Trampas y mensajes de error

  • Tipo de argumento incorrecto. Buffer.from(123, "base64") lanza 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). Normalmente se ha colado un undefined o un número desde un JSON.
  • new Buffer(). El constructor está obsoleto (DEP0005) por motivos de seguridad. Usa Buffer.from o Buffer.alloc.
  • Decodificar binario como texto. .toString("utf8") sobre los bytes de una imagen produce caracteres de reemplazo (U+FFFD) sin ningún error, y el archivo queda corrupto al escribirlo de nuevo. Mantén los datos binarios en un Buffer.
  • El prefijo del data URI. Buffer.from("data:image/png;base64,iVBOR...", "base64") no fallará: decodifica el prefijo como basura. Corta antes por la coma.
  • Saltos de línea. Node ignora \n y \r al decodificar, así que las entradas con cortes MIME funcionan, pero el codificador nunca corta líneas. Usa b64.match(/.{1,76}/g).join("\r\n") cuando necesites líneas MIME.

Notas de versión

  • Codificación base64url: Node.js 15.7.0 y 14.18.0.
  • Globales btoa y atob: Node.js 16.0, marcados como heredados.
  • Uint8Array.prototype.toBase64 y Uint8Array.fromBase64 no están disponibles en Node.js 24 (typeof devuelve "undefined"). Llegarán con versiones más nuevas de V8; hasta entonces, Buffer es el camino.
  • Buffer.from(string, "base64") ha ignorado los espacios en blanco y los caracteres no válidos en todas las versiones, así que no confíes en él para validar.

Preguntas frecuentes

¿Buffer.from(str, 'base64') lanza un error con una entrada no válida?

No. Node se salta los caracteres que están fuera del alfabeto y se detiene en el primer =, así que la basura se decodifica a algunos bytes sin ningún error. Valida tú la cadena si la entrada no es de confianza.

¿Qué versión de Node.js añadió base64url?

La codificación base64url llegó en Node.js 15.7.0 y se portó a la 14.18.0. En cualquier versión actual puedes contar con ella.

¿Debo usar btoa en Node.js?

Node marca btoa y atob como heredados y recomienda Buffer. Existen por compatibilidad con los navegadores y tienen la misma limitación a Latin-1 que se describe en Base64 en JavaScript.

Sigue leyendo