TechEarl

Using Web Crypto (globalThis.crypto) in Node.js

globalThis.crypto (the Web Crypto API) is a global in Node 19+ with no shim or flag: crypto.randomUUID(), getRandomValues(), and crypto.subtle. How it differs from the node:crypto module, and when you still need a shim.

Ishan Karunaratne⏱️ 6 min readUpdated
Share thisCopied
Use the Web Crypto API in Node.js through globalThis.crypto: randomUUID, getRandomValues, and crypto.subtle, available globally in Node 19+ with no shim.

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:

javascript
// 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 itimport { createHash } from "node:crypto"global, no import
StyleNode-specific, mostly synchronousW3C standard, promise-based
HashingcreateHash("sha256").update(x).digest()await crypto.subtle.digest("SHA-256", x)
Random bytesrandomBytes(16) (Buffer)crypto.getRandomValues(new Uint8Array(16))
PortabilityNode onlyNode, 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:

javascript
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):

javascript
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:

javascript
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").webcrypto in CommonJS, or import { webcrypto } from "node:crypto"; globalThis.crypto ??= webcrypto in an ES module.
  • jsdom test environments. This is the common modern case. Jest's jsdom test environment provides a window but historically did not wire up crypto.subtle, so a component that calls crypto.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:
javascript
// 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

Sources

Authoritative references this article was fact-checked against.

TagsNode.jsWeb CryptoglobalThis.cryptocrypto.subtlerandomUUIDgetRandomValuesJavaScript

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

LUKS Disk Encryption With cryptsetup

Set up LUKS disk encryption with cryptsetup: format a block device, open it, put a filesystem on it, and mount it. The four commands, plus the header backup nobody warns you about.

Using Claude CLI to Manage WordPress Sites

How I use Claude CLI to run WordPress and ACF work end-to-end: ACF field group generation, WP-CLI orchestration, log triage, plugin debugging, bulk content ops. Concrete prompts, what it gets wrong, and where it fits in an agency workflow.

Fix NODE_MODULE_VERSION Mismatch in Node.js

The NODE_MODULE_VERSION mismatch error means a native addon was compiled against a different Node ABI than the one now running it. Here is what the numbers mean, the one-line fix, and the Electron, Docker, and CI variants that catch people out.