Base64 pojawia się wszędzie, gdy zaczniesz szukać — tokeny JWT, URI danych, załączniki e-mail, ładunki API przenoszące pliki binarne. Samo kodowanie jest zdefiniowane w RFC 4648 i jest martwe proste w koncepcji: pobierz dowolne bajty i reprezentuj je używając tylko 64 drukowalnych znaków ASCII. Co przysparza problemów, to implementacja w JavaScript — różne API w przeglądarce i Node.js, pułapka Unicode, przez którą btoa() rzuca wyjątek, oraz wariant bezpieczny dla URL, od którego zależą JWT. Ten przewodnik omawia wszystko z działającym kodem.

btoa() i atob() w przeglądarce

Przeglądarki mają od dawna btoa() i atob(). Nazwy są mylące (binary to ASCII i z powrotem), ale użycie jest proste dla prostych ciągów:

js
// 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 uses
Pułapka Unicode: btoa() obsługuje tylko ciągi, w których każdy znak ma punkt kodowy ≤ 255 (zakres Latin-1). Przekazanie ciągu zawierającego emoji lub znak nielacińskie spowoduje natychmiastowy InvalidCharacterError. To jeden z najczęstszych błędów Base64 w kodzie przeglądarkowym.
js
// ❌ 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: ...

Bezpieczna obsługa Unicode w przeglądarce

Naprawą jest najpierw zakodowanie ciągu do bajtów UTF-8, a następnie zakodowanie tych bajtów w Base64. Klasyczne podejście używa encodeURIComponent i sztuczki z dekodowaniem procentowym. Nowoczesne podejście używa TextEncoder, który jest dostępny we wszystkich nowoczesnych przeglądarkach i Node.js 11+:

js
// ✅ 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é"

Zachowaj te dwie funkcje narzędziowe gdzieś w swojej bazie kodu i zapomnij o gołym btoa(). Para TextEncoder/TextDecoder to właściwe narzędzie do wszystkiego poza czystym ASCII. Możesz to teraz wypróbować za pomocą narzędzia Koder Base64.

Buffer.from() w Node.js

Node.js ma własne API do tego przez klasę Buffer, która obsługuje kodowanie/dekodowanie czyściej. Nie ma tu pułapki Unicode, ponieważ jawnie określasz kodowanie wejścia:

js
// 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"

Pamiętaj, że btoa() i atob() są dostępne w Node.js 16+ jako zmienne globalne (dla kompatybilności z przeglądarką), ale API Buffer jest bardziej idiomatyczne w Node.js i istnieje od Node.js v0.1. Do kodowania specyficznego dla JSON narzędzie JSON do Base64 jest przydatne do szybkich ręcznych konwersji.

Base64 bezpieczne dla URL — co naprawdę używają JWT

Standardowe Base64 używa + i / w swoim alfabecie. Oba te znaki są specjalne w URL — + oznacza spację w ciągach zapytania, a / to separator ścieżki. Gdy potrzebujesz Base64 w URL lub jako segment JWT, używasz wariantu bezpiecznego dla URL: zastąp + znakiem - i / znakiem _, a następnie usuń dopełnienie =. Jest to znormalizowane w RFC 4648 §5 i jest tym, czego każda biblioteka JWT używa wewnętrznie:

js
// 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}

Dlatego widzisz ciągi Base64 takie jak eyJhbGciOiJIUzI1NiJ9 w JWT — bez dopełnienia, myślniki zamiast plusów. Przy wysyłaniu zakodowanych danych jako parametr zapytania URL, zawsze używaj wariantu bezpiecznego dla URL, aby uniknąć uszkodzonych URL. Narzędzie Dekoder Base64 obsługuje zarówno standardowe, jak i bezpieczne dla URL Base64 automatycznie.

Kodowanie pliku za pomocą API FileReader

Typowe zadanie w przeglądarce: użytkownik wybiera obraz lub dokument i musisz wysłać go do API jako Base64. API FileReader ma readAsDataURL() dokładnie do tego — daje pełny URI danych z dołączonym typem MIME:

js
// 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);
  }
});

Jeśli potrzebujesz pełnego URI danych (z prefiksem MIME) zamiast tylko surowego Base64, pomiń .split(',')[1] i użyj bezpośrednio reader.result. Do masowej konwersji plików narzędzie Image to Base64 obsługuje obrazy bez pisania żadnego kodu.

