This short post explains a useful trick for using Make to run multiple development services at the same time. For example, one of my recent web projects required three active services in order to develop locally:
- Running the Go backend
- Running esbuild to (re)build the React + TypeScript frontend
- Running the Tailwind CSS CLI to re(build) the Tailwind CSS styles
Historically, I’d run each of these programs in a separate terminal window (or use something fancy like tmux). This is a fairly straightforward solution but I always found it somewhat clunky: alt-tabbing between terminals to restart or stop the services.
The Trick Link to heading
If you are using Make to manage your project, you can utilize its builtin concurrency to run each service in parallel.
With only a single terminal and one command, you can start and stop all services effortlessly.
This works as long as you allow Make to run simulataneous “jobs” via the -j
flag.
You also need to structure your Makefile such that each development service has its own target and that these targets are mutually independent (they don’t depend on each other directly or indirectly).
Here is an example from the aforementioned web project:
.PHONY: run-frontend-js
run-frontend-js:
# build and bundle the frontend whenever files change
npx esbuild --watch ...
.PHONY: run-frontend-css
run-frontend-css:
# build Tailwind CSS styles whenever files change
npx tailwindcss --watch ...
.PHONY: run-frontend
run-frontend: run-frontend-js run-frontend-css
.PHONY: run-backend
run-backend:
# run the backend API web server
go run main.go
.PHONY: run
run: run-frontend run-backend
When these targets are invoked via make -j run
, the following chain of events occurs:
run
invokesrun-frontend
andrun-backend
run-frontend
invokesrun-frontend-js
andrun-frontend-css
run-frontend-js
starts the esbuild servicerun-frontend-css
starts the tailwindcss servicerun-backend
starts the backend web service
Within a few seconds, all services will be running simultaneously!
Note that without concurrency, Make would execute until it encounters the first service and try to wait for it to finish. However, since a service never truly finishes execution, none of the others would ever get a chance to start.
Conclusion Link to heading
Ever since discovering this trick, I use it everywhere. For my personal develop workflow, it “just works”. I no longer have to fuss with a bunch of terminals and trying to remember which is which. If you are a fellow Make user (or would like to join the club), give this trick a shot!