If you are reaching for window.crypto or crypto.subtle in Node, the short answer is: just use it. As of Node 19 the Web Crypto API is exposed as a global, so globalThis.crypto works out of the box with no import, no shim, and no flag. The browser code you copied runs unchanged:
// Works in Node 19+ and in any browser, no import:
const id = globalThis.crypto.randomUUID();
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes);globalThis.crypto is the standard name; crypto (bare, no import) resolves to the same global in a module, the same way it does in a browser. That is the whole point of Web Crypto being on globalThis: one piece of crypto code that runs in Node, the browser, Deno, and the edge runtimes without branching.
This was not always the case, which is why so many old snippets still carry a shim. Web Crypto landed in Node 15 (October 2020) as experimental, reachable only through require('node:crypto').webcrypto or behind the --experimental-global-webcrypto flag. Node 19 (November 2022) flipped that flag on by default and graduated the API to stable, so the global is now the default. If you are on a current LTS (Node 20, 22, or 24), you are well past that line.
globalThis.crypto vs the node:crypto module
Node ships two crypto surfaces, and confusing them is the usual source of "why doesn't this method exist" errors.
node:crypto (the Node module) | globalThis.crypto (Web Crypto) | |
|---|---|---|
| How you get it | import { createHash } from "node:crypto" | global, no import |
| Style | Node-specific, mostly synchronous | W3C standard, promise-based |
| Hashing | createHash("sha256").update(x).digest() | await crypto.subtle.digest("SHA-256", x) |
| Random bytes | randomBytes(16) (Buffer) | crypto.getRandomValues(new Uint8Array(16)) |
| Portability | Node only | Node, browsers, Deno, edge |
They overlap but are not interchangeable. node:crypto is the older, broader, Node-native API (HMAC, ciphers, key pair generation, the lot), and most of it is synchronous and returns Buffers. Web Crypto's crypto.subtle is the standardized async interface that returns Promises and typed-array ArrayBuffers, and it is the one to use when the same code has to run in a browser too.
crypto.webcrypto from the module is the same object as the global, so require('node:crypto').webcrypto === globalThis.crypto holds. Reach for the explicit form only when you want to be unambiguous, or when a tool still strips globals.
The three calls you actually use
A UUID. crypto.randomUUID() returns a v4 (random) UUID string. It is the cleanest way to mint an ID, no dependency required:
const orderId = crypto.randomUUID();
// "6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b"Random bytes. crypto.getRandomValues() fills a typed array with cryptographically strong random values in place (and returns the same array):
function te_token(len = 32) {
const buf = crypto.getRandomValues(new Uint8Array(len));
return Buffer.from(buf).toString("hex");
}Note getRandomValues mutates the array you pass and caps at 65,536 bytes per call. If you only need bytes and are Node-only, node:crypto's randomBytes(len) is shorter and has no size cap.
A hash. crypto.subtle.digest() is async and works on bytes, not strings, so encode first:
async function te_sha256(text) {
const data = new TextEncoder().encode(text);
const hash = await crypto.subtle.digest("SHA-256", data);
return [...new Uint8Array(hash)]
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}The Node-native one-liner for the same result is createHash("sha256").update(text).digest("hex"). Use the subtle version when portability matters; use the module version when you are staying in Node and want it synchronous.
When you still need a shim
The global is there in modern Node, so the only reason to shim is an environment that hides it:
- Old runtimes. Anything before Node 15 has no Web Crypto at all; Node 15 to 18 has it but only as a global behind the flag (or via
crypto.webcrypto). If you must support those, set the global yourself:globalThis.crypto ??= require("node:crypto").webcryptoin CommonJS, orimport { webcrypto } from "node:crypto"; globalThis.crypto ??= webcryptoin an ES module. - jsdom test environments. This is the common modern case. Jest's
jsdomtest environment provides awindowbut historically did not wire upcrypto.subtle, so a component that callscrypto.randomUUID()blows up under test even though it works in the real browser and in plain Node. The fix is the same one-liner in your test setup file:
// jest.setup.js, give jsdom the real Web Crypto
import { webcrypto } from "node:crypto";
if (!globalThis.crypto) globalThis.crypto = webcrypto;That is the whole shim. Avoid the crypto-browserify / crypto-js polyfills for this: they predate native Web Crypto and you do not need them on any supported Node.
A last gotcha worth naming: Web Crypto requires a secure context in the browser (HTTPS or localhost), so crypto.subtle is undefined on a plain-HTTP page even though crypto.randomUUID() works. That constraint does not apply in Node, but it bites when the same code runs in both places.
See also
- JavaScript Promises explained: a complete guide:
crypto.subtlereturns promises, so theawaitpatterns and pitfalls there apply directly. - Read environment variables in Node.js without dotenv: keep secrets and keys out of the source tree, the natural companion to doing crypto in Node.
Sources
Authoritative references this article was fact-checked against.
- Web Crypto API — Node.js documentation (official)nodejs.org
- Crypto — Node.js documentation (official)nodejs.org
- Web Crypto API — MDNdeveloper.mozilla.org
- Crypto.randomUUID() — MDNdeveloper.mozilla.org





