Saltar al contenido principal

Clase 04 — Consumir APIs con curl y jq

curl + jq: La combinación definitiva

# curl: Hace la petición HTTP
# jq: Procesa la respuesta JSON
# Juntos: El cliente REST perfecto desde la terminal
curl -s https://api.ejemplo.com/datos | jq '.campo'

Patrones esenciales

GET + Filtrar con jq

# Obtener un campo
curl -s https://pokeapi.co/api/v2/pokemon/pikachu | jq '.name'

# Múltiples campos
curl -s https://pokeapi.co/api/v2/pokemon/pikachu | jq '{
id: .id,
nombre: .name,
peso: (.weight / 10),
altura: (.height / 10)
}'

# Arrays → extraer valores
curl -s https://pokeapi.co/api/v2/pokemon/pikachu | jq '[.types[].type.name]'

# Transformar datos
curl -s https://api.github.com/users/octocat/repos | \
jq '[.[] | {repo: .name, estrellas: .stargazers_count, lenguaje: .language}] | sort_by(-.estrellas) | .[0:5]'

POST + Capturar respuesta

# Crear recurso y obtener el ID
NEW_ID=$(curl -s -X POST https://jsonplaceholder.typicode.com/posts \
-H "Content-Type: application/json" \
-d '{"title": "Nuevo post", "body": "Contenido", "userId": 1}' | jq '.id')

echo "Recurso creado con ID: $NEW_ID"

# Crear con datos dinámicos
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
HOSTNAME=$(hostname)

curl -s -X POST https://httpbin.org/post \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg ts "$TIMESTAMP" \
--arg host "$HOSTNAME" \
--arg user "$(whoami)" \
'{timestamp: $ts, hostname: $host, user: $user, action: "deploy"}'
)" | jq '.json'

PUT/PATCH + Verificar

# Verificar HTTP code + body
HTTP_CODE=$(curl -s -o /tmp/response.json -w "%{http_code}" \
-X PUT https://jsonplaceholder.typicode.com/posts/1 \
-H "Content-Type: application/json" \
-d '{"id":1,"title":"Completo","body":"Nuevo body","userId":1}')

if [[ "$HTTP_CODE" == "200" ]]; then
echo "✅ Actualizado:"
jq '.' /tmp/response.json
else
echo "❌ Error HTTP $HTTP_CODE"
fi

Proyecto: Cliente de PokéAPI

#!/bin/bash
# pokedex.sh - Pokédex desde la terminal

POKE="https://pokeapi.co/api/v2"

pokemon_info() {
local name="${1,,}"
local data
data=$(curl -s "$POKE/pokemon/$name")

if echo "$data" | jq -e '.id' &>/dev/null; then
echo "$data" | jq '{
id: .id,
nombre: .name,
tipos: [.types[].type.name],
habilidades: [.abilities[].ability.name],
stats: ([.stats[] | {(.stat.name): .base_stat}] | add),
peso_kg: (.weight / 10),
altura_m: (.height / 10),
sprite: .sprites.front_default
}'
else
echo "❌ Pokémon '$name' no encontrado"
return 1
fi
}

pokemon_por_tipo() {
local tipo="${1,,}"
curl -s "$POKE/type/$tipo" | jq '[.pokemon[].pokemon.name] | sort | .[0:20]'
}

pokemon_versus() {
local p1="${1,,}" p2="${2,,}"
local d1 d2
d1=$(curl -s "$POKE/pokemon/$p1")
d2=$(curl -s "$POKE/pokemon/$p2")

echo "=== $p1 vs $p2 ==="
for stat in hp attack defense speed; do
v1=$(echo "$d1" | jq ".stats[] | select(.stat.name==\"$stat\") | .base_stat")
v2=$(echo "$d2" | jq ".stats[] | select(.stat.name==\"$stat\") | .base_stat")
printf " %-18s %3s vs %-3s\n" "$stat" "$v1" "$v2"
done
}

case "${1:-help}" in
info) pokemon_info "$2" ;;
tipo) pokemon_por_tipo "$2" ;;
vs) pokemon_versus "$2" "$3" ;;
*) echo "Uso: $0 {info|tipo|vs} <pokemon> [pokemon2]" ;;
esac

Proyecto: Cliente de GitHub

#!/bin/bash
# github-cli.sh - Mini cliente de GitHub

GH="https://api.github.com"
AUTH=()
[[ -n "$GITHUB_TOKEN" ]] && AUTH=(-H "Authorization: token $GITHUB_TOKEN")

