Anyone can make an API that technically returns JSON. Making one that's pleasant to work with — predictable, consistent, hard to misuse — is a different skill, and it's mostly a handful of habits applied consistently rather than any single clever trick.
Pick one casing convention and never break it
JSON APIs are almost always camelCase (firstName, createdAt) because JSON grew up
alongside JavaScript, where camelCase is the norm. Some APIs use snake_case instead (first_name),
often because the backend is Python or Ruby. Either is fine. What's not fine is mixing them:
{
"userId": 1,
"first_name": "Alice",
"LastName": "Chen"
}
This is a real API response pattern, and every consumer of it has to remember which field uses which convention. Pick one casing style for your whole API and apply it everywhere, including in nested objects.
Be consistent about null vs. missing
JSON gives you two ways to say "no value here": omit the key entirely, or include it with a value of
null. These are not the same thing to most JSON parsers, and mixing them inconsistently forces
every client to handle both cases everywhere:
{ "middleName": null } // key present, value is null
{ } // key absent entirely
Decide on a convention — many APIs always include the key with null when a value is
genuinely absent, which lets consumers check response.middleName === null without also needing
to check 'middleName' in response — and apply it consistently.
Use plural nouns for collections, singular for single resources
GET /users → { "users": [ ... ] }
GET /users/42 → { "id": 42, "name": "..." }
This sounds minor, but it removes an entire category of guessing. If someone sees /orders in
your API, they should be able to correctly guess it returns a list, without checking documentation.
Wrap collections in an envelope (usually)
Compare returning a bare array:
[
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" }
]
against wrapping it in an object:
{
"users": [
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" }
],
"total": 214,
"page": 1
}
A bare top-level array works fine for a small, static list, but it can't hold pagination metadata, and adding a new top-level field later is a breaking change (you'd have to convert the array into an object, which breaks every existing consumer). An envelope object costs almost nothing up front and leaves room to grow.
Design errors as carefully as success responses
A shockingly large number of APIs put real thought into their success responses and then return errors as an inconsistent mess — sometimes a string, sometimes an HTML error page, sometimes a JSON object with different fields every time. A consistent error shape saves every consumer from writing brittle, error-message-specific parsing code:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Email address is not valid.",
"field": "email"
}
}
The code field matters more than it looks — it gives client code something stable to
branch logic on, since error message text can (and does) change wording over time without warning, but a
code like VALIDATION_FAILED is a contract you can rely on.
Use ISO 8601 for dates, always
"2026-08-12T14:30:00Z", not "08/12/2026" or "Aug 12, 2026 2:30 PM".
ISO 8601 is unambiguous (no confusion between month/day order across locales), sorts correctly as plain text,
and every mainstream language can parse it without a custom format string. If you need to send a raw Unix
timestamp instead, be explicit and consistent about whether it's in seconds or milliseconds — that
single ambiguity causes an enormous number of real bugs.
Think about versioning before you need it
Your API will change. The question is whether existing consumers break when it does. A version in the URL
(/v1/users) or a request header is the simplest approach, and deciding on one before you have
external consumers is much less painful than retrofitting it after you do.
Don't return more than what's needed by default
It's tempting to return every field of an internal database row. Resist it. Extra fields are extra surface area: consumers start depending on fields you never meant to expose long-term, and now removing them is a breaking change. Return what the endpoint's purpose requires, and add fields deliberately when there's a real need, not because it was easy to include.
Putting it together
None of these individually is complicated. What makes an API feel well-designed is applying all of them consistently, everywhere, from the very first endpoint — because retrofitting consistency onto an API that already has external consumers is far more painful than establishing it from day one.
Frequently Asked Questions
2026-08-12T14:30:00Z) are generally friendlier since they're human-readable and unambiguous. If you use Unix timestamps, always be explicit and consistent about seconds vs. milliseconds, since that mismatch is one of the most common real-world API bugs./v1/...) is the simplest and most discoverable approach. Header-based versioning is also common and keeps URLs cleaner, but is less obvious to someone just browsing your API. Either works — the important part is deciding early, before you have consumers who'll be broken by an unversioned change.