Kodowanie danych binarnych i Uint8Array

Czasami nie zaczynasz od ciągu ani pliku — masz surowe bajty z operacji WebCrypto, eksportu canvas lub modułu WebAssembly. Oto jak przejść z Uint8Array do Base64 i z powrotem w obu środowiskach:

js
// --- 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=="

Osadzanie obrazów jako URI danych

Jednym z najbardziej praktycznych zastosowań Base64 w tworzeniu stron internetowych jest osadzanie obrazów bezpośrednio w HTML lub CSS, eliminując żądanie HTTP. Prawdopodobnie widziałeś URI danych w wbudowanych SVG lub szablonach e-mail. Oto wzorzec:

html
<!-- Inline image in HTML — no separate network request -->
<img
  src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
  alt="1x1 transparent pixel"
  width="1"
  height="1"
/>
css
/* 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;
}
js
// 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-image
Ostrzeżenie o rozmiarze: kodowanie Base64 zwiększa rozmiar pliku o około 33%. Obraz 100 KB staje się ~133 KB tekstu Base64. URI danych są najlepsze dla małych zasobów (ikony, SVG, małe sprite'y) — nie dla zdjęć ani dużych obrazów. Dla nich multipleksowanie HTTP/2 sprawia, że oddzielne żądania są szybsze niż wbudowywanie.

Kompaktowy moduł narzędziowy dla obu środowisk

Zamiast rozrzucać wywołania btoa() po całej bazie kodu, warto mieć jeden moduł narzędziowy, który obsługuje Unicode, warianty bezpieczne dla URL i działa zarówno w przeglądarce, jak i Node.js. Oto taki, który robi to wszystko:

js
// 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));
}
js
// 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]"

Typowe pułapki, na które warto zwrócić uwagę

  • btoa() rzuca wyjątek dla znaków spoza Latin — każdy znak powyżej punktu kodowego 255 powoduje InvalidCharacterError. Zawsze używaj podejścia z TextEncoder lub Buffer.from(str, 'utf8') w Node.js.
  • Dopełnienie ma znaczenie dla dekodowania — ciągi Base64 muszą mieć długość będącą wielokrotnością 4. Brakujące dopełnienie = powoduje, że atob() cicho zwraca śmieci lub rzuca wyjątek, w zależności od przeglądarki. Zawsze przywracaj dopełnienie przed dekodowaniem ciągów bezpiecznych dla URL.
  • Buffer vs kodowanie ciągów w Node.jsBuffer.from(str) domyślnie używa UTF-8, ale Buffer.from(str, 'binary') traktuje ciąg jako bajty Latin-1. Używanie złego kodowania podczas dekodowania produkuje zniekształcone wyjście, które może być trudne do debugowania.
  • Typ MIME URI danychdata:;base64,... (bez typu MIME) będzie działać w niektórych przeglądarkach, ale nie w innych. Zawsze dodawaj typ MIME: data:image/png;base64,....
  • Podziały wierszy w MIME Base64 — RFC 4648 pozwala implementacjom wstawiać podziały wierszy co 76 znaków (jak robią to kodery e-mail). atob() i Buffer.from() oba to obsługują, ale jeśli sam generujesz Base64, nie dodawaj podziałów wierszy, chyba że docelowy system ich oczekuje.

Podsumowanie

Base64 w JavaScript jest jednym z tych tematów, które wyglądają trywialnie, dopóki cię nie ugryzie. Krótka wersja: nigdy nie używaj gołego btoa() do czegokolwiek generowanego przez użytkownika — owiń go TextEncoder do poprawnej obsługi Unicode. W Node.js Buffer.from(str, 'utf8').toString('base64') to właściwy idiom. Gdy zakodowany ciąg trafia do URL lub JWT, przełącz się na wariant bezpieczny dla URL. Do szybkich eksperymentów lub jednorazowych konwersji, narzędzia Koder Base64, Dekoder Base64, i JSON do Base64 oszczędzają czas. Strona słownika Base64 MDN ma też solidne materiały referencyjne skupione na przeglądarce, jeśli potrzebujesz drugiej opinii.