gh_get() { curl -s "${AUTH[@]}" -H "Accept: application/vnd.github.v3+json" "$GH$1"; }

repos() {
local user="$1"
gh_get "/users/$user/repos?per_page=10&sort=updated" | \
jq -r '.[] | "\(.name)\t⭐\(.stargazers_count)\t\(.language // "N/A")"' | column -t
}

buscar() {
local query="$1"
gh_get "/search/repositories?q=$query&sort=stars&per_page=10" | \
jq -r '.items[] | "\(.full_name)\t⭐\(.stargazers_count)\t\(.language // "N/A")"' | column -t
}

repo_info() {
local repo="$1"
gh_get "/repos/$repo" | jq '{
nombre: .full_name, descripcion: .description,
estrellas: .stargazers_count, forks: .forks_count,
issues: .open_issues_count, lenguaje: .language,
url: .html_url
}'
}

commits() {
local repo="$1"
gh_get "/repos/$repo/commits?per_page=5" | \
jq -r '.[] | "\(.sha[0:7]) \(.commit.author.date[0:10]) \(.commit.message | split("\n")[0])"'
}

rate_limit() {
gh_get "/rate_limit" | jq '.rate | {
limite: .limit, usado: .used, restante: .remaining, reset: (.reset | todate)
}'
}

case "${1:-help}" in
repos) repos "$2" ;;
buscar) buscar "$2" ;;
info) repo_info "$2" ;;
commits) commits "$2" ;;
rate) rate_limit ;;
*) echo "Uso: $0 {repos|buscar|info|commits|rate} [args]" ;;
esac

Manejo de errores robusto

#!/bin/bash
api_request() {
local method="$1" url="$2" data="$3"
local tmpfile response http_code body
tmpfile=$(mktemp)

local args=(-s -w "\n%{http_code}" -X "$method" --max-time 30)
[[ -n "$data" ]] && args+=(-H "Content-Type: application/json" -d "$data")

response=$(curl "${args[@]}" "$url" 2>"$tmpfile")
local curl_exit=$?
http_code=$(echo "$response" | tail -1)
body=$(echo "$response" | sed '$d')
rm -f "$tmpfile"

if [[ $curl_exit -ne 0 ]]; then
echo "{\"error\": \"Network error\", \"curl_code\": $curl_exit}" >&2
return 1
fi

case "$http_code" in
2[0-9][0-9]) echo "$body"; return 0 ;;
400) echo "❌ 400 Bad Request" >&2 ;;
401) echo "❌ 401 Unauthorized" >&2 ;;
403) echo "❌ 403 Forbidden" >&2 ;;
404) echo "❌ 404 Not Found" >&2 ;;
429) echo "⚠️ 429 Rate Limited" >&2 ;;
5[0-9][0-9]) echo "💥 $http_code Server Error" >&2 ;;
*) echo "❓ HTTP $http_code" >&2 ;;
esac
return 1
}

# Uso
if result=$(api_request GET "https://pokeapi.co/api/v2/pokemon/pikachu"); then
echo "$result" | jq '.name'
fi

Guardar y reutilizar respuestas (cache)

CACHE_DIR="/tmp/api-cache"
mkdir -p "$CACHE_DIR"

cached_get() {
local url="$1"
local ttl="${2:-300}"
local cache_file="$CACHE_DIR/$(echo "$url" | md5sum | cut -d' ' -f1).json"

if [[ -f "$cache_file" ]]; then
local age=$(( $(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || stat -c%Y "$cache_file") ))
if [[ $age -lt $ttl ]]; then
cat "$cache_file"
return 0
fi
fi

local response
response=$(curl -s --max-time 10 "$url")
if [[ $? -eq 0 ]]; then
echo "$response" > "$cache_file"
echo "$response"
else
[[ -f "$cache_file" ]] && cat "$cache_file"
return 1
fi
}

# Uso (la segunda vez es instantánea)
cached_get "https://pokeapi.co/api/v2/pokemon/pikachu" 600 | jq '.name'

Ejercicios

  1. Creá un Pokédex CLI que muestre info, tipos, stats y evoluciones de cualquier Pokémon
  2. Implementá un cliente de la GitHub API que busque repos y muestre issues
  3. Creá un script que combine datos de 2 APIs diferentes en una sola salida
  4. Implementá un sistema de cache para peticiones a API con TTL configurable
  5. Construí una tabla formateada con datos de https://jsonplaceholder.typicode.com/users