Werk je met Base64-indeling? Dan is deze site perfect voor jou! Gebruik onze handige online tool om je gegevens te coderen of te decoderen.

Base64-decodering in JavaScript/Node.js: een complete gids

Je applicatie ontvangt een Base64-tekenreeks. Het kan de Authorization-header zijn van een binnenkomende request, een veld in een JSON-payload, een afbeelding die verstopt zit in een data URL, of een certificaat dat in een configbestand geplakt is. Het is allemaal hetzelfde: ruwe bytes in een ASCII-vermomming. Dit artikel gaat over het afschudden van die vermomming in JavaScript en Node.js, en over hoe je dat doet zonder onderweg ook maar een enkele byte te verliezen.

Een kort woordje over het formaat zelf: Base64 is een tekstcodering die elke drie invoerbytes vertaalt naar vier afdrukbare tekens. De startpagina van deze site legt het alfabet, de wiskunde en de padding in al haar details uit, dus hier blijft het bij één zin. Eén gevolg kun je het beste bij de hand houden: gecodeerde data is ongeveer 33 procent groter dan de bytes die hij meedraagt, wat betekent dat decoderen een krimpende operatie is, en er wordt in dit artikel geen gram geheimheid aan toegevoegd of onttrokken. Je pakt een pakket uit, je breekt er geen zegel op.

Het goede nieuws: je hoeft niets te installeren. Browsers leveren atob() al twee decennia mee, Node.js heeft de Buffer-klasse met een ingebouwde base64-mode, en moderne runtimes leveren inmiddels Uint8Array.fromBase64() mee, een strenge en configureerbare nieuwkomer uit de ES2026-specificatie. Het vakmanschap zit hem in het kiezen van het juiste gereedschap voor het werk, en in het precies weten wat elk gereedschap vergeeft, want op een server decodeer je data van vreemden, en het vergevend zijn is waar het scheef gaat.

Een decoder kiezen

Drie API's dekken het overgrote deel van het decoderen. Ze verschillen in temperament, en dat verschil is het hele verhaal:

Decoder Beschikbaar in Temperament
Buffer.from(string, 'base64') Node.js (elke versie die ertoe doet) Vergevingsgezind: slaat onbekende tekens over, stopt bij het eerste =, gooit nooit een fout
atob(string) Alle browsers, Node.js 16 en hoger Streng: gooit InvalidCharacterError bij slechte invoer, slaat ASCII-witte ruimtes over, vergeeft ontbrekende padding
Uint8Array.fromBase64(string) Chrome 140+, Firefox 133+, Safari 18.2+, Node.js 25+ Configureerbaar: jij kiest het alfabet en hoe streng de laatste chunk moet zijn

Alle drie openen dezelfde klassieke payload op dezelfde manier:

// Het werkpaard van Node.js
const { Buffer } = require('node:buffer');
console.log(Buffer.from('aGVsbG8gd29ybGQ=', 'base64').toString('utf8')); // "hello world"
// Het legacy-duo (elke browser, Node.js 16+)
console.log(atob('aGVsbG8gd29ybGQ=')); // "hello world", als binaire tekenreeks
// De moderne ES2026-methode (Chrome 140+, Node.js 25+)
console.log(new TextDecoder().decode(Uint8Array.fromBase64('aGVsbG8gd29ybGQ='))); // "hello world"

Eén waarschuwing voordat je op atob() gaat steunen: het geeft een tekenreeks terug, maar een binaire tekenreeks, een tekenreeks waarin elk teken één ruwe byte draagt als codepunt van 0 tot 255. Dat afdrukken is prima. Zou je hem opslaan in JSON, een database of een cookie, dan gaan die ruwe byte-waarden mee op reis, dus zet hem direct na het decoderen om in echte bytes of echte tekst.

De vergevingsgezinde decoder en wat hij opslorpt

De Buffer van Node is een vergevingsgezinde lezer, en dat is een tweeënzijdig zwaard. Hij is geweldig voor data die ruwe wegen heeft afgelegd: MIME-e-mail met zijn regeleinden, handmatig gekopieerde tekenreeksen, log-uitvoer met willekeurige spaties. Hij is gevaarlijk voor data die je zelf niet hebt geproduceerd, want hij klaagt nooit. Dit is wat er écht gebeurt:

