One library. Every ID format. Every runtime. Zero dependencies.
uniku /uˈniːku/ — Maltese for "unique"
import { uuidv7 } from 'uniku/uuid/v7'
const id = uuidv7()
// => "018e5e5c-7c8a-7000-8000-000000000000"
// Time-ordered: IDs sort by creation time
const [first, second, third] = [uuidv7(), uuidv7(), uuidv7()]
console.log(first < second && second < third) // true| uniku | uuid | nanoid | ulid | cuid2 | ksuid | |
|---|---|---|---|---|---|---|
| UUID v4 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| UUID v7 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| ULID | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ |
| CUID2 | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ |
| Nanoid | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| KSUID | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
| Tree-shakeable | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| ESM-only | ✅ | ✅¹ | ✅ | ❌ | ✅ | ❌ |
| Edge/Workers | ✅ | ✅ | ✅ | ✅ | ||
| Byte ↔ String | ✅ | ✅ | - | - | ✅ |
Notes:
- Byte ↔ String conversion doesn't make sense for nanoid and cuid2, since they are string-native formats with no canonical binary representation.
- ¹
uuid@13is ESM-only; earlier versions support CommonJS.- ²
ulidonly provides timestamp encoding/decoding, not full binary serialization.
Uses globalThis.crypto (Web Crypto API) — no Node.js-specific APIs.
- ESM-only — No CommonJS or legacy support.
- Universal runtime support — Works everywhere: Node.js, Cloudflare Workers, Vercel Edge, Deno, browsers.
- Truly tree-shakeable — Separate entry points mean you only bundle what you import. No barrel
index.tsre-exports. - Byte-level access — Convert between UUID strings and
Uint8Arraywith built-intoBytes()andfromBytes(). - Modern standards — Includes UUID v7 (RFC 9562) for time-ordered, sortable identifiers with monotonic sequencing.
- Lightweight — Designed for use in performance-sensitive contexts like ORMs.
Benchmarks comparing uniku string ID generation with equivalent npm packages:
| Generator | uniku vs npm |
|---|---|
| ULID | 85× faster |
| CUID2 | 8× faster |
| KSUID | 1.5× faster |
| UUID v7 | 1.1× faster |
| Nanoid | ~comparable speed |
| UUID v4 | npm is 1.1× faster |
| Use Case | Recommended | Why |
|---|---|---|
| Database primary keys | UUID v7 or ULID | Time-ordered for index efficiency |
| URL shorteners | Nanoid | Compact, URL-safe characters |
| Prevent enumeration | CUID2 | Non-sequential, secure |
| Maximum compatibility | UUID v4 | Universal standard |
| Distributed systems | ULID | Sortable + high entropy |
# pnpm (recommended)
pnpm add uniku
# bun
bun add uniku
# npm
npm install uniku
# yarn
yarn add uniku
# deno
deno install npm:unikuOnly import what you use — each entry point is independently tree-shakeable:
| Import | Minified + gzipped |
|---|---|
uniku/uuid/v4 |
~940 B |
uniku/uuid/v7 |
~1.1 KB |
uniku/ulid |
~1.5 KB |
uniku/cuid2 |
~1.1 KB* |
uniku/nanoid |
~938 B |
uniku/ksuid |
~1.0 KB |
Every pull request gets a preview release via pkg.pr.new. Install directly from a PR:
pnpm add https://pkg.pr.new/jkomyno/uniku@<pr-number>import { uuidv4 } from 'uniku/uuid/v4'
// Generate a UUID string
const id = uuidv4()
// => "550e8400-e29b-41d4-a716-446655440000"
// Convert to bytes
const bytes = uuidv4.toBytes(id)
// => Uint8Array(16)
// Convert back to string
const str = uuidv4.fromBytes(bytes)
// => "550e8400-e29b-41d4-a716-446655440000"For advanced use cases, you can provide custom random bytes or write directly to a buffer:
import { uuidv4 } from 'uniku/uuid/v4'
// Custom random source (note: bytes at index 6 and 8 will be modified for version/variant bits)
const id = uuidv4({ random: myRandomBytes })
// Write to existing buffer at offset
const buffer = new Uint8Array(32)
uuidv4(undefined, buffer, 8) // writes 16 bytes starting at offset 8UUID v7 embeds a timestamp, making IDs sortable by creation time — ideal for database primary keys:
import { uuidv7 } from 'uniku/uuid/v7'
// Generate a time-ordered UUID
const id = uuidv7()
// => "018e5e5c-7c8a-7000-8000-000000000000"
// IDs generated in sequence are lexicographically sortable
const ids = [uuidv7(), uuidv7(), uuidv7()]
ids.sort() // Already in creation order
// Byte conversions work the same way
const bytes = uuidv7.toBytes(id)
const str = uuidv7.fromBytes(bytes)For testing or advanced use cases:
import { uuidv7 } from 'uniku/uuid/v7'
// Specify timestamp and sequence
const id = uuidv7({
msecs: 1702387456789,
seq: 0,
random: new Uint8Array(16)
})
// Write to buffer
const buffer = new Uint8Array(32)
uuidv7(undefined, buffer, 8)ULID is a 26-character, lexicographically sortable identifier:
import { ulid } from 'uniku/ulid'
// Generate a ULID string
const id = ulid()
// => "01HW9T2W9W9YJ3JZ1H4P4M2T8Q"
// Convert to bytes
const bytes = ulid.toBytes(id)
// Convert back to string
const str = ulid.fromBytes(bytes)CUID2 generates secure, collision-resistant identifiers using SHA3-512 hashing. Unlike time-ordered IDs, CUID2 prevents enumeration attacks:
import { cuid2 } from 'uniku/cuid2'
// Generate a CUID2 string
const id = cuid2()
// => "pfh0haxfpzowht3oi213cqos"
// Custom length (2-32 characters)
const shortId = cuid2({ length: 10 })
// => "tz4a98xxat"
// Validation (type guard)
const maybeId: unknown = getUserInput()
if (cuid2.isValid(maybeId)) {
console.log(maybeId) // TypeScript knows this is a string
}Nanoid generates compact, URL-friendly unique string IDs with 126 bits of entropy (comparable to UUID):
import { nanoid } from 'uniku/nanoid'
// Generate a 21-character URL-safe ID
const id = nanoid()
// => "V1StGXR8_Z5jdHi6B-myT"
// Custom size
const shortId = nanoid(10)
// => "IRFa-VaY2b"
// Custom alphabet and size via options
const hexId = nanoid({ alphabet: '0123456789abcdef', size: 12 })
// => "4f90d13a42bc"
// Validation (default alphabet only)
nanoid.isValid(id)
// => trueKSUID is a 27-character, K-Sortable Unique Identifier with second-precision timestamps:
import { ksuid } from 'uniku/ksuid'
// Generate a KSUID string
const id = ksuid()
// => "2QnJjKLvpSfpZqGiPPxVwWLMy2p"
// Convert to bytes
const bytes = ksuid.toBytes(id)
// Convert back to string
const str = ksuid.fromBytes(bytes)
// Extract timestamp (returns milliseconds for API consistency)
const ts = ksuid.timestamp(id)
// => 1702387456000
// Validate
ksuid.isValid(id)
// => true- import { v4 as uuidv4 } from 'uuid'
+ import { uuidv4 } from 'uniku/uuid/v4'
- import { v7 as uuidv7 } from 'uuid'
+ import { uuidv7 } from 'uniku/uuid/v7'- import { nanoid } from 'nanoid'
+ import { nanoid } from 'uniku/nanoid'API is identical — drop-in replacement.
- import { ulid } from 'ulid'
+ import { ulid } from 'uniku/ulid'API is identical — drop-in replacement.
- import { createId } from '@paralleldrive/cuid2'
+ import { cuid2 } from 'uniku/cuid2'
- const id = createId()
+ const id = cuid2()- import { KSUID } from '@owpz/ksuid'
+ import { ksuid } from 'uniku/ksuid'
- const id = KSUID.random().toString()
+ const id = ksuid()
- const bytes = KSUID.random().toBuffer()
+ const bytes = ksuid(undefined, new Uint8Array(20))
- const parsed = KSUID.parse(str)
- const timestamp = parsed.timestamp
+ const timestamp = ksuid.timestamp(str)
- const fromBuf = KSUID.fromBytes(buffer).toString()
+ const fromStr = ksuid.fromBytes(bytes)Key differences:
- uniku uses a functional API (
ksuid()) vs class-based API (KSUID.random()) - uniku uses standard
Uint8Arrayinstead of Node.jsBuffer - uniku's
timestamp()returns milliseconds (for API consistency with ulid/uuidv7) - uniku doesn't include
Sequence,CompressedSet, or sorting utilities
uuidv4(options?: UuidV4Options): string
uuidv4(options: UuidV4Options | undefined, buf: Uint8Array, offset?: number): Uint8Array
uuidv4.toBytes(id: string): Uint8Array
uuidv4.fromBytes(bytes: Uint8Array): string
uuidv4.isValid(id: unknown): id is string
uuidv4.NIL // "00000000-0000-0000-0000-000000000000"
uuidv4.MAX // "ffffffff-ffff-ffff-ffff-ffffffffffff"Options:
random?: Uint8Array— 16 bytes of random data (note: bytes at index 6 and 8 will be modified in-place)
uuidv7(options?: UuidV7Options): string
uuidv7(options: UuidV7Options | undefined, buf: Uint8Array, offset?: number): Uint8Array
uuidv7.toBytes(id: string): Uint8Array
uuidv7.fromBytes(bytes: Uint8Array): string
uuidv7.timestamp(id: string): number
uuidv7.isValid(id: unknown): id is string
uuidv7.NIL // "00000000-0000-0000-0000-000000000000"
uuidv7.MAX // "ffffffff-ffff-ffff-ffff-ffffffffffff"Options:
msecs?: number— Timestamp in milliseconds (defaults toDate.now())seq?: number— Sequence number for monotonicityrandom?: Uint8Array— 16 bytes of random data
ulid(options?: UlidOptions): string
ulid(options: UlidOptions | undefined, buf: Uint8Array, offset?: number): Uint8Array
ulid.toBytes(id: string): Uint8Array
ulid.fromBytes(bytes: Uint8Array): string
ulid.timestamp(id: string): number
ulid.isValid(id: unknown): id is string
ulid.NIL // "00000000000000000000000000"
ulid.MAX // "7ZZZZZZZZZZZZZZZZZZZZZZZZZ"Options:
msecs?: number— Timestamp in milliseconds (defaults toDate.now())random?: Uint8Array— 16 bytes of random data (only first 10 bytes used)
cuid2(options?: Cuid2Options): string
cuid2.isValid(id: unknown): id is stringOptions:
length?: number— ID length (2-32, default 24)random?: Uint8Array— Custom random bytes for testing
Note: Unlike UUID and ULID, CUID2 does not provide toBytes/fromBytes because it is a string-native format with no canonical binary representation.
nanoid(): string
nanoid(size: number): string
nanoid(options: NanoidOptions): string
nanoid.isValid(id: unknown): id is string
// Constant
URL_ALPHABET // "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"Options:
size?: number— ID length (default 21, max 2048)alphabet?: string— Custom alphabet (2-256 printable ASCII characters)random?: Uint8Array— Custom random bytes for testing
Note: isValid() only validates IDs against the default URL-safe alphabet (A-Za-z0-9_-). IDs generated with custom alphabets cannot be validated with this method.
ksuid(options?: KsuidOptions): string
ksuid(options: KsuidOptions | undefined, buf: Uint8Array, offset?: number): Uint8Array
ksuid.toBytes(id: string): Uint8Array
ksuid.fromBytes(bytes: Uint8Array): string
ksuid.timestamp(id: string): number
ksuid.isValid(id: unknown): id is string
ksuid.NIL // "000000000000000000000000000"
ksuid.MAX // "aWgEPTl1tmebfsQzFP4bxwgy80V"Options:
secs?: number— Timestamp in seconds since Unix epoch (defaults toMath.floor(Date.now() / 1000))random?: Uint8Array— 16 bytes of random data for the payload
Note: KSUID uses
secs(seconds) while UUID v7 and ULID usemsecs(milliseconds). This reflects KSUID's native second-precision timestamps.
Generate, validate, and inspect IDs from the command line with @uniku/cli:
# Install pre-built binary (macOS, Linux)
curl -fsSL https://raw.githubusercontent.com/jkomyno/uniku/main/install.sh | sh
# Or install via npm
pnpm add -g @uniku/cli
# Generate IDs
uniku uuid --version 7 --count 3
uniku nanoid --size 12 --alphabet hex
# Validate an ID (type auto-detected)
uniku validate 018e5e5c-7c8a-7000-8000-000000000000
# Inspect an ID's metadata
uniku inspect 018e5e5c-7c8a-7000-8000-000000000000See the CLI README for the full list of commands and options.
Third-party libraries that inspired this project:
- uuid: the most popular UUID library for JavaScript
- ulid: the reference ULID implementation for JavaScript
- @paralleldrive/cuid2: secure, collision-resistant IDs
- @owpz/ksuid: K-Sortable Unique Identifier
- nanoid: tiny, URL-friendly unique string ID generator
Other:
- pnpm-monorepo-template: the template I used to create this library
Hi, I'm Alberto Schiabel, aka @jkomyno. You can follow me on:
Give a star if this project helped or inspired you!
MIT — see LICENSE