The Art of API Design: Making Complex Systems Simple
Good API design is invisible. Users don't notice it. Bad API design creates support tickets. Practical patterns for designing REST and gRPC APIs that developers actually enjoy using.
I've designed APIs consumed by hundreds of internal and external teams. The most important lesson: the quality of your API is measured by how few questions developers ask when integrating with it. If your API needs a 30-page guide to make a basic request, you've failed.
Start with the Use Case, Not the Data Model
The most common API design mistake is exposing your internal data model directly. Your database has a user_account_entity table with 47 columns? That doesn't mean your API should return all 47 fields. Start by listing the top 5 things consumers will do with your API, then design resources and operations around those use cases.
This is where REST and gRPC diverge in interesting ways. REST pushes you toward resource-oriented thinking, nouns over verbs. gRPC pushes you toward service-oriented thinking, operations that may span multiple resources. Neither is universally better; the right choice depends on your consumers.
Pagination, Filtering, and the Art of Defaults
Every list endpoint needs pagination from day one. I've seen teams skip it "because we'll only have a few items" and then scramble when a customer has 50,000 records. Cursor-based pagination is almost always the right choice — it's stable under concurrent writes and performs well with large datasets.
// Cursor-based pagination
GET /api/v1/evaluations?cursor=eyJpZCI6MTIzfQ&limit=25
// Response includes next cursor
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTQ4fQ",
"has_more": true
}
}
Filtering is where things get subjective. My rule: support the filters your consumers actually need, not every possible combination. Start minimal, add based on feedback. Every filter you add is a query path you need to optimize and maintain.
Versioning: Pick a Strategy and Commit
URL-based versioning (/v1/resource) is the simplest and most visible approach. Header-based versioning is cleaner in theory but causes confusion in practice: developers forget to set the header and get unexpected behavior. I've shipped both and URL versioning causes fewer support tickets.
More importantly, design your v1 with future versions in mind. Use envelope response formats so you can add metadata without breaking changes. Make fields optional where possible. Use semantic types (ISO 8601 dates, not Unix timestamps) so the interpretation is unambiguous.
Error Responses Deserve Design Attention
Your error responses will be read more carefully than your success responses. When something goes wrong, the developer needs to know: what happened, why, and what they can do about it. A generic {"error": "Bad Request"} is useless. A structured error with a machine-readable code, human-readable message, and a link to documentation is a gift.
{
"error": {
"code": "INVALID_DATE_RANGE",
"message": "end_date must be after start_date",
"field": "end_date",
"docs": "https://api.example.com/docs/errors#INVALID_DATE_RANGE"
}
}