The earlier worked example on this page imported verifyWebhook from @example/webhooks and celebrated a successful event without supplying the dependency or a command to start the server. Copy that import into a clean directory and the whole performance falls over before a request reaches the handler.

I don't call a page a tutorial because it has numbered steps and cheerful code blocks. A tutorial has to carry one reader from a stated starting point to a result they can see, then survive the obvious ways that path can break.

I use that complete path as the outline. The synthetic fixture below accepts a signed event, rejects altered bytes, and supplies the full source for a clean run.

Define a reader and a result you can test

Write the starting state before the steps. For this example, the reader can run JavaScript from a terminal and wants to understand what a webhook tutorial must supply.

Requirement Check If it fails
Node.js 24.18.0, the recorded test version node --version Use the recorded version before comparing results
An extracted copy of the fixture Confirm service.mjs, webhook.mjs, and check.mjs exist Extract the complete archive
A successful event and a rejected signature Run node check.mjs Inspect the failing assertion before describing the example as runnable

The finish line is deliberately bounded: accept an event signed with the fixture's public test value, reject altered bytes, and confirm rejected events were not recorded. Calling this a production webhook system would turn a useful teaching fixture into bullshit.

If the reader only needs a signature field definition, give them reference documentation instead of this build sequence. The documentation types guide separates those jobs.

Build the tutorial around observable checkpoints

The fixture uses Node's built-in HTTP and cryptography modules to compute a lowercase hexadecimal HMAC-SHA256 digest over the exact request bytes. Removing the unexplained package makes the example self-contained, but also makes the protocol our responsibility to describe.

Step 1: Create the project from the complete fixture

Download the writing lab, extract it into an empty directory, and open the writing-lab folder in a terminal. The archive contains the implementation and its checks, with no npm dependencies.

node --version
node check.mjs

The recorded runtime is v24.18.0. The check ends with PASS and closes the servers it creates.

If a file cannot be imported, compare the extracted directory with the archive before installing packages guessed from an error message. Reproduce the earlier omission separately in an empty directory:

node --input-type=module -e 'import { verifyWebhook } from "@example/webhooks"'

The recorded ERR_MODULE_NOT_FOUND establishes a missing dependency path in the supplied instructions, not a claim about registry availability. Replacing one unexplained import with another would leave the tutorial's problem intact.

Step 2: Keep the bytes attached to the signature

The complete verifier lives in webhook.mjs. The public value belongs only to this local example.

import { createHmac, timingSafeEqual } from 'node:crypto';

export const fixtureSecret = 'local-writing-example';
export function sign(body) {
  return createHmac('sha256', fixtureSecret).update(body).digest('hex');
}
export function verify(body, signature) {
  if (typeof signature !== 'string' || !/^[a-f0-9]{64}$/.test(signature)) return false;
  return timingSafeEqual(Buffer.from(sign(body), 'hex'), Buffer.from(signature, 'hex'));
}

The format check ensures both buffers passed to timingSafeEqual have the same length. Node's cryptography reference requires equal byte lengths and cautions that this function alone does not make surrounding code timing-safe.

The handler in service.mjs verifies the received buffers before parsing JSON, because parsing and reserializing can change the bytes. The altered-body check makes the consequence visible: a trailing space changes the signature even though the JSON value remains equivalent.

Step 3: Inspect success and failure separately

The check sends a valid event, then changes its signature and body in separate requests. The recorded webhook output is:

valid signature: 204
wrong, malformed, missing signatures: 401
body changed after signing: 401
signed invalid JSON: 400

The assertions require exactly one recorded event after those requests. An HTTP 204 by itself would establish only that the handler returned success.

If an invalid signature returns 204, stop: the example has not demonstrated rejection. If signed invalid JSON returns 204, the validation path disagrees with the stated event contract.

Step 4: Start and stop the manual server

To inspect the service independently, run:

node service.mjs

Use the printed loopback URL in a second terminal. The port below is only an example:

export LAB_URL=http://127.0.0.1:49152
curl --fail-with-body "$LAB_URL/health"

Expect {"status":"ok"}. For connection refusal, correct the URL or start the server before sending another request.

Press Ctrl-C in the server terminal when finished. Restarting clears the server's in-memory state.

Separate a teaching fixture from a production promise

Anyone can compute a signature using the fixture's public value, so the example demonstrates byte verification instead of protection from someone who knows that value. Hiding that boundary would make the tutorial look stronger and teach the reader less.

Production question What this fixture establishes What remains unresolved
Can a changed body pass the original signature? The tested changed-body request returns HTTP 401 A complete security review
Can a valid event be replayed? Repeated valid requests are accepted Timestamp policy and duplicate handling
Is an accepted event durable? It is appended to an in-memory array Storage and crash recovery
Does the code follow a vendor protocol? It follows the protocol defined here The actual provider's headers and signing rules

The Node HTTP reference documents the request and response primitives used by the fixture. A vendor tutorial needs the provider's documented signing protocol and real integration checks before it can replace this local example.

Review the rendered path before publishing

I keep the review focused on what the next instruction needs: run the archive from a new directory and compare the rendered code with the supplied files. Record the runtime and raw output with the revision so another reviewer can repeat the check.

Review question Evidence to retain Release decision
Is every imported module supplied or installed? Clean-directory command output Hold if dependency resolution fails
Can the reader start and stop the service? The printed URL and health response Repair the missing command or prerequisite
Does the failure case actually fail? Response status and stored-state assertion Hold if rejected input changes accepted state
Does the prose exceed the test? A claim beside its assertion Narrow the claim or add a relevant test
Can an intended reader finish without help? Their actual task log Do not infer usability from the author's successful run

The recorded checks passed on Node.js 24.18.0 on 10 September 2026. No human usability study was conducted.

The documentation review checklist provides the wider release review; developer onboarding documentation applies the same prerequisite discipline to a contributor's first task.