API Platform 4 Dropped the hydra: Prefix, and Every List in My CRM Went Empty

By Lars Nielsen ·

I run a small CRM for myself called CaRMen — a Symfony 7 API Platform backend with a React frontend. In August I had to bump api-platform/core from ^3.3 to ^4.0, not because I wanted the new major version, but because composer's own security-advisory audit flagged every 3.3.x and 3.4.x release in a row. There was no patched 3.x left to stay on.

The mechanical part of the upgrade went fine. Two config flags that only existed as a 3.x-to-4.x bridge, event_listeners_backward_compatibility_layer and keep_legacy_inflector, were gone from api_platform.yaml because the thing they bridged no longer existed either. I committed the resolved composer.lock alongside the version bump, since I'd rather pin the exact Symfony 7.4.* chain that composer picked than let it re-solve on the next install and maybe land somewhere slightly different.

What actually broke

API Platform 4 dropped the hydra: prefix from JSON-LD collection and error responses. hydra:member became member, hydra:totalItems became totalItems, hydra:description became description. That's a one-line summary of a change that took down every list, board and dropdown in the frontend at once, because every one of them was still reading the old keys.

Nothing crashed. There was no error in the console, no failed request, no red banner. Each component asked a now-renamed property for an array, got undefined, and rendered whatever it does for an empty list: a blank table, an empty Kanban board, a dropdown with nothing in it. The API was answering every request with a 200 and real data; the frontend just couldn't find it anymore, and "I couldn't find it" and "there's nothing here" look identical to a user staring at an empty page.

The fix was mechanical once I knew what to look for. The shared response type went from this:

  • 'hydra:member': T[]
  • 'hydra:totalItems': number

to this:

  • member: T[]
  • totalItems: number

and every call site that destructured the old bracketed keys — the Kanban board's drag-and-drop optimistic update, the deal and contact list pages, the account detail view — had to be updated to match. TypeScript didn't catch any of this in advance. The interface itself was the thing that was wrong; the compiler was perfectly happy agreeing with a shape that no longer matched what the API actually sent, because nothing type-checks the wire format against a live server at compile time. A type that's wrong about the outside world still type-checks.

The tests agreed with the bug for twenty minutes

The backend's own controller tests — AccountTest, ActivityTest, ContactTest, DealTest — were still reading hydra:member and hydra:totalItems after the upgrade. Against the new responses those keys don't exist, so run against the upgraded API these tests could only fail: a count assertion compared against a missing key, and IRIs looked up in a list that was no longer there. The frontend went quiet; the backend tests didn't. I fixed all four files to read member and totalItems instead, in a follow-up commit about twenty minutes after the upgrade.

One more deprecation, caught the same evening

While I was in there, API Platform's test harness warned that ApiTestCase::createClient() currently always boots the Symfony kernel, but won't by default once API Platform 5 ships. My test helpers — the ones that log in as an admin, a sales user, or a plain user before each test — all rely on the kernel being booted to work at all, so I pinned static::$alwaysBootKernel = true explicitly rather than let a future upgrade silently change that default out from under me. Small, but it's the same shape as the main bug: a default changing underneath code that was quietly depending on it.

What I'd actually do differently

Nothing about this upgrade was unreasonable on API Platform's part — renaming a JSON-LD convention in a major version is a legitimate thing to do, and it's documented as a breaking change, not a bug. The part worth remembering is narrower: a frontend and a backend that agree on a shared TypeScript interface are only as honest as the last time someone checked that interface against a real response body. The type system will never tell you the interface is wrong, because from its own point of view, it isn't. Only the running list or board going quietly empty tells you that.

If I build another full-stack project on this stack again, one smoke test that pulls a real paginated collection and asserts on its actual shape, not a mocked one, would have caught this upgrade in CI instead of in the browser.

Filed under

Leave a comment

Comments are reviewed before they appear.

Basic HTML allowed: links, emphasis, code and lists. Web addresses become links automatically.