How to Design APIs That Survive Product Growth
The Problem
As products grow, their APIs often face challenges in maintaining backward compatibility, performance, and scalability. Without proper design, APIs can become rigid and difficult to evolve, leading to technical debt and frustrated users.
1. Versioning pattern
Use /v1/users to identify the version of the API. Keep versions isolated.
Example:
For version 1:
/v1/users/123
{
"id": 123,
"fullName": "Eric Khanh",
"email": "[email protected]"
}
For version 2:
/v2/users/123
{
"id": 123,
"fullName": "Eric Khanh",
"email": "[email protected]",
"address": "127 Nguyen Thi Dinh, Ha Noi, Viet Nam"
}
Over time, APIs evolve and require versioning to maintain backward compatibility. Maintain consistency instead avoid breaking existing clients.
Overriding logic, changing data type, introducing new structures,… User can continue use their version without being affected by these changes. If they want to use the new version, they can switch to that one.
2. Pagination pattern
Pagination divides data into chunks and lets users access the exact data they want.
GET /users?page=2&limit=100 (page, limit, sort, search)
{
"data": [
{ "id": 1001, "name": "Eric" }
.... 99 items more
],
"meta": {
"page": 2,
"limit": 100,
"total": 982,
}
}
Pagination allows users to select the correct page they want. Users can jump to the middle, last page to more easily view that data.
Benefits:
- Improved response times
- Better data accessbility
- Improved user experience
- Better scalability
3. Filtering pattern
GET /users?active=true&role=admin&createdAt>=2024-01-01
- active=true: filtering active user
- role=admin: filter user role
- createdAt>=2024-01-01: fitler by creation date
- …
This pattern helps clients query the data they need, increase user experience, get insights from data.
4. Field selection pattern
In case user only need some fields of the api response. They can specify the fields they need and server returns following that pattern. This is similar to GraphQL-style field selection in a REST API.
GET /users/123?field=id,name,avt,phone
In some cases we only need the id or name and phone number but the /users return so much data. With field filtering, we can get the correct field and reducing bandwidth usage.
Tip: Group fields under predefined keys.
?field=basic => id, name, avt
?fields=details => id, name, avt, address, phone, email
?fields=profile => id, name, avt, bio, website, resume, linkedin, github
By default, return the full response.
In case client need data that do not exist on the grouping, using the full data can solve that case.
Monitor API usage patterns and create predefined groups when the same set of fields is frequently requested together.
5. Expansion pattern
In some cases, clients need additional data for a new feature that need to collect more data but they don’t want to use multiple API calls. They can expand the current api with the expansion pattern to reduce API calls:
`GET /users/9213?expand=profile,orders,carts“
The primary purpose is to reduce API calls. We have the BFF terminology for this kind of optimization. Normally server should organize their data following the domain logic - This means structuring APIs around business domains.
This design allows each domain to scale independently without turning the User API into a monolithic endpoint responsible for orders, carts, login history, payment and other unrelated concerns.
For example:
/orders /carts /users/<user_id> /paymentsWith BFF layer, data can be aggregated and cached before beingg returned to the client:
/users/<user_id> { "id": 1234 "name": "Eric" "carts": { "itemCount": 12 }, "orders":{ "inprogress": 3 "canceled": 2 } }
Expansion is useful for reducing client round trips, but large-scale aggregation is often delegated to a BFF layer rather than expanding responsibilities of the underlying domain APIs.
Conclusion
Designing APIs that can evolve with your product is crucial for long-term success. By implementing patterns like versioning, pagination, filtering, field selection, and expansion, you can ensure your APIs remain robust and adaptable as your product grows. These patterns help maintain backward compatibility, improve performance, and enhance user experience, ultimately contributing to the longevity and success of your product.