API contract testing: an n8n exercise before a simulated write
Practise API contract testing in n8n with synthetic responses, explicit field checks and a simulated write. An editorial exercise with proposed expected results.

Checked against the cited sources on .
Your API contract testing task and boundaries
This editorial exercise uses API contract testing to answer a narrow question: does a controlled response contain the fields your proposed write needs? Build three Code nodes named Mock response, Check contract and Simulated write. Your task is to let compatible responses reach a write marker and stop incompatible responses before it.
Prerequisites are an editable n8n development workflow and basic JavaScript. n8n documents generating mock data with Code or Edit Fields nodes. Use synthetic data only: no credentials, external API requests or real writes. The supplied documentation specifies no n8n version or edition, so it does not establish universal runtime or plan compatibility.
The fixtures, algorithm and expected results below are proposed learning material, not an executed or validated workflow. These are handwritten JavaScript assertions, not JSON Schema or a provider-verified consumer-driven contract. HTTP status handling, regression infrastructure, rollback and production deployment are outside this exercise.
Sources: n8n-docs/docs/build/work-with-data/pin-and-mock-data.md at main · n8n-io/n8n-docs · GitHub
Prepare an API contract testing example

Use an editorial envelope containing fixtureName and body. In Mock response, return one fixture per run as an array containing an object whose json property holds that envelope. n8n requires object-valued json inside its array-of-items structure; a correct wrapper alone does not validate the embedded response.
The proposed success contract requires body to be a non-null, non-array object with its own id string and amount number. Allow unrelated extra fields and do not coerce values. We think a useful practice contract should express what the consumer needs, rather than freeze every field the provider happens to return.
Use these suggested fixtures. Give each its row name as fixtureName; the table shows the body value and proposed outcome.
| Fixture | body | Expected outcome |
|---|---|---|
| baseline | {"id":"order-a","amount":12} | Write marker |
| extra-field | {"id":"order-a","amount":12,"note":"ok"} | Write marker |
| missing-id | {"amount":12} | Contract error: id |
| string-amount | {"id":"order-a","amount":"12"} | Contract error: amount |
| null-body | null | Contract error: body |
| array-body | [] | Contract error: body |
| upstream-error | {"error":{"code":"RATE_LIMITED","message":"Try later"}} | Upstream error |
| missing-code | {"error":{"message":"Try later"}} | Contract error: error.code |
| wrong-message | {"error":{"code":"RATE_LIMITED","message":12}} | Contract error: error.message |
Sources: Common issues | Nodes | n8n Docs, Object.hasOwn() - JavaScript | MDN, typeof - JavaScript | MDN
Build the contract check before the marker

Set Check contract to Run Once for All Items and read const items = $input.all(). n8n documents this mode and input accessor. Implement the following suggested algorithm, examining every item before returning anything.
- Loop over items, reading each item's json.fixtureName and json.body.
- Reject body when it is null, typeof body is not "object", or Array.isArray(body) is true.
- If Object.hasOwn(body, "error"), require error to be a non-null, non-array object. Require its own code and message properties to have typeof "string".
- For malformed error objects, throw new Error with the fixture name, "Contract error" and the failed field. For a valid error shape, throw an "Upstream error" containing the fixture name and error.code.
- Otherwise require Object.hasOwn(body, "id") and typeof body.id === "string"; require Object.hasOwn(body, "amount") and typeof body.amount === "number". Throw a descriptive contract error for either failure.
- Only after the entire loop finishes, return items. Do not return accepted items from inside the loop.
Object.hasOwn() distinguishes an own property from an absent or inherited one; presence still needs a value check. JavaScript typeof reports both null and arrays as objects, so guard against them explicitly. Confirm Object.hasOwn() is available in your runtime.
Set the validator's On Error option to Stop Workflow: n8n documents that it prevents further node execution after an error. Continue instead proceeds with the last valid data. Stopping does not undo earlier writes, which is why this exercise places its marker after validation.
Sources: skills/skills/n8n-code-nodes-official/SKILL.md at main · n8n-io/skills · GitHub, Nodeinputdata, Object.hasOwn() - JavaScript | MDN, typeof - JavaScript | MDN, throw - JavaScript | MDN, Work with nodes, n8n-docs/docs/build/understand-workflows/workflow-components/work-with-nodes.md at main · n8n-io/n8n-docs · GitHub
Run separately and record completion evidence

In Simulated write, use $input.all().map(item => ({json: {wouldWrite: true, id: item.json.body.id}})) as the returned value. This marker is solely an exercise artifact. Run the complete sequence separately for every fixture and record the observed diagnostic and whether the marker node executed.
Use the checklist below as suggested completion criteria for API contract testing. In our view, the decisive observation is whether execution reaches the marker after rejection; a readable error message alone is insufficient.
If an unexpected marker appears, inspect the validator's On Error setting and whether it throws before returning. If Code output fails, inspect the array wrapper and object-valued json. If null or arrays pass, revisit the body guards rather than relying on typeof alone.
Sources: Nodeinputdata, typeof - JavaScript | MDN, throw - JavaScript | MDN, Work with nodes, n8n-docs/docs/build/understand-workflows/workflow-components/work-with-nodes.md at main · n8n-io/n8n-docs · GitHub, Common issues | Nodes | n8n Docs
Reflect on what passing mocks establishes
For this API contract testing exercise, the suggested solution is a full-input validation pass followed by a separate marker node. Reflect on why an added field is allowed, why a well-formed upstream error still blocks the marker, and which required fields your actual consumer would need.
Passing these mocks cannot prove that a live provider still conforms. Martin Fowler's 2011 article Contract Test distinguishes testing against a double from checking whether the external service matches it. Treat your recorded results as evidence about these fixtures and this workflow configuration.
Sources: Contract Test