Invoer Wat Buffer.from(input, 'base64') doet
'!!!' Geeft een lege Buffer terug. Alle rommel wordt overgeslagen, er decodeert niets, geen fout.
'aGVsbG8== garbage' Geeft "hello" terug. Het eerste = beëindigt het decoderen; de rest wordt genegeerd.
'aG!VsbG8' Geeft "hello" terug. Het uitroepteken wordt overgeslagen, geen fout.
'aGVs=bG8' Geeft "hel" terug. Een = midden in de tekenreeks stopt de show te vroeg.
'aGVsbG8====' Geeft "hello" terug. Extra padding aan het eind wordt genegeerd.
'=aGVsbG8' Geeft een lege Buffer terug. Padding vóór de data betekent niets.

De oplossing voor onbetrouwbare invoer is een validator, en de grammatica van Base64 is klein genoeg om in één reguliere expressie te passen:

const STRICT = /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$/;
function decodeStrict (base64) {
  if (!STRICT.test(base64)) {
    throw new TypeError('Not a valid base64 string');
  }
  return Buffer.from(base64, 'base64');
}
console.log(decodeStrict('aGVsbG8gd29ybGQ=').toString('utf8')); // "hello world"
try {
  decodeStrict('aGVs!bG8');
} catch (error) {
  console.log(error.message); // "Not a valid base64 string"
}

De regex controleert de vorm: groepen van vier met de juiste padding. Eén regel kan hij niet controleren: de canonieke-coderingsregel van RFC 4648, die zegt dat de ongebruikte pad-bits van de laatste groep nul moeten zijn. De strict-mode van Uint8Array.fromBase64() controleert die wél, dus op Node.js 25 of een moderne browser kun je de regex helemaal overslaan en het platform de audit laten doen:

console.log(new TextDecoder().decode(Uint8Array.fromBase64('aGVsbG8'))); // "hello", loose mode vergeeft ontbrekende padding
try {
  Uint8Array.fromBase64('QQB=', { lastChunkHandling: 'strict' });
} catch (error) {
  console.log(error.name); // "SyntaxError", de pad-bits zijn niet nul
}

De optie lastChunkHandling heeft drie instellingen die de moeite waard zijn om te kennen. "loose" (de standaard) slaat witte ruimtes over, accepteert ontbrekende padding en negeert overgebleven pad-bits. "strict" eist een complete, opgepadde laatste groep met alle pad-bits op nul gezet. En "stop-before-partial" decodeert alleen complete groepen van vier tekens en laat het fragment aan het eind achter om door jou mee te nemen; dat is het onderdeel dat streamend decoderen prettig maakt, zoals je later in dit artikel ziet.

Van bytes naar tekst: het charset-besluit

Als je Base64 decodeert, krijg je bytes in handen. Bytes worden pas tekst als je een charset kiest, en dat is jouw keuze, meestal op basis van wat de afzender beloofde. De standaard van Node is precies de keer die het vaakst uitkomt:

const { Buffer } = require('node:buffer');
const bytes = Buffer.from('w6k=', 'base64'); // de twee bytes C3 A9
console.log(bytes.toString('utf8'));   // "é", de twee bytes smelten samen tot één teken
console.log(bytes.toString('latin1')); // "é", dezelfde bytes gelezen één teken tegelijk

Er zit één hapering in UTF-8: als een bytesequentie geen geldige UTF-8 is, gooit Node geen fout. Hij vervangt hem door het Unicode-vervangingsteken (U+FFFD, de ruit met een vraagteken) en gaat door, wat betekent dat een beschadigde payload je pipeline kan doorseilen tot in je database. De echte tekstdecoder van het platform, TextDecoder (een global in Node.js en elke browser), heeft een fatal-optie die corruptie verandert in een vangbare TypeError:

const stray = new Uint8Array([0xe9]); // één eenzame byte, geen geldige UTF-8
console.log(new TextDecoder().decode(stray)); // het vervangingsteken, geen fout
try {
  new TextDecoder('utf-8', { fatal: true }).decode(stray);
} catch (error) {
  console.log(error.name); // "TypeError"
}

Legacy-systemen sterven nooit, en TextDecoder weet ze nog steeds te lezen. Hij accepteert de volledige labeltabel van de WHATWG Encoding Standard, dus een Base64-payload uit een Windows-app uit de jaren 1990, een Japanse mainframe of een oude FTP-mirror kan nog steeds gedecodeerd worden met labels als 'windows-1250', 'shift_jis', 'euc-kr' of 'gb18030', allemaal ongevoelig voor hoofd- en kleine letters. Eén label verdient een waarschuwing, want het heeft al échte debugtijd gekost: de specificatie maakt 'iso-8859-1', 'latin1' en zelfs 'us-ascii' tot alias van de Windows-1252-decoder. Byte 0x80, in écht Latin-1 een controleteken, komt eruit als het euromerk:

