cURL Advanced

API Testing & Tooling — cURL Advanced
API Testing & Tooling
Chapter 7 · cURL Advanced

🚀 cURL Advanced

Chapter 6 covered the flags. This chapter covers the patterns that make cURL genuinely usable day to day: real auth handling, file uploads, readable JSON output, and turning a one-off command into something saved, shared, and re-run.

Bearer Tokens & API Keys, in Practice

Chapter 6's basic auth example (-u) is fine for that specific auth style, but most real APIs use a bearer token or an API key instead. Both go in a header — the practical question is where the token's actual value comes from.

# Hardcoding the token directly — works, but see this chapter's closing gotcha curl -H "Authorization: Bearer abc123realtoken" https://api.example.com/me # Reading it from an environment variable instead — the safer default curl -H "Authorization: Bearer $TOKEN" https://api.example.com/me

An API key sometimes goes in a custom header (X-API-Key: ...) instead of Authorization — check the API's documentation for which it expects. Either way, avoid putting a key in the URL as a query parameter (?api_key=...) if a header option exists: URLs routinely end up in server access logs, browser history, and proxy logs, all places a header's contents typically don't.

File Uploads: -F

Uploading a file needs multipart/form-data, a different body format from -d's JSON or form-urlencoded — that's what -F is for. The @ prefix tells curl to read a field's value from a file on disk rather than treating it as literal text.

curl -X POST https://api.example.com/upload \ -F "file=@photo.jpg" \ -F "caption=Sunset over the harbor"

Mixing -F with -d in the same request doesn't work as expected — they build genuinely different body formats, so a request is either -d's style or -F's multipart style, not both at once.

Piping to jq for Readable JSON

A raw JSON response prints as one dense, hard-to-read line. Piping it through jq — already covered for general shell use in bash_intermediate_07 — pretty-prints it, and can filter down to just the fields that matter.

curl -s https://api.example.com/products/42 | jq # Just the name field: curl -s https://api.example.com/products/42 | jq '.name' # Every item's price, from an array response: curl -s https://api.example.com/orders/55 | jq '.items[] | .price'

The -s (silent) flag suppresses curl's progress meter, which would otherwise clutter the output being piped into jq.

Saving & Replaying Requests as Scripts

A command retyped (or found again in shell history) every time isn't much better than starting from scratch. Saving it as a small shell script makes it reusable and shareable — the cURL equivalent of Chapter 5's .http files.

#!/bin/bash # create-product.sh curl -X POST "$BASE_URL/products" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ --data @product.json

--data @product.json loads the body from a separate file rather than an inline string — useful once a body gets long enough to be awkward as one quoted line. Both $BASE_URL and $TOKEN come from the environment, not hardcoded — set once, reused by any script that needs them.

An Inline Command vs. a Saved Script

Inline, Retyped Each Time

Works once, then has to be found again in shell history or retyped from memory — easy to introduce a small mistake re-copying a long command.

Saved as a Script

chmod +x create-product.sh && ./create-product.sh — reusable, shareable with a teammate, and can be committed to the repo like any other script.

Flag / PatternPurpose
-F "field=@file"Multipart file upload
--data @file.jsonLoad the request body from a file instead of inline text
-o fileSave the response body to a file instead of printing it
| jqPretty-print and filter a JSON response
$VAR in a scriptReads a value from the environment rather than hardcoding it

Putting It Together: A Reusable Upload Script

#!/bin/bash # upload-avatar.sh curl -s -X POST "$BASE_URL/users/me/avatar" \ -H "Authorization: Bearer $TOKEN" \ -F "avatar=@$1" \ | jq '.avatarUrl'

Run as ./upload-avatar.sh photo.jpg$1 is the shell's first script argument, so the same script uploads any file passed to it, authenticates via an environment variable, and prints just the resulting URL, readably, via jq.

💻 Coding Challenges

Challenge 1: Fix the Upload Command

A developer writes curl -X POST https://api.example.com/upload -d "file=@photo.jpg" and the file arrives corrupted/unusable server-side. What's wrong, and what's the fix?

Goal: Practice recognizing that -d and -F build genuinely different body formats, not interchangeable syntax for "attach a file."

→ Solution

Challenge 2: Filter a Response With jq

Given a response { "id": 42, "items": [{ "name": "Mouse", "price": 24.99 }, { "name": "Keyboard", "price": 59.99 }] }, write the jq filter to print just the array of item names.

Goal: Practice writing a jq filter over an array field, building on bash_intermediate_07's coverage.

→ Solution

Challenge 3: Make a Script Reusable

Rewrite this hardcoded command as a script using environment variables instead: curl -H "Authorization: Bearer abc123" https://api.example.com/orders, so the same script works for any base URL or token without editing the file.

Goal: Practice the environment-variable substitution pattern this chapter uses for reusable scripts.

→ Solution

⚠️ Gotcha: A Real Token Hardcoded Into a Saved, Committed Script

Saving a script like Chapter 6/7's examples feels like good practice — until the token inside it is a real, working credential, and the script gets committed to a shared Git repository. This is the exact same mistake as a hardcoded database connection string or a committed API key covered elsewhere on this site: once pushed, that token is compromised, regardless of whether the file is later edited or deleted. Every script in this chapter deliberately reads $TOKEN from the environment rather than embedding a literal value — that's not a style preference, it's the difference between a script that's safe to commit and one that isn't.

🎯 What's Next

The next chapter is Browser-Based API Testing — the simplest tool of all: URL query parameters, HTML forms, and reading real requests in the DevTools Network tab.