DEV Community

Jeff
Jeff

Posted on Originally published at powerduck.com

Your API Returned 200. Your Integration Is Still Broken.

The request is green. The dashboard is blank.

Here is a hypothetical response from an account endpoint:

{
  "id": "acct_42",
  "displayName": null,
  "plan": "free"
}
Enter fullscreen mode Exit fullscreen mode

The server returned 200 OK. The frontend called displayName.trim().

Both teams can point to something that worked. Neither team has a working integration.

A successful HTTP request is only one part of a successful API interaction. The other part is whether the response means what the caller expects.

Disclosure: this article was prepared for the Powerduck team. The checks below are tool-independent.

1. Test the shape you promised

Start with the fields your consumer actually uses. In OpenAPI 3.1, this schema requires an account ID and explicitly allows a nullable display name:

type: object
required: [id, displayName]
properties:
  id:
    type: string
  displayName:
    type: [string, "null"]
  plan:
    type: string
Enter fullscreen mode Exit fullscreen mode

Required and non-null are separate decisions. A field listed under properties is not automatically required, and a missing property differs from a property whose value is null. The JSON Schema object reference explains these distinctions.

Now the frontend has an explicit decision to make: show a fallback name, hide the label, or reject this state. That decision should not depend on which test account someone happened to use.

2. Keep an example that makes the UI uncomfortable

A beautiful example is often a weak test fixture:

{"id":"acct_42","displayName":"Ada","plan":"pro"}
Enter fullscreen mode Exit fullscreen mode

It exercises the easiest path. Keep that example, then add the nullable version. If the contract allows omission, add a missing-field example too.

For a list endpoint, try an empty result. For a display name, try a long string. For pagination, try the last page. For a permission-dependent field, compare callers with different access levels.

Do not generate edge cases at random. Pick states the application can actually produce, then connect each fixture to a documented rule.

An example demonstrates one possibility. It does not establish every possibility.

3. Check what arrived before parsing it

The following is a small Node.js check for a hypothetical local service. It makes no claim to replace a schema validator:

import assert from "node:assert/strict";

const response = await fetch("http://localhost:3000/accounts/acct_42");
assert.equal(response.status, 200);

const mediaType = (response.headers.get("content-type") ?? "")
  .split(";", 1)[0].trim().toLowerCase();
assert.equal(mediaType, "application/json");

const body = await response.json();
assert.ok(body !== null && typeof body === "object");
assert.equal(Array.isArray(body), false);
assert.equal(typeof body.id, "string");
assert.equal(Object.hasOwn(body, "displayName"), true);
assert.ok(body.displayName === null || typeof body.displayName === "string");
Enter fullscreen mode Exit fullscreen mode

Run it against a seeded development or test environment. The point is to turn an assumption into a failure you can reproduce.

For a larger API, derive validation from the OpenAPI document instead of maintaining hundreds of hand-written assertions. Keep business assertions alongside it: a response can match its schema and still describe the wrong account.

4. Give failures their own contract

Try the request with an invalid ID, without credentials, and with a caller who lacks permission.

You are checking two things: whether the service refuses the operation appropriately, and whether the consumer can understand that refusal.

A generic “request failed” message is sometimes all the user should see. Your client code may still need a stable error identifier to distinguish an expired session from a validation problem.

Write down the failures your implementation intentionally supports. Avoid adding an impressive list of status codes that the server never returns.

The OpenAPI Response Object lets you describe response content and headers. Use that space for real success and error behavior, not just a placeholder success response.

5. Decide whether the implementation or the document is wrong

When a check fails, updating the schema to match the latest payload is tempting. Sometimes that is the right fix. Sometimes it quietly converts a regression into a promise.

Ask three questions before changing either side:

  1. What behavior did existing consumers rely on?
  2. Was this change intentional?
  3. Which test will prevent the disagreement from returning?

The answer might be a server fix, a corrected contract, or a coordinated migration. “Make the warning disappear” is not enough information to choose.

Try this on one endpoint

Pick an endpoint your frontend uses every day. Save its normal response, one valid edge case, and one expected failure. Compare all three with the contract. Add a regression check for the first disagreement you find.

Keep the document, the request, and the regression check connected. A correction should reach the next person reading or calling the API. It still takes engineering judgment to decide what the contract should promise.

What is the smallest response change that has broken one of your integrations? A missing field, a new enum value, or something less obvious?

Top comments (0)