Base64 Encode JavaScript β btoa() & Buffer
Use the free online Base64 Encode Online directly in your browser β no install required.
Try Base64 Encode Online Online βWhen you embed an image in a CSS data URI, pass credentials in an HTTP Authorization header, or store a binary certificate in an environment variable, you need to base64 encode JavaScript data reliably across both browser and Node.js. JavaScript provides two distinct built-in APIs:btoa() for browser environments (also available in Node.js 16+) and Buffer.from() for Node.js β each with different constraints around Unicode, binary data, and URL safety. For a quick one-off encoding without writing any code, ToolDeck's Base64 Encoder handles it instantly in the browser. This guide covers both environments with production-ready examples: Unicode handling, URL-safe variants, file and API response encoding, CLI usage, and the four mistakes that consistently cause bugs in real codebases.
- βbtoa() is browser-native and available in Node.js 16+ globally, but only accepts Latin-1 (code points 0β255) β Unicode input throws a DOMException
- βBuffer.from(text, "utf8").toString("base64") is the Node.js equivalent and handles Unicode natively with no extra steps
- βURL-safe Base64 replaces + β -, / β _, and strips = padding β use Buffer.from().toString("base64url") in Node.js 18+ for a one-liner
- βFor binary data (ArrayBuffer, Uint8Array, files), use Buffer in Node.js or the arrayBuffer() + Uint8Array approach in the browser β never response.text()
- βUint8Array.prototype.toBase64() (TC39 Stage 3) is already available in Node.js 22+ and Chrome 130+ and will unify both environments
What is Base64 Encoding?
Base64 converts arbitrary binary data into a string built from 64 printable ASCII characters: AβZ, aβz, 0β9, +, and /. Every 3 bytes of input map to exactly 4 Base64 characters; if the input length isn't a multiple of 3, one or two = padding characters are appended. The encoded output is always about 33% larger than the original.
Base64 is notencryption β it provides no confidentiality. Anyone with the encoded string can decode it in one function call. Its purpose is transport safety: many protocols and storage formats were designed for 7-bit ASCII text and can't handle arbitrary binary bytes. Base64 bridges that gap. Common JavaScript use cases include data URIs for inlining assets, HTTP Basic Auth headers, JWT token segments, email MIME attachments, and storing binary blobs in JSON APIs.
deploy-bot:sk-prod-a7f2c91e4b3d8
ZGVwbG95LWJvdDpzay1wcm9kLWE3ZjJjOTFlNGIzZDg=
btoa() β The Browser-Native Encoding Function
btoa() (binary-to-ASCII) has been available in browsers since IE10 and became a global in Node.js 16.0 as part of the WinterCG compatibility initiative. It also works natively in Deno, Bun, and Cloudflare Workers. No import is needed.
The function takes a single string argument and returns its Base64-encoded form. The symmetric counterpart atob() (ASCII-to-binary) decodes it back. Both are synchronous and run in constant memory relative to the input size.
Minimal working example
// Encoding an API credential pair for an HTTP Basic Auth header
const serviceId = 'deploy-bot'
const apiKey = 'sk-prod-a7f2c91e4b3d8'
const credential = btoa(`${serviceId}:${apiKey}`)
// β 'ZGVwbG95LWJvdDpzay1wcm9kLWE3ZjJjOTFlNGIzZDg='
const headers = new Headers({
Authorization: `Basic ${credential}`,
'Content-Type': 'application/json',
})
console.log(headers.get('Authorization'))
// Basic ZGVwbG95LWJvdDpzay1wcm9kLWE3ZjJjOTFlNGIzZDg=Decoding with atob()
// Round-trip: encode, transmit, decode const payload = 'service:payments region:eu-west-1 env:production' const encoded = btoa(payload) const decoded = atob(encoded) console.log(encoded) // c2VydmljZTpwYXltZW50cyByZWdpb246ZXUtd2VzdC0xIGVudjpwcm9kdWN0aW9u console.log(decoded === payload) // true
btoa() and atob() are part of the WinterCG Minimum Common API β the same spec that governs Fetch, URL, and crypto in non-browser runtimes. They behave identically in Node.js 16+, Bun, Deno, and Cloudflare Workers.Handling Unicode and Non-ASCII Characters
The most common btoa() pitfall is its strict Latin-1 boundary. Any character with a code point above U+00FF causes an immediate exception:
btoa('MΓΌller & SΓΈren') // β Uncaught DOMException: String contains an invalid character
btoa('rΓ©sumΓ©') // β 'Γ©' is U+00E9 = 233 β within Latin-1, this one actually works
btoa('η°δΈε€ͺι') // β Throws β all CJK characters are above U+00FFThe correct approach is to encode the string to UTF-8 bytes first, then Base64-encode those bytes. JavaScript provides TextEncoder for exactly this purpose:
TextEncoder approach β safe for any Unicode input
// Utility functions for Unicode-safe Base64
function toBase64(text: string): string {
const bytes = new TextEncoder().encode(text)
const chars = Array.from(bytes, byte => String.fromCharCode(byte))
return btoa(chars.join(''))
}
function fromBase64(encoded: string): string {
const binary = atob(encoded)
const bytes = Uint8Array.from(binary, ch => ch.charCodeAt(0))
return new TextDecoder().decode(bytes)
}
// Works with any language or script
const orderNote = 'Confirmed: η°δΈε€ͺι β SΓ£o Paulo warehouse, qty: 250'
const encoded = toBase64(orderNote)
const decoded = fromBase64(encoded)
console.log(encoded)
// Q29uZmlybWVkOiDnlKjkuK3lpKngQeKAkyBTw6NvIFBhdWxvIHdhcmVob3VzZSwgcXR5OiAyNTA=
console.log(decoded === orderNote) // trueBuffer.from(text, 'utf8').toString('base64'). It handles Unicode natively and is faster for large strings.Buffer.from() in Node.js β Complete Guide with Examples
In Node.js, Buffer is the idiomatic API for all binary data operations, including encoding conversions. It predates TextEncoder by years and remains the preferred choice for server-side code. Key advantages over btoa(): native UTF-8 support, binary data handling, and the 'base64url' encoding shortcut available since Node.js 18.
Basic text encoding and decoding
// Encoding a server configuration object for storage in an env variable
const dbConfig = JSON.stringify({
host: 'db-primary.internal',
port: 5432,
database: 'analytics_prod',
maxConnections: 100,
ssl: { rejectUnauthorized: true },
})
const encoded = Buffer.from(dbConfig, 'utf8').toString('base64')
console.log(encoded)
// eyJob3N0IjoiZGItcHJpbWFyeS5pbnRlcm5hbCIsInBvcnQiOjU0MzIsImRhdGFiYXNlIjoiYW5h...
// Decoding back
const decoded = Buffer.from(encoded, 'base64').toString('utf8')
const config = JSON.parse(decoded)
console.log(config.host) // db-primary.internal
console.log(config.maxConnections) // 100Encoding binary files from disk
import { readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
// Read a TLS certificate and encode it for embedding in a config file
const certPem = readFileSync(join(process.cwd(), 'ssl', 'server.crt'))
const certBase64 = certPem.toString('base64')
// Store as a single-line string β suitable for env vars or JSON configs
writeFileSync('./dist/cert.b64', certBase64, 'utf8')
console.log(`Certificate encoded: ${certBase64.length} characters`)
// Certificate encoded: 2856 characters
// Restore the binary cert from the encoded value
const restored = Buffer.from(certBase64, 'base64')
console.log(restored.equals(certPem)) // trueAsync file encoding with error handling
import { readFile } from 'node:fs/promises'
async function encodeFileToBase64(filePath: string): Promise<string> {
try {
const buffer = await readFile(filePath)
return buffer.toString('base64')
} catch (err) {
const code = (err as NodeJS.ErrnoException).code
if (code === 'ENOENT') throw new Error(`File not found: ${filePath}`)
if (code === 'EACCES') throw new Error(`Permission denied: ${filePath}`)
throw err
}
}
// Encode a PDF for an email attachment payload
const reportBase64 = await encodeFileToBase64('./reports/q1-financials.pdf')
const emailPayload = {
to: 'finance-team@company.internal',
subject: 'Q1 Financial Report',
attachments: [{
filename: 'q1-financials.pdf',
content: reportBase64,
encoding: 'base64',
contentType: 'application/pdf',
}],
}
console.log(`Attachment: ${reportBase64.length} chars`)JavaScript Base64 Functions β Parameters Reference
Unlike Python's base64module, JavaScript has no single unified Base64 function. The API depends on the target environment. Here's the complete reference for all native approaches:
| Function | Input type | Unicode | URL-safe | Available in |
|---|---|---|---|---|
| btoa(string) | string (Latin-1) | β throws above U+00FF | β manual replace | Browser, Node 16+, Bun, Deno |
| atob(string) | Base64 string | β returns binary string | β manual replace | Browser, Node 16+, Bun, Deno |
| Buffer.from(src, enc) .toString(enc) | string | Buffer | Uint8Array | β utf8 encoding | β base64url in Node 18+ | Node.js, Bun |
| TextEncoder().encode(str) + btoa() | string (any Unicode) | β via UTF-8 bytes | β manual replace | Browser, Node 16+, Deno |
| Uint8Array.toBase64() (TC39) | Uint8Array | β binary | β omitPadding + alphabet | Chrome 130+, Node 22+ |
The Buffer.from(src, enc).toString(enc) signature accepts several encoding values relevant to Base64:
URL-safe Base64 β Encoding for JWTs, URLs, and Filenames
Standard Base64 uses + and /, which are reserved in URLs β + is decoded as a space in query strings, and / is a path separator. JWTs, URL parameters, filenames, and cookie values all require the URL-safe variant: + β -, / β _, trailing = removed.
Browser β manual character replacement
function toBase64Url(text: string): string {
// For ASCII-safe input (e.g., JSON with only ASCII chars)
return btoa(text)
.replace(/+/g, '-')
.replace(///g, '_')
.replace(/=/g, '')
}
function fromBase64Url(encoded: string): string {
// Restore standard Base64 characters and padding before decoding
const base64 = encoded.replace(/-/g, '+').replace(/_/g, '/')
const padded = base64 + '==='.slice(0, (4 - base64.length % 4) % 4)
return atob(padded)
}
// JWT header β must be URL-safe Base64
const header = JSON.stringify({ alg: 'HS256', typ: 'JWT' })
const encoded = toBase64Url(header)
console.log(encoded) // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
const decoded = fromBase64Url(encoded)
console.log(decoded) // {"alg":"HS256","typ":"JWT"}Node.js 18+ β native 'base64url' encoding
// Node.js 18 added 'base64url' as a first-class Buffer encoding
const sessionPayload = JSON.stringify({
userId: 'usr_9f2a1c3e8b4d',
role: 'editor',
workspaceId:'ws_3a7f91c2',
exp: Math.floor(Date.now() / 1000) + 3600,
})
const encoded = Buffer.from(sessionPayload, 'utf8').toString('base64url')
// No + or / or = characters in the output
// eyJ1c2VySWQiOiJ1c3JfOWYyYTFjM2U4YjRkIiwicm9sZSI6ImVkaXRvciIsIndvcmtzcGFjZUlkIjoid3NfM2E3ZjkxYzIiLCJleHAiOjE3MTcyMDM2MDB9
const decoded = Buffer.from(encoded, 'base64url').toString('utf8')
console.log(JSON.parse(decoded).role) // editorEncoding Files and API Responses in JavaScript
In production code, Base64 encoding is most often applied to files being transmitted and to responses from external APIs that deliver binary content. The patterns differ between browser and Node.js, and binary data requires special care.
Browser β encode a file from an input element
// Modern approach: File.arrayBuffer() (Chrome 76+, Firefox 69+, Safari 14+)
async function encodeFile(file: File): Promise<string> {
const buffer = await file.arrayBuffer()
const bytes = new Uint8Array(buffer)
const chars = Array.from(bytes, b => String.fromCharCode(b))
return btoa(chars.join(''))
}
const uploadInput = document.getElementById('avatar') as HTMLInputElement
uploadInput.addEventListener('change', async (e) => {
const file = (e.target as HTMLInputElement).files?.[0]
if (!file) return
try {
const encoded = await encodeFile(file)
const dataUri = `data:${file.type};base64,${encoded}`
// Preview the image inline
const img = document.getElementById('preview') as HTMLImageElement
img.src = dataUri
img.hidden = false
console.log(`Encoded ${file.name} (${file.size} bytes) β ${encoded.length} Base64 chars`)
} catch (err) {
console.error('Encoding failed:', err)
}
})Fetching a Base64-encoded binary from an API
// GitHub Contents API returns file content as Base64 with embedded newlines
async function fetchRepoFile(
owner: string,
repo: string,
path: string,
token: string,
): Promise<string> {
const res = await fetch(
`https://api.github.com/repos/${owner}/${repo}/contents/${path}`,
{
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github.v3+json',
},
}
)
if (!res.ok) throw new Error(`GitHub API ${res.status}: ${res.statusText}`)
const data = await res.json() as { content: string; encoding: string; size: number }
if (data.encoding !== 'base64') {
throw new Error(`Unexpected encoding from GitHub: ${data.encoding}`)
}
// GitHub wraps output at 60 chars β strip newlines before decoding
const clean = data.content.replace(/\n/g, '')
return atob(clean)
}
const openApiSpec = await fetchRepoFile(
'acme-corp', 'platform-api', 'openapi.json', process.env.GITHUB_TOKEN!
)
const spec = JSON.parse(openApiSpec)
console.log(`API version: ${spec.info.version}`)When you just need to inspect an encoded response during API debugging without setting up a script, paste the Base64 value directly into the Base64 Encoder β it decodes as well, with immediate output. Useful for inspecting GitHub API responses, JWT payloads, and webhook signatures.
Command-Line Base64 Encoding in Node.js and Shell
For CI/CD scripts, Makefile targets, or one-off debugging, you rarely need a full script. Both system tools and Node.js one-liners cover most cases cross-platform.
# ββ macOS / Linux system base64 βββββββββββββββββββββββββββββββββββββββ
# Standard encoding
echo -n "deploy-bot:sk-prod-a7f2c91e4b3d8" | base64
# ZGVwbG95LWJvdDpzay1wcm9kLWE3ZjJjOTFlNGIzZDg=
# URL-safe variant (replace chars and strip padding)
echo -n "deploy-bot:sk-prod-a7f2c91e4b3d8" | base64 | tr '+/' '-_' | tr -d '='
# Encode a file inline (macOS: -b 0 removes line wrapping; Linux: --wrap=0)
base64 -b 0 ./config/production.json
# or on Linux:
base64 --wrap=0 ./config/production.json
# Decode
echo "ZGVwbG95LWJvdDpzay1wcm9kLWE3ZjJjOTFlNGIzZDg=" | base64 --decode
# ββ Node.js one-liner β works on Windows too βββββββββββββββββββββββββββ
node -e "process.stdout.write(Buffer.from(process.argv[1]).toString('base64'))" "my:secret"
# bXk6c2VjcmV0
# URL-safe from Node.js 18+
node -e "process.stdout.write(Buffer.from(process.argv[1]).toString('base64url'))" "my:secret"
# bXk6c2VjcmV0 (same here since there are no special chars)
# Decode in Node.js
node -e "console.log(Buffer.from(process.argv[1], 'base64').toString())" "ZGVwbG95LWJvdA=="base64 wraps output at 76 characters by default. This breaks downstream parsing. Always add -b 0 (macOS) or --wrap=0 (Linux) when you need a single-line result β for example, when writing to an environment variable or a config field.High-Performance Alternative: js-base64
The built-in APIs are fine for most use cases. The main reason to reach for a library is cross-environment consistency: if you ship a package that runs in both browser and Node.js, using Buffer requires either environment detection or bundler configuration, while btoa() requires the Unicode workaround. js-base64 (100M+ weekly npm downloads) handles both transparently.
npm install js-base64 # or pnpm add js-base64
import { toBase64, fromBase64, toBase64Url, fromBase64Url, isValid } from 'js-base64'
// Standard encoding β Unicode-safe, works in browser and Node.js
const telemetryEvent = JSON.stringify({
eventId: 'evt_7c3a9f1b2d',
type: 'checkout_completed',
currency: 'EUR',
amount: 14900,
userId: 'usr_4e2b8d6a5c',
timestamp: 1717200000,
})
const encoded = toBase64(telemetryEvent)
const urlEncoded = toBase64Url(telemetryEvent) // No +, /, or = characters
const decoded = fromBase64(encoded)
console.log(JSON.parse(decoded).type) // checkout_completed
// Binary data β pass a Uint8Array as second argument
const pngMagicBytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
const binaryEncoded = toBase64(pngMagicBytes, true) // true = binary mode
// Validation before decoding
const suspicious = 'not!valid@base64#'
console.log(isValid(suspicious)) // falseUnder the hood, js-base64 uses native Bufferwhen available and falls back to a pure-JS implementation in the browser. It's 2β3Γ faster than the TextEncoder+btoa approach for large Unicode strings, and the symmetric API (toBase64 / fromBase64) eliminates the mental overhead of remembering which direction btoa and atob go.
Encoding Large Binary Files with Node.js Streams
When you need to encode files larger than ~50 MB, loading the entire file into memory with readFileSync() becomes a problem. Node.js streams let you process the data in chunks β but Base64 encoding has a constraint: you must feed the encoder in multiples of 3 bytes to avoid incorrect padding at chunk boundaries.
import { createReadStream, createWriteStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'
// Stream a large binary file to a Base64-encoded output file
async function streamEncodeToBase64(
inputPath: string,
outputPath: string,
): Promise<void> {
const readStream = createReadStream(inputPath, { highWaterMark: 3 * 1024 * 256 }) // 768 KB chunks (multiple of 3)
const writeStream = createWriteStream(outputPath, { encoding: 'utf8' })
let buffer = Buffer.alloc(0)
await pipeline(
readStream,
async function* (source) {
for await (const chunk of source) {
buffer = Buffer.concat([buffer, chunk as Buffer])
// Encode in complete 3-byte groups to avoid mid-stream padding
const remainder = buffer.length % 3
const safe = buffer.subarray(0, buffer.length - remainder)
buffer = buffer.subarray(buffer.length - remainder)
if (safe.length > 0) yield safe.toString('base64')
}
// Flush remaining bytes (may add 1 or 2 '=' padding chars)
if (buffer.length > 0) yield buffer.toString('base64')
},
writeStream,
)
}
// Usage: encode a 200 MB video attachment
await streamEncodeToBase64(
'./uploads/product-demo.mp4',
'./dist/product-demo.b64',
)
console.log('Stream encoding complete')= padding in the middle of the output. The example uses 3 * 1024 * 256 = 786,432 bytes (768 KB) β adjust highWaterMark based on your memory budget. For files under 50 MB, readFile() + Buffer.toString('base64') is simpler and fast enough.Common Mistakes
I've reviewed many JavaScript codebases with Base64 encoding, and these four mistakes appear consistently β often undiscovered until a non-ASCII character or a binary file reaches the encoding path in production.
Mistake 1 β Passing Unicode directly to btoa()
Problem: btoa() only accepts characters with code points 0β255. Characters like Γ±, emoji, or CJK ideographs cause an immediate DOMException. Fix: encode with TextEncoder first, or use Buffer.from(text, 'utf8').toString('base64') in Node.js.
// β DOMException: The string to be encoded contains // characters outside of the Latin1 range const username = 'ΠΠ»Π΅ΠΊΡΠ΅ΠΉ ΠΠ²Π°Π½ΠΎΠ²' const encoded = btoa(username) // throws
// β
Encode as UTF-8 bytes first
function safeEncode(text: string): string {
const bytes = new TextEncoder().encode(text)
const chars = Array.from(bytes, b => String.fromCharCode(b))
return btoa(chars.join(''))
}
const encoded = safeEncode('ΠΠ»Π΅ΠΊΡΠ΅ΠΉ ΠΠ²Π°Π½ΠΎΠ²')
// 0JDQu9C10LrRgdC10Lkg0JjQstCw0L3QvtCyMistake 2 β Forgetting to restore padding before atob()
Problem: URL-safe Base64 strips the = padding. Passing the stripped string directly to atob() produces incorrect output or throws depending on the string length. Fix: restore + and / and re-add the correct amount of padding before calling atob().
// β atob() may return wrong data or throw // on URL-safe Base64 without padding const jwtSegment = 'eyJ1c2VySWQiOiJ1c3JfOWYyYTFjM2UifQ' const decoded = atob(jwtSegment) // Unreliable
// β
Restore characters and padding first
function decodeBase64Url(input: string): string {
const b64 = input.replace(/-/g, '+').replace(/_/g, '/')
const pad = b64 + '==='.slice(0, (4 - b64.length % 4) % 4)
return atob(pad)
}
const decoded = decodeBase64Url('eyJ1c2VySWQiOiJ1c3JfOWYyYTFjM2UifQ')
// {"userId":"usr_9f2a1c3e"}Mistake 3 β Concatenating encoded chunks instead of raw buffers
Problem: Each call to btoa() or .toString('base64') adds its own padding. Concatenating two padded Base64 strings produces invalid output because padding only belongs at the very end. Fix: concatenate the raw data before encoding.
// β Both parts are padded independently β
// the combined string is not valid Base64
const part1 = Buffer.from('webhook-secret').toString('base64')
// d2ViaG9vay1zZWNyZXQ= β has padding
const part2 = Buffer.from('-v2').toString('base64')
// LXYy β correct in isolation
const combined = part1 + part2 // β Invalid β padding in the middle// β
Concatenate raw Buffers before encoding
const combined = Buffer.concat([
Buffer.from('webhook-secret'),
Buffer.from('-v2'),
]).toString('base64')
// d2ViaG9vay1zZWNyZXQtdjI= β single valid Base64 stringMistake 4 β Using response.text() to read binary API data before encoding
Problem: response.text() interprets the raw bytes as UTF-8 and replaces unrecognised byte sequences with the replacement character U+FFFD. Any binary content β images, PDFs, audio β is silently corrupted before it reaches btoa(). Fix: use response.arrayBuffer() to get raw bytes.
// β response.text() corrupts binary data
const res = await fetch('/api/exports/invoice.pdf')
const text = await res.text() // β PDF bytes mangled as UTF-8
const encoded = btoa(text) // β Corrupted Base64// β
arrayBuffer() preserves raw bytes
const res = await fetch('/api/exports/invoice.pdf')
const buffer = await res.arrayBuffer()
const bytes = new Uint8Array(buffer)
const chars = Array.from(bytes, b => String.fromCharCode(b))
const encoded = btoa(chars.join('')) // β
Valid Base64JavaScript Base64 Methods β Quick Comparison
| Method | Unicode | Binary data | URL-safe | Environments | Requires install |
|---|---|---|---|---|---|
| btoa() / atob() | β Latin-1 | β workaround needed | β manual replace | Browser, Node 16+, Bun, Deno | No |
| TextEncoder + btoa() | β UTF-8 | β via Uint8Array | β manual replace | Browser, Node 16+, Deno | No |
| Buffer.from().toString() | β utf8 | β native | β base64url (Node 18+) | Node.js, Bun | No |
| Uint8Array.toBase64() (TC39) | β binary | β native | β alphabet option | Chrome 130+, Node 22+ | No |
| js-base64 | β always | β Uint8Array | β built-in | Universal | npm install |
Choose btoa() only when the input is provably ASCII-only β hex digests, numeric IDs, or pre-validated Latin-1 strings. For user-provided text in a browser, use TextEncoder + btoa(). For all Node.js server-side code, Buffer is the right default. For libraries that need to run in both environments without bundler configuration, js-base64 removes all the edge cases.
Frequently Asked Questions
Related Tools
For a one-click encode or decode without writing any code, paste your string or binary directly into the Base64 Encoder β it handles standard and URL-safe modes instantly in your browser.
Alex is a front-end and Node.js developer with extensive experience building web applications and developer tooling. He is passionate about web standards, browser APIs, and the JavaScript ecosystem. In his spare time he contributes to open-source projects and writes about modern JavaScript patterns, performance optimisation, and everything related to the web platform.
Sophie is a full-stack developer focused on TypeScript across the entire stack β from React frontends to Express and Fastify backends. She has a particular interest in type-safe API design, runtime validation, and the patterns that make large JavaScript codebases stay manageable. She writes about TypeScript idioms, Node.js internals, and the ever-evolving JavaScript module ecosystem.