Task Runners: Make, Just, Task, and npm Scripts
Task Runners: Make, Just, Task, and npm Scripts
Every project has commands that developers run repeatedly: start the dev server, run tests, build for production, lint the code, deploy. The question is how you document and run them.
README files with "run these 5 commands in order" inevitably become outdated. npm scripts work for JavaScript projects but feel wrong for non-JS tasks. Makefiles are universal but have arcane syntax. Modern alternatives like just and task solve the common complaints.
npm Scripts
Every JavaScript/TypeScript project already has package.json, so npm scripts are the zero-setup option.
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint . --fix",
"test": "vitest",
"test:ci": "vitest run --coverage",
"db:migrate": "drizzle-kit migrate",
"db:studio": "drizzle-kit studio",
"typecheck": "tsc --noEmit"
}
}
npm run dev
bun run dev # Faster with Bun
bun dev # Even shorter — Bun allows dropping "run"
Strengths:
- Zero setup in JS/TS projects
bun run/npm runis familiar to everyone- Pre/post hooks:
pretestruns beforetest,postbuildruns afterbuild - Access to
node_modules/.bin— no need fornpx
Weaknesses:
- JSON doesn't support comments (what does
"db:seed:prod"actually do?) - No parameters or conditional logic without shell escapes
- Awkward for non-JS tasks (database operations, Docker commands, deployment)
- Multi-line commands are unreadable in JSON
Make
Make has been available on every Unix system since 1976. It's the lowest common denominator.
# Makefile
.PHONY: dev build test lint deploy db-migrate
dev:
bun run dev
build:
bun run build
test:
bun test
lint:
bun run lint
# Commands with dependencies — db-migrate runs before seed
db-seed: db-migrate
bun run db/seed.ts
db-migrate:
bunx drizzle-kit migrate
# Parameterized commands
deploy:
@echo "Deploying to $(ENV)..."
./scripts/deploy.sh $(ENV)
# Multi-step with error handling
ci: lint typecheck test build
@echo "All checks passed"
# Commands with environment variables
docker-build:
docker build -t myapp:$(shell git rev-parse --short HEAD) .
make dev
make ci
make deploy ENV=staging
Strengths:
- Available everywhere (no installation)
- Dependencies between tasks (
db-seed: db-migrate) - Parallel execution (
make -j4) - Widely understood
Weaknesses:
- Tab indentation is required (spaces silently break things)
- Designed for file-based build systems, not task running —
.PHONYeverywhere - Variable syntax is confusing (
$(VAR),${VAR},$VARall behave differently) - Error messages are cryptic
- No built-in help/list command
just
just is a command runner that looks like Make but is designed specifically for running commands, not building files. It fixes Make's biggest pain points.
# justfile
# List available commands
default:
@just --list
# Development server
dev:
bun run dev
# Run all checks (CI simulation)
ci: lint typecheck test build
@echo "All checks passed"
# Run tests with optional filter
test filter="":
bun test {{filter}}
# Database commands
db-migrate:
bunx drizzle-kit migrate
db-seed: db-migrate
bun run db/seed.ts
db-reset:
bunx drizzle-kit drop
bunx drizzle-kit migrate
bun run db/seed.ts
# Deploy with required environment parameter
deploy env:
#!/usr/bin/env bash
set -euo pipefail
echo "Deploying to {{env}}..."
./scripts/deploy.sh {{env}}
# Docker build with git SHA tag
docker-build:
docker build -t myapp:$(git rev-parse --short HEAD) .
# Load environment from .env file
set dotenv-load
# Choose shell (bash with strict mode)
set shell := ["bash", "-euo", "pipefail", "-c"]
brew install just
just # Shows available commands
just dev
just test # Runs all tests
just test auth # Runs tests matching "auth"
just deploy staging
Strengths:
- Spaces or tabs (doesn't matter!)
- Built-in
--listcommand with descriptions - Typed parameters with defaults
set dotenv-loadreads.envfiles- Multi-line scripts with shebang support
- Cross-platform (works the same on Linux, macOS, Windows)
Weaknesses:
- Requires installation (not pre-installed like make)
- Different syntax from Make (team needs to learn it)
- Less ecosystem support (fewer examples and Stack Overflow answers)
Task (go-task)
Task uses YAML configuration and has built-in features for watching files, running tasks conditionally, and parallel execution.
# Taskfile.yml
version: '3'
dotenv: ['.env']
tasks:
default:
desc: Show available tasks
cmds:
- task --list
dev:
desc: Start development server
cmds:
- bun run dev
test:
desc: Run tests
cmds:
- bun test {{.CLI_ARGS}}
lint:
desc: Lint and format code
cmds:
- bun run lint
ci:
desc: Run all CI checks
deps: [lint, typecheck] # Run lint and typecheck in parallel
cmds:
- task: test
- task: build
db:migrate:
desc: Run database migrations
cmds:
- bunx drizzle-kit migrate
db:seed:
desc: Seed database
deps: [db:migrate]
cmds:
- bun run db/seed.ts
build:
desc: Build for production
cmds:
- bun run build
sources:
- src/**/*.ts
- package.json
generates:
- dist/**/*
watch:test:
desc: Run tests on file change
watch: true
sources:
- src/**/*.ts
- test/**/*.ts
cmds:
- bun test
brew install go-task
task # Shows available tasks
task dev
task ci
task test -- --verbose # Pass args through
task watch:test # Watch mode
Strengths:
- YAML is widely known and IDE-supported
- Built-in watch mode (re-run on file changes)
- Parallel task execution with
deps - Source/generate tracking (skip tasks when inputs haven't changed)
--dryflag shows what would run without executing
Weaknesses:
- YAML can be verbose for simple commands
- Requires installation
- Namespacing with colons (
:) can look noisy
Comparison
| Feature | npm scripts | Make | just | Task |
|---|---|---|---|---|
| Installation | Built-in (JS) | Built-in (Unix) | Required | Required |
| Config format | JSON | Makefile | justfile | YAML |
| Parameters | Awkward | Yes | Yes (typed) | Yes |
| Dependencies | Pre/post hooks | Yes | Yes | Yes (parallel) |
| Watch mode | No (need nodemon) | No | No | Yes |
| Comments | No (JSON) | Yes | Yes | Yes |
| List commands | No | No | --list |
--list |
| .env loading | No | No | Built-in | Built-in |
Recommendations
JavaScript/TypeScript projects: Start with npm scripts for standard commands (dev, build, test, lint). Add just or task when you need database commands, Docker operations, or deployment scripts that don't fit naturally in package.json.
Multi-language projects: just or Task. Both handle polyglot projects well. just is simpler; Task is more powerful (watch mode, parallel deps, source tracking).
Open source projects: Make. It's everywhere and contributors don't need to install anything. Accept the quirks — the universality is worth it.
Personal preference: If you hate tabs-vs-spaces issues, use just. If you want YAML and watch mode, use Task. If you want zero dependencies, use Make.
The best task runner is the one your team will actually use. Pick one, document your commands, and move on.