Library / SDK
dankogai/js-base64 avatar
dankogai/js-base64

js-base64: a pure-JS Base64 transcoder with a UTF-8 aware decode

Base64 implementation for JavaScript

4,361 stars1,292 forksJavaScriptBSD-3-Clause

At a glance

What is it?
The npm package js-base64 handles Base64 in browsers and Node with one API, and its decode() returns UTF-8 text where browser atob() returns bytes. Here is how the pieces fit, and where the package is the wrong tool.
Who is it for?
Adopt js-base64 when you need one Base64 API across browser and Node, and when decode() should return UTF-8 text rather than raw bytes. Do not adopt it if you are handling binary payloads such as a PNG data URI and plan to call decode() on them; the README explicitly says to use atob() or toUint8Array() instead.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 11 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 27, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What js-base64 solves, and for whom

Base64 appears in a JavaScript codebase in places that have nothing to do with each other: a JSON Web Token payload, an inline image data URI, a query parameter that has to survive a URL, a byte array coming back from a fetch call. The platform offers btoa() and atob(), but they only exist in browsers, they throw on non-Latin-1 input, and they hand back a binary string that is not what most callers want.

js-base64 targets that gap. It is a pure-JavaScript transcoder published as js-base64 on npm, licensed BSD-3-Clause, with the same surface in the browser, in Node via require, and in an ES module build. The README describes it as "Yet another Base64 transcoder", so the author is not claiming to have invented anything. The claim is narrower and more useful: one API, correct handling of UTF-8 text, and a clear separation between decoding to a string and decoding to bytes.

It suits application developers who need Base64 at the edges of a web app or a Node service and do not want to pull in a general-purpose encoding library. It does not suit anyone who needs streaming Base64 over a large file, or a decoder that validates strictly by default.

How the encode, decode and atob paths differ

The package exposes a Base64 namespace object, plus named exports encode and decode for callers who prefer no namespace. The README's synopsis shows the behaviour that matters: Base64.encode('dankogai') returns 'ZGFua29nYWk=', and Base64.encode('小飼弾') returns '5bCP6aO85by+'. That second line is the whole point. The browser's btoa() raises an exception on the same input, as the README demonstrates.

The decode side is where the design starts to bite. Base64.decode() decodes to a UTF-8 string. Base64.atob() decodes to bytes, matching the browser's built-in atob(), which the README notes is absent in Node. Base64.toUint8Array() returns a Uint8Array. The README is blunt about the consequence: given a Base64-encoded 1x1 transparent PNG, do not call Base64.decode() on it, use atob() or toUint8Array(). Running decode() over binary data gives you mojibake, and the README shows exactly that, with Base64.atob('5bCP6aO85by+') returning nonsense characters.

There is also a URI-safe family. encodeURI() and the second argument to encode() both strip padding and swap the alphabet, so '小飼弾' becomes '5bCP6aO85by-' with a hyphen instead of a plus. decode() accepts both flavors, so the README notes that decodeURI() is unnecessary on the way back.

Validation is separate again. isValid() returns true for an empty string, for 'ZA==' and for unpadded 'ZA', tolerates whitespace such as 'Z A=', accepts either the standard or the URL-safe alphabet, and returns false only when the two alphabets are mixed, as in '+-'. That is a permissive validator by design, and callers who need strict rejection of malformed input will have to add their own check.

Installing js-base64 and encoding your first string

The README gives one install command for the npm package. Run it in the project where you need the transcoder.

bash
npm install --save js-base64

In Node with CommonJS, the package is required and the global context is not modified. The README states this explicitly, contrasting it with the browser script tag.

javascript
const {Base64} = require('js-base64');

For an ES module build, import the namespace or the bare functions. The package.json maps the import condition to base64.mjs and the require condition to base64.js, with separate type declarations for each, so bundlers and TypeScript resolve the right file without configuration.

javascript
import { Base64 } from 'js-base64';
javascript
// or if you prefer no Base64 namespace
import { encode, decode } from 'js-base64';

In a browser without a build step, the README offers a script tag. This loads Base64 into the global window object, and the README warns that you should consider the ES module form to avoid tainting window, even though Base64.noConflict() exists.

html
<script src="https://cdn.jsdelivr.net/npm/[email protected]/base64.min.js"></script>

A first real use is the round trip that trips people up: encoding a non-Latin-1 string and getting it back as text, and converting a byte array in both directions.

javascript
Base64.decode(Base64.encode('小飼弾')); // 小飼弾
Base64.fromUint8Array(u8s);       // ZGFua29nYWk=
Base64.fromUint8Array(u8s, true); // ZGFua29nYWk
Base64.toUint8Array('ZGFua29nYWk=');// u8s above

The decode trap with binary data such as PNG and images

The single most common way to misuse this library is to take a data URI from a canvas or a file upload, strip the prefix, and pass the rest to Base64.decode(). The README anticipates this and puts a warning in bold: given the Base64-encoded 1x1 transparent PNG it shows, do not use Base64.decode(). Use Base64.atob() instead, or better, Base64.toUint8Array().

The reason is not a bug. decode() is specified to produce a UTF-8 string, which is the correct behaviour for a JWT payload or a config value and the wrong behaviour for image bytes. atob() produces bytes and mirrors the browser function of the same name, which the README points out does not exist in Node, so the package is filling a genuine platform gap rather than duplicating one. If your pipeline ends in a Blob, a File or an ArrayBuffer, toUint8Array() is the entry point, and any further conversion to a Blob or ArrayBuffer is your code, not the library's. The README does not document Blob or File helpers.

