JSON:API vs Custom JSON in Rails: Consistency for React Clients

JSON:API vs Custom JSON in Rails: Consistency for React Clients
When your frontend grows, inconsistent APIs become a bigger bottleneck than raw performance.
After working on several Rails + React systems, one problem keeps showing up:
Inconsistent JSON.
It starts innocent: one endpoint returns { user: { name } }, another returns { name } on the root, a third nests everything under data. Each screen works—until a second or third React app depends on the same API. Then every change becomes a coordination exercise, and “quick fixes” turn into permanent glue code.
This post is about choosing a shape and sticking to it—whether that’s JSON:API-style discipline or a small, documented custom contract—and how to keep Rails honest as the team grows.
Why ad hoc JSON stops scaling
Custom JSON per endpoint feels fast in week one:
- You shape each response for the exact screen you’re building.
- You avoid “ceremony” from serializers or hypermedia.
By month six you usually have:
- Field name drift (
user_idvsuserIdvs nesteduser.id). - Null vs missing keys handled differently in each React hook.
- Pagination that works on list A but not list B.
- Errors as strings in one place, objects in another.
React clients don’t care which style you pick—they care that the rules are predictable and tests can lock them in.
Path A: JSON:API-style discipline
JSON:API (the spec) is heavier than many teams need end-to-end, but JSON:API-shaped responses buy you a lot without adopting every feature:
- Stable resource objects (
type,id,attributes, optionalrelationships). - A clear place for meta (pagination, totals).
- A clear pattern for compound documents (include related resources in one round trip).
In Rails, you might get there with:
jsonapi-serializer(formerly fast_jsonapi),blueprinter,alba, or a thin wrapper you own.- Integration tests that assert keys and types for each public endpoint—not only status codes.
Trade-off: More structure up front; less arguing later about “what does this endpoint return?”
Path B: pragmatic custom JSON (with contracts)
If full JSON:API feels like overkill, a small internal standard still helps:
- Envelope — e.g. always
{ data: ..., meta: ... }for collections and{ data: ... }for singles. - Errors — always
{ errors: [{ field, message }] }or a single agreed shape. - Pagination — same keys everywhere:
page,per_page,total,total_pages(you already use this pattern in many Rails apps). - Naming — pick
snake_case(Rails-native) orcamelCase(JS-native) and transform at the boundary once, not per endpoint.
Document the rules in a short internal doc or OpenAPI—even a minimal schema beats tribal knowledge.
What I optimize for in reviews
When I review Rails APIs that feed React:
- Can a new developer guess the next endpoint’s shape from the last one?
- Are list and show responses consistent (same resource representation, not a random subset of fields)?
- Do we test the JSON, not only
response.successful?? - Are N+1 and over-fetching addressed without breaking the contract (includes, sparse fieldsets, or dedicated “summary” types)?
JSON:API vs custom isn’t a religious choice—it’s whether you want the spec to be the referee or you’re willing to be the referee yourself. Either works; only winging it doesn’t, once multiple clients depend on you.
Next steps for your codebase
- Pick one approach (JSON:API-shaped vs documented custom) for new public endpoints.
- Add one serializer layer or presenter pattern so controllers stop hand-assembling hashes.
- Add one request spec per resource that snapshots allowed keys (or a JSON schema) for
indexandshow.
Small consistency wins compound—your future React you will thank you.
What’s your team using today—spec-heavy JSON:API, a thin custom envelope, or something in between?