What are your do's and don'ts when starting a new Django project? Without going into specifics of the project, can you share apps you'd use and general best practices like "create your own user model" etc? I understand that your business is based around offering this advice for money, but maybe there are already great blog posts or cookiecutter templates you could point to?
Hello Christian!
Thanks for the great question. One of the things I love about this Ask Frank series is you all are asking a lot of the quetions I’ve been meaning to write about for a long time and this helps give me a push to actually do it.
As you can imagine, every project is different and you should avoid prematurely optimizing for things you aren’t sure to need, but here are a few things we always do.
We have a private copier template we use at work that I sadly can’t share with everyone, but all of these tips are in there.
Custom User model
We always start off with users.User as our User model which is a “custom” but basically standard Django User model.
The only thing we really add is a data = models.JSONField(default=dict) to ensure we can add a bit of preferneces to a user without
necssarily needing a database migration right away.
To be clear, we MOSTLY define actual fields on the User model, but it can be handy to have this field in large deployment sitautions
where adding a field is a bit tricker and this is a premeptive escape hatch of sorts.
One Settings File
We have config/settings.py. I know many people who use multiple settings files, importing from a common.py or base.py and that
might be our fault. We pushed that pattern A LOT years ago and it served us fairly well, but over time we found it to be harder to
reason about what was set where than anything.
In our Docker based world, environment variables make much more sense and provide more flexibility both locally and in deployments. So
everything lives in config/settings.py.
NOTE: We’ve also abandoned the practice of having our “Django project” live in a directory named after the project. So for say the revsys.com website we might have previously had a structure like:
src/
revsys.com/
revsys/
settings.py
urls.py
wsgi.py
users/
models.py We’ve moved to having ALL of our “projects” but named config, because that is a better description of what lives in there. And this makes
it the same for us across all of our projects. No more wondering, did we name the project revsys or rs or site or what?
We use django-environ configured to load a .env file. This allows for really flexible
local customization between a developer’s .env file and Docker Compose overlays to pretty much allow you to customize your local environment
as much as without Docker.
Anytime you find yourself itching to use a settings overlay, you just need to add another env var and set the default to be whatever was there previously and your co-workers don’t even need to be enformed of the change.
Justfile
We also have a standarized set of just recipes we always use. Things that standaridize copying .env-dist to .env
when setting up a project for the first for example.
We also bake in useful things like just manage which will run a manage.py command inside of the right docker container. just shell to
get a bash shell in that same container to poke around, use an IPython REPL, etc.
It’s handy to have things like just makemigrations and just migrate, things you do a lot to remove a little bit of friction.
Useful packages
As a team you come to agree and like certain packages and use them often. So we always start off with most of these:
- django-db-geventpool to make our ORM calls async
- django-click and django-typer so we build lots more and nicer to use management commands
- django-ninja for our APIs
- django-structlog for good structured logging
- django-extensions for things like
TimeStampedModelandshell_plus - django-hijack to make it trivial to switch between users locally
- pytest and pytest-django for obvious reasons
- model-bakery to make building pytest fixtures simpler
Consistency is Key
It is going to sound weird, but consistency is a HUGE deal in a code base. No, it’s a bigger deal than you think. Yeah, bigger than that even!
Are you going to have templates in a common templates/... folder structure? Or use <app>/templates/<app>/... style? Or a mixture of both?
PICK ONE and stick with it across all projects.
Are app names plural and models singular? Good. Reverse is also fine as long as it’s 100% of the time always the same. Yeah even
when in that one made up special case you just made in your head sounds weird or funny. Even then! Yes even for moose.Moose should you
ever find yourself writing US National Park software. Even then.
Do you have a top level static/ folder for static assets that are presented on a /static/ URL and deployed into a deployed_static/ folder?
Great, do it that way in EVERY project.
The 10-15 minutes each you spend debugging silly inconsistencies like this add up. It is not even that time as much as the time
lost context switching from what you were really working on. You aren’t working on “static assets” you’re adding another bit of JS to the mix and
now you’ve dropped entirely out of flow on that task to trace through your static file settings to realize you co-worker called it images/ this time.
Reinvent the wheel in other cooler places in your code base, not in the core of the group’s understanding of the structure of their world.
devdata
You’re going to need local data for manual testing of your app. Don’t make this a second class citzen by doing it haphazardously in your test fixtures. This is an opportunity to reap amazing dividends with a little bit of discipline from the start.
I’ve written about crafting developer data for testing and how much of a development velocity boost it can be.
You can construct a few builder functions that can be used by your ./manage.py devdata <scenario> command AND your pytest fixtures so they can play
double duty.
This also has the benefit of having some consistency and story, if you will, around this test data. You see [email protected] in a test and you
remember from your local testing that is the head hancho, aka is_superuser=True, of the Park Service because you’ve hijacked him a million times as
you’ve built out features. All because you’re reusing that persona in the same way in both.
Developer Experience Bits
I suppose all of these are developer experience, but I needed a header…
You want CI/CD as early as possible. Optiminally, on day one or two. If it’s week two and you don’t have CI? Stop what you’re doing and set it up right now.
You aren’t saving time, you’re actively wasting it.
You aren’t working on something more important. There is nothing more important.
You want something like pre-commit ensuring that your team has their ruff and ty configurations
set up correctly and to enforce any other rules you might have to keep everyone honest and providing
no surprises.
What about AI and LLMs?
Guess what is great about all of these things I’ve listed above?
LLMs love them!
If you’re following alone we’ve now:
- List of commonly needed and useful commands documented in a
Justfile - One place for settings and one way to set them
- Clear rules around formatting, structure, etc that you can bake into a
AGENTS.mdfile, skills, and prompts. - Code/data isolation in Docker
- Example data from your
devdataand pytest fixtures - Tests! Lots of tests!
- CI to allow your LLM of choice to run loose and have confidence it’s not actually letting a moose run roughshod through your code.
Hope you found all of these tips useful! Keep bringing the great questions!
P.S. What’s up with the moose? It was just the first thing I thought of that is both plural and singular and the last time I saw one in person was at a National Park.