oid64
v6.1.0
Published
Fast, strict, URL-safe base64 for MongoDB ObjectIds, UUIDs, integers and bigints. Zero dependencies, ESM, TypeScript.
Maintainers
Readme
oid64
Shorten MongoDB ObjectIds and UUIDs into URL-safe base64 strings, and decode them back. Fast, strict, zero dependencies, TypeScript.
ObjectId 581653766c5dbc10f0aceb55 -> WBZTdmxdvBDwrOtV (24 -> 16 chars)
UUID 6d2bb408-3176-42d3-b473-3d251f19569f -> bSu0CDF2QtO0cz0lHxlWCf (36 -> 22 chars)
number 6083061 -> XNH1
bigint 27261671373252370877777767253n -> WBZTdmxdvBDwrOtVUse it for shorter URLs, share links, QR codes, slugs, cache keys and log lines. The default alphabet is A-Z a-z 0-9 - _, without padding, suitable for URL paths and query values. ObjectIds use standard base64url. UUIDs retain the oid64 5.x final-byte layout; see Format before integrating another codec.
Why oid64
- Every input is validated. Wrong length, wrong type, characters outside the alphabet, uppercase vs lowercase, out-of-range numbers: all of it either decodes correctly or throws an
Oid64Error. Nothing ever returns"undefined",NaNor garbage bytes. - Fast. Shared lookup tables, strict validation integrated into conversion, and optional reusable decode buffers. Compare runtimes and native codecs with the benchmarks.
- Compatible with standard base64url. Encoded ObjectIds are byte-for-byte what
Buffer#toString("base64url")produces, so other languages can decode them with their standard library. - Small. One runtime module, no dependencies, ESM with TypeScript declarations.
- Portable. Tested on Node 24 and the pinned Bun version. The library uses no Node-specific APIs; other runtimes need modern JavaScript, including BigInt and private class fields.
New in 6.1.0
Default-alphabet encoders now share lookup tables, substantially reducing the memory cost of additional instances. Binary decoders leave supplied buffers unchanged on invalid input, and the alphabet is protected against runtime reassignment. Method signatures and valid encoded outputs remain compatible with 6.0.0.
This release also adds reproducible Node/Bun benchmarks and tests the installed npm archive, including TypeScript declarations. See the 6.1.0 release notes and measured results.
Install
bun add oid64@^6.1.0
# or
npm install oid64@^6.1.0Quick start
import { oid64 } from "oid64";
// MongoDB ObjectId (24 hex chars) <-> 16 chars
oid64.fromObjectId("581653766c5dbc10f0aceb55"); // "WBZTdmxdvBDwrOtV"
oid64.toObjectId("WBZTdmxdvBDwrOtV"); // "581653766c5dbc10f0aceb55"
// UUID (36 chars, dashed) <-> 22 chars (oid64 format, not standard base64url)
oid64.fromUUID("6d2bb408-3176-42d3-b473-3d251f19569f"); // "bSu0CDF2QtO0cz0lHxlWCf"
oid64.toUUID("bSu0CDF2QtO0cz0lHxlWCf"); // "6d2bb408-3176-42d3-b473-3d251f19569f"
// Integers (0 .. Number.MAX_SAFE_INTEGER)
oid64.fromInt(6083061); // "XNH1"
oid64.toInt("XNH1"); // 6083061
// BigInts of any size (Snowflake ids, 128-bit values, ...)
oid64.fromBigInt(1234567890123456789n); // "BEiEPR96YEV"
oid64.toBigInt("BEiEPR96YEV"); // 1234567890123456789nWith the MongoDB driver
import { ObjectId } from "mongodb";
import { oid64 } from "oid64";
const doc = await users.findOne({ email });
const shortId = oid64.fromObjectId(doc._id.toHexString()); // "WBZTdmxdvBDwrOtV"
// Later, from a URL parameter:
const _id = new ObjectId(oid64.toObjectId(req.params.id));Or skip the hex round-trip entirely and work on the raw bytes:
oid64.fromBinObjectId(doc._id.id); // ObjectId#id is a 12-byte Uint8Array
new ObjectId(oid64.toBinObjectId(shortId));With crypto.randomUUID()
const id = oid64.fromUUID(crypto.randomUUID()); // 22 chars instead of 36Handling bad input
import { oid64, Oid64Error } from "oid64";
try {
oid64.toObjectId(req.params.id);
} catch (error) {
if (error instanceof Oid64Error) {
// "encoded ObjectId must be 16 characters, received 9"
// "encoded ObjectId contains characters outside the alphabet"
return res.status(400).send("invalid id");
}
throw error;
}API
All methods live on an Encoder instance. Prefer oid64, the shared default instance. The alphabet is read-only at runtime, tables are private, and default-alphabet instances share their tables. Custom-alphabet instances build their own tables; reuse them across calls.
| Method | Input | Output |
| --- | --- | --- |
| fromObjectId(hex) | 24 hex chars, either case | 16 chars |
| toObjectId(id) | 16 chars | 24 lowercase hex chars |
| fromBinObjectId(bytes) | Uint8Array of 12 bytes | 16 chars |
| toBinObjectId(id, out?) | 16 chars, optional target buffer | supplied buffer or new 12-byte Uint8Array |
| fromUUID(uuid) | 36 chars dashed or 32 hex chars, either case | 22 chars |
| toUUID(id) | 22 chars | 36 chars, lowercase, dashed |
| fromBinUUID(bytes) | Uint8Array of 16 bytes | 22 chars |
| toBinUUID(id, out?) | 22 chars, optional target buffer | supplied buffer or new 16-byte Uint8Array |
| fromInt(n) | safe integer 0 .. 2^53-1 | 1 to 9 chars |
| toInt(id) | 1 to 9 chars | safe integer |
| fromBigInt(n) | non-negative bigint | 1+ chars |
| toBigInt(id) | 1+ chars | bigint |
Every method throws Oid64Error (a subclass of Error) on invalid input. Node Buffer is accepted wherever a Uint8Array is expected.
toBinObjectId and toBinUUID accept an optional output buffer of at least 12 or 16 bytes and return that same buffer, including any extra capacity. Only the first 12 or 16 bytes are written. Invalid input leaves the entire buffer unchanged.
const scratch = new Uint8Array(12);
for (const shortId of shortIds) {
oid64.toBinObjectId(shortId, scratch);
consumeSynchronously(scratch); // The next decode overwrites these bytes.
}If a consumer retains the bytes or uses them asynchronously, give it a copy or a separate buffer. When MongoDB bytes are already available, fromBinObjectId(doc._id.id) also avoids converting them to hex first.
Custom alphabet
import { Encoder } from "oid64";
const encoder = new Encoder("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_");
encoder.fromObjectId("581653766c5dbc10f0aceb55"); // "wbztDMXDVbdWRoTv"The alphabet must be exactly 64 unique ASCII characters. The constructor throws otherwise. A custom alphabet changes the output but not its length. URL safety depends on the characters you choose; ASCII validation alone does not guarantee it.
Exports
import { Encoder, Oid64Error, DEFAULT_BASE, oid64 } from "oid64";Format
All formats use the configured 64-character alphabet without padding. Their bit layouts differ:
- ObjectId: 96 bits divide evenly into 16 groups of 6 bits. With the default alphabet, the result is identical to
Buffer.from(bytes).toString("base64url"). - UUID: the first 15 bytes use standard base64 grouping (20 characters). The final byte
bbecomesalphabet[b >> 6] + alphabet[b & 63], preserving the 5.x format. Standard base64url instead usesalphabet[b >> 2] + alphabet[(b & 3) << 4]. For the UUID above, oid64 ends inCf, standard base64url innw. Use an oid64-compatible decoder; the two formats cannot be reliably autodetected. Decoding rejects a final pair greater than 255, so every UUID has one accepted encoding. UUID parsing checks hex and separator placement, not UUID version or variant bits. - Integers: numeric base 64, most-significant group first, no leading zero characters;
0encodes asAwith the default alphabet. This is not byte-oriented base64url. Decoding accepts leading zero characters within the input length limit, so numeric encodings are not required to be canonical.toBigInthas no library length limit; applications can enforce one before decoding externally supplied values.
Performance
Run the same deterministic corpus of 1,024 varying IDs on each runtime:
bun run bench:node # Node, with explicit garbage collection available
bun run bench:bun # Bun
bun run bench:memory # Node import cost and retained memory per encoderbun run bench defaults to Node. The benchmark prints CPU, runtime, latency distributions and estimates of allocation/GC overhead. It compares equivalent operations against a pinned 5.2.1 snapshot, and ObjectId hex encoding against native Buffer. All inputs are valid; native Buffer and 5.x do not provide the current strict validation contract. Integer comparisons with 5.x stay below 2^31.
Corpus generation happens outside the timed region. The measurements include selecting the next input and dispatching the conversion, and represent warmed-up throughput. Run on an idle machine; results depend on CPU, runtime and GC. Constructor and memory measurements cover costs that throughput alone misses. No benchmark runs as a timing gate in CI.
A dated sample result records the environment and measured values.
For application performance, reuse oid64, pass existing bytes directly, and reuse output buffers when their lifetime permits it.
Can the output be even shorter?
An arbitrary 96-bit ObjectId needs at least 16 characters with this 64-character alphabet. RFC 3986 defines 66 unreserved URL characters; even using all 66 still requires 16 characters for 96 bits. Shorter output therefore needs a different alphabet with different escaping rules, a smaller ID space, or a stored mapping. For example, a 64-bit ID takes at most 11 characters with fromBigInt; an application can also store a shorter slug alongside its ObjectId.
Migrating from 5.x
Valid ObjectId and UUID encodings produced by 5.x keep their format. Nonnegative integers below 2^31 keep their encoding except zero. Existing strings resulting from the old integer overflow cannot recover the original value; regenerate them from the source IDs.
- The package is ESM-only and requires Node 24+.
require()still works, Node 24 loads ES modules natively. - Invalid input throws
Oid64Errorinstead of returning nonsense. fromIntandtoIntare correct for the full safe-integer range; 5.x silently overflowed at 2^31.fromInt(0)returns"A"instead of"";toInt("")now throws. Migrate stored empty zero encodings explicitly.- The
noLookupconstructor flag and thehexToBase/baseToHexproperties are gone. fromBigInt/toBigIntare back.
See CHANGELOG.md for the full list.
Development
bun install
bun run check # lint + typecheck + tests + build + installed-package checks
bun test # tests only
bun run bench:node # Node benchmarksCI uses the Bun version in packageManager and Node 24. Package checks install a temporary npm archive, exercise ESM and require(), and compile a TypeScript consumer without Bun ambient types. Run bun run build && bun run test:package to repeat that check alone.
Releases are published to npm with provenance when a version-matching v*.*.* tag is pushed. The release workflow runs the full check once before publishing. Local npm publish runs the same check through prepublishOnly.
Credits
Forked from objectid64 by Maga D. Zandaqo. Rewritten for strict validation, lookup-table performance and modern tooling.
