A minimal, terminal-first HTTP client in the spirit of
restclient.el: plain-text .http
files are the source of truth, the TUI is a vim-flavored editor and runner
around them. No collections database, no mouse, nothing between you and the
request.
$ req api.http
→ cursor on a request, Enter → response opens in a split, pretty-printed
┌───────────── editor (vim) ───────────────┬──── response ─────────────────┐
│ @base = http://localhost:3000 │ 200 OK · 12ms · 1.2 KB · login│
│ │ { │
│ ### Login │ "token": "…" │
│ # @name login │ } │
│ POST {{base}}/auth/login │ │
│ Content-Type: application/json │ │
│ │ │
│ {"user": "me", "pass": "{{$dotenv PW}}"} │ │
├──────────────────────────────────────────┴───────────────────────────────┤
│ NORMAL │ api.http │ env:local 4:1 │
└───────────────────────────────────────────────────────────────────────────┘
| Key | Action |
|---|---|
Enter |
send the request under the cursor |
Esc |
cancel the in-flight request |
]] / [[ |
next / previous request |
Tab |
cycle focus editor ↔ response (↔ tree) |
ge |
environment picker |
gs |
request picker — fuzzy-pick a request and send it |
gt |
toggle the file/request tree |
gH |
history (Enter re-runs, ctrl-y copies as curl) |
yc |
yank request under cursor as a curl command |
: |
command line |
| vim | hjkl w b e 0 $ gg G i a o dd yy ciw v / n N u ctrl-r … |
Pickers filter as you type (fzf-style subsequence match): arrows or
ctrl-n/ctrl-p move, Enter picks, Backspace widens, Esc closes.
In the response pane: q close, gh toggle headers, gr raw/pretty, /
search, y yank — the pane is a read-only vim buffer.
Commands: :w :q :wq :env <name> :var <name> [value] :send :hist
:curl <paste a curl command> :sav <path> (write response body to a file).
Compatible with the common restclient.el / VS Code REST Client / IntelliJ HTTP Client subset:
@base = http://localhost:3000 # file-level variable
### Login # ### separates requests
# @name login # names it for chaining
POST {{base}}/auth/login
Content-Type: application/json
{"user": "a", "pass": "b"}
### Profile
GET {{base}}/me
Authorization: Bearer {{login.response.body.$.token}} # JSONPath into a named response
### Upload from a file
POST {{base}}/import
Content-Type: application/json
< ./payload.jsonPer-request directives: # @name x, # @timeout 30s, # @no-redirect,
# @no-cookie-jar, # @no-verify-ssl.
Variables resolve in this order: --var CLI overrides (headless send) →
chained captures → file @vars → selected environment → .env file. Builtins: {{$uuid}}, {{$timestamp}},
{{$isoTimestamp}}, {{$randomInt 1 100}}, {{$processEnv NAME}},
{{$dotenv NAME}}. Bare localhost:3000/x URLs work — http:// is implied.
http-client.env.json next to your .http files (VS Code REST Client
compatible), plus an optional http-client.env.json.user overlay for
secrets — gitignore that one:
{
"$shared": { "api_version": "v2" },
"local": { "base_url": "http://localhost:8080" },
"staging": { "base_url": "https://staging.example.com" }
}Switch with ge or :env staging; the last-used environment per project is
remembered across sessions. With exactly one environment defined it is
selected automatically.
:var token abc123 sets a variable in the active environment and writes it
to http-client.env.json (key order preserved); :var token shows the
resolved value. Keys defined in the .user overlay still win — the status
line says so if your new value is shadowed.
~/.config/req/config.toml, overridden by ./req.toml in the project,
overridden per request by # @directives:
timeout = "30s"
follow_redirects = true
verify_tls = true # false: accept self-signed certs everywhere
cookie_jar = true # session-wide cookie jar (in memory)
proxy = "" # explicit proxy; HTTP(S)_PROXY env is respected
[default_headers]
User-Agent = "req"Every executed request (as actually sent, variables resolved) is appended to
a per-project JSONL file under the platform data dir, capped at 10 MB. gH
lists it newest-first; Enter re-runs an entry, y copies it as curl.
Note: headers are stored as sent — including Authorization.
req send api.http --name login --env local | jq .token
req send api.http -i # include status line + headers
req send api.http --fail # exit 22 on HTTP 4xx/5xx (for CI)
req send api.http --var token=abc --var host=dev.local # per-run overrides
req list api.http # request names, one per line
req send api.http -n "$(req list api.http | fzf)" # fuzzy-pick + sendBody goes to stdout, the 200 OK · 12ms · 1.2 KB summary to stderr.
--var name=value beats every other variable source for that run.
Every req list line is a valid --name, including unnamed
METHOD url requests.
cargo build --release # binary at target/release/req
cargo test