← Blog

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

3 min read

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_id vs userId vs nested user.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, optional relationships).
  • 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:

  1. Envelope — e.g. always { data: ..., meta: ... } for collections and { data: ... } for singles.
  2. Errors — always { errors: [{ field, message }] } or a single agreed shape.
  3. Pagination — same keys everywhere: page, per_page, total, total_pages (you already use this pattern in many Rails apps).
  4. Naming — pick snake_case (Rails-native) or camelCase (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 index and show.

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?