Building Harbour's API v2: Resource Design and Modeling (Part 3)
After establishing our foundation and authentication system, the next critical challenge in building Harbour's API v2 was designing our resources and data models. This article shares our approach to creating a flexible, intuitive resource structure that serves both internal and external needs.
Series Overview:
Context and Challenge
Our existing API had grown organically, leading to inconsistent resource naming, complex relationships that needed to be made easier to navigate, and inflexible query patterns. With API v2, we needed to design a resource model that would:
Design Considerations
Resource Naming Convention
We established strict naming conventions based on three principles:
This led to endpoints like:
/documents
/versions
/files
We deliberately avoided endpoints like /getDocuments or /createUser, as these verb-based names violate RESTful principles and make the API less intuitive.
Modeling Business Relationships
Complex business relationships presented a significant challenge. Consider a typical scenario in our domain:
Documents
-> DocumentVersions
-> Files
-> Fields
Rather than creating deeply nested URLs like
/documents/{id}/versions/{version}/files/{file}
We opted for flatter resource paths with relationship parameters:
/documents?version_id={version}
This approach provides several benefits:
Query Parameter Design
We implemented a consistent query parameter structure across all endpoints:
/documents?status=draft&version=0
/documents?order_by=+created_at
领英推荐
After evaluating various pagination methods, we chose page/size pagination for its simplicity and familiarity. Our implementation includes:
/documents?page=2&size=100
Response:
{
"items": [...],
"total": 300,
"page": 2,
"size": 100,
"pages": 3
}
This approach provides several advantages:
Implementation Details
Query Parameter Parsing
We standardized parameter parsing across all endpoints:
def parse_query_params(request):
return {
'filters': _parse_filters(request.args),
'order_by': _parse_sort(request.args.get('sort')),
'pagination': {
'page': int(request.args.get('page', 1)),
'size': min(int(request.args.get('size', 20)), 100)
}
}
Resource Serialization
We implemented a consistent serialization layer:
class ResourceSerializer:
def serialize(self, resource):
return {
'type': self.resource_type,
'id': resource.id,
'attributes': self._get_attributes(resource),
}
Lessons Learned
The journey of building our API v2 has taught us several valuable lessons about REST API design and implementation. REST consistency proved to be fundamental to our success. By maintaining strict conventions for resource naming and HTTP methods, we significantly improved the developer experience for both our internal teams and external clients. Our engineers now spend less time debating API design decisions and more time building features.
Documentation quality directly influences adoption rates. The consistent patterns in our REST design made our documentation more intuitive, which could significantly reduce onboarding friction. New team members and external developers could quickly understand and start using our API without extensive support.
Our experience with pagination taught us valuable lessons about performance and usability. While the page/size approach is straightforward, we needed to implement proper limits and optimizations:
Future Enhancements
Looking ahead, we've identified several opportunities to enhance our API's capabilities while maintaining its simplicity and consistency. We plan to introduce field selection, allowing clients to request specific fields and reduce response payload sizes. This will be particularly valuable for mobile clients and bandwidth-constrained environments.
Resource expansion represents another significant enhancement on our roadmap. This feature will allow clients to reduce the number of API calls by expanding related resources in a single request. Key planned improvements include:
Next Steps
Our immediate priority is strengthening our API's foundation through comprehensive automated testing. We're developing a suite of performance tests focused on complex query patterns to ensure consistent response times as our dataset grows.
Another key initiative is automating our REST API design enforcement. We're building internal tools to validate that new endpoints adhere to our established conventions, catching potential inconsistencies before they reach production. This tooling will help maintain the high quality of our API as we scale our development team.
Our immediate focus areas include:
Watch for Part 4 of this series, where we'll dive into our approach to error handling and validation in API v2.