A second limitation is scale. Nothing in the README or the package layout suggests a streaming or incremental API. The package ships four files, base64.js, base64.mjs and two declaration files, and the synopsis is entirely whole-string. If you need to encode a multi-gigabyte upload without holding it in memory, this is not the tool, and you should say so before a reviewer finds out for you.

js-base64 against the platform's own btoa and atob

The real alternative is not another npm package. It is the built-in btoa() and atob() that every browser already has, plus Buffer.from(str, 'base64') in Node. The difference in approach is worth stating plainly, because it decides most adoption questions.

The platform functions operate on Latin-1. btoa('小飼弾') throws, as the README shows. If your input is guaranteed ASCII, the built-ins cost nothing and add no dependency. The moment a user's name, a localized string or an emoji enters the payload, you need a UTF-8 step, and that step is what js-base64 packages. It also smooths over the browser and Node split: the README notes that Node has no atob, so code that runs in both environments otherwise needs a shim or an environment check.

The package adds a third thing the built-ins do not have: Base64.isValid(), a predicate you can call on untrusted input before decoding. The built-ins have no equivalent, so the usual pattern is a try/catch around atob(). Whether that matters depends on how much untrusted Base64 you parse.

The trade-off is a dependency of a few kilobytes in exchange for those three conveniences. For a script that base64-encodes an ASCII token once, the built-ins win. For a web app that handles user-supplied strings in both the browser and a Node backend, the package removes a class of environment-specific bugs.

Optional prototype extensions and what they cost

The README documents a set of opt-in extensions. By default the package leaves built-in prototypes untouched, and the README states this as a design decision. You can call Base64.extendString() to add toBase64(), toBase64URI(), toBase64URL(), fromBase64() and toUint8Array() to String.prototype, or Base64.extendUint8Array() to add toBase64(), toBase64URI() and toBase64URL() to Uint8Array.prototype, or Base64.extendBuiltins() to do both at once.

The README is explicit that you have to extend the prototypes yourself, and the reason to hesitate is the same reason the default is off. Patching String.prototype in a library that other code also loads is how you get two packages fighting over the same method name. The extension is convenient inside an application you control; inside a package you publish, it is a side effect your users did not ask for. Note also that package.json sets "sideEffects": false, which tells bundlers the module can be tree-shaken, and calling extendBuiltins() at import time works against that expectation.

There is a naming detail worth reading twice. toBase64URI() and toBase64URL() are aliases, and toBase64(true) is equivalent to toBase64URI(). The README shows all three producing '5bCP6aO85by-' for the same input. If you only need the URI-safe form once, the second argument to encode() avoids touching any prototype at all.

Build layout, licence and the cost of upgrading

The repository is not just a single .js file dropped in a folder. Since version 3.3, per the README's brief history, the source is base64.ts, and base64.js and base64.mjs are compiled from it by Rollup. The build script runs rollup, then tsc with --declaration --emitDeclarationOnly, then copies base64.d.ts to base64.d.mts. The published files list is limited to those four artifacts plus package.json, so consumers do not receive the TypeScript source or the test directory.

For anyone consuming the package, that means the upgrade cost is low: the API in the synopsis has been stable, and the version script regenerates the compiled files and syncs the version string across base64.js, base64.mjs, base64.d.ts, base64.d.mts and README.md before committing. There are no runtime dependencies; everything in devDependencies is a build or test tool. The last push to the repository was on 2026-09-19, and the repository is not archived.

The licence is BSD-3-Clause, which is permissive and imposes the usual condition of retaining the copyright notice and disclaimer when you redistribute. That is a summary of the identifier in package.json, not legal advice; check LICENSE.md in the repository and your own obligations before shipping it in a product.

One compatibility note from the README's history matters for upgrades: since version 3.0 the package switched to ES2015 modules and is no longer compatible with legacy browsers such as IE, though since 3.7 the base64.js build is ES5-compatible again. If you are maintaining an old bundle, check which of the two files your bundler resolves.

Editorial conclusion

Adopt js-base64 when you need one Base64 API across browser and Node, and when decode() should return UTF-8 text rather than raw bytes. Do not adopt it if you are handling binary payloads such as a PNG data URI and plan to call decode() on them; the README explicitly says to use atob() or toUint8Array() instead. Before wiring it in, run Base64.isValid() against the strings you expect to receive, because it accepts unpadded input, whitespace and either alphabet, and rejects only a mix of the two alphabets.

Frequently asked questions

What does == mean in Base64?

The equals signs are padding. The README's isValid() examples show that padding can be omitted, since both 'ZA==' and 'ZA' are accepted as valid, and that encode() with the second argument set to true skips the padding entirely.

What is Base64 used for?

The README does not discuss use cases in general. Its examples are about transcoding text and byte arrays, including a Base64-encoded 1x1 transparent PNG used to illustrate why decode() and atob() differ.

How to Base64 encode in nodejs with js-base64?

Install the package with npm install --save js-base64, then require it and call Base64.encode(). The README notes that unlike the browser script tag, the CommonJS require does not modify the global context.

How do I decode Base64 data in JavaScript with js-base64?

Use Base64.decode() for UTF-8 text and Base64.atob() or Base64.toUint8Array() for bytes. The README warns against calling decode() on binary data such as the PNG example, because decode() returns a UTF-8 string.

Official sources

  1. dankogai/js-base64 on GitHub
  2. Issues
  3. License: BSD-3-Clause
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/dankogai-js-base64.svg)](https://hysenlabs.com/projects/dankogai-js-base64)