console.log(new TextDecoder('iso-8859-1').decode(new Uint8Array([0x80]))); // "€", niet het Latin-1 waar je om vroeg
// Voor een écht byte-voor-byte Latin-1-lezen, gebruik de Buffer-zijde:
console.log(Buffer.from('gA==', 'base64').toString('latin1')); // het ruwe 0x80-controleteken

Als je die ruwe afbeelding écht nodig hebt, koppelt de 'latin1'-codering van Buffer (wiens legacy-alias 'binary' is en volgens de Node-documentatie een erg misleidende naam) byte N aan codepunt N zonder de Windows-omweg. Voor alles dat modern is, is UTF-8 plus fatal: true het veilige paar.

Een JWT openbreken

De meest voorkomende Base64-payload die een JavaScript-service decodeert is een JSON Web Token, de xxxxx.yyyyy.zzzzz-tekenreeks die in de Authorization-header van de helft van het web reist. Volgens RFC 7515 is een compacte JWS drie puntgescheiden delen, en zijn de eerste twee JSON-objecten die met base64url zonder padding zijn gecodeerd. Het lezen ervan in Node.js gaat zonder omhaal, want de base64url-mode is een eersteklas codering:

const { Buffer } = require('node:buffer');
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJuYW1lIjoiQWRhIn0.JMjpmDdNzQZpTuUO1H33GJsj7nWhBu-qxkPD0GL2uaA';
const [head, body, signature] = token.split('.');
console.log(JSON.parse(Buffer.from(head, 'base64url').toString('utf8'))); // { alg: 'HS256', typ: 'JWT' }
console.log(JSON.parse(Buffer.from(body, 'base64url').toString('utf8'))); // { sub: '123', name: 'Ada' }

Vaak genoeg gezegd, maar herhalen mag: decoderen is geen verificatie. De header en de payload zijn vermomd, niet versleuteld, en iedereen die het token heeft kan beide lezen. Het deel dat je moet controleren is het derde, de handtekening. Voor een klassiek HMAC-SHA256-token is de hele check een paar regels van de ingebouwde crypto-module, en het enige subtiële stukje is vergelijken met timingSafeEqual, zodat een aanvaller je byte-voor-byte-vergelijking niet kan timen:

const crypto = require('node:crypto');
const expected = crypto.createHmac('sha256', 'topsecret').update(head + '.' + body).digest();
const actual = Buffer.from(signature, 'base64url');
console.log(crypto.timingSafeEqual(expected, actual)); // true
console.log(crypto.timingSafeEqual(crypto.createHmac('sha256', 'wrong-secret').update(head + '.' + body).digest(), actual)); // false

In een echte service bouw je dit meestal niet zelf. Het jose-pakket (nul dependencies, draait in Node.js, browsers en edge-runtimes) en het langbestaande jsonwebtoken-pakket (Node.js) wikkelen de dans om, behandelen de RSA- en ECDSA-algoritmefamilies en handhaven de exp, aud en iss-claims. Welke bibliotheek je ook kiest, de Base64-leidingen eronder zijn dezelfde twee calls die je zojuist zag.

HTTP: headers, query strings en cookies

Drie hoeken van de draad zitten vol Base64. De oudste is HTTP Basic-authenticatie, gedefinieerd in RFC 7617: de client stuurt Authorization: Basic plus de Base64 van user-id:password. Op de server is het één snij en één decode, met het kleine protocoldetail dat alleen de eerste dubbele punt de gebruikersnaam scheidt van het wachtwoord; een wachtwoord mag dus meer dubbele punten bevatten, een gebruikersnaam niet:

const { Buffer } = require('node:buffer');
const header = 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==';
const credentials = Buffer.from(header.slice(6), 'base64').toString('utf8');
const [user, ...rest] = credentials.split(':');
console.log(user, rest.join(':')); // "Aladdin" "open sesame"

En onthoud wat Basic-authenticatie in werkelijkheid is: vermomming, niet veiligheid. De credentials reizen in een vermomming over de draad, en daarom is dit mechanisme alleen aanvaardbaar over HTTPS. De tweede hoek is de query string, en daar zit de ergste landmijn van het Base64-landschap:

const params = new URLSearchParams('token=aGVs+bG8=');
console.log(params.get('token')); // "aGVs bG8=", de plus is een spatie geworden

