← All articles
TESTING API Testing Tools Guide: Bruno, Hoppscotch, HTTPie, ... 2026-02-15 · 10 min read · api · testing · bruno

API Testing Tools Guide: Bruno, Hoppscotch, HTTPie, and curl

Testing 2026-02-15 · 10 min read api testing bruno hoppscotch httpie curl rest http

API Testing Tools Guide: Bruno, Hoppscotch, HTTPie, and curl

The era of Postman as the default API client is over. Between cloud-gated features, mandatory accounts, and increasingly aggressive upselling, developers have moved on. The alternatives are better in almost every way: faster, open source, git-friendly, and designed for how developers actually work. This guide covers four tools that cover the spectrum from GUI clients to command-line utilities, with practical examples for each.

Bruno API client logo

Quick Comparison

Feature Bruno Hoppscotch HTTPie curl
Type Desktop GUI Web + Desktop CLI + Web CLI
Open source Yes (MIT) Yes (MIT) Partially (CLI is OSS) Yes
Storage Filesystem (git-friendly) Cloud or local History file None (stdin/stdout)
Scripting JavaScript JavaScript N/A Bash
Environment management Yes (.env files) Yes (GUI) Yes (sessions) Manual (variables)
Collection testing Yes Yes N/A Shell scripts
CI integration CLI runner CLI runner Direct Direct
Collaboration Git (shared collections) Cloud sync or git N/A Scripts in repo
Offline support Full Desktop app only Full Full
Price Free Free (self-host) or cloud Free (CLI) / $8/mo (web) Free

Bruno: The Git-Native API Client

Bruno stores everything as plain files on your filesystem. No cloud sync, no accounts, no lock-in. Your API collections live in your git repository alongside your code, reviewed in PRs just like any other code change.

Collection Structure

api-collections/
├── collection.bru
├── environments/
│   ├── local.bru
│   ├── staging.bru
│   └── production.bru
├── auth/
│   ├── login.bru
│   ├── refresh-token.bru
│   └── register.bru
├── users/
│   ├── get-user.bru
│   ├── list-users.bru
│   ├── create-user.bru
│   └── update-user.bru
└── orders/
    ├── create-order.bru
    ├── get-order.bru
    └── list-orders.bru

Request Files

Each .bru file is a plain text file describing a single request:

meta {
  name: Create User
  type: http
  seq: 1
}

post {
  url: {{baseUrl}}/api/users
  body: json
  auth: bearer {{accessToken}}
}

headers {
  Content-Type: application/json
  X-Request-ID: {{$guid}}
}

body:json {
  {
    "name": "Alice Johnson",
    "email": "[email protected]",
    "role": "admin"
  }
}

script:pre-request {
  // Run before the request
  const timestamp = new Date().toISOString();
  bru.setVar("requestTime", timestamp);
}

script:post-response {
  // Run after the response
  if (res.status === 201) {
    bru.setEnvVar("lastUserId", res.body.id);
  }
}

tests {
  test("should return 201", function() {
    expect(res.status).to.equal(201);
  });

  test("should return user with id", function() {
    expect(res.body.id).to.be.a("string");
    expect(res.body.name).to.equal("Alice Johnson");
  });

  test("should set created timestamp", function() {
    expect(res.body.createdAt).to.be.a("string");
  });
}

Environment Files

// environments/local.bru
vars {
  baseUrl: http://localhost:3000
  accessToken: dev-token-12345
}

vars:secret [
  dbConnectionString
]
// environments/staging.bru
vars {
  baseUrl: https://api-staging.example.com
  accessToken: {{process.env.STAGING_TOKEN}}
}

Running Collections in CI

# Install the Bruno CLI
npm install -g @usebruno/cli

# Run all requests in a collection
bru run --env local

# Run a specific folder
bru run auth/ --env local

# Run with a specific environment and output JUnit XML
bru run --env staging --output results.xml --format junit

CI Integration with GitHub Actions

name: API Tests
on: [push]

jobs:
  api-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm install -g @usebruno/cli
      - name: Start API server
        run: |
          npm start &
          sleep 5
      - name: Run API tests
        run: bru run api-collections/ --env local --output results.xml --format junit
      - uses: dorny/test-reporter@v1
        if: always()
        with:
          name: API Test Results
          path: results.xml
          reporter: jest-junit

When to Choose Bruno

