Every developer has seen those long strings of random-looking characters — something like SGVsbG8gV29ybGQ= — and wondered what on earth they are. That's Base64 encoding, and once you understand it, you'll start seeing it everywhere: in JWTs, email attachments, CSS stylesheets, API payloads, and more.
Let me break it down properly — not just the "what", but the "how" and "why" too.
So what actually is Base64?
Base64 is a way to represent binary data as plain text.
Computers store everything as binary — images, PDFs, audio files, executables — it's all 1s and 0s underneath. The problem is that many systems (HTTP headers, JSON, email, XML) were designed to handle text only. If you try to shove raw binary data through a text-based system, it gets corrupted. Characters get misinterpreted, null bytes get stripped, line endings get mangled.
Base64 solves this by converting binary data into a safe set of 64 printable ASCII characters:
A-Z (26 characters)
a-z (26 characters)
0-9 (10 characters)
+, / ( 2 characters)
─────────────────────
64 total
That's where the name comes from — it uses exactly 64 characters.
How does the translation actually work?
This is the part most articles skip. Let's go deep.
Step 1 — Everything starts as binary
Take the word "Man". In ASCII:
M = 77 = 01001101
a = 97 = 01100001
n = 110 = 01101110
Put them together as a stream of bits:
01001101 01100001 01101110
Step 2 — Split into 6-bit groups
Normal bytes are 8 bits. Base64 works in 6-bit chunks because 2⁶ = 64 (exactly the size of our character set):
010011 010110 000101 101110
19 22 5 46
Step 3 — Map each 6-bit value to a character
Base64 has a lookup table (called the Base64 alphabet):
0 = A 16 = Q 32 = g 48 = w
1 = B 17 = R 33 = h 49 = x
2 = C 18 = S 34 = i 50 = y
...
19 = T 22 = W 5 = F 46 = u
So our values 19, 22, 5, 46 map to:
19 → T
22 → W
5 → F
46 → u
"Man" → TWFu
You can verify this yourself:
btoa("Man") // "TWFu"
What about the = padding?
Base64 works in groups of 3 bytes (24 bits → four 6-bit chunks). If your input isn't divisible by 3, it pads with =:
btoa("M") // "TQ==" (1 byte → needs 2 padding chars)
btoa("Ma") // "TWE=" (2 bytes → needs 1 padding char)
btoa("Man") // "TWFu" (3 bytes → no padding needed)
The = characters are just filler — they tell the decoder how many real bytes were in the last group.
The size trade-off
Base64 is not free. Encoding increases file size by approximately 33%.
Why? Because you're representing 3 bytes of data using 4 characters:
Original: 3 bytes = 24 bits
Encoded: 4 chars = 4 × 6 bits = 24 bits of data
but each char is stored as 8-bit ASCII
so: 4 × 8 = 32 bits on disk
Overhead: 32 - 24 = 8 bits extra per 3 bytes ≈ 33% larger
For a 100KB image, the Base64 version will be ~133KB. Keep this in mind when embedding large files.
Real-world use cases with code
1. Embedding images in HTML/CSS
Instead of making an HTTP request for an image file, you can embed it directly:
<!-- Normal image — requires HTTP request -->
<img src="/logo.png" alt="Logo" />
<!-- Base64 embedded — zero HTTP requests -->
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..." alt="Logo" />
Same in CSS:
/* Requires HTTP request */
.icon {
background-image: url('/icon.png');
}
/* Self-contained, no request needed */
.icon {
background-image: url('data:image/png;base64,iVBORw0KGgo...');
}
This is useful for small icons, logos, and inline SVGs where eliminating an HTTP round-trip matters more than file size.
2. Sending files through APIs
Most REST APIs communicate in JSON, which is text-only. To send a file:
// Read a file and convert to Base64
const fileToBase64 = (file) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result.split(',')[1]);
reader.onerror = reject;
reader.readAsDataURL(file);
});
// Send it in a JSON payload
const file = document.querySelector('input[type="file"]').files[0];
const base64 = await fileToBase64(file);
await fetch('/api/upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filename: file.name,
content: base64,
}),
});
On the server (Node.js), decode it back:
const buffer = Buffer.from(base64String, 'base64');
fs.writeFileSync('output.png', buffer);
3. JWT tokens
If you've used authentication, you've used Base64. A JWT looks like this:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Each section separated by . is Base64URL encoded (a variant that replaces + with - and / with _ to be URL-safe). You can decode the payload right in your browser:
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
const payload = token.split('.')[1];
// Base64URL → Base64
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
console.log(JSON.parse(atob(base64)));
// { sub: "1234567890", name: "John" }
Note: JWTs are encoded, not encrypted. Anyone can decode the payload. Never put sensitive data in a JWT payload unless it's also encrypted.
4. Email attachments (MIME)
This is why Base64 was invented. When you send an email with an attachment, your email client encodes the file as Base64 and wraps it in MIME format:
Content-Type: application/pdf; name="invoice.pdf"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="invoice.pdf"
JVBERi0xLjQKJeLjz9MKNiAwIG9iago8PAovVHlwZSAvUGFnZQovUGFy
ZW50IDMgMCBSCi9SZXNvdXJjZXMgPDwKL0ZvbnQgPDwKL0YxIDcgMCBS
...
The email server sees only plain text — no raw binary — so nothing gets corrupted in transit.
Base64 in the browser — btoa and atob
JavaScript has two built-in functions:
// Encode: Binary To Ascii
btoa("Hello World") // "SGVsbG8gV29ybGQ="
// Decode: Ascii To Binary
atob("SGVsbG8gV29ybGQ=") // "Hello World"
The names are confusing — btoa encodes to Base64, atob decodes from Base64. Just remember: binary to ascii = btoa.
Important caveat: btoa breaks on non-Latin characters:
btoa("Hello 🌍") // ❌ InvalidCharacterError
For Unicode strings, use TextEncoder:
function encodeUnicode(str) {
const bytes = new TextEncoder().encode(str);
return btoa(String.fromCharCode(...bytes));
}
function decodeUnicode(str) {
const bytes = Uint8Array.from(atob(str), c => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
encodeUnicode("Hello 🌍") // works ✅
The privacy problem nobody talks about
Here's something most Base64 articles completely ignore.
Because Base64 looks like gibberish, people assume it's secure. It is absolutely not. It's encoding, not encryption. Anyone can decode it in seconds:
atob("SGVsbG8sIHRoaXMgaXMgbm90IGEgc2VjcmV0")
// "Hello, this is not a secret"
But there's a deeper privacy concern — the tools you use to encode/decode.
Most online Base64 tools work like this:
You select a file
↓
File is uploaded to their server
↓
Server encodes/decodes it
↓
Result sent back to you
↓
Your file now lives on someone else's server
For a text string this might not matter. But what about:
- A confidential PDF document?
- An image with personal information?
- A file containing API keys or credentials?
You have no idea what happens to your data once it hits that server. It could be logged, stored, analyzed, or breached.
The correct way to handle this is entirely browser-based processing. The Web File API and JavaScript are powerful enough to encode and decode any file locally — no server needed:
// This runs entirely in YOUR browser
// Nothing leaves your device
const reader = new FileReader();
reader.onload = (e) => {
const base64 = e.target.result.split(',')[1];
console.log(base64); // encoded, locally
};
reader.readAsDataURL(file);
I built base64convertor.com (https://base64convertor.com) specifically for this reason — 60+ Base64 tools that run entirely in your browser. No uploads, no server, no logs. Your files never leave your device.
Base64 vs Base64URL
Quick distinction worth knowing:
Base64 Base64URL
Characters: A-Z a-z 0-9 A-Z a-z 0-9
+ / - _
Padding: = Optional
Safe in URLs: No Yes
Used in: Files, emails JWTs, OAuth,
CSS URL params
// Base64URL encode
const base64url = btoa(str)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
// Base64URL decode
const base64 = base64url
.replace(/-/g, '+')
.replace(/_/g, '/');
const decoded = atob(base64);
Quick reference
// Encode string
btoa("hello") // "aGVsbG8="
// Decode string
atob("aGVsbG8=") // "hello"
// Encode Unicode string
const bytes = new TextEncoder().encode("héllo");
btoa(String.fromCharCode(...bytes)) // "aMOpbGxv"
// Encode file (browser)
reader.readAsDataURL(file) // "data:image/png;base64,..."
// Decode to Blob (browser)
const bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
const blob = new Blob([bytes], { type: 'image/png' });
// Encode in Node.js
Buffer.from("hello").toString('base64') // "aGVsbG8="
// Decode in Node.js
Buffer.from("aGVsbG8=", 'base64').toString() // "hello"
Wrapping up
Base64 is one of those foundational concepts that shows up constantly in web development — APIs, auth tokens, emails, CSS, file handling. Understanding how it works at the bit level makes it far less mysterious.
Key takeaways:
- Base64 converts binary → 64 printable ASCII characters
- It works in 3-byte input → 4-character output chunks
- Output is ~33% larger than input
- It is encoding, not encryption — never use it for security
- Browser-based tools are always safer than uploading files to unknown servers
If you need to encode or decode Base64 — images, PDFs, audio, video, documents — base64convertor.com (https://base64convertor.com) does it all, privately, right in your browser.
Have questions or something to add? Drop a comment below.
— Ken D'Souza
Tags: #webdev #javascript #tutorial #security
Top comments (0)