Skip to main content

Base64 Encoding: Converting Binary Data to Safe Text

Imagine you're trying to send a photo through email or embed an image directly in a JSON file. Here's the problem: images are binary data (just sequences of 0s and 1s), but email and text-based formats like JSON expect text characters. Base64 encoding is the solution—it converts that binary data into a safe, text-friendly format that can travel anywhere text can go.

In this guide, you'll learn exactly how Base64works, why it exists, and how to use it in real-world scenarios.

What You Need to Know First​

To get the most out of this guide, you should understand:

  • Binary basics: What bytes and bits are, and that computers store everything as 0s and 1s
  • JavaScript fundamentals: Variables, functions, basic syntax, and the Uint8Array type
  • Character encoding basics: That text is stored as numbers in computers (like ASCII, where "A" = 65)

If you're unfamiliar with binary numbers or how computers represent data, we recommend reading about the binary number system first.

What We'll Cover in This Article​

By the end of this guide, you'll understand:

  • What Base64 encoding is and why it solves a real problem
  • How Base64 actually works at a bit level
  • The difference between standard Base64 and Base64url
  • How to encode and decode data in JavaScript
  • Real-world uses like embedding images in web pages
  • Common pitfalls and how to avoid them

What We'll Explain Along the Way​

Don't worry if you haven't seen these before—we'll explain each one with examples:

  • 6-bit grouping: Why Base64 uses groups of 6 bits instead of 8
  • Padding characters: What the = signs mean at the end of Base64 strings
  • Character alphabets: The standard set of 64 characters used in Base64
  • Data URIs: How to embed Base64 data directly in HTML and CSS

Why Base64 Exists: The Problem It Solves​

Let's start with a real scenario. You want to send an image file through email. Email was designed to work with text—specifically, plain ASCII text. But an image file is binary data: raw bytes that represent pixel colors and image metadata. When email tries to send binary data as-is, corruption often occurs because some binary values don't translate to valid text characters.

Base64 solves this by converting binary data into a format using only safe, printable ASCII characters. Think of it like translating a secret binary language into a common alphabet that everyone can read and transport safely.

What Makes It "64"?​

The name comes from using 64 different characters to represent data. Here's why that number matters:

  • 64 = 2^6, which means each character represents exactly 6 bits of binary data
  • Six bits can represent 64 different values (0 through 63)
  • Using 6 bits is safer than 8 bits because it stays well within ASCII boundaries

We'll explore this more deeply in the next section.


How Base64 Actually Works: The Mechanics​

The Core Concept: 6-Bit Grouping​

Here's where Base64 gets interesting. Instead of working with 8-bit bytes like most computer systems, Base64 groups data into 6-bit chunks and maps each chunk to a character.

Let's work through a concrete example. Say you want to encode the text "ABC":

Step 1: Convert each character to its binary form

Character: A B C
ASCII: 65 66 67
Binary: 01000001 01000010 01000011

Step 2: Combine all bits and group into 6-bit chunks

Combined: 01000001 01000010 01000011
^^^^^^ ^^^^^^ ^^^^^^ ^^^^^^
010000 010100 001001 000011
(16) (20) (9) (3)

Step 3: Map each 6-bit group to a Base64 character

The Base64 alphabet is:

  • A-Z = 0-25
  • a-z = 26-51
  • 0-9 = 52-61
  • + = 62
  • / = 63

So our 6-bit values map to:

16 → Q (uppercase letters start at 0)
20 → U
9 → J
3 → D

Result: "ABC" encodes to "QUJD"

That's the fundamental process! Every 3 bytes of input produce 4 Base64 characters.

Why Three Bytes Become Four Characters​

This is a natural consequence of the math:

  • 3 bytes = 24 bits (3 × 8)
  • 24 bits ÷ 6-bit groups = 4 characters (24 ÷ 6)

This relationship is important because it means Base64 output is always a multiple of 4 characters. If your input isn't a multiple of 3 bytes, padding characters (=) are added to maintain this ratio.


The Base64 Alphabet: Your 64 Safe Characters​

Base64 uses exactly these 64 characters (in this specific order):

0-25: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
26-51: a b c d e f g h i j k l m n o p q r s t u v w x y z
52-61: 0 1 2 3 4 5 6 7 8 9
62-63: + /

