Building Resilient API-to-Shell Workflows
The strongest Bash API workflows are layered: transport retrieves bytes, HTTP logic classifies the response, a structured parser validates data, and only then does business logic act. That separation makes errors, retries, and recovery much easier to reason about.
Learning objectives
By the end of this lesson
- Separate transport, protocol, parsing, and business layers.
- Validate dependencies before requests.
- Classify HTTP outcomes before parsing success data.
- Retry only transient repeat-safe failures.
- Persist remote identifiers for reconciliation.
1. Separate transport, protocol, parsing, and business decisions
flowchart LR C["curl transport"] --> H["HTTP classification"] H --> J["JSON parser"] J --> V["validation"] V --> A["action"]
A production API script is easier to reason about when each layer has one job: transport gets bytes, HTTP logic classifies the response, a format parser extracts data, validation checks business rules, and only then does the script mutate state.
2. Validate dependencies and configuration before the request
require_command() {
command -v "$1" >/dev/null 2>&1 || {
printf 'required command missing: %s\n' "$1" >&2
return 69
}
}
require_command curl || exit $?
require_command jq || exit $?
: "${API_BASE_URL:?API_BASE_URL is required}"
: "${API_TOKEN:?API_TOKEN is required}"Failing before network mutation is safer than discovering missing dependencies halfway through a transaction.
3. Capture body, transport status, and HTTP status independently
body_file=$(mktemp) || exit 1
if http_code=$(
curl \
--connect-timeout 5 \
--max-time 30 \
--silent \
--show-error \
--output "$body_file" \
--write-out '%{http_code}' \
--header 'Accept: application/json' \
--header "Authorization: Bearer $API_TOKEN" \
"$url"
); then
:
else
status=$?
printf 'transport_error=%d\n' "$status" >&2
exit "$status"
fi4. Classify HTTP responses before parsing success data
case $http_code in
2??)
;;
401|403)
printf 'authentication_or_authorization_failure http=%s\n' \
"$http_code" >&2
exit 77
;;
429|5??)
printf 'transient_http_failure=%s\n' "$http_code" >&2
exit 75
;;
*)
printf 'unexpected_http_status=%s\n' "$http_code" >&2
exit 70
;;
esacDo not run a “success response” jq filter against an error document unless the API contract says the schemas are identical.
5. Validate response structure before using fields
if ! jq -e '
.service
and (.replicas | type == "number")
and (.status | type == "string")
' "$body_file" >/dev/null; then
printf 'response schema validation failed\n' >&2
exit 65
fiA syntactically valid JSON document can still be the wrong shape for your workflow.
6. Extract only the values Bash actually needs
service=$(jq -r '.service' "$body_file") || exit 1
replicas=$(jq -r '.replicas' "$body_file") || exit 1
status_text=$(jq -r '.status' "$body_file") || exit 1Keep complex filtering and nested traversal inside jq so Bash receives a small set of well-defined scalar values.
7. Retry only the layers classified as transient
Transport timeouts, connection failures, HTTP 429, and selected 5xx statuses may be retryable. Authentication failures, invalid request payloads, and schema errors usually are not.
For POST or other mutating requests, use API-provided idempotency keys or reconciliation before retrying an ambiguous outcome.
8. Pagination is part of the API contract
page=1
while :; do
response=$(fetch_page "$page") || exit $?
jq -c '.items[]' <<< "$response"
has_more=$(jq -r '.has_more' <<< "$response")
[[ $has_more == true ]] || break
((page += 1))
doneCursor-based APIs require storing and forwarding the server-provided cursor instead of inventing page numbers.
9. Respect rate-limit headers and server guidance
APIs may expose retry-after durations, reset timestamps, or quota headers. A resilient client treats those as control inputs rather than hammering the service with a fixed local retry loop.
10. Keep machine output clean
printf '%s\n' "$artifact_id" # stdout result
printf 'created artifact=%q\n' "$artifact_id" >&2 # diagnosticThis separation lets other automation safely capture the result while operators still receive useful logs.
11. Persist remote identifiers when later recovery depends on them
tmp=$(mktemp "$state_dir/.request.XXXXXX") || exit 1
jq -n \
--arg request_id "$request_id" \
--arg artifact_id "$artifact_id" \
'{
request_id: $request_id,
artifact_id: $artifact_id
}' > "$tmp"
mv -- "$tmp" "$state_dir/request.json"If the remote operation succeeded and the local script later fails, persisted identifiers allow reconciliation instead of duplicate creation.
12. Hands-on lab: resilient local API-response pipeline
mkdir -p "$HOME/devops-academy/bash/chapter13/lesson05"
cd "$HOME/devops-academy/bash/chapter13/lesson05"
cat > response.json <<'EOF'
{
"request_id": "req-123",
"service": "api",
"status": "ready",
"replicas": 3
}
EOF
http_code=200
body_file=response.json
case $http_code in
2??) ;;
429|5??)
printf 'retryable HTTP status=%s\n' "$http_code" >&2
exit 75
;;
*)
printf 'non-success HTTP status=%s\n' "$http_code" >&2
exit 70
;;
esac
if ! jq -e '
(.request_id | type == "string")
and (.service | type == "string")
and (.status == "ready")
and (.replicas | type == "number")
' "$body_file" >/dev/null; then
printf 'invalid API response\n' >&2
exit 65
fi
request_id=$(jq -r '.request_id' "$body_file")
service=$(jq -r '.service' "$body_file")
replicas=$(jq -r '.replicas' "$body_file")
printf 'request_id=%s\n' "$request_id" >&2
printf '%s\t%s\n' "$service" "$replicas"Verification checklist
13. Knowledge check
Question 1. What four layers should an API workflow separate?
Question 2. Should a 401 normally be retried like a timeout?
Question 3. Why persist request or artifact IDs?
Question 4. Why classify HTTP status before applying a success-schema jq filter?
14. Summary
A resilient API-to-shell workflow separates transport, HTTP classification, structured parsing, validation, and mutation. Bound requests, retry only transient and repeat-safe operations, persist remote identifiers when recovery matters, and keep stdout/stderr contracts clean.
15. Further reading
- curl documentation — status, headers, write-out, retries, and timeouts.
- jq Manual — validation, construction, and extraction.
- HTTP semantics — methods, status codes, rate limiting.
- Site Reliability Engineering guidance — retries, overload, and recovery.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.