A JSON API can be launched in an afternoon and regretted for years. The syntax is rarely the problem. Most long-term pain comes from choices that seemed harmless at the beginning: inconsistent field names, ambiguous nulls, identifiers encoded as numbers, response shapes that change between endpoints, and clients that depend on undocumented behavior. A durable API treats every JSON document as a public contract, even when the first consumer belongs to the same team.
Start with resources and actions, not database tables
Database schemas are optimized for storage and relationships. API payloads are consumed by people and programs trying to complete tasks. Exposing tables directly often leaks internal naming, join structures, and fields that should remain private. It also makes database refactoring unnecessarily dangerous because clients become coupled to storage decisions.
Design responses around the concepts clients understand. An order response can include a concise customer summary without reproducing every customer column. A payment action can return its result and relevant next step rather than exposing an internal transaction row. The API should be stable even when the implementation behind it changes.
Make types boring and explicit
Predictability is more valuable than compactness. Keep identifiers as strings when clients should treat them as opaque values. Represent timestamps in a documented ISO 8601 format with a timezone. Decide whether monetary values use integer minor units or decimal strings and apply the decision everywhere. Avoid fields that are sometimes an object, sometimes an array, and sometimes a boolean.
Null deserves special attention. A missing property may mean “not requested,” while a property set to null may mean “known to have no value.” If that distinction matters, document it. If it does not, choose one representation consistently. Clients become complicated when they must guess what absence means on every endpoint.
Choose naming conventions once
Whether an API uses camelCase or snake_case matters less than using one convention consistently. The same principle applies to pagination fields, embedded relationships, error objects, and date names. Consumers learn patterns and build reusable helpers around them. An API that invents a new shape for each endpoint forces clients to write defensive code instead.
Names should describe meaning rather than presentation. A field named display_name clearly communicates that it is prepared for display. A field named value says almost nothing. Avoid abbreviations that make sense only inside the producing team, because internal context disappears when an API spreads to other services or external users.
Add fields freely; change meanings carefully
Many JSON clients tolerate additional properties, making additive changes relatively safe. A server can often introduce a new optional field without breaking old consumers. Removing a field, changing its type, or changing its meaning is far more dangerous. Even correcting a spelling mistake can break code that relies on the original key.
Compatibility requires knowing how clients behave, not merely how they should behave. Contract tests, usage telemetry, deprecation notices, and migration periods reduce surprises. When a breaking change is unavoidable, versioning can create a clean boundary, but versions should not replace thoughtful evolution. Maintaining many nearly identical versions becomes expensive quickly.
Return errors clients can act on
An HTTP status code communicates the broad class of failure, while a JSON error body can explain what happened and what the client should do. Useful errors include a stable machine-readable code, a human-readable message, and field-level details when validation fails. They should not expose stack traces, SQL messages, or secrets.
Error shapes should remain consistent across the API. If one endpoint returns an array of field problems and another returns a string under an unrelated key, every client needs endpoint-specific handling. A predictable envelope allows shared logging, user messaging, and retry logic.
Pagination and filtering are part of the contract
List endpoints often begin by returning every record, then become unusable as data grows. Pagination should be designed before scale forces a rushed change. Offset pagination is simple but can shift when records are inserted or removed. Cursor pagination is more stable for large or frequently changing collections but requires a clear ordering.
Filtering and sorting rules should be explicit. Define which fields are accepted, how repeated parameters behave, and whether comparisons are case-sensitive. Return metadata or links that help clients navigate without reconstructing undocumented URLs.
Validate both requests and responses
Request validation protects the server from malformed or unexpected input. Response validation protects clients from accidental contract drift. A typed schema can serve documentation, generate client code, and power automated tests, but only when it reflects real behavior. A stale schema creates false confidence.
Testing should include more than ideal examples. Exercise missing fields, unknown fields, null values, large identifiers, empty arrays, Unicode strings, and malformed numbers. These edges reveal assumptions that ordinary fixtures hide.
A stable API feels unsurprising
The best JSON APIs are not admired for unusual payload structures. They are trusted because similar operations behave similarly, changes arrive predictably, and failures explain themselves. That trust reduces the amount of defensive code every consumer must write.
JSON provides a flexible envelope, but durability comes from discipline around it. Design for client understanding, specify ambiguous details, prefer additive evolution, and treat the published shape as a commitment. Those habits matter far more than the choice between braces and any other serialization syntax.