Ok, I have a pitch on what framing to use when putting together tools for an API. We should think in terms of templates, not serializers, is the TL;DR.
The problem with serializers to dump our models into API responses is that they quickly break down. What starts as a straightforward serialize command quickly has overrides to not include specific fields, or translate fields to a different format or structure, or to merge in fields from another table. It gets harder and harder to maintain as time goes on, if you’re trying to maintain a consistent API.
This can work fine for internal APIs when you can change the API and the code that uses it too, but even when I’ve been working in small teams on internal APIs, I prefer APIs to be more intentional and maintainable. Thus, the simple template approach.
def employee_history_template(employee):
return {
'data': {
'employee': employee_template(employee)
'events': [event_template(event) for event in employee.events()]
}
}
def event_template(event):
if event.event_type() == BONUS_EVENT: return bonus_event_template(event)
if event.event_type() == GRANT_EVENT: return grant_event_template(event)
def bonus_event_template(event):
return {
'amount': event.amount,
'amountpct': event.percent_amount(),
'currency': api_display(event.currency)
}
def grant_event_template(event):
return {
'grant_id': event.grant_id(),
'shares': event.number_shares(),
'canceled': event.number_canceled() or 0,
'type': event.display_grant_type(),
'vested': event.percent_vested
}
This is a slight re-imagining (and deleting a lot more events and other data types) of real maintainable testable code from a previous project, where employee events were models inheriting from an abstract base EmployeeEvent model, and grant events linked to a RSUGrant or OptionGrant… yet the complexity of the model is appropriately hidden in these templates.
I’ve now done this across almost 10 different projects mostly in django, because it’s no more work to get started and has so many advantages over time…
- It’s very clear what’s in the API and what isn’t
- Nobody accidentally adds something to the API (that might be sensitive!) by adding fields to models.
- It keeps names of things in the API consistent even if names change in the data models.
- Code reviews are easier. It’s easier to see what the developer intends to do or change in the API.
- It keeps display/formatting (e.g. displaying date time or currency for the API differently than the GUI) where it belongs, in the view files, not in the model files.
- The API is explicitly designed, not incidentally spit out. This is even more important for external APIs which should have a little advance thought go into what things are called and what format fields use!
Doing the templates as simple methods that return dicts is only one possibility. Another logical possibility is to define a template file format for JSON responses the same way django already defines a template file format for HTML responses.
I wrote blog posts about this in 2011, including one on testing with a similar focus on readable and maintainable API tests from a time I was doing Rails, but some blog posts have been inexplicably unpublished in more recent years, argh.
How does this impact the proposal on explictly doing REST in django? I think that it’s a good idea to offer more to developers, especially the way some are envisioning it as a combination of documentation, existing core tools, new core tools, and recommended libraries. These templates don’t require any new tools but that’s because my teams keep implementing templates within django as it stands.
I also have a pitch to make about using decorators on API views to make obvious, up-front statements that the view does things like:
- use @requires_api_key on every view method that requires an API key
- use @json_response on every API view that returns JSON
- use @api_error_handling to invoke a common set of API-friendly (developer friendly) response formats in case of exceptions raised
But this post is too long already.