Imran's Blog
Stuff I feel like blogging about.

Django Rest Framework

Posted on

In one of my previous roles I had to use Django Rest Framework (drf) to make APIs and I enjoyed it. It fit my mental model for building robust APIs.

My mental model is as follows:

  1. A user sends a request
  2. Check auth
  3. Check request body (do we have the right fields/structure)
  4. Perform basic validation (types)
  5. Perform business logic validation
  6. Persist to data store

drf makes this super easy with its serializers. Views + permissions take care of auth in most cases and serializers do the rest.

A small amount noob traps do exist.

# Using ModelSerializer

This is the first trap most people fall into. ModelSerializers are convenient when prototyping but most of the time the input that you want from a user does not map nicely onto an existing model. This is true the larger/more complex your web API gets.

# Trying to use the same serializer for update and create

This is another noob trap that ends up resulting in some crazy complex and hard to follow code. Most of the time it turns out the logic you want for create operations ends up being quite different from the logic you want when it comes to update operations. This results in a lot of validation code that depends on the existence of self.instance.

It's trivially easy to switch out which serializer gets used based on action

Using a serializer per action lets you have nicely segregated logic per verb per endpoint. This makes it super clear what code gets run for POST /something vs PUT /something vs GET /something. This in turn leads pretty easy maintainability when needing to make a change as you can be sure a change to the behaviour of one action does not impact the others. Which in turn leads to pretty easy testing, you just need to test each specific serializer plus its validation logic.

# Not using a separate serializer for retrieve vs list

For reference a retrieve is asking for a single record i.e. GET /things/1 and list is asking for more than one record i.e. GET /things

Generally the data you want in a detail view (retrieve) is different from the data you want in a list view and your serializers should reflect this. Detail serializers might need to do a lot of extra look up to get related records to present a nice detailed record.

A mistake I've seen made is folks end up using this same detail serializer for their list operations. This results in an unholy amount of N+1 queries and abysmal performance.

# Not using a separate serializer for read vs write operations

This is not quite a noob trap but it does go a long way in the simplification of code. I find that more often than not you would like to receive data in a field as one type, but when returning the data it needs to be another type. e.g. Take in a foreign key but return a string from the related object. drf does not make it easy to re-use a field if its a different type on the way out. The maintainable/easy way out of this predicament is to just use a different serializer for incoming data vs outgoing.

# Conclusion

The general takeaway from this is that you probably want to avoid using ModelSerializers and don't try to make a single serializer a one size fits all solution.