Bekzod Erkinov
Listen to Article
Loading...Table of contents · 9 sections
5 Essential Developer Tools Worth Installing, Measured on a Real Repo
rg returned the same 183 matches as git grep in 51 ms versus 87 ms. Plain grep -r took 3.5 s and returned 228, because it also searched node_modules. Below: ripgrep, fzf, jq, HTTPie and tmux, run against the nestjs/nest repo, with what each one gave me and when it's worth installing. Only ripgrep gets a real benchmark; fzf gets a timing, tmux a survival test, jq and HTTPie character counts.
| Tool | Licence | Free alternative |
|---|---|---|
| ripgrep | MIT / Unlicense | git grep, grep |
| fzf | MIT | IDE file switcher, bash Ctrl-R |
| jq | MIT | node -e, python -m json.tool (format only) |
| HTTPie CLI | BSD-3-Clause | curl |
| tmux | ISC | GNU screen, nohup |
All five are free. The only paid product I mention is Bruno Pro, in the HTTPie section.
How I tested the developer tools
One throwaway Docker container (node:22-bookworm, Debian 12) on a Windows laptop. The repo is shallow-cloned at commit 35142c3, then npm install --ignore-scripts --legacy-peer-deps (a plain npm install --ignore-scripts exits 1 with npm error code ERESOLVE while resolving @apollo/[email protected] against [email protected]). Result: 2,468 tracked files plus 51,281 files and 593 MB in node_modules.
Everything came from Debian's apt, which lags upstream: ripgrep 13.0.0, fzf 0.38.0, tmux 3.3a, jq 1.6, HTTPie 3.2.1, hyperfine 1.15.0, GNU grep 3.8. Upstream is newer (ripgrep 15.2.0, fzf 0.74.4, jq 1.8.2, tmux 3.7c) and I ran only upstream fzf 0.74.4 (see the fzf section), none of the others. These are single-machine numbers from one repo, not a general benchmark.
Best CLI tools for developers, #1: ripgrep vs grep vs git grep
ripgrep (rg) respects .gitignore and skips hidden and binary files by default. One hyperfine session, all five commands, searching for useFactory:
hyperfine --warmup 3 --runs 20 -N 'grep -rn useFactory .' \
'grep -rn useFactory --exclude-dir=node_modules --exclude-dir=.git .' \
'git grep -n useFactory' 'rg -n useFactory' 'rg -n -uuu useFactory'
| Command | Mean ± σ | Relative | Matches |
|---|---|---|---|
grep -rn useFactory . |
3.518 s ± 0.814 | 69.1 | 228 |
grep -rn … --exclude-dir=node_modules --exclude-dir=.git . |
95.4 ms ± 20.5 | 1.87 | 183 |
git grep -n useFactory |
87.1 ms ± 18.7 | 1.71 | 183 |
rg -n useFactory |
50.9 ms ± 10.1 | 1.00 | 183 |
rg -n -uuu useFactory (ignore nothing) |
594.6 ms ± 85.9 | 11.68 | 228 |
Match counts came from piping each command to wc -l:
$ grep -rn useFactory . | wc -l
228
$ git grep -n useFactory | wc -l
183
$ rg -n useFactory | wc -l
183
$ rg -n -uuu useFactory | wc -l
228
$ grep -rn useFactory . | cut -d/ -f2 | sort | uniq -c | sort -rn
118 packages
56 integration
45 node_modules
9 sample
All 45 extra matches are in node_modules (every one under node_modules/@nestjs). None are in .git: the shallow clone's objects are compressed, so a plain-text search doesn't see them.
What the table says:
- The 69x gap is mostly not reading 593 MB of dependencies. With
rg -uuuit shrinks to 11.7x, and it still returns the 228 matches. - Against a fair
grep(same exclusions) orgit grep, ripgrep is about 1.7 to 1.9x faster. The standard deviations (±10 to ±20 ms) overlap those means, so read that as "somewhat faster", not a precise ratio. git grepalready skipsnode_modulesand ships with Git. What ripgrep adds is ergonomics:-tfile-type filters, one tool for tracked and ignored files, and-uuuwhen you want everything.
Install it if you ever type --exclude-dir=node_modules. For small repos, grep is fine.
Gotcha: rg silently skips anything you've gitignored (a generated client, a local .env). If a search can't find something that exists, retry with rg -uuu.
fzf: fuzzy file search and shell history
fzf is a fuzzy finder. With upstream 0.48 or newer, eval "$(fzf --bash)" (zsh: source <(fzf --zsh)) turns on shell integration. Debian's 0.38.0 rejects it (unknown option: --bash); there, use source /usr/share/doc/fzf/examples/key-bindings.bash. Once enabled, Ctrl-T pastes a file path, Ctrl-R searches history and Alt-C jumps to a directory. I can't show the interactive UI in a batch run, so I used --filter, which runs the same matcher non-interactively:
$ git ls-files | fzf --filter "mdlmodts" | head -5
packages/core/middleware/middleware-module.ts
integration/lazy-modules/src/lazy.module.ts
packages/core/test/middleware/middleware-module.spec.ts
integration/scopes/src/durable/durable-cross-module.modules.ts
sample/07-sequelize/src/users/models/user.model.ts
Eight typed characters, skipping letters, put the file I wanted first out of 2,468. The whole command, including process start, took 21 ms (time, one run). I also downloaded upstream fzf 0.74.4 and ran the same --filter: identical top five (same md5sum), 19 ms. I only checked this filter, not the interactive UI or key bindings. Ranking gets loose after the first hit (the fifth result is user.model.ts): fuzzy matching returns more than you asked for and you pick visually.
It composes with ripgrep: rg -l useFactory | fzf --filter appmod | head -3 returned sample/37-outbox/src/app.module.ts first, then two integration/microservices app modules.
Install it if you re-run commands from history or open files by name in a terminal. If you live in an IDE with "go to file", you already have this.
jq: filter JSON on the command line
jq filters JSON on the command line. To make the next two sections copy-paste runnable, here is the whole test server (Node, no dependencies). Save it as server.js and run node server.js:
const http = require('http');
http.createServer((req, res) => {
let body = '';
req.on('data', c => body += c).on('end', () => {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
method: req.method, path: req.url, auth: req.headers.authorization,
received: body ? JSON.parse(body) : null,
items: [{ id: 1, name: 'a', tags: ['x'] }, { id: 2, name: 'b', tags: [] }, { id: 3, name: 'c', tags: ['y', 'z'] }],
}));
});
}).listen(8080);
Items with at least one tag, jq versus a Node one-liner:
$ http --ignore-stdin -b localhost:8080/x "Authorization:Bearer s3cret" | jq -c '.items[] | select(.tags|length>0) | {id,name}'
{"id":1,"name":"a"}
{"id":3,"name":"c"}
$ curl -s localhost:8080/x | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>JSON.parse(s).items.filter(i=>i.tags.length).forEach(i=>console.log(JSON.stringify({id:i.id,name:i.name}))))'
{"id":1,"name":"a"}
{"id":3,"name":"c"}
Identical output. Counted with wc -c: the jq filter alone is 45 characters and the full jq -c '…' command is 53. The Node one-liner is 163 characters, 173 with node -e '…' around it.
Install it if you explore API responses or write CI scripts. Once the logic has branches or needs tests, move to a real script.
HTTPie vs curl: a shorter way to call an API
HTTPie CLI (http) is a curl alternative. The same POST to the server above, both ways:
curl -s -X POST localhost:8080/api/users -H "Authorization: Bearer s3cret" -H "Content-Type: application/json" -d "{\"name\":\"Ada\",\"role\":\"admin\"}"
http POST localhost:8080/api/users "Authorization:Bearer s3cret" name=Ada role=admin
That's 153 characters against 84 (counted with wc -c). HTTPie sends name=Ada role=admin as JSON and sets the content type itself, so there's no escaped-quote soup. With --print=hb the response was:
HTTP/1.1 200 OK
Content-Type: application/json
Date: Wed, 07 Oct 2026 10:16:12 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 211
{"method":"POST","path":"/api/users","auth":"Bearer s3cret","received":{"name":"Ada","role":"admin"},"items":[{"id":1,"name":"a","tags":["x"]},{"id":2,"name":"b","tags":[]},{"id":3,"name":"c","tags":["y","z"]}]}
Gotchas I hit:
- When stdin isn't a terminal (CI,
docker exec, piped input), HTTPie reads it as the body and refuses to mix it withkey=value: "Request body (from stdin, --raw or a file) and request data (key=value) cannot be mixed. Pass --ignore-stdin to let key/value take priority." Add--ignore-stdinin scripts. - Piped output isn't colourised or pretty-printed by default. Pass
--pretty=all. --session=dev -A bearer -a s3cretsaved the credentials, and a second call with only--session=devstill sentBearer s3cret(checked through the server'sauthfield).
Cost. The CLI is free (BSD-3-Clause; 3.2.4 is listed as latest stable on the install page, I ran 3.2.1). If you want a GUI client with requests stored in your repo, Bruno has a free open-source tier. Per its pricing page on 2026-10-07 (annual billing), Pro is $6/user/month and includes native Git for clone, init, diff, pull, and add and commit. Push, branch creation and checkout, remote connection, stash and merge-conflict resolution need Ultimate at $11/user/month. I didn't run Bruno. Start with the free tier.
tmux over SSH: keep long jobs alive
tmux keeps programs running after your terminal goes away. This time I tested the real thing, in the same container with a local sshd: start a job in tmux over SSH, kill -9 the SSH client, check the job, then reattach.
$ ssh -tt localhost tmux new-session -s job2 'bash /tmp/job.sh' # job.sh appends "tick N <time>" to /tmp/job2.log every 0.5 s
$ tail -1 /tmp/job2.log
tick 4 10:23:09
$ pkill -9 -x ssh # the SSH client is gone
$ pgrep -c -x ssh
0
$ tmux ls
job2: 1 windows (created Wed Oct 7 10:23:08 2026)
$ tail -1 /tmp/job2.log # right after the kill
tick 5 10:23:10
$ tail -1 /tmp/job2.log # 3 s later
tick 11 10:23:13
$ ssh -tt localhost tmux attach -t job2 &
$ tmux list-clients -t job2
/dev/pts/0: job2 [80x24 xterm-256color] (attached,focused)
$ tail -1 /tmp/job2.log
tick 15 10:23:15
The job kept ticking with no client, and tmux attach -t job2 reattached to the same session. In my harness the ssh command ran in the background with a sleep piped into it so it had a tty to attach to; interactively you just type the commands. This was one kill -9 of the client, not a flaky network, but it exercises the same path: the process lives under the tmux server, not your login shell.
Install it if you run migrations, builds or log tails over SSH. Locally, terminal tabs and splits do most of the work. nohup cmd & also survives a disconnect, but gives you no way to reattach to a live view. I ran tmux 3.3a, not 3.7c, and didn't check what changed between them.
Which of the five to install first
| Your situation | Install first | Why |
|---|---|---|
Big node_modules or many files, searching from the terminal |
ripgrep | git grep is close behind; rg adds file types and -uuu |
| You work on remote servers over SSH | tmux | a dropped connection no longer kills the job |
| Heavy shell history, no IDE | fzf | Ctrl-R and Ctrl-T replace retyping |
| You script around JSON APIs or CI | jq, then HTTPie | jq for filtering, HTTPie for readable calls |
| Team already shares requests in a GUI client | skip HTTPie | requests stored in the repo suit a team better |
What I left out: a database client
A full-stack setup usually has one, but I didn't test a DB client in this container, so I won't give you numbers. For Postgres, start from the built-in psql, and see Postgres JSONB vs separate tables for queries worth running.
What to do next
- Run
rg -n yourFunctionin your biggest repo and compare it with your currentgrep -rn. Re-run the table above on your own repo withhyperfine: yournode_modulesratio decides whether it matters. - Install fzf, add the one-line shell integration to your
.bashrcor.zshrc, and use Ctrl-R for a day. - Replace one
curl -H ... -d '{...}'in your README or scripts with anhttpcall, adding--ignore-stdinif it runs in CI. - Next time you run something long over SSH, start it with
tmux new -s name.
Related on nextgenbeing.com: Docker layer caching strategies that actually work and Dockerizing a Laravel app.
Keep reading
Bekzod Erkinov
AuthorFounder of NextGenBeing. Software engineer working with Laravel, Python, and cloud infrastructure. Writes about patterns that actually hold up in production. Based in Tashkent, Uzbekistan.
Get the AI-Assisted Developer's Field Guide
The workflow, prompts, and tools I use to ship faster with AI — free when you subscribe. Plus new deep-dives in your inbox. No spam, unsubscribe anytime.
Comments (0)
Please log in to leave a comment.
Log In