Clase 06 — Trabajando con APIs REST
Anatomía de una API REST
https://api.ejemplo.com/v2/usuarios/42?include=perfil&format=json
\___/ \______________/\__/\_________/\_/ \________________________/
| | | | | |
protocolo host versión recurso id query params
Patrón CRUD en APIs REST
| Operación | Método | Endpoint | Body |
|---|---|---|---|
| Listar | GET | /api/usuarios | No |
| Obtener | GET | /api/usuarios/42 | No |
| Crear | POST | /api/usuarios | Sí |
| Actualizar | PUT | /api/usuarios/42 | Sí |
| Patch | PATCH | /api/usuarios/42 | Sí |
| Eliminar | DELETE | /api/usuarios/42 | No |
PokéAPI — Práctica con API real
# Base URL
POKE="https://pokeapi.co/api/v2"
# Listar Pokémon (paginado)
curl -s "$POKE/pokemon?limit=5&offset=0" | jq '{
total: .count,
siguiente: .next,
pokemon: [.results[].name]
}'
# Detalles de un Pokémon
curl -s "$POKE/pokemon/charizard" | 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
}'
# Pokémon por tipo
curl -s "$POKE/type/fire" | jq '[.pokemon[].pokemon.name] | .[0:10]'
# Evoluciones
curl -s "$POKE/evolution-chain/1" | jq '
.chain | {
base: .species.name,
evoluciones: [.evolves_to[] | {
nombre: .species.name,
evoluciones: [.evolves_to[].species.name]
}]
}
'
GitHub API — API con autenticación
# Sin autenticación (60 req/hora)
GH="https://api.github.com"
# Info de usuario
curl -s "$GH/users/roxsross" | jq '{
login, name, bio, public_repos, followers, created_at
}'
# Con autenticación (5000 req/hora)
export GITHUB_TOKEN="ghp_xxxxx"
AUTH=(-H "Authorization: token $GITHUB_TOKEN" -H "Accept: application/vnd.github.v3+json")
# Mis repos
curl -s "${AUTH[@]}" "$GH/user/repos?per_page=5&sort=updated" | \
jq '.[] | {nombre: .full_name, estrellas: .stargazers_count, lenguaje: .language}'
# Crear un repo
curl -s -X POST "${AUTH[@]}" "$GH/user/repos" \
-d '{
"name": "test-curl",
"description": "Repo creado con curl",
"private": true,
"auto_init": true
}' | jq '{id, full_name, html_url, private}'
# Crear un issue
curl -s -X POST "${AUTH[@]}" \
"$GH/repos/OWNER/REPO/issues" \
-d '{
"title": "Bug reportado via curl",
"body": "Descripción del bug\n\n## Steps to reproduce\n1. ...",
"labels": ["bug"]
}' | jq '{number, title, html_url}'
# Listar PRs abiertos
curl -s "${AUTH[@]}" "$GH/repos/kubernetes/kubernetes/pulls?state=open&per_page=5" | \
jq '.[] | {numero: .number, titulo: .title, autor: .user.login}'
# Rate limit
curl -s "${AUTH[@]}" "$GH/rate_limit" | jq '.rate'
JSONPlaceholder — API de pruebas
API gratuita para practicar CRUD. No persiste datos.
API="https://jsonplaceholder.typicode.com"
# GET - Listar posts
curl -s "$API/posts?_limit=3" | jq '.[] | {id, title}'
# GET - Un post
curl -s "$API/posts/1" | jq '.'
# POST - Crear
curl -s -X POST "$API/posts" \
-H "Content-Type: application/json" \
-d '{
"title": "Mi Post desde curl",
"body": "Contenido del post creado con curl",
"userId": 1
}' | jq '.'
# PUT - Reemplazar
curl -s -X PUT "$API/posts/1" \
-H "Content-Type: application/json" \
-d '{
"id": 1,
"title": "Título actualizado",
"body": "Body actualizado",
"userId": 1
}' | jq '.'
# PATCH - Actualizar parcial
curl -s -X PATCH "$API/posts/1" \
-H "Content-Type: application/json" \
-d '{"title": "Solo cambio el título"}' | jq '.'
# DELETE
curl -s -X DELETE "$API/posts/1" -o /dev/null -w "HTTP %{http_code}\n"
# Recursos anidados
curl -s "$API/posts/1/comments" | jq '.[0] | {name, email, body}'
# Filtrar
curl -s "$API/posts?userId=1" | jq 'length'
httpbin.org — API para debug
BASE="https://httpbin.org"
# Ver qué headers envías
curl -s "$BASE/headers" | jq '.headers'
# Tu IP
curl -s "$BASE/ip" | jq '.origin'
# Echo de datos enviados
curl -s -X POST "$BASE/anything" \
-H "Content-Type: application/json" \
-H "X-Custom: test" \
-d '{"key":"value"}' | jq '{method, headers: .headers, json}'
# Simular delays
curl -s -w "\nTiempo: %{time_total}s\n" "$BASE/delay/2"
# Simular status codes
for code in 200 201 301 404 500; do
status=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/status/$code")
echo "Pedido: $code → Recibido: $status"
done
# Basic Auth test
curl -s -u "user:pass" "$BASE/basic-auth/user/pass" | jq '.'
Paginación
#!/bin/bash
# paginate.sh - Recorrer API paginada
API="https://pokeapi.co/api/v2/pokemon"
LIMIT=20
OFFSET=0
TOTAL=0
PAGINA=1
while true; do
response=$(curl -s "${API}?limit=${LIMIT}&offset=${OFFSET}")
count=$(echo "$response" | jq '.results | length')
next=$(echo "$response" | jq -r '.next')
total=$(echo "$response" | jq '.count')
echo "Página $PAGINA (offset: $OFFSET, items: $count, total: $total):"
echo "$response" | jq -r '.results[].name' | sed 's/^/ /'
TOTAL=$((TOTAL + count))
OFFSET=$((OFFSET + LIMIT))
((PAGINA++))
[[ "$next" == "null" ]] && break
[[ $PAGINA -gt 3 ]] && { echo "...parando en página 3 (demo)"; break; }
done
echo "Total obtenidos: $TOTAL"
Manejo de errores en APIs
#!/bin/bash
# api-safe-call.sh - Llamada a API con manejo de errores
api_call() {
local method="$1" url="$2" data="$3"
local response http_code body
if [[ -n "$data" ]]; then
response=$(curl -s -w "\n__HTTP_CODE__%{http_code}" \
-X "$method" -H "Content-Type: application/json" \
-d "$data" --max-time 10 "$url")
else
response=$(curl -s -w "\n__HTTP_CODE__%{http_code}" \
-X "$method" --max-time 10 "$url")
fi
http_code=$(echo "$response" | grep "__HTTP_CODE__" | sed 's/__HTTP_CODE__//')
body=$(echo "$response" | grep -v "__HTTP_CODE__")
case "$http_code" in
2[0-9][0-9])
echo "$body" | jq '.' 2>/dev/null || echo "$body"
return 0
;;
401) echo "❌ Error 401: No autorizado" >&2; return 1 ;;
403) echo "❌ Error 403: Prohibido" >&2; return 1 ;;
404) echo "❌ Error 404: No encontrado" >&2; return 1 ;;
429) echo "⚠️ Error 429: Rate limit excedido" >&2; return 1 ;;
5[0-9][0-9]) echo "❌ Error $http_code: Error del servidor" >&2; return 1 ;;
000) echo "❌ Error: No se pudo conectar (timeout)" >&2; return 1 ;;
*) echo "❌ Error HTTP $http_code" >&2; return 1 ;;
esac
}
# Uso
api_call GET "https://pokeapi.co/api/v2/pokemon/pikachu"
api_call GET "https://httpbin.org/status/404"
api_call POST "https://httpbin.org/post" '{"test":true}'
Ejemplo: Cliente REST completo
#!/bin/bash
# rest-client.sh - Cliente REST genérico
BASE_URL="${1:?Uso: $0 <base_url>}"
shift
cmd_get() { curl -s "${AUTH[@]}" "$BASE_URL/$1" | jq '.'; }
cmd_post() { curl -s -X POST "${AUTH[@]}" -H "Content-Type: application/json" -d "$2" "$BASE_URL/$1" | jq '.'; }
cmd_put() { curl -s -X PUT "${AUTH[@]}" -H "Content-Type: application/json" -d "$2" "$BASE_URL/$1" | jq '.'; }
cmd_delete() { curl -s -X DELETE "${AUTH[@]}" "$BASE_URL/$1" -w "HTTP %{http_code}\n"; }
AUTH=()
[[ -n "$API_TOKEN" ]] && AUTH=(-H "Authorization: Bearer $API_TOKEN")
case "${1:-help}" in
get) cmd_get "$2" ;;
post) cmd_post "$2" "$3" ;;
put) cmd_put "$2" "$3" ;;
delete) cmd_delete "$2" ;;
*) echo "Uso: $0 <url> {get|post|put|delete} <path> [data]" ;;
esac
# Ejemplos:
# ./rest-client.sh https://jsonplaceholder.typicode.com get posts/1
# ./rest-client.sh https://jsonplaceholder.typicode.com post posts '{"title":"test"}'
Ejercicios
- Usá la PokéAPI para obtener los 10 Pokémon de tipo "water" con más HP
- Usá JSONPlaceholder para hacer un CRUD completo (crear, leer, actualizar, eliminar)
- Consultá la GitHub API para listar los últimos 5 commits de un repositorio público
- Creá un script que pagine automáticamente por una API y recopile todos los resultados
- Implementá un cliente REST con manejo de errores para cualquier API