Hoppscotch: The Fast, Minimal Web Client

Hoppscotch (formerly Postwoman) is a lightweight, web-based API client. It loads instantly, works in any browser, and can be self-hosted. The desktop app adds local file access and system proxy support.

Key Features

Hoppscotch is opinionated about staying fast. The interface is stripped down to essentials:

# Quick request from the web app:
# 1. Open https://hoppscotch.io
# 2. Set method and URL
# 3. Hit Send
# No account required for basic use

Request Scripting

Hoppscotch uses pre-request and post-request scripts, similar to Postman:

// Pre-request script
const timestamp = Date.now();
pw.env.set("timestamp", timestamp.toString());

// Generate HMAC signature for authenticated API
const crypto = require("crypto-js");
const secret = pw.env.get("apiSecret");
const message = `${pw.env.get("apiKey")}:${timestamp}`;
const signature = crypto.HmacSHA256(message, secret).toString();
pw.env.set("signature", signature);
// Post-request script
const response = pw.response.body;

// Chain requests: save token for subsequent calls
if (response.token) {
  pw.env.set("authToken", response.token);
}

// Assertions
pw.test("Status is 200", () => {
  pw.expect(pw.response.status).toBe(200);
});

pw.test("Response has data array", () => {
  pw.expect(response.data).toBeType("array");
  pw.expect(response.data.length).toBeGreaterThan(0);
});

Self-Hosting

# Docker (single container)
docker run -d \
  --name hoppscotch \
  -p 3000:3000 \
  -p 3100:3100 \
  -p 3170:3170 \
  -e DATABASE_URL="postgresql://user:pass@db:5432/hoppscotch" \
  -e JWT_SECRET="your-secret" \
  -e REDIRECT_URL="http://localhost:3000" \
  -e WHITELISTED_ORIGINS="http://localhost:3000" \
  hoppscotch/hoppscotch:latest

# Docker Compose (full setup with database)
# See: https://github.com/hoppscotch/hoppscotch/blob/main/docker-compose.yml

When to Choose Hoppscotch

HTTPie: The Developer-Friendly CLI

HTTPie makes HTTP requests from the command line feel natural. Where curl requires you to remember flags and quoting rules, HTTPie uses intuitive syntax that reads almost like English.

Basic Requests

# GET request (HTTPie adds https:// and Accept: application/json by default)
http GET api.example.com/users

# POST with JSON body (key=value becomes JSON automatically)
http POST api.example.com/users name="Alice" email="[email protected]" age:=30

# Note: = for strings, := for raw JSON (numbers, booleans, arrays)
http POST api.example.com/users \
  name="Bob" \
  active:=true \
  tags:='["admin", "beta"]' \
  metadata:='{"source": "api"}'

# Headers
http GET api.example.com/users Authorization:"Bearer token123" Accept:application/json

# Query parameters
http GET api.example.com/users page==2 limit==50 sort==name

Comparison: HTTPie vs curl

The same request in both tools:

# curl (verbose, hard to read)
curl -X POST https://api.example.com/users \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer token123" \
  -d '{"name": "Alice", "email": "[email protected]", "role": "admin"}'

# HTTPie (concise, readable)
http POST api.example.com/users \
  Authorization:"Bearer token123" \
  name="Alice" \
  email="[email protected]" \
  role="admin"

Sessions (Persistent State)

HTTPie sessions persist headers, cookies, and auth across requests:

# Create a named session and login
http --session=dev POST api.example.com/auth/login \
  email="[email protected]" \
  password="secret"

# Subsequent requests reuse the session (cookies, headers)
http --session=dev GET api.example.com/users

# The session stores cookies automatically
# No need to manually copy-paste tokens between requests

File Upload and Download

# Upload a file
http POST api.example.com/upload file@./report.pdf

# Upload with form fields
http --form POST api.example.com/documents \
  file@./report.pdf \
  title="Q4 Report" \
  category="finance"

# Download a file
http --download GET api.example.com/reports/q4.pdf

# Pipe response to a file
http GET api.example.com/data > output.json

Scripting with HTTPie

#!/bin/bash
# API health check script using HTTPie

ENDPOINTS=(
  "api.example.com/health"
  "api.example.com/users"
  "api.example.com/orders"
)

