Makefiles: The Ancient Tool You Should Still Be Using
Makefiles versus package.json scripts: why Make is still the better, self-documenting task runner in 2026, and an answer to the claim that Make is hard.
Here’s the reality: your package.json scripts are a mess.
I’ve seen it a hundred times. A team starts with a simple npm start. Six months later, it’s npm run build:prod calling npm run lint calling npm run test:unit, all wrapped in a shell script that nobody remembers how to execute.
We’ve normalized this complexity. We’ve accepted that our task runners should be buried inside a JSON file that doesn’t support comments, variables, or basic logic.
Stop it. The solution has existed since 1976. It’s called make.
The Problem with npm run
JSON was never designed to be a task runner. It’s a data format. When you shove complex build logic into a "scripts" block, you’re fighting the tool.
- No Comments: You can’t explain why a flag is there.
- No Variables: You repeat paths and constants everywhere.
- No Dependency Tracking: This is the big one.
npmscripts run blindly. They don’t know if a file actually needs to be rebuilt.
If you change one CSS file, your build script shouldn’t recompile your entire backend. But with npm run, it usually does.
Enter the Makefile
Make is simple. It maps a target to a set of dependencies and a recipe. If the dependencies haven’t changed, the target isn’t rebuilt. That’s it.
Here is a Makefile for a modern TypeScript project. Drop this in your root directory.
# Variables - define them once, use them everywhere
NODE_BIN := ./node_modules/.bin
SRC_DIR := src
DIST_DIR := dist
# .PHONY tells make these aren't real files
.PHONY: all clean test build deploy
all: install build test
# The first rule is the default.
install: package.json
npm install
touch install # Create a marker file to track timestamps
# Only rebuild if source files changed
build: install $(SRC_DIR)/**/*
@echo "Building the project..."
$(NODE_BIN)/tsc
$(NODE_BIN)/vite build
test: install
$(NODE_BIN)/jest
clean:
rm -rf $(DIST_DIR) coverage
deploy: build test
@echo "Deploying to production..."
./scripts/deploy.sh
Why This Wins
1. It’s Self-Documenting
Run make and it does the right thing. Open the file, and you see clear targets with comments. No more hunting through a 50-line JSON object.
2. Intelligent Caching
Look at the build target. It depends on install and $(SRC_DIR)/**/*. If you run make build twice in a row without changing any source files, Make sees that the dependencies are older than the target and does nothing.
This saves seconds on every run. Across a team of 20 engineers, that’s hours per week.
3. Language Agnostic
Your backend is Go? Your frontend is React? Your infrastructure is Terraform?
Great. One Makefile handles it all.
backend:
cd api && go build
frontend:
cd web && npm run build
infra:
terraform apply
Developers don’t need to know the specific incantations for every part of your stack. They just run make backend or make infra.
The “But It’s Hard” Argument
I hear this often: “Make syntax is weird. Tabs vs spaces is annoying.”
Yes, Make requires tabs for indentation. It’s a quirk from the 70s. Configure your editor to handle it and move on. The mental overhead of learning basic Make syntax is infinitesimal compared to the mental overhead of debugging a 200-character single-line shell command inside a JSON string.
Final Thought
Don’t use a hammer to drive a screw. package.json is for dependencies. Shell scripts are for execution. Makefiles are for orchestration.
Next time you find yourself chaining three npm run commands together, create a Makefile. Your future self will thank you.
Related Posts
My Claude Code Setup: Rule Files, Memory Plugins, and Two Personalities
My Claude Code setup: layered CLAUDE.md rule files, a lazy senior dev mode called ponytail, a caveman mode that saves tokens, memory plugins and skills.
This Post Published Itself: Scheduled Posts on a Static Site with One Predicate and a Cron
Scheduled posts on a static Astro site, with no CMS or server. It takes a pure date predicate, a publish filter on every route and a weekly GitHub Actions cron.
Cloudflare Drop: Drag a Folder, Get a Live Site, No Account Required
Cloudflare Drop deploys a static site from a folder or zip, with no account or config. The preview URL stays live for one hour. What it does and where it fits.