skip to content

You need a SHA-256 hash of a cookie value inside `middleware.ts` in a Next.js app. Why does `import { createHash } from 'node:crypto'` not work there, and what do you use instead?

level: juniorimportance: should knowfreq 46%

answer

  1. no Node modules in this runtime
  2. the standard is already a global
  3. you do not import it
  4. subtle.digest returns a promise of bytes
  5. hex formatting is on you

basics

~20 s

Node's crypto module is not part of the Edge Runtime that Next.js middleware runs in. Use the Web Crypto global instead: encode the string with TextEncoder and await crypto.subtle.digest('SHA-256', bytes), then format the resulting ArrayBuffer yourself.

solid answer

~40 s

Middleware runs in the Edge Runtime, which exposes Web-platform APIs rather than Node's module set, so `node:crypto` and its `createHash` are not there. The replacement is Web Crypto, available as the global `crypto` with no import at all: `await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value))`. Two practical differences trip people up. It is asynchronous — it returns a promise of an `ArrayBuffer`, so your middleware function has to be `async`. And it gives you raw bytes, not a hex string; you convert with something like `[...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join('')`. The same global also provides `crypto.randomUUID()` and `crypto.getRandomValues()`. Password hashing is a different matter: bcrypt and argon2 bindings are native addons and cannot load in this runtime at all, so that work belongs on a Node surface.

code

typescript · 14 lines
typescript
import { NextResponse, type NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  const token = request.cookies.get('session')?.value ?? ''
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(token))
  const hex = [...new Uint8Array(digest)]
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('')

  if (hex === process.env.REVOKED_SESSION_HASH) {
    return new NextResponse(null, { status: 401 })
  }
  return NextResponse.next()
}

go deeper

for a junior

Know that the global crypto object exists in middleware with no import, and that crypto.subtle.digest is the SHA-256 entry point. Say out loud that Node's createHash is not available there.

for a middle

Explain the three concrete porting differences — async instead of sync, bytes in and out instead of strings, and a narrower API — and show the TextEncoder-plus-hex conversion without hesitating.

for a senior

Demonstrate judgment about what cryptographic work belongs on this path at all: cheap digests and signature checks yes, deliberately slow password hashing no, and pick libraries by which runtime primitives they were built on.

for a principal

Own the policy question of where cryptographic material and verification live across tiers, so that key handling is not duplicated between an edge path and a Node path with two different libraries and two different failure modes.

## The runtime, not the package, is the problem `middleware.ts` is bundled for Next's Edge Runtime, whose globals are Web-platform APIs. Node's module namespace — `node:crypto` among them — is simply not part of that environment, so `createHash` has nothing to resolve to and the build reports an unsupported Node module. No polyfill or bundler alias fixes this honestly, because Node's hashing is backed by native bindings the runtime does not host. ## Web Crypto is the replacement, and it is a global The Edge Runtime exposes the standard `crypto` object, the same one browsers and other Web-standard runtimes provide. You do not import it: ```ts const bytes = new TextEncoder().encode('hello') const digest = await crypto.subtle.digest('SHA-256', bytes) ``` `crypto.subtle.digest` accepts an algorithm name — `'SHA-1'`, `'SHA-256'`, `'SHA-384'`, `'SHA-512'` — and a `BufferSource`. It returns a promise resolving to an `ArrayBuffer`. ## The three differences from Node's API **It is async.** `createHash('sha256').update(x).digest('hex')` is synchronous and chainable; `subtle.digest` is not. Your middleware function must be declared `async` and you must `await` the digest. This is the single most common porting mistake — code that logs `[object Promise]` or compares a promise to a string. **It takes and returns bytes, not strings.** Node lets you feed a string and ask for `'hex'` or `'base64'` out. Web Crypto takes a `BufferSource` in and hands an `ArrayBuffer` back, so you encode on the way in with `TextEncoder` and format on the way out yourself: ```ts const hex = [...new Uint8Array(digest)] .map((b) => b.toString(16).padStart(2, '0')) .join('') ``` For base64url — the encoding tokens usually want — combine `btoa` with `String.fromCharCode`, or reach for a library built on Web Crypto. **Its surface is narrower.** Web Crypto covers digests, HMAC, symmetric and asymmetric encryption, signing and key derivation through `crypto.subtle.importKey`, `sign`, `verify`, `encrypt`, `decrypt` and `deriveBits`. It does not cover Node's grab-bag of conveniences, and it deliberately does not cover password hashing. ## What the same global gives you for free Beyond `subtle`, the Edge Runtime's `crypto` provides `crypto.randomUUID()` for request identifiers and `crypto.getRandomValues()` for filling a typed array with cryptographically strong random bytes. Both are synchronous and both are frequently all a piece of middleware actually needs. ## Where the line is Hashing a cookie value to compare against a denylist, computing an HMAC to check that a header was not tampered with, verifying a signed token — all of these are Web Crypto work and belong in middleware if they belong anywhere. Password verification is not: bcrypt and argon2 ship native addons, they cannot load in this runtime, and even if they could, their whole design goal is to burn CPU, which is the last thing you want on a code path that runs before every matched request. That work belongs on a Node surface such as a route handler or Server Action. ## Library choice follows the same rule When you need JWT verification rather than a raw digest, pick a library written against Web Crypto — `jose` is the common choice — rather than one written against Node's `crypto` module, such as `jsonwebtoken`. The API you are calling matters less than which runtime primitives the library was built on.

  • Your middleware compares the digest to a stored string and the comparison never matches, though the input is right. What is the usual bug?
    Forgetting to await. `crypto.subtle.digest` returns a promise, so the comparison runs against a `Promise` object rather than the hash. The second most common cause is comparing raw bytes to a hex string — the `ArrayBuffer` has to be converted before it can equal anything you stored.
  • Would you verify a JWT in Next.js middleware, and what would you use?
    Signature verification is reasonable there: it is pure computation over the request, needs no database, and Web Crypto supports it. Use a library built on Web Crypto, such as `jose`, rather than one built on Node's `crypto` module, such as `jsonwebtoken` — the latter cannot load in the Edge Runtime at all.
  • Is `crypto.randomUUID()` available in middleware, and what would you use it for?
    Yes — it is part of the same Web Crypto global and is synchronous. The common use is minting a request or correlation identifier early in the request lifecycle so downstream logs can be tied together, which is a natural fit for middleware because it runs before the route does.

saying these in an interview costs you the question

  • Says you can shim node:crypto with a bundler alias
  • Treats crypto.subtle.digest as synchronous
  • Expects a hex string back instead of an ArrayBuffer
  • Proposes bcrypt password hashing inside middleware
  • Thinks Web Crypto is a weaker or non-standard substitute

context