Je Base64 is niet vanzelf beschadigd. De URL-laag deed het beleefd namens de form-coderingsregels, die + behandelen als een spatie. Daarom gebruiken tokens die in query strings wonen precies het URL-veilige alfabet, behandeld in de sectie hieronder. De derde hoek is de cookie: cookies zijn puur ASCII, dus elke niet-ASCII-waarde die erin bewaard is, is bijna zeker Base64, en het oude patroon van een JSON-blob in Base64 in een cookie stoppen leeft in verrassend veel productiesystemen. De decode is dezelfde die je al kent; valideer eerst de vorm, want een cookie is het soort plek waar een gebruiker, of een browserextensie, je rommel kan aanreiken.

Bestanden, afbeeldingen en data URLs

Het bestandssysteem van Node spreekt Base64 rechtstreeks, dus een heel bestand kan in één regel een JSON-grens oversteken:

const fs = require('node:fs');
const base64 = fs.readFileSync('./photo.png', 'base64');
console.log(base64.length); // het bestand, zo'n 33 procent zwaarder
const bytes = Buffer.from(base64, 'base64');
fs.writeFileSync('./photo.copy.png', bytes);

De andere payload met de vorm van een bestand is de data URL, de data:image/png;base64,...-tekenreeks die front ends liefhebben voor inline afbeeldingen. Het recept is in elke runtime hetzelfde: knip bij de eerste komma, parse de metadata ervoor, decodeer de rest. Hier is een echte PNG van één pixel die weer tot leven komt:

const dataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
const comma = dataUrl.indexOf(',');
const meta = dataUrl.slice(5, comma);
const bytes = Buffer.from(dataUrl.slice(comma + 1), 'base64');
console.log(meta); // "image/png;base64"
console.log(bytes.subarray(0, 8).toString('hex')); // "89504e470d0a1a0a", de PNG-signatuur

De signatuur checken is een goedkope gewoonte. De eerste acht bytes van een PNG zijn altijd 89 50 4E 47 0D 0A 1A 0A, en een JPEG begint met FF D8 FF. Als een "base64-afbeelding" van een client niet begint met de magische bytes die hij belooft, dan weet je dat nu voordat je er iets kostelijks mee doet.

URL-veilige Base64: het alfabet voor tokens

Klassieke Base64 gebruikt + en / als zijn twee speciale tekens (RFC 4648, sectie 4), en beide zijn in URLs een probleem: + wordt bij form-decoderen een spatie, en / is een scheidingsteken voor paden. De voor URLs en bestandsnamen veilige variant uit sectie 5, die iedereen base64url noemt, wisselt ze om met - en _, en mag de afsluitende =-padding helemaal weggelaten wanneer de lengte uit de context bekend is. Dat is precies de combinatie die JWTs, OAuth-tokens en deep links nodig hebben, dus base64url is het alfabet dat je in de wildernis het vaakst zult tegenkomen.

De Buffer van Node maakt het hele ding tot een niet-probleem. Zowel de 'base64'- als de 'base64url'-decodermode accepteert alle vier de speciale tekens en kappt ze af naar dezelfde waarden, dus een JWT-deel, een OAuth-token en een klassieke Base64-blob decodeeren allemaal zonder ceremonie van tekenwisselen:

const { Buffer } = require('node:buffer');
console.log(Buffer.from('aGVs-bG8', 'base64').toString('hex'));    // "68656cf9b1bc"
console.log(Buffer.from('aGVs+bG8', 'base64url').toString('hex')); // "68656cf9b1bc", precies dezelfde zes bytes

De ES2026-API is opzettelijk kieskeuriger, en geeft je dezelfde flexibiliteit met een expliciete schakelaar. De optie alphabet kiest tussen "base64" (de standaard, + en /) en "base64url" (- en _), en een teken uit het verkeerde alfabet is een SyntaxError, geen stille cross-alfabet-decode:

console.log(Uint8Array.fromBase64('aGVs-bG8', { alphabet: 'base64url' }).length); // 6
try {
  Uint8Array.fromBase64('aGVs-bG8'); // het standaardalfabet is de klassieke
} catch (error) {
  console.log(error.name); // "SyntaxError", de streep is geen klassiek teken
}

In een browser die de nieuwe methoden nog niet heeft, is de omweg een klein wisseltrucje voordat je de tekenreeks aan atob() geeft, die alleen het klassieke alfabet kent. Je moet ook de padding herstellen als de afzender hem is weggegooid, wat de norm is voor payload van token-stijl:

function decodeBase64Url (value) {
  const classic = value.replace(/-/g, '+').replace(/_/g, '/');
  const padded = classic + '='.repeat((4 - (classic.length % 4)) % 4);
  const binary = atob(padded);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes;
}
console.log(new TextDecoder().decode(decodeBase64Url('aGVsbG8gd29ybGQ'))); // "hello world"

