Personally, I’d avoid django-restframework or any JSON API if at all possible. Server-side rendered HTML is the golden path for Django. Slapping a JSON serialization layer right in the middle of your architecture is a great way to kill your velocity and lose many of the benefits of Django.
When you need more on the frontend, sprinkle in some htmx or Alpine or something similar. If, and only if, you’ve discovered that these aren’t going to cut it for your UI needs, choose an appropriate SPA technology and use where necessary - but I’d argue they make a very poor default. If I knew from the beginning that an app was going to be mostly JSON APIs on the backend, I’d seriously consider whether Django was the best choice at all - I’ve tried it, it’s pretty painful and there must be better options these days.
Django works beautifully with htmx. I wrote a reasonably-sized app a few years ago with Django and its predecessor, intercooler, and I was able to re-use so many templates. The only thing I really felt I was missing that would have been useful was server-sent events.
I tend to agree. I’ve used DRF a couple of times and even if you try very hard, you’re sort of sucked into building castles of class inheritance. It gets too complex too quickly. If you just need a pure JSON API, it will probably be easier to use FastAPI or something because Django isn’t adding a ton of value. Django is great for HTML website plus instant backend.
How do you manage data access when you add a JSON API? Let’s say, you have been building an Django site for a while and you need to add the API for partners. You’ve made a big investment in the Django ORM and now you are adding a separate Python stack into the mix. Do you reuse anything in the ORM layer, or duplicate with something more appropriate for FastAPI?
I was assuming it’s greenfield. If you have an existing Django project, you should see if you can just return the JSON in a view, and then if it’s too complex for that, yeah, DRF.
I’m quite happy with DRF and using it to build backend APIs. But I think you and I would disagree on patterns and approaches to doing so (I think I like generic views a lot more than you do, and also I suspect I like the thin-controller style way more than most people do), and I think that’s probably the source of it – I fully agree that if you’re not doing things the way Django/DRF are pushing you to, it will feel like you’re fighting the framework and it’s getting in your way.
The N+1 pattern is a real problem in any Django codebase. Thank you for mentioning it. There’s no silver bullet; sometimes tools like Django Debug Toolbar can help.
It’s been a problem with every ORM I’ve ever used. It’s no silver bullet, but most of them have some mechanism to let you prefetch columns you care about on related entities so you can work around it if you see it happening.
switch to asgi and use daphne+channels to handle websocket connections.
While no doubt you have good reasons to recommend this, I would provide a counterpoint. At my previous job we had nothing but trouble with daphne+channels, to the point where it took a concerted effort to get rid of them.
Anecdotally, Daphne also went for a year or so without a maintainer. Also, the original ASGI spec had several alternative backends, among which there was also a sysv IPC one, which to me looked a lot simpler to set up than the current default of using memcached. Turns out this was completely broken and never really worked properly in the first place. I wasted quite a bit of time on getting that to work.
Now, it may be that in the intervening years things have improved, especially since Channels is now an official part of Django (although even after it was initially adopted, we still had many issues with it). But I’d still tread very cautiously.
Did you choose python-decouple over django-environ for any strong reason? I’ve had it on my todo list for a couple of weeks to see what people are really using in production to get sane casts for various config items from environment variables, and that’s the one I’d been using as a placeholder.
I only knew of python-decouple as an option for separating settings from code at the time. I spoke with a couple of friends and found out that they were still maintaining multiple settings files, which just seems wrong to me. From a cursory look at download stats and the GitHub pages, django-environ seems slightly more popular and has a considerably higher number of releases. Thanks for mentioning it, I will probably be switching over to it for my next project.
Enable Django sites framework with the setting SITE_ID=1 as recommended in the official docs.
Ugh. Django sites framework is broken by design. Unfortunately, it’s pretty well entangled with existing apps, so it’s hard to get out of using it. My advice is to never use it voluntarily and anywhere you have a choice, just code your own thing instead of relying on it.
There is something I need from many production apps I deploy.
In my reality, dns names are expensive. So I want to use example.com/mytool to reach mytool and example.com/3rdpartytool to reach 3rdpartytool.
This can be done one of two ways. The reverse proxy can lie and say 3rdpartytool is / or it can tell 3rdpartytool the exact query that the client sent.
That part is fine. Django chose to only support the rewrite mechanism. That’s fine.
But then what about urls returned to the user? If the app doesn’t know where it is, they all need to be relative to the returned page. Sometimes this is unwieldy (think of a javascript file sourced from multiple paths, where another javascript resource needs to be imported from under /js/ or something).
So the pattern I follow on my apps is the reverse proxy tells the truth, and my app is configured (env var typically) with the real location of the app. This works great.
It’s also really hard to retrofit this ability to a Django app, unless I and others are misinformed.
Can this be a best practice ability please? Let me decide where my app belongs when I know: at runtime.
Production tips for django apps:
switch to asgi and use daphne+channels to handle websocket connections.
makemigrations every time you change your choices lists.
use django-extensions. Instead of creating the database manually, create your user, then use django extensions to reset_db
use django-restframework
beware of the N+1 problem when using django-restframework serializers.
Personally, I’d avoid django-restframework or any JSON API if at all possible. Server-side rendered HTML is the golden path for Django. Slapping a JSON serialization layer right in the middle of your architecture is a great way to kill your velocity and lose many of the benefits of Django.
When you need more on the frontend, sprinkle in some htmx or Alpine or something similar. If, and only if, you’ve discovered that these aren’t going to cut it for your UI needs, choose an appropriate SPA technology and use where necessary - but I’d argue they make a very poor default. If I knew from the beginning that an app was going to be mostly JSON APIs on the backend, I’d seriously consider whether Django was the best choice at all - I’ve tried it, it’s pretty painful and there must be better options these days.
Django works beautifully with htmx. I wrote a reasonably-sized app a few years ago with Django and its predecessor, intercooler, and I was able to re-use so many templates. The only thing I really felt I was missing that would have been useful was server-sent events.
I tend to agree. I’ve used DRF a couple of times and even if you try very hard, you’re sort of sucked into building castles of class inheritance. It gets too complex too quickly. If you just need a pure JSON API, it will probably be easier to use FastAPI or something because Django isn’t adding a ton of value. Django is great for HTML website plus instant backend.
How do you manage data access when you add a JSON API? Let’s say, you have been building an Django site for a while and you need to add the API for partners. You’ve made a big investment in the Django ORM and now you are adding a separate Python stack into the mix. Do you reuse anything in the ORM layer, or duplicate with something more appropriate for FastAPI?
I was assuming it’s greenfield. If you have an existing Django project, you should see if you can just return the JSON in a view, and then if it’s too complex for that, yeah, DRF.
I would ignore the naysayers and just use rest-framework. See my other comment for why I think it’s fine.
I’m quite happy with DRF and using it to build backend APIs. But I think you and I would disagree on patterns and approaches to doing so (I think I like generic views a lot more than you do, and also I suspect I like the thin-controller style way more than most people do), and I think that’s probably the source of it – I fully agree that if you’re not doing things the way Django/DRF are pushing you to, it will feel like you’re fighting the framework and it’s getting in your way.
Thanks for posting more tips!
Mind if I add some of them to my original article?
Will checkout
django-extensions
, have never heard of it before. I usedjango-allauth
amongst other things though and wouldn’t give that a skip.The N+1 pattern is a real problem in any Django codebase. Thank you for mentioning it. There’s no silver bullet; sometimes tools like Django Debug Toolbar can help.
It’s been a problem with every ORM I’ve ever used. It’s no silver bullet, but most of them have some mechanism to let you prefetch columns you care about on related entities so you can work around it if you see it happening.
While no doubt you have good reasons to recommend this, I would provide a counterpoint. At my previous job we had nothing but trouble with daphne+channels, to the point where it took a concerted effort to get rid of them.
Anecdotally, Daphne also went for a year or so without a maintainer. Also, the original ASGI spec had several alternative backends, among which there was also a sysv IPC one, which to me looked a lot simpler to set up than the current default of using memcached. Turns out this was completely broken and never really worked properly in the first place. I wasted quite a bit of time on getting that to work.
Now, it may be that in the intervening years things have improved, especially since Channels is now an official part of Django (although even after it was initially adopted, we still had many issues with it). But I’d still tread very cautiously.
Great write up. I’m going to implement some of the tips right away.
Just a note that the link around gunicorn config is broken.
Thanks for bringing this to my notice. The link is now fixed :)
If someone wants more tips of this kind have a look at the “Two Scoops of Django 3.x” book - it’s full of very interesting ideas.
Thanks for posting that.
Did you choose python-decouple over django-environ for any strong reason? I’ve had it on my todo list for a couple of weeks to see what people are really using in production to get sane casts for various config items from environment variables, and that’s the one I’d been using as a placeholder.
Anytime!
I only knew of
python-decouple
as an option for separating settings from code at the time. I spoke with a couple of friends and found out that they were still maintaining multiple settings files, which just seems wrong to me. From a cursory look at download stats and the GitHub pages,django-environ
seems slightly more popular and has a considerably higher number of releases. Thanks for mentioning it, I will probably be switching over to it for my next project.Ugh. Django sites framework is broken by design. Unfortunately, it’s pretty well entangled with existing apps, so it’s hard to get out of using it. My advice is to never use it voluntarily and anywhere you have a choice, just code your own thing instead of relying on it.
There is something I need from many production apps I deploy.
In my reality, dns names are expensive. So I want to use example.com/mytool to reach mytool and example.com/3rdpartytool to reach 3rdpartytool.
This can be done one of two ways. The reverse proxy can lie and say 3rdpartytool is / or it can tell 3rdpartytool the exact query that the client sent.
That part is fine. Django chose to only support the rewrite mechanism. That’s fine.
But then what about urls returned to the user? If the app doesn’t know where it is, they all need to be relative to the returned page. Sometimes this is unwieldy (think of a javascript file sourced from multiple paths, where another javascript resource needs to be imported from under /js/ or something).
So the pattern I follow on my apps is the reverse proxy tells the truth, and my app is configured (env var typically) with the real location of the app. This works great.
It’s also really hard to retrofit this ability to a Django app, unless I and others are misinformed.
Can this be a best practice ability please? Let me decide where my app belongs when I know: at runtime.