for endpoint in "${ENDPOINTS[@]}"; do
  status=$(http --check-status --print=h HEAD "$endpoint" 2>/dev/null | head -1)
  if [ $? -eq 0 ]; then
    echo "OK: $endpoint"
  else
    echo "FAIL: $endpoint"
  fi
done

When to Choose HTTPie

curl: The Universal Standard

curl is installed on virtually every Unix system. It is the lingua franca of HTTP examples -- every API's documentation includes curl examples. It is not the friendliest tool, but it is the most universal and powerful.

Essential curl Commands

# GET request
curl https://api.example.com/users

# POST with JSON
curl -X POST https://api.example.com/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "[email protected]"}'

# POST with data from a file
curl -X POST https://api.example.com/users \
  -H "Content-Type: application/json" \
  -d @user.json

# Include response headers in output
curl -i https://api.example.com/users

# Only show response headers
curl -I https://api.example.com/users

# Verbose mode (shows request/response headers, TLS handshake)
curl -v https://api.example.com/users

# Silent mode (no progress bar)
curl -s https://api.example.com/users | jq .

# Follow redirects
curl -L https://example.com/old-endpoint

# Set timeout
curl --max-time 10 https://api.example.com/slow-endpoint

Authentication

# Bearer token
curl -H "Authorization: Bearer eyJhbGci..." https://api.example.com/users

# Basic auth
curl -u username:password https://api.example.com/admin

# API key in header
curl -H "X-API-Key: abc123" https://api.example.com/data

# Client certificate
curl --cert client.pem --key client-key.pem https://api.example.com/secure

Advanced curl Patterns

# Timing breakdown (useful for performance debugging)
curl -w "\nDNS: %{time_namelookup}s\nConnect: %{time_connect}s\nTLS: %{time_appconnect}s\nFirst byte: %{time_starttransfer}s\nTotal: %{time_total}s\n" \
  -o /dev/null -s https://api.example.com/users

# Output:
# DNS: 0.012s
# Connect: 0.045s
# TLS: 0.123s
# First byte: 0.234s
# Total: 0.267s

# Retry with exponential backoff
curl --retry 3 --retry-delay 2 --retry-all-errors \
  https://api.example.com/flaky-endpoint

# Send multipart form data
curl -F "[email protected]" \
  -F "title=Q4 Report" \
  -F "category=finance" \
  https://api.example.com/upload

# Test multiple endpoints in parallel
curl --parallel --parallel-max 5 \
  https://api.example.com/users \
  https://api.example.com/orders \
  https://api.example.com/products

curl in CI Pipelines

#!/bin/bash
# API smoke test for CI

BASE_URL="${API_URL:-http://localhost:3000}"

check_endpoint() {
  local path=$1
  local expected_status=$2
  local actual_status

  actual_status=$(curl -s -o /dev/null -w "%{http_code}" "${BASE_URL}${path}")

  if [ "$actual_status" = "$expected_status" ]; then
    echo "PASS: ${path} (${actual_status})"
    return 0
  else
    echo "FAIL: ${path} expected ${expected_status}, got ${actual_status}"
    return 1
  fi
}

failures=0
check_endpoint "/health" "200" || ((failures++))
check_endpoint "/api/users" "401" || ((failures++))  # Should require auth
check_endpoint "/api/nonexistent" "404" || ((failures++))

if [ $failures -gt 0 ]; then
  echo "${failures} endpoint(s) failed"
  exit 1
fi
echo "All endpoints passed"

When to Choose curl

Environment Management Patterns

Managing different environments (local, staging, production) is a critical part of API testing. Each tool handles this differently.

Bruno: File-Based Environments

// environments/local.bru
vars {
  baseUrl: http://localhost:3000
  apiKey: dev-key-123
}

// environments/staging.bru
vars {
  baseUrl: https://staging-api.example.com
  apiKey: {{process.env.STAGING_API_KEY}}
}
# Switch environments from CLI
bru run --env local
bru run --env staging

HTTPie: Named Sessions

# Each session stores state independently
http --session=local GET localhost:3000/users
http --session=staging GET staging-api.example.com/users

curl: Shell Variables and .env Files

# .env.local
API_URL=http://localhost:3000
API_KEY=dev-key-123

# .env.staging
API_URL=https://staging-api.example.com
API_KEY=staging-key-456

# Usage
source .env.local
curl -H "X-API-Key: ${API_KEY}" "${API_URL}/users"

Shared Pattern: dotenv + Makefile