Base64 in de wildernis: waar payloads verstopt zitten

Base64 is de postdienst voor bytes in de JavaScript-wereld. Een rondje door de plekken waar het opduikt, met het decode-recept voor elke stop:

  • JSON API-velden, verreweg de meest voorkomende vervoerder: avatars, thumbnails, gegenereerde documenten en uploads arriveren als Base64-tekenreeksen in gewone JSON, omdat JSON geen woord heeft voor "dit zijn bytes". Decodeer het veld voordat je er iets anders mee doet.
  • Omgevingsvariabelen en configbestanden: een aantal secret-managers, CI-systemen en de npm-CLI zelf reikt je Base64-blobs aan (oudere npm-versies bewaarden de registry-credential in .npmrc als de Base64 van user:password; moderne npm schrijft een ruwe bearer token in _authToken). Decodeer één keer bij het opstarten en houd de plaintext in het geheugen niet langer dan je hem nodig hebt.
  • Kubernetes en clustertooling: k8s-secrets zijn beroemd om het feit dat ze in de API en in etcd Base64-gecodeerd zijn, en de officiële documentatie herhaalt telkens dat het codering is, geen versleuteling. Je decodecode moet het resultaat behandelen als een geheim, niet als bewijs van veiligheid.
  • Databases: alles binair dat in een JSON-kolom bewaard wordt (Postgres jsonb, MongoDB-documenten, Redis) is vaak een Base64-tekenreeks. Decodeer hem in de read-path naar een Buffer of een Uint8Array, en laat de database puur tekstueel.
  • E-mail: MIME Base64 met zijn regeleinde van 76 tekens is de manier waarop bijlagen en binaire headers SMTP oversteken, een protocol dat oorspronkelijk slechts 7-bit was. De decoder van Node slaat de regeleinden voor je over, dus het hele body decodeert in één call zonder opruimen.
  • CI- en CD-pipelines: bouwsystemen en secret-injectors passen tokens door als Base64-omgevingswaarden; decodeer in het pipeline-script en echo de gedecodeerde waarde nooit in een log.
  • Directory- en SAML-data: LDIF-bestanden bewaren binaire attributen (denk aan: certificaten) als Base64, en SAML-responses worden vaak gecomprimeerd en daarna Base64-gecodeerd voordat ze een HTTP-grens oversteken.
  • Worker threads en edge-runtimes: Base64-tekenreeksen steken de grens van worker_threads over als simpele, structured-cloneable tekenreeksen, dus een zware decode kan in een worker wonen terwijl de event loop van de main thread vrij blijft.

Twee van die stops verdienen een blik dichterbij, want ze duiken op in zowel interviews als in productie:

const { Buffer } = require('node:buffer');
// Omgevingsvariabele: het geheim arriveert Base64-gecodeerd
const token = Buffer.from(process.env.REGISTRY_TOKEN_B64, 'base64').toString('utf8');
// JSON API-veld: uitpakken voordat je iets anders doet
const body = { attachment: 'iVBORw0KGgo...' };
const imageBytes = Buffer.from(body.attachment, 'base64');
console.log(imageBytes.subarray(0, 4).toString('hex')); // "89504e47", weer de PNG-signatuur
// MIME-e-mail: de regeleinden worden overgeslagen, geen opruimen nodig
const mimeBody = 'SGVsbG8sIHdyYXBw\nZWQgYmFzZTY0IQ==';
console.log(Buffer.from(mimeBody, 'base64').toString('utf8')); // "Hello, wrapped base64!"

Het anti-patroon dat je op deze rondrit moet spotten is overal hetzelfde: Base64 op een plek waar ruwe bytes al toegestaan waren. Een WebSocket-frame, een bestandstream, een Postgres bytea-kolom, ze dragen allemaal bytes natief, dus een Base64-ronddrip daar is pure overhead, de 33-procents-groottebelasting zonder nut om te tonen. Als er een native binaire weg bestaat, neem die.

Stukje voor stukje decoderen: streams en grote data

Base64-groepen van vier tekens coderen drie bytes, dus een stream van chunks kan een groep in tweeën snijden. De naïeve aanpak - elke chunk decoderen en bidden - beschadigt de uitvoer op willekeurige grenzen. De ES2026-API is precies hiervoor ontworpen: setFromBase64() schrijft naar een vooraf gereserveerde array en rapporteert hoeveel invoertekens hij verbruikte, en de "stop-before-partial"-mode zorgt dat hij stopt bij de laatste complete groep en het fragment overlaat aan de volgende chunk. Het patroon is het spiegelbeeld van de stream-API van TextDecoder:

