I’m a big believer in developer experience. We can think of programming activity as a series of bigger and smaller feedback loops. There’s a great piece on this on Martin Fowler’s site. Interacting with a project and its codebase is one of such feedback loops. That’s why great discoverability is a must for high performing teams. To improve your codebase discoverability, make sure you have:
- a well-written
README.md
- a clear project directory structure
- conventions like
TODO: refactor this
(good greppability) - scripts with common project actions (automation)
That last part here is interesting. I’ve seen people write scripts like install.sh
, setup.sh
, deploy.sh
, migrate-database-from-environment-x.sh
. I’ve personally written a good bunch of those. The problem is that they’re hard to maintain, and often scattered throughout the repository.
Use Makefile
Makefile
has been around for ages. You can type make
on virtually any workstation (sorry Windows, not you) or server, and it works. And it’s quite powerful as well. Sure, there are some quirks, but the undeniable simplicity of it is what’s so compelling.
The structure is readable, almost like yaml + bash:
command:
cp file some/dir/file
echo "Success!"
You have the whole UNIX toolset at your disposal. It’s often simpler for basic stuff than writing scripting language code (Node.js, Python, Ruby, etc.).
Combining several commands allows printing out great–looking lists of such commands. Think of it as a minimum effort tech for building a CLI for your project.
The UX/DX can be quite good:
- Commands with required params
- Commands with required ENV VARs
- Commands with helpful comments and example usage
A good template to start
Long ago, a friend gave me a great template for such a Makefile
. Many projects later, after using it numerous times, I’ve made a couple of tweaks of my own, and finally put this on GitHub: https://github.com/awinecki/magicfile.
You can add it to your project:
curl https://raw.githubusercontent.com/awinecki/magicfile/main/Makefile > Makefile
And then just run
make
There’s a bunch of examples and interesting things you can do. Check out the file and you’ll quickly get a hang of it.
Tips & Tricks
- Splitting commands on multiple lines is problematic in makefiles, but can be done with
/
- Add
@
in front of a command to prevent make from printing it - Add checks as make required targets
- You can use
make command
in another make command - Use
$(ENV_VAR)
to inject an ENV VAR within a make target ($ENV_VAR
or${ENV_VAR}
might not work) - Use namespaces for similar commands (my repo supports
.
) – for exampledb.init
,db.migrate
- Use parameters for frequently used options and required checks –
make deploy target=production
- Use ENV VARs for more persistent setup –
AWS_PROFILE=mycompanyacc make terraform
Documenting local development
There are many things you can do in a codebase of considerable complexity. Often, there will also be clever hacks & tricks (you know, that magic awk
one-liner that nobody understands but gets the job done). But it’s hard to propagate such knowledge. There’s always documentation and README
, but I find having commands at my fingertips a superior approach.
What to document? Whatever’s useful!
Todos:
todos:
grep -r TODO: .
Common debugging commands:
logs.remote:
heroku logs -t -a my-app
logs.local:
docker-compose logs -f my-service
Deployments & rollbacks:
deploy:
docker build ...
docker tag ...
docker push ...
heroku release ...
rollback:
LAST=<some command to find last good release>
make deploy version=$(LAST)
Local dev commands:
dev:
rsync dir other/dir
npm run build
npm run watch
Hope you find this approach useful. If you have any ideas for improvement or know a project that does this better, please let me know! Feel free to open up a PR on my repo. And for a closing note, here’s an inspiration for how to build great CLI experiences.