< See all articles

Generate deterministic UUID v4 in JavaScript

Published on Tue Aug 27

A lot of applications use UUIDs v4 as a primary key for entities. In tests you do not want to rely on an auto-generated value. Let's see how to generate deterministic UUIDs v4 in JavaScript.

When you write your code, you want it to be as deterministic as possible. But for some parts, you just cannot. For example when you manipulate date/time, external data or auto-generated values.

Let's say you have a function that creates a new entry in the database and you want to assert that the output matches the Vitest snapshot. The first time you execute the test, it passes ✅. But the second time, it fails ❌.

Why ? Because in your code, you are probably generating the UUID v4 with a method like crypto.randomUUID.

So you basically have 2 solutions here :

  • Mock the crypto.randomUUID function 😬
  • Structure your code so you can inject other implementations of undeterministic functions 🤩

In both cases, you need a new implementation, generating UUID v4 deterministically (i.e. always identical). Here is an example of such a function :

Do not use this code in production ! Only in tests ! Do not use this code in production ! Only in tests !
import { createHash } from 'node:crypto';

let idx = 0; // Value used as the base of our hash generation

function deterministicUUIDv4() {
    idx += 1; // At each call of the function, we increment the base to get the "next" UUID v4

    const hash = createHash('sha1').update(idx.toString());
    const hashBytes = hash.digest().subarray(0, 16);

    const bytes = Array.from(hashBytes);
    bytes[6] = (bytes[6] & 0x0f) | 0x40; // We set the version to 4 (UUID v4)
    bytes[8] = (bytes[8] & 0x3f) | 0x80; // We set the variant to the correct RFC 4122 variant (see Wikipedia)

    const raw = bytes
        .map((byte) => (byte + 0x1_00).toString(16).substring(1)) // We convert byte to hex to get a unique raw string
        .join('');

    // We finally add the separators where needed to format it correctly
    return [
        raw.substring(0, 8),
        raw.substring(8, 12),
        raw.substring(12, 16),
        raw.substring(16, 20),
        raw.substring(20, 32),
    ].join('-');
}

console.log(deterministicUUIDv4()); // 356a192b-7913-404c-9457-4d18c28d46e6
console.log(deterministicUUIDv4()); // da4b9237-bacc-4df1-9c07-60cab7aec4a8
console.log(deterministicUUIDv4()); // 77de68da-ecd8-43ba-bbb5-8edb1c8e14d7

Et voilà !

Alternatively, you can use UUID v3 or v5, which are deterministic by nature.

Chafik H'nini