const { Buffer } = require('node:buffer');
const chunks = ['aGVsbG8', 'gd29ybGQ='];
let leftover = '';
const parts = [];
for (const chunk of chunks) {
  const pending = leftover + chunk;
  const space = new Uint8Array(Math.ceil(pending.length * 3 / 4));
  const { read, written } = space.setFromBase64(pending, { lastChunkHandling: 'stop-before-partial' });
  parts.push(Buffer.from(space.buffer, space.byteOffset, written));
  leftover = pending.slice(read);
}
parts.push(Buffer.from(Uint8Array.fromBase64(leftover)));
console.log(Buffer.concat(parts).toString('utf8')); // "hello world"

Op runtimes zonder de nieuwe methoden (en de LTS-lijn van Node had ze geruime tijd niet) werkt dezelfde loop met een kleine userland-decoder die een onvolledige groep bijhoudt, of je buffert de binnenkomende chunks simpelweg totdat je kunt splitsen op groepsgrenzen. Het cruciale idee is de carry: decodeer nooit een fragment op zijn eigen.

Grote payloads trekken twee grenzen meer naar je aandacht. Eerst de tekenreeks zelf: buffer.constants.MAX_STRING_LENGTH van Node is 536870888 tekens, ruwweg 512 MiB tekst, wat decodeert naar zo'n 400 MB bytes. Een "base64-bestand" groter dan dat heeft een streamende aanpak nodig, niet één enkele readFileSync. Ten tweede het geheugen: de gecodeerde tekenreeks woont in de JavaScript-heap als UTF-16, twee bytes per teken, en de gedecodeerde Buffer is een tweede kopie van de data. Bij grote payloads houd je beide even vast, dus houd de gecodeerde vorm zo kort mogelijk in leven en kies streams voor alles wat bestandsformaat is.

Vanuit de terminal

Node doet dienst als een prima command-line Base64-decoder, wat handig is als je een request debugt of een config-waarde inspecteert:

# Een klassieke Base64-tekenreeks decoderen die als argument is meegegeven
node -e 'console.log(Buffer.from(process.argv[1], "base64").toString("utf8"))' "aGVsbG8gd29ybGQ="
# De URL-veilige variant, padding optioneel
node -e 'console.log(Buffer.from(process.argv[1], "base64url").toString("utf8"))' "aGVsbG8gd29ybGQ"
# Decoderen vanaf stdin, waar pipes voor zijn
echo -n "aGVsbG8gd29ybGQ=" | node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>console.log(Buffer.from(d.trim(),"base64").toString("utf8")))'

Alle drie geven hello world uit. Als je op de machine ook het klassieke base64-commando uit coreutils hebt, doet het hetzelfde werk met base64 -d, maar de Node-versies weten iets van base64url, wat het traditionele tooltje niet doet.

Valkuilen met een JavaScript-accent

