When debugging an API endpoint, checking service latency, or writing integration tests, desktop graphical interfaces add operational friction. Developers must switch contexts, navigate complex menus, and cannot easily automate sequences or store requests in Git version control systems. Terminal API clients—curl, HTTPie, and Bruno CLI—provide alternative methods for interacting with HTTP APIs, representing different tradeoffs between baseline ubiquity and developer experience.
Understanding the Network Metrics of API Requests
When evaluating API performance in the terminal, it is helpful to analyze the distinct phases of an HTTP connection. A request is not a single transaction; it is a sequence of network operations:
- DNS Lookup (
time_namelookup): The time required to contact DNS servers and translate the hostname into an IP address. - TCP Connection (
time_connect): The duration of the three-way handshake required to establish a TCP channel between the client host and the remote server. - TLS/SSL Handshake (
time_appconnect): The time required to exchange certificates, negotiate cipher suites, and establish cryptographic tunnels. - Time to First Byte (TTFB -
time_starttransfer): The duration from connection initialization until the remote web server returns the first byte of response data, representing server-side processing latency. - Content Transfer (
time_totalminustime_starttransfer): The time required to download the full payload over the established socket network.
Connection Timeline:
├─► DNS Lookup (time_namelookup)
│ ├─► TCP Handshake (time_connect)
│ │ ├─► TLS Handshake (time_appconnect)
│ │ │ ├─► TTFB / Server Processing (time_starttransfer)
│ │ │ │ ├─► Content Download (time_total)
Understanding these latency boundaries allows you to isolate performance issues. If a response is slow, checking these metrics reveals whether the latency is caused by a slow database query (high TTFB) or a slow network connection (high content transfer time).
curl: The Universal Diagnostic Client
curl is installed by default on Unix systems and is universally available in shell scripts. Its flags can be verbose, but it offers precise control over connection parameters and protocol features.
Installation
# macOS via Homebrew (installs updated versions with HTTP/3 support)
brew install curl
# Ubuntu/Debian Linux environments
sudo apt install curl
# Check active protocols and version strings
curl --version
Common Request Patterns
# GET request with Bearer authentication and JSON output formatting
curl -sS \
-H "Authorization: Bearer $API_TOKEN" \
-H "Accept: application/json" \
https://api.example.com/users | jq .
# POST request passing a JSON payload
curl -sS -X POST \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Alice Chen", "email": "alice@example.com", "role": "admin"}' \
https://api.example.com/users | jq .
# POST request performing a multipart file upload
curl -sS -X POST \
-H "Authorization: Bearer $API_TOKEN" \
-F "file=@./report.pdf" \
-F "description=Monthly report" \
https://api.example.com/uploads
# PUT request loading JSON payload from a local file
curl -sS -X PUT \
-H "Content-Type: application/json" \
-d @payload.json \
https://api.example.com/users/42
# GraphQL query execution
curl -sS -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $API_TOKEN" \
-d '{"query": "{ users { id name } }"}' \
https://api.example.com/graphql | jq '.data.users'
# Force HTTP/2 protocol mapping
curl --http2 -sS https://api.example.com/status
HTTPie: Ergonomic Command Line Testing
HTTPie provides an ergonomic alternative to curl. It includes automatic JSON serialization, syntax-highlighted responses, and intuitive request parameters by default.
Installation
# macOS via Homebrew
brew install httpie
# Linux environments
sudo apt install httpie
# Check installation status
http --version
Common Request Patterns
# GET request with automated response highlighting
http https://api.example.com/users \
Authorization:"Bearer $API_TOKEN"
# POST request with JSON parameters (HTTPie defaults to JSON content)
http POST https://api.example.com/users \
Authorization:"Bearer $API_TOKEN" \
name="Alice Chen" \
email="alice@example.com" \
role="admin"
# POST request with nested JSON variables using raw value assignments (:=)
http POST https://api.example.com/users \
name="Alice Chen" \
metadata:='{"source": "cli", "verified": true}' \
tags:='["admin", "billing"]'
# Multipart file upload
http -f POST https://api.example.com/uploads \
Authorization:"Bearer $API_TOKEN" \
file@./report.pdf \
description="Monthly report"
# Show request and response headers only
http -h GET https://api.example.com/users
Bruno CLI: Version-Controlled Collections
Bruno stores API requests in folders as human-readable .bru files, making them easy to track in Git, review in pull requests, and run in CI/CD pipelines via @usebruno/cli.
Installation
# Install the Node.js runner utility globally
npm install -g @usebruno/cli
Example .bru Request Declaration
meta {
name: Create User
type: http
seq: 1
}
post {
url: {{baseUrl}}/users
body: json
auth: bearer
}
auth:bearer {
token: {{authToken}}
}
headers {
Content-Type: application/json
}
body:json {
{
"name": "{{userName}}",
"email": "{{userEmail}}",
"role": "member"
}
}
assert {
res.status: eq 201
res.body.id: isDefined
}
Running Collections in Headless Mode
# Execute all requests inside the users directory
bru run users/ --env local
# Generate test results report in JUnit XML format for CI engines
bru run api-tests/ --env staging --reporter junit --output report.xml
Tool Feature Matrix
| Functional Metric | curl CLI | HTTPie Client | Bruno CLI runner |
|---|---|---|---|
| Command Verbosity | High (Requires manual header flags) | Low (Implicit formats and parameters) | Low (Stored inside config files) |
| JSON Serialization | Manual serialization required | Automatic JSON structures parsing | Built-in UI and file mapping |
| Git Compatibility | Shell scripts only | Shell scripts only | Native .bru text files |
| Header Printing | Needs -i or -v flags | Printed by default | Printed inside test reports |
| Environment Sets | Managed via shell variables | Managed via shell variables | Native .bru environment files |
What Breaks in Production: Failure Modes and Mitigations
Using terminal API clients in local scripts and CI pipelines introduces several runtime risks.
1. Shell Parameter Expansion Breaks JSON Syntax
When executing API calls containing inline JSON strings from Zsh, Bash, or PowerShell, double and single quoting rules vary across operating systems.
- Failure Mode: Declaring a payload like
'{"name": "$USER"}'works on macOS/Linux, but Windows CMD parses the single quotes literally, sending corrupted JSON payload strings to the server and causing a 400 Bad Request error. - Mitigation: Use local payload files with the
-d @filename.jsonsyntax incurl, or letHTTPiehandle serialization natively using parameter assignments (name="$USER").
2. Secret Exposure inside Shell History Logs
Executing commands with sensitive tokens inline (such as passing auth keys in the header) writes the secrets to the system history logs.
- Failure Mode: Tokens are saved in plain text in
~/.zsh_historyor~/.bash_history, where they can be read by other processes or exposed in system backups. - Mitigation: Use environment variables (e.g.,
Authorization: Bearer $SECRET_TOKEN) instead of writing raw keys, or load variables dynamically from files using configuration managers.
3. Buffered Outputs Block EventStreams (SSE)
When checking streaming endpoints (such as Server-Sent Events or WebSockets connections), curl and HTTPie buffer network packets locally to optimize write operations.
- Failure Mode: The terminal remains blank, and events are not displayed in real time. The client only prints output when the buffer fills up or the connection closes, making it difficult to debug live streams.
- Mitigation: Disable write buffering. Pass
-Nor--no-bufferincurl, or runHTTPiewith the--streamflag.
4. Silent Connection Success on Error Status Codes
In automated integration check scripts, developers use curl commands to query health check endpoints.
- Failure Mode: If the server fails and returns an HTTP
500 Internal Server Error,curlstill exits with status code0because a connection was successfully established. This allows failing builds to pass in CI pipelines. - Mitigation: Enforce exit status errors on non-2xx codes. Use
-for--failincurlto return exit status 22 on server errors, or runhttpwith--check-statusinHTTPie.
Frequently Asked Questions
How do I prevent curl from saving API tokens to my bash or zsh history files?
You can execute your terminal commands with a leading space (e.g., curl -H "Authorization..."). Most default Unix shells are configured to skip logging commands that start with a space to prevent writing sensitive credentials to history logs.
Why does my curl command hang when testing an HTTP/3 endpoint locally?
Most standard, pre-installed system curl binaries do not support HTTP/3 or QUIC. Attempting to force connections using --http3 on an unsupported binary will fail or fallback to slower TCP protocols. You must compile curl with SSL engines like quiche or ngtcp2 to enable HTTP/3 support.
How can I make curl fail with a non-zero exit code when the API returns a 500 error?
Pass the --fail or -f flag to the command. This tells curl to terminate and return exit code 22 if the server returns an HTTP status code greater than or equal to 400.
What is the easiest way to inspect raw HTTP request headers sent by HTTPie?
Run your request with the --print=Hh flag (or -p Hh). This instructs HTTPie to output both the request headers (H) and the response headers (h) directly to the console before printing the response body.
How do I use client certificates for SSL authentication in curl and HTTPie?
For certificate-based authentication, pass the certificate and key files directly. In curl, use the --cert and --key flags:
curl --cert client.crt --key client.key https://api.example.com/secure
In HTTPie, use the --cert and --cert-key flags:
http --cert=client.crt --cert-key=client.key https://api.example.com/secure
Can I mock requests with custom User-Agent headers to debug CDN routing issues?
Yes. CDNs and load balancers often route traffic or apply rate limits based on user-agent strings. You can override the default headers easily. In curl, pass the -A flag:
curl -A "CustomCDNTestSuite/1.0" https://api.example.com/data
In HTTPie, override the User-Agent key directly:
http https://api.example.com/data User-Agent:"CustomCDNTestSuite/1.0"
Wrapping Up
curl remains the standard client for universal compatibility in shell environments. HTTPie provides an ergonomic developer experience, making it a good choice for manual API exploration. Bruno CLI is designed for team environments, allowing collections to be stored in version control and executed headless in CI pipelines.