A pattern that works with any CLI tool:

# Makefile
ENV ?= local

include .env.$(ENV)
export

test-api:
	bru run api-tests/ --env $(ENV)

smoke-test:
	./scripts/smoke-test.sh

health-check:
	curl -sf $(API_URL)/health && echo "OK" || echo "FAIL"
# Usage
make test-api ENV=local
make test-api ENV=staging
make smoke-test ENV=production

Request Chaining and Workflow Testing

Real API testing involves sequences: authenticate, create a resource, verify it, update it, delete it. Here is how to build that workflow in Bruno and with shell scripts.

Bruno: Chained Requests

// 1-login.bru
meta {
  name: Login
  seq: 1
}

post {
  url: {{baseUrl}}/auth/login
  body: json
}

body:json {
  {
    "email": "[email protected]",
    "password": "{{adminPassword}}"
  }
}

script:post-response {
  bru.setEnvVar("accessToken", res.body.token);
}
// 2-create-user.bru
meta {
  name: Create User
  seq: 2
}

post {
  url: {{baseUrl}}/users
  body: json
  auth: bearer {{accessToken}}
}

body:json {
  {
    "name": "Test User {{$timestamp}}",
    "email": "test-{{$randomInt}}@example.com"
  }
}

script:post-response {
  bru.setEnvVar("testUserId", res.body.id);
}

tests {
  test("user created", function() {
    expect(res.status).to.equal(201);
  });
}
// 3-verify-user.bru
meta {
  name: Verify User
  seq: 3
}

get {
  url: {{baseUrl}}/users/{{testUserId}}
  auth: bearer {{accessToken}}
}

tests {
  test("user exists", function() {
    expect(res.status).to.equal(200);
    expect(res.body.id).to.equal(bru.getEnvVar("testUserId"));
  });
}

Shell: curl + jq Workflow

#!/bin/bash
set -euo pipefail

BASE_URL="http://localhost:3000"

# Step 1: Login
echo "Logging in..."
LOGIN_RESPONSE=$(curl -s -X POST "${BASE_URL}/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]", "password": "secret"}')
TOKEN=$(echo "$LOGIN_RESPONSE" | jq -r '.token')

# Step 2: Create user
echo "Creating user..."
CREATE_RESPONSE=$(curl -s -X POST "${BASE_URL}/users" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${TOKEN}" \
  -d '{"name": "Test User", "email": "[email protected]"}')
USER_ID=$(echo "$CREATE_RESPONSE" | jq -r '.id')

# Step 3: Verify
echo "Verifying user ${USER_ID}..."
GET_RESPONSE=$(curl -s "${BASE_URL}/users/${USER_ID}" \
  -H "Authorization: Bearer ${TOKEN}")
FETCHED_NAME=$(echo "$GET_RESPONSE" | jq -r '.name')

if [ "$FETCHED_NAME" = "Test User" ]; then
  echo "PASS: User created and verified"
else
  echo "FAIL: Expected 'Test User', got '${FETCHED_NAME}'"
  exit 1
fi

# Step 4: Cleanup
curl -s -X DELETE "${BASE_URL}/users/${USER_ID}" \
  -H "Authorization: Bearer ${TOKEN}" > /dev/null
echo "Cleanup complete"

Decision Framework

If you need... Choose
Git-friendly API collections with tests Bruno
Browser-based, instant, no install Hoppscotch
Human-readable CLI for daily API work HTTPie
Maximum portability and scripting power curl
CI smoke tests with zero dependencies curl
Team collaboration without cloud vendor Bruno (git) or Hoppscotch (self-hosted)
Postman migration Bruno (imports Postman collections)

Combining Tools

In practice, most developers use multiple tools:

There is no conflict between these tools. Use Bruno for structured test collections, HTTPie for interactive exploration, and curl when you need maximum portability. The APIs do not care which client sends the requests.

Summary

The API testing tool landscape has fragmented in a healthy way. Bruno gives you git-native collections that live alongside your code. Hoppscotch provides an instant, browser-based experience you can self-host. HTTPie makes the command line feel human. And curl remains the universal constant -- available everywhere, capable of everything. Pick the GUI client that fits your workflow (Bruno for teams, Hoppscotch for speed), add HTTPie for comfortable terminal work, and keep curl in your back pocket for scripts and CI. The best API testing setup uses multiple tools, each for what it does best.