Elk van deze is ooit iemands verloren middag geweest in JavaScript of Node.js:

  • De stille decoder: Buffer.from('!!!', 'base64') geeft een lege Buffer terug, geen fout. Half beschadigde invoer decodeert naar half beschadigde data zonder waarschuwing. Valideer onbetrouwbare invoer met de strenge regex (of de strenge fromBase64-mode) en behandel een lege Buffer uit een niet-lege tekenreeks als een rode vlag.
  • Het ontbrekende coderingsargument: Buffer.from('aGVsbG8=') zonder tweede argument decodeert niets. Hij bouwt een Buffer uit de UTF-8-bytes van die letters, dus je "gedecodeerde" data is de letters zelf, opnieuw verpakt als bytes. Het 'base64'-argument is de hele truc.
  • De binary-string-vermomming: de uitvoer van atob() is geen tekst totdat jij zegt dat het dat is. Het erin stoppen in een JSON-response, een cookie of een log-regel "werkt", en het behoudt ook elke null-byte, wat log shippers en serializers in gelijke mate verrast. Zet hem direct om met charCodeAt() naar een Uint8Array, of naar UTF-8-tekst.
  • De plus in de query string: een + in een form-gedecodeerde query-waarde is een spatie tegen de tijd dat URLSearchParams hem je aanreikt. Kies base64url voor alles dat in een URL woont, en plak nooit een klassiek Base64-token zonder escaping in een query string.
  • Het vervangingsteken: ongeldige UTF-8 wordt in de UTF-8-mode van Buffer een stille ruit-vraagteken i.p.v. een fout, zodat een beschadigde payload je pipeline kan passeren en in een database kan belanden. Zet fatal: true aan bij TextDecoder waar corruptie een luidruchtige fout zou moeten zijn.
  • De Windows-omweg: als je TextDecoder om 'iso-8859-1' of 'latin1' vraagt, krijg je de Windows-1252-decoder, waar byte 0x80 het euromerk wordt. Voor écht byte-voor-byte Latin-1, lees de Buffer in plaats daarvan in met toString('latin1'). En onthoud dat 'binary' alleen maar een misleidende alias is voor dezelfde Latin-1-afbeelding.
  • De grootteplafonds: buffer.constants.MAX_LENGTH is 9007199254740991 bytes (2 tot de 53e, minus één) op 64-bit-systemen, maar de tekenreeks die de Base64 vervoert kan niet groeien tot voorbij MAX_STRING_LENGTH van 536870888 tekens. Een enkele tekenreeks kan dus slechts iets meer dan 400 MB gedecodeerde data vervoeren; daarbovenop, stream het.
  • De geheugenrekening: een Base64-tekenreeks kost twee heap-bytes per teken (UTF-16), en de gedecodeerde Buffer is een volledige tweede kopie. Een bestand van 100 MB wordt kortstondig in je proces zo'n 133 MB tekenreeks plus 100 MB Buffer. Maak het venster waarin de gecodeerde vorm aangesproken blijft zo klein mogelijk.
  • De mismatch tussen strengen extern en vergevingsgezind lokaal: je Node-decoder vergeeft wat een strenge decoder elders afwijst (een Python-script, een Go-service, een mobiele app). Als de ene kant van je systeem streng is en de andere vergevingsgezind, dan komt de bug alleen voor bij bepaalde payloadlengtes, en dat is het ergste soort bug. Kom over de strengheid overeen op het protocolniveau, niet in je hoofd.

Hoe JavaScript zijn decoders groeide

De browserszijde heeft een lange, saaie, betrouwbare historie. atob() en btoa() werden gespecificeerd in het HTML5-ontwerp in het begin van 2011 (de browsers hadden ze vóórdat de specificatie ze had), en ze zitten sindsdien in elke grote browser, in gedrag ongewijzigd al meer dan een decennium. Ze zijn ouder dan typed arrays in de taalspecificatie (ES2015), en daarom spreken ze in "binaire tekenreeksen" in plaats van bytes.

Node.js groeide zijn decoder op een ander tijdschema. De Buffer-klasse werd een global in versie 0.1.103, in de zomer van 2010, bijna vijf jaar vóór Node 1.0, en hij droeg de 'base64'-mode vanaf het begin met zich mee. Het grootste deel van het leven van Node was dat de enige decoder in de stad. Toen kwam de golf van web-standaarden: Node 16 in 2021 voegde atob() en btoa() toe als globals, zodat code die voor de browser was geschreven op de server kon draaien zonder polyfill, en markeerde beide vanaf dag één als Legacy. Node 25, uitgebracht op 15 oktober 2025, verhoogde V8 naar 14.1 en bracht de ES2026-methoden, Uint8Array.fromBase64(), setFromBase64() en hun hex-broers, in de runtime. Onderweg werd de oude new Buffer()-constructor afgeschreven (Node 10 startte de waarschuwingen in 2018) ten faveure van Buffer.from(), alloc() en allocUnsafe(), deels omdat een niet-geïnitieerde allocatie alle geheugen dat daar vóór zat kon lekken.

In de browsers landde dezelfde golf iets vroeger: Firefox 133 en Safari 18.2 leverden de nieuwe methoden in 2024, en Chrome 140 (stable op 2 september 2025) vulde de set aan, waarna het feature werd verklaard als Baseline Newly available in het Baseline-programma van de browserfabrikanten. Bun, de all-in-one JavaScript-runtime, kreeg ze in versie 1.1.22 in augustus 2024. En als je niet kunt vereisen dat de runtime recent is, leveren core-js en het es-arraybuffer-base64-pakket van het es-shims-project polyfills voor het hele pakket, en dat is ook de route die de meeste frameworks intern nemen.

Het formaat dat ze bedienen heeft een nog oudere genealogie. Het alfabet werd voor het eerst gestandaardiseerd voor Privacy-Enhanced Mail in 1987 (RFC 989), de herziening van 1993 (RFC 1421) behield hetzelfde alfabet, en MIME nam het op in 1996 (RFC 2045), zo'n drie jaar na die herziening, met zijn regeleinde van 76 tekens; RFC 3548 in 2003 voegde base16, base32 en base64 samen in één document, en RFC 4648 in 2006 bracht het opnieuw uit, met behoud van het URL-veilige alfabet dat RFC 3548 had toegevoegd - het alfabet dat een decennium later in elke JWT zou belanden. De URL-veilige variant is een leuk weetje: het werd voorgesteld in een mailing-list-post uit 2001 over peer-to-peer-identiteiten, vóórdat het ooit een token tegenkwam.

