Makefiles: The Ancient Tool You Should Still Be Using
Stop burying your build logic in package.json scripts. Discover why Makefiles remain the superior, self-documenting choice for modern task orchestration in 2026.
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
The LinkedIn URL trick nobody talks about
LinkedIn's job search UI fights you at every step. But their URL parameters are surprisingly powerful - so I built SnipeDin to surface them.
My Claude Code Setup: Rule Files, Memory Plugins, and Two Personalities
A tour of my actual Claude Code configuration: layered CLAUDE.md rule files, a lazy-senior-dev mode called ponytail, a token-saving caveman mode, persistent memory plugins, and the skills that enforce discipline. With honest notes on what actually matters.
PgBouncer: Stop Drowning Your Database in Connections
How to use PgBouncer to manage PostgreSQL connections efficiently