5 Essential Developer Tools Worth Installing, Measured on a Real Repo - NextGenBeing
Back to discoveries

5 Essential Developer Tools Worth Installing, Measured on a Real Repo

Five essential developer tools (ripgrep, fzf, jq, HTTPie, tmux) tested on a real repo: timings, licences, free alternatives and which to install first.

AI Tutorials 7 min read
Bekzod Erkinov

Bekzod Erkinov

May 4, 2026 • 161 views
Size:
Height:
7 min read 2,605 words Focus mode: Eye care:

Listen to Article

Loading...
0:00 / 0:00
0:00 0:00
Low High
0% 100%
⏸ Paused ▶️ Now playing... Ready to play ✓ Finished
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 -uuu it shrinks to 11.7x, and it still returns the 228 matches.
  • Against a fair grep (same exclusions) or git 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 grep already skips node_modules and ships with Git. What ripgrep adds is ergonomics: -t file-type filters, one tool for tracked and ignored files, and -uuu when 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 with key=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-stdin in scripts.
  • Piped output isn't colourised or pretty-printed by default. Pass --pretty=all.
  • --session=dev -A bearer -a s3cret saved the credentials, and a second call with only --session=dev still sent Bearer s3cret (checked through the server's auth field).

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

  1. Run rg -n yourFunction in your biggest repo and compare it with your current grep -rn. Re-run the table above on your own repo with hyperfine: your node_modules ratio decides whether it matters.
  2. Install fzf, add the one-line shell integration to your .bashrc or .zshrc, and use Ctrl-R for a day.
  3. Replace one curl -H ... -d '{...}' in your README or scripts with an http call, adding --ignore-stdin if it runs in CI.
  4. 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.

Bekzod Erkinov

Bekzod Erkinov

Author

Founder of NextGenBeing. Software engineer working with Laravel, Python, and cloud infrastructure. Writes about patterns that actually hold up in production. Based in Tashkent, Uzbekistan.

Free guide

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

Related Articles

Don't miss the next deep dive

Get one well-researched tutorial in your inbox each week. No spam, unsubscribe anytime.