Why these specific characters? Because they're considered "safe" for transmission through most systems:

  • They're all printable
  • They avoid special characters that might be interpreted as escape codes
  • Email, HTTP, and databases handle them reliably

Visualizing the Alphabet​

Index Char Index Char Index Char Index Char
--- ---- --- ---- --- ---- --- ----
0 A 16 Q 32 g 48 w
1 B 17 R 33 h 49 x
2 C 18 S 34 i 50 y
3 D 19 T 35 j 51 z
4 E 20 U 36 k 52 0
5 F 21 V 37 l 53 1
6 G 22 W 38 m 54 2
7 H 23 X 39 n 55 3
8 I 24 Y 40 o 56 4
9 J 25 Z 41 p 57 5
10 K 26 a 42 q 58 6
11 L 27 b 43 r 59 7
12 M 28 c 44 s 60 8
13 N 29 d 45 t 61 9
14 O 30 e 46 u 62 +
15 P 31 f 47 v 63 /

Understanding Padding: What About the = Signs?​

You've probably seen Base64 strings ending with one or two = signs. These are padding characters, and they exist for a specific reason.

Why Padding Is Necessary​

Remember how Base64 output must always be a multiple of 4 characters? That works great when you're encoding exactly 3 bytes (producing 4 characters), 6 bytes (producing 8 characters), and so on. But what if you're encoding 1 byte? Or 2 bytes?

Here's what happens:

Encoding 1 byte:

  • 1 byte = 8 bits
  • Grouped into 6-bit chunks: XXXXXX + XX0000 (we add 4 zeros for padding)
  • This gives us 2 Base64 characters, but we need 4
  • Solution: add 2 padding characters (==)
  • Output: 4 characters total ✓

Encoding 2 bytes:

  • 2 bytes = 16 bits
  • Grouped into 6-bit chunks: XXXXXX + XXXXXX + XX0000
  • This gives us 3 Base64 characters, but we need 4
  • Solution: add 1 padding character (=)
  • Output: 4 characters total ✓

The Padding Rules​

Input bytes Bits 6-bit groups Output chars Padding needed
1 8 1 full + 1 partial 2 ==
2 16 2 full + 1 partial 3 =
3 24 3 full 4 (none)

Key insight: The number of = characters tells the decoder how many "real" bits are in the last group.


Encoding in JavaScript: Converting Binary to Base64​

JavaScript provides the btoa() function for Base64 encoding, but with an important limitation: it only works with Latin1 characters (ASCII values 0-255).

Using btoa() for Simple Text​

// Purpose: Encode simple text to Base64
// Context: When you're working with ASCII text

const originalText = "Hello, World!";
const encoded = btoa(originalText);

console.log(encoded); // SGVsbG8sIFdvcmxkIQ==

// Breaking it down:
// - "Hello, World!" gets converted to Base64
// - The output is 24 characters long
// - Notice the "==" at the end (padding for alignment)

The Problem: Binary Data Beyond ASCII​

btoa() can't handle arbitrary binary data directly. If you try to pass bytes with values above 255, it fails:

// This causes an error!
const problematicData = "\u0100"; // Character beyond ASCII range
try {
btoa(problematicData);
} catch (error) {
console.error(error);
// Error: The string to be encoded contains characters outside the Latin1 range
}

The Solution: Using Uint8Array with btoa()​

To encode arbitrary binary data, convert it to a Latin1 string first:

// Purpose: Encode binary data (from Uint8Array) to Base64
// Input: Binary data as Uint8Array
// Output: Base64 string

function encodeToBase64(binaryData: Uint8Array): string {
// Step 1: Convert Uint8Array to a Latin1 string
let binaryString = "";
for (let i = 0; i < binaryData.length; i++) {
// Convert each byte (0-255) to its corresponding character
binaryString += String.fromCharCode(binaryData[i]);
}

// Step 2: Use btoa() to encode the string
return btoa(binaryString);
}

// Example usage:
const imageData = new Uint8Array([137, 80, 78, 71]); // First 4 bytes of PNG
const base64Result = encodeToBase64(imageData);
console.log(base64Result); // iVBO

// What happened:
// 1. [137, 80, 78, 71] → string with those byte values
// 2. btoa() converted to Base64: "iVBO"