Leuke weetjes voor je volgende standup

  • De voorbeeldkey uit de WebSocket-RFC, dGhlIHNhbXBsZSBub25jZQ==, decodeert naar de woorden "the sample nonce". Het standaardisatiecomité verstopte een knipoog in zijn eigen voorbeeld, en atob() van Node krakt de grap in één call.
  • Buffer.from('!!!', 'base64') geeft een Buffer van lengte nul terug. Een echte allocatie met niets erin. Niets. Het is het dichtst dat Node ooit bij een schouderophalen komt.
  • De Base64-decoders van Node zijn tweetalig op een manier die de specificatie nooit vroeg: +, -, / en _ zijn allemaal welkom in zowel 'base64'- als 'base64url'-mode, waarbij elk paar naar dezelfde waarde afbeeldt.
  • De Node-documentatie voor atob() bevat de zin "Gebruik in plaats daarvan Buffer.from(data, 'base64')". Een runtime die je vertelt om met het gebruik van een van zijn eigen globals te stoppen, compleet met een officiële codemod (npx codemod@latest @nodejs/buffer-atob-btoa) die de migratie voor je doet.
  • Kleine Buffers worden uit een gedeelde plaat gesneden: Buffer.poolSize is 65536 bytes, en elke kleine allocatie hergebruikt stukken van die pool. Het is de reden waarom het aanmaken van Buffers snel is, en waarom "unsafe" allocatie een uiting is waarvan je de betekenis moet kennen.
  • Het kleine base64-js-pakket, drie functies en nul dependencies, trekt ruim 100 miljoen downloads per week op npm, bijna allemaal als verborgen dependency binnen andere pakketten. Base64 is het meest gesmokkelde code in het ecosysteem.
  • Uint8Array.fromBase64() heeft een mode genaamd "stop-before-partial" die puur bestaat zodat je een stream kunt decoderen zonder ooit een groep van vier tekens te splitsen. Een mode vernoemd naar wat het weigert te doen is een zeldzaam stukje API-poezie.
  • De wereld van Unix-wachtwoorden gebruikt zijn eigen Base64-achtige alfabetten, zonder padding, en tot verwarring staan ze niet allemaal in dezelfde volgorde. Het klassieke "hash64"-alfabet van crypt(3) is ./0-9A-Za-z, maar bcrypt schudt dezelfde 64 tekens in plaats daarvan in ./A-Za-z0-9. Je zult de bcrypt-versie tegenkomen in de $2b$-hashes die veel JavaScript-projecten voor gebruikerswachtwoorden bewaren, en dat is de reden waarom "base64" in een veiligheidscontext meerdere verschillende alfabetten kan betekenen, niet alleen maar twee.

Nog één richting te gaan

Base64 decoderen in JavaScript en Node.js is een stapel van drie eerlijke gereedschappen: Buffer.from(string, 'base64'), het vergevingsgezinde werkpaard dat beide alfabetten accepteert en elk dwalend teken overslaat, het best bewaakt door een strenge regex; TextDecoder, voor échte tekst in elke charset die het oude web ooit verzon, met de fatal-mode voor wanneer corruptie moet pijn doen; en de nieuwe Uint8Array.fromBase64(), voor bytes-eerst-code die strenge alfabetten, strenge pad-bits en streaming zonder acrobatiek wil. Bepaal de charset, valideer wat vreemden je sturen, compareer handtekeningen met timingSafeEqual, en het formaat stopt met het een mysterie te zijn aan beide zijden van de browser/server-scheiding.

En als je klaar bent met pakketten openen, onthoud dan dat iemand ze had moeten verzegelen. De coderingszijde heeft zijn eigen valkuilen: de Unicode-muur die btoa() midden in een zin stopt, MIME-regelomwikkeling, base64url-paddingregels, en de nieuwe Uint8Array.toBase64() met zijn omitPadding-optie. Dat verhaal, met codevoorbeelden voor elke stap, wordt in diepte behandeld in het gerelateerde Base64-coderingsartikel op onze zustersite. Lees het als volgende, want de valkuilen zijn anders en grappiger aan die kant van het alfabet.

Laatst bijgewerkt: 2026-10-06

Gerelateerd artikel: Base64-codering in JavaScript/Node.js: een complete gids