Hello! I’m on a funny journey right now where I’m trying to learn how to make websites in a sort of 2010 style, where I have an SQL database and render some HTML on the backend.
It’s kind of an interesting journey because it doesn’t necessarily feel “easy” to me to make websites in this way: I never learned how to do it in the 2000s or 2010s, and there’s a lot I need to learn.
So here are some Django features that make building this kind of site feel more achievable than when I was trying and failing to use Go’s standard library or Flask. And I’ll talk about a couple of issues with Django I’ve run into.
Previously the toolkit I felt confident with for making websites was:
I really liked this frontend-heavy approach for these super simple applications but when I started thinking about making something with a lot of different pages (instead of literally just one page), I didn’t feel so excited about the options I saw that involved a lot of frontend code. So I figured I’d try the backend.
Writing a backend-focused site that uses as little JS as possible feels the same to me in a way as writing a single-page JS website that does as little on the backend as possible, even though they might seem like opposites. In both cases I’m just trying to keep as much of the logic as possible in one place.
Now for some thoughts about Django!
I learned that I can define a “query set” class in Django with a bunch of
methods with different WHERE statements I might want to use while constructing
a query:
Here’s how I use it in my view code once I’ve defined what all the methods mean:
Events.objects.approved()
.for_tab(tab)
.with_festivals(tab_params.festival_slugs)
.is_free(tab_params.free)
.is_outdoors(tab_params.outdoors)
and here’s how I define the methods:
class EventQuerySet(SearchableQuerySetMixin, models.QuerySet):
def approved(self):
return self.filter(approved_at__isnull=False)
def future(self):
today = timezone.localdate()
return self.filter(end__gt=self._midnight(today))
def with_tags(self, tags):
if tags:
return self.filter(tags__name__in=tags).distinct()
return self
The syntax for defining the filters isn’t my favourite, but I spend most of my time just using the methods, and it feels super readable and nice to use, and it makes me want to look into other query builder libraries in the future. In the past I thought “I know SQL, who needs a query builder?”, but this kind of structure does make it really nice to read.
I found an example of someone who wrote their own small query builder in Python that I want to read later to think about whether I would enjoy using a more minimal version of this.
There are a bunch of little quality of life filters available in Django templates that are super useful for generating HTML. The ones I’ve used so far are:
<br> ({{ event.description|urlize|linebreaksbr }} ){{ row.date|date:"M j" }})json_script, which takes a Python dictionary and automatically converts it to JSON and inserts it into the HTML as a <script> tag in a safe wayThese are all small things individually but I feel like it makes a big difference somehow to just have them available.
querystring is cool
I think my favourite template filter is querystring: in this site sometimes
we use filters like ?date=2026-06-01 to decide what’s displayed. querystring
that will make a link to the same query string with one change, like this to
link to the previous date:
<a href="{% querystring date=nav.prev_date%}">
Or to remove the outdoors parameter:
<a href="{% querystring outdoors=None %}">
I still really love Django’s automatic database system. It’s amazing to be able to just edit a model to add a new field or whatever, and then Django automatically generates the migration.
So far we have done 19 database migrations and I think there will probably be more! It makes a huge difference for me to be able to just easily change the database as my understanding of the problem changes.
Django’s documentation sometimes offers the option of using class-based views and inheritance to organize the code in your views. For example I have four views that share a lot of code, and I could use inheritance to manage that by defining some kind of parent class and then having my other views inherit from it.
I tried it out and I did not enjoy the experience of using inheritance to share code between views. I switched to using functions instead, sort of how this post advocates, and that was a lot more straightforward. I’ve never had a good experience using inheritance in Python and I don’t think I’ll try to use it again.
But I don’t mind using inheritance to use the interfaces Django itself provides: for
example if I want to define a query set I need to write something like
class EventQuerySet(SearchableQuerySetMixin, models.QuerySet).
I don’t think too hard about it and it seems to work.
(as a meta comment: I’ve been working on talking about my programming opinions by just saying “THING does not feel good to me, I prefer OTHER THING instead”. That post I linked to says that function-based views are the “right way”. I’m not very invested in whether it’s “right”, but it’s validating to know that other people feel similarly to me about inheritance)
At some point the LLM scrapers discovered our site, and started sending us maybe 10 requests per second. I blocked them which is working for now, but it made me think about what the site’s capacity is. I’m used to writing Go backends where the performance situation is pretty straightforward (usually everything is just fast enough), and a Django site is very different.
Some light load testing (with (ab -n 1000 -c 1) shows that right now we can
serve about 2-3 requests per second (on a ~$10/month VM).
It’s tempting for me to go down a rabbit hole where I do a bunch of profiling to figure out what’s slow and try to make it faster (there’s py-spy for that, and py-spy is great and super easy to use, and profiling is fun!) But I really don’t understand what I should expect in terms of performance from a Django site and how I should be thinking about at a higher level.
Some things I haven’t figured out yet:
I think one thing I’m learning about Django is that because it’s a Framework (tm), it’s easy to accidentally misconfigure it. For example, when I was thinking about why my site was slow just now, I read the django performance docs and I noticed a comment saying:
Enabling the cached template loader often improves performance drastically, as it avoids compiling each template every time it needs to be rendered.
Clicking through the link, I saw that the cached template loader was supposed to be on by default, but I’d turned it off by accident while trying to do something else. I think this “I turned off the cached template loader by default” things is an example of how I still find the django settings file to be pretty confusing and difficult. I guess I should just be careful when I go in there.
Anyway I don’t want to get too far into site performance. Like I said it’s easy for me to get interested in profiling, but actually I know a lot about profiling and it’s not the most important thing for me to learn about.
I might say more about what I’m enjoying (or having a hard time with!) about Django later. Trying to write some shorter blog posts recently.