API Design Principles I Wish I'd Known Earlier
Good API design is the gift that keeps giving. Bad API design is the debt that compounds every time someone writes a new client.
I've designed a few APIs from scratch, inherited several messy ones, and spent more time than I'd like debugging integrations where the API response format was a polite fiction. Here are the principles I try to follow.
Name Things for the Caller, Not the Implementation
The most common mistake I see: naming fields after the database column or the internal variable.
// Bad — "usr_id", "ts", "flg" are implementation details
{
"usr_id": 42,
"ts": 1712345678,
"active_flg": 1
}
// Good — readable by any developer without context
{
"userId": "usr_42",
"createdAt": "2026-04-17T10:00:00Z",
"isActive": true
}The people who have to read your API response are not your database administrator. Use camelCase for JSON (or snake_case consistently — just pick one), use ISO 8601 for dates, and use booleans for booleans.
Always Return the Same Error Shape
Clients have to handle errors. If your error format varies by endpoint — some return { error: "..." }, others { message: "..." }, others { errors: [...] } — every client has to write bespoke error handling.
Pick a shape and stick to it everywhere:
interface ApiError {
code: string; // machine-readable, e.g. "VALIDATION_FAILED"
message: string; // human-readable
details?: unknown; // optional context (validation errors, etc.)
}The code field is particularly valuable — it lets clients branch on specific error types without string-matching the human-readable message (which you might want to change later).
Never include stack traces or internal error messages in production API responses. Log them server-side instead.
Version From Day One
Even if you think no one will use your API, version it. /api/v1/users costs nothing and saves enormous pain later. When you need to make a breaking change, you add /api/v2/users and run both in parallel during a transition period.
Not versioning is a trap that looks fine until the moment it isn't.
Be Explicit About Pagination
Returning a list endpoint that returns all records works fine in development with 10 test records. In production with 100,000 records it's either extremely slow or silently returns a truncated result.
Always paginate list endpoints from the start. The simplest approach that works well:
interface PaginatedResponse<T> {
data: T[];
meta: {
total: number;
page: number;
pageSize: number;
hasNextPage: boolean;
};
}Cursor-based pagination is better for large datasets and real-time data, but page-number pagination is fine for most use cases.
Document the Unhappy Paths
Most API documentation describes what happens when everything goes right. The integration failures I've debugged were almost always about what happens when something goes wrong — and the docs either said nothing or said "returns an error."
For each endpoint, document:
- What can cause a 4xx and what
codeis returned - Rate limiting behaviour (when, how many requests, what the response looks like)
- What fields are optional vs required, and what happens if optional fields are missing
Good error documentation is as important as good happy-path documentation.