Saltar al contenido principal

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ónMétodoEndpointBody
ListarGET/api/usuariosNo
ObtenerGET/api/usuarios/42No
CrearPOST/api/usuarios
ActualizarPUT/api/usuarios/42
PatchPATCH/api/usuarios/42
EliminarDELETE/api/usuarios/42No

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

  1. Usá la PokéAPI para obtener los 10 Pokémon de tipo "water" con más HP
  2. Usá JSONPlaceholder para hacer un CRUD completo (crear, leer, actualizar, eliminar)
  3. Consultá la GitHub API para listar los últimos 5 commits de un repositorio público
  4. Creá un script que pagine automáticamente por una API y recopile todos los resultados
  5. Implementá un cliente REST con manejo de errores para cualquier API