Base64url in JavaScript

Buffer.from(text).toString('base64url')

In Node.js, Buffer.from(text).toString('base64url') encodes a string as base64url, and Buffer.from(encoded, 'base64url').toString() decodes it. Both read and write the text as UTF-8.

const text = '{"user":"Zoë","admin":true}';
const encoded = Buffer.from(text).toString('base64url');
// 'eyJ1c2VyIjoiWm_DqyIsImFkbWluIjp0cnVlfQ'
Buffer.from(encoded, 'base64url').toString();
// '{"user":"Zoë","admin":true}'

Base64url is RFC 4648 section 5: the same encoding as Base64, with - and _ in place of + and /. The = padding is usually left off as well, and JWTs (RFC 7515) must leave it off. The same JSON in standard Base64 is eyJ1c2VyIjoiWm/DqyIsImFkbWluIjp0cnVlfQ==. The tool below shows the base64url form, from the URL-safe (- _) alphabet.

In the browser

There is no base64url mode in btoa, so encode as standard Base64 and swap the characters. atob doesn’t need the padding back, so decoding only swaps them back:

function toBase64Url(text) {
  const bytes = new TextEncoder().encode(text);
  const base64 = btoa(Array.from(bytes, (byte) => String.fromCharCode(byte)).join(''));
  return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

function fromBase64Url(encoded) {
  const base64 = encoded.replace(/-/g, '+').replace(/_/g, '/');
  const bytes = Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}

toBase64Url('{"user":"Zoë","admin":true}');
// 'eyJ1c2VyIjoiWm_DqyIsImFkbWluIjp0cnVlfQ'

Keep the g flag, or use replaceAll: replace('+', '-') with a string swaps only the first one. TextEncoder and TextDecoder are what keep ë intact; btoa alone would write it as a single Latin-1 byte.

Why not standard Base64 in a URL

+, / and = all mean something in a URL. In a query string, + is read as a space:

new URLSearchParams('t=ab+cd/ef==').get('t');
// 'ab cd/ef=='

encodeURIComponent keeps it safe by escaping, at the cost of length: eyJ1c2VyIjoiWm%2FDqyIsImFkbWluIjp0cnVlfQ%3D%3D. Base64url needs no escaping in a path, a query or a file name.

Gotchas

  • atob rejects base64url as it is: atob('eyJ1c2VyIjoiWm_DqyIsImFkbWluIjp0cnVlfQ') throws InvalidCharacterError because of the _.
  • Buffer.from(s, 'base64') and 'base64url' each read both alphabets, so decoding works either way, but neither ever throws on bad input.
  • Some decoders, Python’s base64.urlsafe_b64decode among them, need the padding restored first: add '=' until the length divides by 4.

Recent browsers can skip the swapping: new TextEncoder().encode(text).toBase64({ alphabet: 'base64url', omitPadding: true }) encodes, and Uint8Array.fromBase64(encoded, { alphabet: 'base64url' }) gives the bytes back. Node 22 has neither.

Try it in the Base64 Encoder & Decoder

Nothing leaves your browser: the conversion happens here, in the page.

Base64
eyJ1c2VyIjoiWm_DqyIsImFkbWluIjp0cnVlfQ

28 bytes of UTF-8, written as 38 Base64 characters.

Runs in your browser. Nothing you enter is sent anywhere.