A Modern Alternative: Using the Buffer API (Node.js)​

If you're working in Node.js, the Buffer API is more straightforward:

import { Buffer } from "buffer";

// Purpose: Encode binary data using Node.js Buffer
// Context: Server-side JavaScript/Node.js

const binaryData = Buffer.from([72, 101, 108, 108, 111]); // "Hello"
const base64String = binaryData.toString("base64");

console.log(base64String); // SGVsbG8=

// Reverse process (decode):
const decoded = Buffer.from(base64String, "base64");
console.log(decoded.toString()); // "Hello"

Decoding: Converting Base64 Back to Binary​

The reverse process is called decoding. JavaScript provides the atob() function, which reverses what btoa() does.

Using atob() for Decoding​

// Purpose: Decode a Base64 string back to binary
// Input: Base64 string
// Output: Binary data

function decodeFromBase64(base64String: string): Uint8Array {
// Step 1: Use atob() to decode the Base64 string
// This returns a binary string (Latin1 encoded)
const binaryString = atob(base64String);

// Step 2: Convert the binary string to Uint8Array
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
// Get the character code (0-255) and store it as a byte
bytes[i] = binaryString.charCodeAt(i);
}

// Step 3: Return the binary data
return bytes;
}

// Example usage:
const base64String = "SGVsbG8="; // "Hello" in Base64
const originalData = decodeFromBase64(base64String);

console.log(originalData);
// Uint8Array(5) [72, 101, 108, 108, 111]

console.log(String.fromCharCode(...originalData));
// "Hello"

Complete Encoding and Decoding Workflow​

// Full cycle: Text → Base64 → Binary → Text

const originalText = "Base64 is awesome!";

// Encode to Base64
const encoded = btoa(originalText);
console.log("Encoded:", encoded);
// SGFzZTY0IGlzIGF3ZXNvbWUh

// Decode back to text
const decoded = atob(encoded);
console.log("Decoded:", decoded);
// Base64 is awesome!

// Verify they match
console.log("Match:", originalText === decoded); // true

Real-World Example: Embedding Images as Base64​

One of the most practical uses of Base64 is embedding images directly in HTML or CSS without needing separate image files.

Encoding an Image File (Node.js)​

import fs from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Purpose: Read an image file and convert it to Base64
// Use case: Preparing images for data URIs

async function imageToBase64(imagePath: string): Promise<string> {
// Step 1: Read the image file as binary data
const imageBuffer = await fs.readFile(imagePath);

// Step 2: Convert buffer to Base64 string
const base64String = imageBuffer.toString("base64");

// Step 3: Return the encoded string
return base64String;
}

// Example usage:
const pngPath = path.join(__dirname, "photo.png");
const base64Image = await imageToBase64(pngPath);

console.log("First 50 characters:", base64Image.substring(0, 50));
// iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==

Decoding and Saving Back to File​

import fs from "fs/promises";
import path from "path";

// Purpose: Take a Base64 string and save it as an image file
// Use case: Receiving images from API or data URIs

async function base64ToImage(
base64String: string,
outputPath: string
): Promise<void> {
// Step 1: Create a Buffer from the Base64 string
const imageBuffer = Buffer.from(base64String, "base64");

// Step 2: Write the buffer to a file
await fs.writeFile(outputPath, imageBuffer);

console.log(`Image saved to: ${outputPath}`);
}

// Example usage:
const base64Data = "iVBORw0KGgoAAAANSUhEUgAAA..."; // Truncated for brevity
await base64ToImage(base64Data, "./restored-photo.png");
// Image saved to: ./restored-photo.png

Using Base64 in HTML (Data URIs)​

<!-- Embed a Base64 image directly in HTML -->
<img
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
alt="A small embedded image"
/>

<!-- The format is: data:[MIME type];base64,[Base64 string] -->
<!-- Common MIME types: -->
<!-- image/png, image/jpeg, image/gif, image/svg+xml -->
<!-- image/webp, text/plain, application/json -->

Base64 vs Base64url: Which Should You Use?​

While standard Base64 is great, it has a problem: the +, /, and = characters don't work well in URLs and file names. That's where Base64url comes in.

The Differences​

