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