Chapter 13Lesson 05~110 minutes

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.

IntermediateAPIs & structured dataHands-on lab

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

Resilient API workflow
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"
fi

4. 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
    ;;
esac

Do 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
fi

A 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 1

Keep 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.

Side effects change retry policy

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))
done

Cursor-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  # diagnostic

This 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.
Next lesson

DNS, TCP, and Endpoint Checks from Bash

Chapter 14 will move these data and reliability patterns into network checks, SSH, file transfer, remote quoting, and fleet automation.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.