JSON on the command line: curl and jq
Call APIs with curl, fail on HTTP errors, and pull values out of JSON with jq paths and select.
Create a free account Try part of it here
Why it matters
APIs speak JSON, and engineers poke at them constantly: checking what an endpoint really returns, pulling one field out of a large response, or spotting the product that's out of stock before the dashboard does. curl fetches, jq reads JSON the way grep reads text, and together they let you inspect an API in seconds without writing a script or opening Postman. This lesson works against a simulated shop API, so every command here is safe to try, and nothing you send leaves the page.
What you'll be able to do
- Call an API from the terminal with curl, and make HTTP errors fail the command
- Pull values out of JSON with jq paths, iterators and raw output
- Filter JSON with select and chain filters with pipes
- Combine curl, jq and the text tools to answer a question about an API in one line
What it covers
- curl: HTTP from the terminal
- jq: paths through JSON
- select: keep the items that match
- When jq, when Python
Show the answer
jq, which picks fields out of JSON
jq understands JSON's structure, so .products[].sku gives exactly the SKUs. grep sees only lines of text and breaks as soon as the formatting changes.
curl: HTTP from the terminal
curl URL sends a GET request and prints the response body. A few flags cover daily use:
-s(silent) hides the progress meter, which you want in pipelines;-fmakes HTTP errors like 404 or 500 fail the command, instead of printing the error page as if it worked;-H 'Authorization: Bearer ...'adds a header; read the token from an environment variable, never type it;-X POST -d '{"q": "tent"}'sends a body, usually with-H 'Content-Type: application/json'so the server knows how to read it.
Without -f, a script that fetches a missing order happily carries on with an error message as its "data". With it, curl exits with a failure code, so && chains stop and scripts can react.
When something goes wrong, curl can show you more. -i prints the response's status line and headers above the body, so you can see a 429 Too Many Requests, a redirect you didn't expect, or a missing Content-Type, and -v shows the whole conversation, request headers included, which is the fastest way to find out whether your Authorization header was sent at all. In scripts, add --max-time 10 so a hung server can't hang your script with it.
Show the answer
So an HTTP error makes curl fail, not pass on the error page
With -f, a 404 or 500 makes curl exit with an error code, so the next command doesn't run on an error message as if it were data.
jq: paths through JSON
jq takes a filter and applies it to JSON, either from a file named after the filter or, more often, from a pipe. Think of a filter as a path through the nested dictionaries and lists, written left to right. The filter . is the whole input, pretty-printed. .products is a key, .products[0] the first item, and .products[] every item, one after another. Paths chain: .products[].sku gives each product's SKU. Add -r (raw) to print strings without their quotes, which is what you want before handing them to other tools.
curl -s https://api.trailhead.test/products | jq '.products | length' curl -s https://api.trailhead.test/products | jq -r '.products[].sku'
Inside a filter, | passes values along, just like the shell's pipe does for lines: .products | length takes the list and counts it. Quote filters in single quotes so the shell leaves them alone.
Two more filters help when an API is new to you. keys lists an object's keys, and length counts a list's items or an object's keys. A key that isn't there gives null rather than an error, which is convenient for exploring but means a typo in a field name fails quietly: if a filter prints null, check the spelling against keys before suspecting the data.
A simulated shop API answers at https://api.trailhead.test. Each task is checked by what your command prints.
jq -r '.items[1].sku' data.jsonShow the answer
MUG-01
Indexes start at 0, so items[1] is the second item, and -r prints its SKU without quotes.
Write a sentence or two, then compare it with a good answer in the lesson.
select: keep the items that match
select(CONDITION) keeps an item when the condition is true and drops it otherwise, like grep for JSON. Compare a field with ==, !=, <, >, <= or >=:
jq -r '.products[] | select(.stock == 0) | .sku' jq -r '.products[] | select(.price < 100) | .name' data/products.json
Read the first left to right: take each product, keep those with no stock, print their SKUs. The second reads a local file instead of a response, which is handy when you've saved an API's output to work on offline or want the same answer every time while you build up a longer filter step by step. Because jq's output is lines, the rest of your toolkit still applies: | sort, | wc -l and | head all work on jq's results.
The order of the stages matters. Filter whole items first and pick the field last: once you've taken .sku, only a string is left, and a string has no price to compare. The same rule holds in Python, where you'd write a list comprehension over the products with the condition on the product and the field in the result. Full jq also offers and, or, map, group_by and much more; the simulated terminal here supports the everyday core.
jq -r '.products[] | select(.price >= 79) | .sku' data/products.jsonShow the answer
TENT-2P, then FLEECE-M
>= includes 79, so the tent (189) and the fleece (79) pass, printed in the file's order.
Show the answer
Dicts and lists, the structure jq walks
JSON objects become dictionaries and arrays become lists, so .products[0].sku in jq is data["products"][0]["sku"] in Python.
When jq, when Python
jq shines for looking: one field from a big response, a count, a filter, a quick check before a deploy. Its language goes much further than this lesson, with functions, maps and grouping, but once a jq filter needs several lines, the same work is usually clearer in Python with requests and ordinary dictionaries, plus tests. A reasonable split: jq for questions you ask by hand today, Python for anything that runs on a schedule or feeds another system, where you want error handling, retries and a test that proves it still works next month.
Two habits keep command-line API work safe. Keep tokens in environment variables, as in -H "Authorization: Bearer $API_TOKEN", so they never land in your shell history or a pasted screenshot. And use -f whenever another command depends on the response, so errors stop the chain instead of flowing into it.
A good routine for an unfamiliar API: fetch one response and pretty-print it with jq ., look at its shape with keys and length, try the path to the field you need on a single item, then add [] and select to handle them all. Save a sample response to a file (curl's -o sample.json does it) while you explore, so you aren't calling a rate-limited API dozens of times while you get a filter right.
Show the answer
Typing the API key itself after Bearer in the command
A key typed into a command is saved in your shell history and shows up in screenshots and logs. A variable keeps the value out of the command line.
Write a sentence or two, then compare it with a good answer in the lesson.
Practice what you just read
- a practice exercise that checks your code
- a 6-question quiz
- 5 review cards for spaced repetition
Try a quiz question
What does jq -r '.users[].email' print for a response with three users?