Lots of good wisdom in here, which is clearly hard-won. I'm seeing a lot of common themes with the pain points that I've found working with Django over the years, and while I agree with (or at least don't disagree with) the majority of these, in some cases I've gone with different solutions:

1. I agree with the recommendation to decouple serializers from models, although I haven't finished this migration in my current codebase. In practice all of our ModelSerializers override many field definitions, and have as much boilerplate as if we built them from scratch. However the ModelSerializer approach is really productive for the first stages of your product when you're just cranking out CRUD views. (Maybe this is a trap.)

2. I've also found "Fat models" to be quite painful in Django. Fat services is probably the most obvious approach to solving this, and it's what we evolved into. Another approach I've been exploring is going full DDD (i.e. your Domain Models are not ActiveRecord / Django DB models).

A common concern I have with both of these approaches is: if you're eschewing the Model<>Serializer linkage, and you're also trying to treat your ORM models as dumb data mappers instead of fat models with logic, then what are you gaining from a heavyweight framework like Django? Wouldn't you be better with FastAPI/Flask using Marshmallow for the API, and SQLAlchemy for the ORM? The main answer I come back to is "Django Admin" but with things like Forest Admin and hopefully one day an OpenAPI admin from FastAPI, this benefit may evaporate too.

3. Regarding "Hungarian notation", I'm a bit dubious about using `_list`, `_set`, etc in variable names; type hints are working out better for me in this area. Prior to adding type hints I did have some coding standards around explicitly suffixing functions that return QuerySets, because this is the one case I think you actually need to care about when reading code -- if you mistake a string for a list, your tests will fail. If you mistake a QS for a list, your code will pass, but you might accidentally trigger O(N) DB lookups where you should have O(1).

4. RE: integration tests on views, my codebase is structured similarly, and I prefer to target services. You should have tests on your views too of course, but I think it's more ergonomic to drive your business logic tests at the service layer, without the DRF API machinery in between you and your code under test (i.e. so you see the actual domain exceptions, and not just the API response codes). We get view coverage by layering some happy-path API tests on top, and a small number of long-form scenario-based integration tests for full workflows using a discursive black-box API testing style.

If your domain is simple, then view-based integration tests probably suffice. It's probably where I'd start for a small project now. Operating in a fairly complex domain model, we write lots of UTs around complex business domain rules. "Test at the lowest level that lets you clearly describe the feature you are testing" is my general approach. In my experience the view layer is pretty boring, and we seldom find any interesting bugs there, so I don't feel the need to spend much time testing it (it's just boilerplate). The domain/service layer is where all our bugs tend to be. If you trust that your service layer is well-tested, then your view-tests can be quite simplistic.

I'd note that this point is also vulnerable to the fuzziness of what actually demarcates UT vs. IT, and I think that definition probably drives a lot of the differences in which slogan folks advocate here. I definitely don't go with the "UTs cannot touch the DB" approach, though I'd like to experiment in that direction.

Perhaps from the author's rigorous definition of API error codes, the response to this point would be something like "your API error responses should be rich enough that you can debug all your integration tests with them". I can see that might be a good line to draw in the sand.