AspectStandard Base64Base64url
Character for 62+- (hyphen)
Character for 63/ (slash)_ (underscore)
Padding= (included)Omitted
Use caseEmail, JSONURLs, JWTs

Side-by-Side Comparison​

// Same data, different encodings

const data = "Hello!";

// Standard Base64
const standard = btoa(data);
console.log("Standard:", standard);
// SGVsbG8h

// Base64url (you have to implement this)
function toBase64Url(base64String: string): string {
return base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
}

const urlSafe = toBase64Url(standard);
console.log("Base64url:", urlSafe);
// SGVsbG8h (same in this case, would differ with other inputs)

When to Use Each​

Use Standard Base64 when:

  • Embedding data in JSON or XML
  • Sending through email
  • Creating data URIs for images
  • General-purpose encoding

Use Base64url when:

  • Including data in URLs or query parameters
  • Working with JWT (JSON Web Tokens)
  • Using as file names
  • Need to avoid special characters in APIs

Common Pitfalls and How to Avoid Them​

Pitfall 1: Using btoa() with Unicode Characters​

The Problem:

// This causes an error!
const text = "Hello, 世界"; // Contains Unicode characters
try {
btoa(text);
} catch (error) {
console.error(error);
// Error: The string to be encoded contains characters outside the Latin1 range
}

The Solution:

// First, encode to UTF-8, then to Base64
function encodeUnicodeToBase64(text: string): string {
// Step 1: Encode string to UTF-8 bytes
const utf8Bytes = new TextEncoder().encode(text);

// Step 2: Convert bytes to Base64
let binaryString = "";
for (let i = 0; i < utf8Bytes.length; i++) {
binaryString += String.fromCharCode(utf8Bytes[i]);
}
return btoa(binaryString);
}

// Usage:
const encoded = encodeUnicodeToBase64("Hello, 世界");
console.log(encoded); // SGVsbG8sIOe+pOeCgQ==

Pitfall 2: Forgetting About Padding​

The Problem:

// Manually creating Base64 without understanding padding
const incomplete = "SGVsbG8"; // "Hello" but missing padding
const decoded = atob(incomplete); // This might work, but is unreliable

The Solution: Always ensure Base64 strings are properly padded. Most decoders handle it automatically, but it's good practice:

function decodeWithValidation(base64String: string): string {
// Add padding if missing
let padded = base64String;
const padding = 4 - (base64String.length % 4);
if (padding !== 4) {
padded += "=".repeat(padding);
}

return atob(padded);
}

// Usage:
console.log(decodeWithValidation("SGVsbG8")); // "Hello"

Pitfall 3: Confusing Base64url with Standard Base64​

The Problem:

// Trying to use Base64url in an HTML image tag
const base64url = "iVBO-w0KGgo_NAAA"; // Using - and _

// This doesn't work!
// <img src="data:image/png;base64,iVBO-w0KGgo_NAAA" />
// Browser expects standard Base64, not Base64url

The Solution: Convert back to standard Base64 before using in data URIs:

function base64urlToStandard(base64url: string): string {
return base64url.replace(/-/g, "+").replace(/_/g, "/");
}

// For HTML data URIs, always use standard Base64
const standardBase64 = base64urlToStandard(base64url);
const dataUri = `data:image/png;base64,${standardBase64}`;

Summary: Key Takeaways​

Let me recap what you've learned:

  • Base64 is about safe transport: It converts binary data into text-friendly characters that work reliably across email, HTTP, JSON, and other text-based systems.

  • The math is elegant: 3 bytes (24 bits) → 4 Base64 characters because 24 ÷ 6 = 4.

  • 64 characters are enough: Using 6-bit groups (2^6 = 64) provides perfect coverage and safety.

  • Padding maintains alignment: The = characters ensure Base64 output is always a multiple of 4 characters.

  • JavaScript has built-in tools: btoa() for encoding, atob() for decoding, but handle Unicode carefully.

  • Real-world uses are everywhere: Data URIs in images, JSON payloads with binary data, JWTs, and file transmission all use Base64.

  • Choose the right variant: Standard Base64 for general use, Base64url for URLs and tokens.

  • Avoid common mistakes: Handle Unicode properly, respect padding, and don't confuse encoding schemes.