Base64 aparece en todas partes una vez que empiezas a buscar — tokens JWT, URIs de datos, archivos adjuntos
de correo electrónico, cargas de API que transportan archivos binarios. La codificación en sí está definida en
RFC 4648
y es conceptualmente sencilla: tomar bytes arbitrarios y representarlos usando solo 64 caracteres
ASCII imprimables. Lo que confunde a la gente es la implementación en JavaScript — diferentes
APIs en el navegador frente a Node.js, el problema Unicode que hace que btoa() lance un error, y la
variante segura para URL de la que dependen los JWT. Esta guía cubre todo con código funcional.
btoa() y atob() en el navegador
El navegador ha tenido
btoa()
y
atob()
desde hace mucho tiempo. Los nombres son confusos (binary to ASCII y viceversa), pero el uso es
sencillo para cadenas simples:
// 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 usesbtoa() solo maneja cadenas donde
cada carácter tiene un punto de código ≤ 255 (el rango Latin-1). Pásale una cadena que contenga
cualquier emoji o carácter no Latino y lanzará InvalidCharacterError inmediatamente.
Este es uno de los errores Base64 más comunes en el código del navegador.// ❌ 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: ...Manejo seguro de Unicode en el navegador
La solución es primero codificar la cadena a bytes UTF-8, luego codificar esos bytes en Base64.
El enfoque clásico usa encodeURIComponent y un truco de decodificación porcentual. El enfoque moderno
usa TextEncoder, que está
disponible en todos los navegadores modernos
y Node.js 11+:
// ✅ 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é"Guarda estas dos funciones utilitarias en algún lugar de tu base de código y olvida que existe btoa()
desnudo. El par TextEncoder/TextDecoder es la herramienta correcta para cualquier cosa más allá del
ASCII puro. Puedes probarlo ahora mismo con la herramienta
Codificador Base64.
Buffer.from() en Node.js
Node.js tiene su propia API para esto a través de la clase Buffer, que maneja la codificación/decodificación de forma más limpia. No hay problema Unicode aquí porque especificas explícitamente la codificación de entrada:
// 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"Ten en cuenta que btoa() y atob() también están disponibles en Node.js 16+
como globales (para compatibilidad con el navegador), pero la API Buffer es más idiomática en
Node.js y ha estado ahí desde
Node.js v0.1.
Para la codificación específica de JSON, la herramienta JSON a Base64 es útil
para conversiones manuales rápidas.
Base64 seguro para URL — Lo que realmente usan los JWT
El Base64 estándar usa + y / en su alfabeto. Ambos caracteres
son especiales en URLs — + significa un espacio en las cadenas de consulta, y /
es un separador de ruta. Cuando necesitas Base64 en una URL o como segmento JWT, usas la variante
segura para URL: reemplaza + por - y / por _,
luego elimina el relleno =. Esto está estandarizado en
RFC 4648 §5
y es lo que usa internamente cada biblioteca JWT:
// 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}Por eso verás cadenas Base64 como
eyJhbGciOiJIUzI1NiJ9 en los JWT — sin relleno, guiones en lugar de signos más.
Cuando envíes datos codificados como parámetro de consulta URL, usa siempre la variante segura para URL para
evitar URLs rotas. La herramienta Decodificador Base64 maneja automáticamente tanto
Base64 estándar como Base64 seguro para URL.
Codificación de archivos con la API FileReader
Una tarea común en el navegador: el usuario elige una imagen o documento, y necesitas enviarlo
a una API como Base64. La
API FileReader
tiene readAsDataURL() exactamente para esto — te da una URI de datos completa con el
tipo MIME incluido:
// 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);
}
});Si necesitas la URI de datos completa (incluyendo el prefijo del tipo MIME) en lugar del
Base64 sin procesar, omite el .split(',')[1] y usa reader.result directamente.
Para la conversión masiva de archivos, la herramienta Imagen a Base64 maneja
imágenes sin escribir ningún código.
Codificación de datos binarios y Uint8Arrays
A veces no empiezas desde una cadena o un archivo — tienes bytes sin procesar de una
operación WebCrypto, una exportación de canvas, o un módulo WebAssembly. Aquí se explica cómo pasar de un
Uint8Array a Base64 y viceversa en ambos entornos:
// --- 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=="Incrustar imágenes como URIs de datos
Uno de los usos más prácticos de Base64 en el desarrollo web es incrustar imágenes directamente en HTML o CSS, eliminando una solicitud HTTP. Probablemente hayas visto URIs de datos en SVGs en línea o plantillas de correo electrónico. Aquí está el patrón:
<!-- Inline image in HTML — no separate network request -->
<img
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
alt="1x1 transparent pixel"
width="1"
height="1"
/>/* 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;
}// 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-imageUn módulo utilitario compacto para ambos entornos
En lugar de dispersar llamadas btoa() por tu base de código, vale la pena
tener un único módulo utilitario que cubra Unicode, variantes seguras para URL, y funcione tanto en
navegador como en Node.js. Aquí hay uno que hace todo eso:
// 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));
}// 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]"Problemas comunes a tener en cuenta
- btoa() lanza error en caracteres no-Latin — cualquier carácter por encima del punto de código 255 causa
InvalidCharacterError. Usa siempre el enfoqueTextEncoderoBuffer.from(str, 'utf8')en Node.js. - El relleno importa para la decodificación — las cadenas Base64 deben tener una longitud que sea un múltiplo de 4. El relleno
=faltante hace queatob()devuelva silenciosamente basura o lance un error, dependiendo del navegador. Restaura siempre el relleno antes de decodificar cadenas seguras para URL. - Codificación de Buffer vs cadena en Node.js —
Buffer.from(str)tiene por defecto UTF-8, peroBuffer.from(str, 'binary')trata la cadena como bytes Latin-1. Usar la codificación incorrecta al decodificar produce salida ilegible que puede ser difícil de depurar. - Tipo MIME de la URI de datos —
data:;base64,...(sin tipo MIME) funcionará en algunos navegadores pero no en otros. Incluye siempre el tipo MIME:data:image/png;base64,.... - Saltos de línea en MIME Base64 — RFC 4648 permite a las implementaciones insertar saltos de línea cada 76 caracteres (como hacen los codificadores de correo electrónico).
atob()yBuffer.from()manejan esto, pero si estás generando Base64 tú mismo, no añadas saltos de línea a menos que el sistema de destino los espere.
Conclusión
Base64 en JavaScript es uno de esos temas que parece trivial hasta que te pica.
La versión corta: nunca uses btoa() desnudo para nada generado por el usuario — envuélvelo
con TextEncoder para manejar Unicode correctamente. En Node.js, Buffer.from(str,
'utf8').toString('base64') es el modismo correcto. Cuando la cadena codificada termina en una
URL o JWT, cambia a la variante segura para URL. Para experimentos rápidos o conversiones únicas, las herramientas
Codificador Base64, Decodificador Base64,
y JSON a Base64 ahorran tiempo. La
página del glosario Base64 de MDN
también tiene una buena referencia centrada en el navegador si necesitas una segunda opinión sobre cualquiera de esto.