DEV Community

Sammit Pal
Sammit Pal

Posted on

genkode — Random ID & String Generator for Node.js

Generating random strings comes up constantly in backend work — session tokens, invoice numbers, API keys, temporary codes, test data. Most of the time you end up copy-pasting the same Math.random().toString(36) snippet or reaching for a heavy library when you only need one function.

I built genkode to fix that for myself, and after using it across a few projects I cleaned it up and published it. Zero dependencies, full TypeScript support, and a small focused API that handles the cases I kept running into.

Here's what it does and how I built it.


Installation

npm install genkode
Enter fullscreen mode Exit fullscreen mode

The basics

The main export is generateCode. At its simplest:

import { generateCode } from 'genkode';

generateCode({ length: 12 });
// → 'rqfvYxJRWfoP'  (alpha by default)

generateCode({ length: 12, type: "alphanumeric" });
// → 'aZ8kL2pQ9xW1'

generateCode({ length: 12, type: "numeric" });
// → '362128126198'
Enter fullscreen mode Exit fullscreen mode

Three types: alpha, numeric, alphanumeric. That covers most cases without any configuration overhead.


Cryptographically secure mode

By default the library uses Math.random — fast, but not suitable for anything security-sensitive. Set secure: true and it switches to crypto.getRandomValues from the Web Crypto API:

generateCode({ length: 32, type: "alphanumeric", secure: true });
// → 'Tz3mW8qA1nXpKj7rBv2cYs9dLf4mNe6h'
Enter fullscreen mode Exit fullscreen mode

There's an important subtlety here though: naively doing byte % charsetLength introduces modulo bias — characters at the start of your charset get picked slightly more often than characters at the end when 256 doesn't divide evenly by your charset size.

genkode uses rejection sampling to eliminate this. Any random byte that would cause bias is discarded and a new one is fetched. The result is a perfectly uniform distribution across the charset.

// Internal logic (simplified)
const threshold = Math.floor(256 / max) * max;
if (byte < threshold) {
  result.push(charset[byte % max]);
}
// Otherwise discard and try again
Enter fullscreen mode Exit fullscreen mode

Performance: buffered generation

Both standard and secure modes use buffered byte generation rather than calling crypto.getRandomValues once per character. The buffer is sized to the request (capped at 65,536 bytes, which is Node's hard limit for a single getRandomValues call) and refilled in chunks as needed.

For most use cases this is invisible, but for high-volume batch generation it makes a meaningful difference.


Prefix and suffix

A pattern I kept needing: codes with a static prefix that identify their type at a glance — INV- for invoices, USR- for users, TKN- for tokens.

generateCode({ length: 8, prefix: "INV-" });
// → 'INV-rqfvYxJR'

generateCode({ length: 8, prefix: "ORD-", suffix: "-2025" });
// → 'ORD-rqfvYxJR-2025'
Enter fullscreen mode Exit fullscreen mode

The length always refers to the generated segment — not the total string length including affixes. So INV- + 8 chars = 12 characters total.


Batch generation

Pass count and you get an array back instead of a single string. The return type is overloaded in TypeScript so you get correct inference automatically:

generateCode({ length: 10, count: 5 });
// → ['rqfvYxJRWf', 'TzAmW8qAnX', 'pLkQmNbVcD', 'HgFsJwKyRt', 'MnPxZuCvBe']
Enter fullscreen mode Exit fullscreen mode

Add unique: true and the library guarantees no duplicates in the batch. If you ask for more unique codes than the charset and length combination can produce, it throws a KodeError upfront rather than looping forever:

generateCode({ length: 10, count: 1000, unique: true });
// → string[] with 1000 guaranteed-unique codes

generateCode({ length: 1, type: "numeric", count: 11, unique: true });
// → throws KodeError: cannot generate 11 unique codes from charset of size 10
Enter fullscreen mode Exit fullscreen mode

Short ID mode

The shortId flag switches to a reduced charset that strips visually ambiguous characters — 0, O, 1, l, I. These are the characters that cause support tickets when users misread a code from a screen or printout.

generateCode({ length: 12, shortId: true });
// → '3Ks9mBx4nPqR'  — no 0/O/1/l/I
Enter fullscreen mode Exit fullscreen mode

The charset is 23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ — 54 characters, still high entropy, fully URL-safe.


Case control

Sometimes you need all uppercase (licence keys, voucher codes) or all lowercase (slugs, usernames). The case option handles this at the charset level rather than post-processing:

generateCode({ length: 12, case: "upper" });
// → 'RQFVYXJRWFOP'

generateCode({ length: 12, case: "lower" });
// → 'rqfvyxjrwfop'

generateCode({ length: 12, type: "alphanumeric", case: "upper" });
// → 'AZ8KL2PQ9XW1'
Enter fullscreen mode Exit fullscreen mode

It composes cleanly with everything else:

// Human-readable voucher code: no ambiguous chars, all caps
generateCode({ length: 8, shortId: true, case: "upper", prefix: "SALE-" });
// → 'SALE-3KS9MBXN'
Enter fullscreen mode Exit fullscreen mode

Error handling

Rather than silently producing garbage for invalid inputs, genkode throws a typed KodeError:

import { generateCode, KodeError } from 'genkode';

try {
  generateCode({ length: 0 });
} catch (err) {
  if (err instanceof KodeError) {
    console.error(err.message);
    // → 'length must be at least 1, got: 0'
  }
}
Enter fullscreen mode Exit fullscreen mode

It validates that length and count are integers, that length is between 1 and 100,000, and that unique batch requests are actually satisfiable.


Quick reference

Option Type Default Description
length number Required. Length of the generated segment (1–100,000)
type "alpha" "numeric" "alphanumeric"
case "upper" "lower"
secure boolean false Cryptographically secure output
prefix string "" Static prefix
suffix string "" Static suffix
count number Batch size — returns string[]
unique boolean false Guarantee uniqueness within batch
shortId boolean false Exclude ambiguous characters

Real-world examples

// API key
generateCode({ length: 32, type: "alphanumeric", secure: true, prefix: "sk_live_" });
// → 'sk_live_Tz3mW8qA1nXpKj7rBv2cYs9dLf4m'

// Invoice number
generateCode({ length: 8, type: "numeric", prefix: "INV-" });
// → 'INV-36212812'

// Voucher code (human-readable, no ambiguous chars)
generateCode({ length: 8, shortId: true, case: "upper", prefix: "SALE-" });
// → 'SALE-3KS9MBXN'

// Seed 100 unique test user IDs
generateCode({ length: 12, count: 100, unique: true, prefix: "user_" });
// → ['user_rqfvYxJRWfoP', ...]

// Short session token
generateCode({ length: 24, type: "alphanumeric", secure: true });
// → 'Tz3mW8qA1nXpKj7rBv2cYs9d'
Enter fullscreen mode Exit fullscreen mode

Links


If you find it useful or have a feature in mind, feedback is welcome. Happy to discuss the rejection sampling approach or the buffering strategy in the comments too.

Top comments (0)