sequenceDiagram
participant U as Usuario
participant API as Tu API
participant EXT as API externa
U->>API: POST /tasks
API->>API: crea la tarea (BD)
API->>EXT: POST /notify (timeout 3s)
alt externa responde bien
EXT-->>API: 200
else externa falla o se cuelga
API->>API: timeout/retry → log + encolar para luego
end
API-->>U: 201 (la tarea existe — Slack es asunto nuestro)
28 19. Necesitamos llamar a otra API desde el servidor
Tu backend ahora es cliente de otro backend

Hasta aquí tu API respondía. Ahora también llama: cuando se crea una tarea, hay que avisar a Slack/email — una API externa que no controlas. Hacer ese request bien no es fetch(url) y ya: es timeout (el que los tutoriales omiten), reintento con backoff, y una API tuya que sigue viva aunque la de ellos esté caída.
28.1 El problema
“Cuando se crea una tarea, notifica a Slack”. Fácil, ¿no? fetch dentro del handler y listo. Hasta que: Slack tarda 30 segundos → tu usuario espera 30 segundos. Slack devuelve 500 → tu endpoint devuelve 500 aunque la tarea sí se creó. Slack está caído un día → tu registro de tareas “está caído” un día. Una dependencia sin reglas convierte sus problemas en tus problemas.
return hace dos cosas a la vez: devuelve el resultado Y termina la función — lo que esté debajo nunca corre. return task = “aquí está el plato, salgo de la cocina”.
Una dependencia es código de terceros que tu proyecto usa: npm install, pip install, go get las traen. Cada una es deuda — ahora funciona, pero hay que mantenerla, actualizarla y confiar en ella.
Un registry es el almacén de imágenes (Docker Hub, ECR, Artifact Registry): el CI empuja nexus:v42, el servidor la jala. Es el intermediario entre “se construyó” y “está corriendo”.
28.2 Cómo lo resuelve un equipo
Un equipo serio trata la API externa como lo que es: algo que falla, lento, y a veces miente. Tres reglas antes de escribir el fetch:
- Timeout siempre — “¿cuánto estoy dispuesto a esperar?” se decide ahora (2–5s típico), no cuando el thread quede colgado.
- Reintenta lo transitorio, con pausa creciente — un 503 puede ser un parpadeo; reintentar al segundo puede salvarlo. Reintentar 50 veces por segundo amplifica el incendio.
- Degrada elegante — si la notificación falla, la tarea ya se creó: responde 201, registra el fallo, reintenta después (cap. 21). El usuario no paga la caída de Slack.
28.3 Conceptos nuevos
- U El backend como cliente HTTP:
fetchnativo vshttpxvsnet/http.Client - U Timeouts: el concepto que los tutoriales omiten y producción exige
- U Reintentos con backoff: reintentar sin amplificar el incendio
- U Degradación elegante: el servicio externo devuelve 500, tu API sigue viva
- U API keys en servidor (nunca en frontend): el patrón BFF
28.4 La explicación visual
La notificación puede fallar; la creación no. Por eso la llamada externa va después de persistir y su fallo no se traduce en error al usuario.
28.5 Implementación
Un pequeño cliente con timeout + retry, usado desde el servicio:
async function notifyExternal(text: string): Promise<void> {
const delays = [0, 500, 1500]; // retry with backoff
for (let attempt = 0; attempt < delays.length; attempt++) {
if (delays[attempt]) await new Promise(r => setTimeout(r, delays[attempt]));
try {
const res = await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text }),
signal: AbortSignal.timeout(3000), // the part tutorials skip
});
if (res.ok) return;
if (res.status < 500) return; // 4xx won't fix itself
} catch { /* timeout or network: retryable */ }
}
throw new Error("external notify failed after retries");
}import httpx
def notify_external(text: str) -> None:
delays = [0, 0.5, 1.5] # retry with backoff
for i, delay in enumerate(delays):
if delay: time.sleep(delay)
try:
res = httpx.post(settings.slack_webhook_url,
json={"text": text}, timeout=3.0)
if res.is_success: return
if res.status_code < 500: return # 4xx won't fix itself
except httpx.HTTPError:
pass # timeout/network: retryable
raise ExternalServiceError("notify failed after retries")var client = &http.Client{Timeout: 3 * time.Second} // no default timeout!
func notifyExternal(text string) error {
body, _ := json.Marshal(map[string]string{"text": text})
for attempt, delay := range []time.Duration{0, 500, 1500} {
if delay > 0 { time.Sleep(delay * time.Millisecond) }
res, err := client.Post(webhookURL, "application/json", bytes.NewReader(body))
if err == nil {
defer res.Body.Close()
if res.StatusCode < 300 { return nil }
if res.StatusCode < 500 { return nil } // 4xx won't fix itself
}
_ = attempt
}
return errors.New("notify failed after retries")
}Crea un módulo para llamar una API externa (webhook de Slack) desde mi
servicio en [Express/FastAPI/net-http]. Requisitos: timeout de 3s por
intento, máximo 3 intentos con backoff exponencial, no reintentar errores
4xx, lanzar un error de dominio ExternalServiceError si todo falla, la
URL viene de variable de entorno validada al arranque, y la llamada se
usa DESPUÉS de persistir la entidad — su fallo no debe cambiar la
respuesta al cliente. Con tests que simulen timeout y 500.
28.6 ¿Por qué cada stack lo hace así?
Lo único que necesitas llevarte: las tres implementaciones son la misma receta — timeout por intento, backoff entre intentos, no reintentar lo que no se arregla solo. Cambia la sintaxis, no el diseño.
- TS:
fetchno tenía timeout hastaAbortSignal.timeout()— por eso años de código legacy cuelga forever. Axios lo trae desde siempre (timeout: 3000). - Python:
requestsno tiene timeout por defecto (¡peligro histórico!);httpxlo pone en 5s de fábrica — por eso usamos httpx. - Go:
http.Client{}vacío = sin timeout — el error de producción más clásico de Go.http.Client{Timeout: ...}es obligatorio.
Librerías de retry existen en los tres (backoff, tenacity, avast/retry-go) — úsalas cuando el patrón se repite, pero entiende la receta manual primero: es 10 líneas y es lo que todo equipo espera que sepas escribir.
28.7 Errores comunes
| Error | Por qué pasa | Fix |
|---|---|---|
| Llamada externa sin timeout | El default del cliente es “esperar siempre” | Timeout explícito por intento |
| Retry sin pausa ni límite | “Si falla, reintenta” | Backoff + tope de intentos; 4xx no se reintenta |
| Fallo externo → 500 al usuario | El error de Slack burbujea al handler | Persistir primero; el fallo externo se loguea y se encola |
| API key en el frontend | “La necesito para llamar la API” | La llamas desde tu backend — la key jamás sale del servidor (BFF) |
| Loguear el request completo | Debugging cómodo | La key/secret queda en logs — loguea status y duración, no headers |
28.8 Buenas prácticas
- Timeout, retry, dedupe — el trío mínimo de toda integración. Sin los tres, no vas a prod.
- Persiste antes de llamar — la tarea existe en tu BD aunque la notificación nunca llegue; el trabajo pendiente se reintenta (cap. 21).
- Mide la dependencia: duración y tasa de fallo de cada llamada externa van a logs (cap. 23) — un proveedor lento es tu endpoint lento.
Un log es el diario del programa: qué pasó, cuándo, con qué request. Estructurado = en JSON con campos (request_id, user_id), para filtrar por máquina y no con los ojos.
- Un adapter, no
fetchdisperso:notifyExternal()es la única puerta a Slack — si cambia la API de ellos, cambias un archivo (y es lo que mockeas en cap. 29).
28.9 Ejercicio
- El cliente de arriba reintenta 4xx si quitas el
if. ¿Por qué es incorrecto reintentar un 400 o un 401? - ¿Qué pasa si
SLACK_WEBHOOK_URLno está definida? ¿Cuándo debería descubrirse ese problema — al arrancar o en el primer request? - Diseña la respuesta al cliente si la notificación falla tras todos los reintentos: ¿qué status, qué body, qué se loguea?
28.10 Mini reto
Responder: ¿qué ve tu usuario si la API externa está caída?
Ejercicio.
- Un 4xx significa que tu request está mal (body inválido, key mala) — reintentar solo repite el mismo request defectuoso y amplifica el problema. Se reintenta lo transitorio: 5xx, timeouts, errores de red.
- El
!/validación al arranque (cap. 24): la app debe reventar al boot si falta la config — descubrirlo en el primer request es descubrirlo a las 3am en prod. 201+ la tarea creada — la notificación es asíncrona por naturaleza. Se logueaERRORcon contexto (task_id, intentos, último status externo) y se encola el reintento. El usuario ya tiene lo que pidió.
Mini reto. Exactamente lo mismo que si estuviera viva: 201 con su tarea. La caída de Slack es problema operativo tuyo (logs, alertas, reintentos), no del usuario — degradación elegante = el usuario nunca se entera, tú sí.
28.11 Vocabulario técnico del capítulo
Una variable es una caja con nombre donde guardas un valor: let total = 42 guarda el 42 bajo el nombre total. Puedes leerla y cambiarla después (total = 50). const = caja que no se puede reemplazar.
Una función es una receta reutilizable: recibe ingredientes (parámetros), hace pasos y devuelve un plato (return). La escribes una vez y la llamas mil veces: add(2, 3) → 5.
Un bucle repite una acción por cada elemento o hasta cumplir una condición: for task in tasks hace algo con cada tarea. map y filter son bucles disfrazados de funciones — transforman/filtran colecciones sin for explícito.
Una clase es el molde de un objeto: define qué datos y qué métodos tiene. class Task es el molde; new Task() es una instancia concreta. Go no tiene clases — usa struct + métodos sueltos.
Un string es texto entre comillas: "hola". El nombre viene de “cadena de caracteres” — una secuencia de letras. Todo lo que llega de un formulario o una URL llega como string, aunque parezca número.
null (TS), None (Py), nil (Go) = “aquí no hay valor”. Es la respuesta a “¿qué devuelvo cuando no hay nada?” — y la fuente del bug más famoso de la historia (su inventor lo llamó “el error del billón de dólares”). Por eso el código revisa if x is not None.
Asíncrono = empezar algo sin esperar sentado a que termine: pides la pizza (async) y sigues trabajando; cuando llega, te avisan. Lo opuesto a síncrono (esperar parado). Vital cuando la espera es larga: red, disco, bases de datos.
Un diccionario (dict en Python, map en Go, objeto en JS) guarda parejas clave→valor: {"ana": 30}. Es la estructura que más aparece en JSON — un objeto JS es un diccionario.
Concurrencia = manejar muchas cosas en progreso (atender 1000 requests intercalando). Paralelismo = ejecutar varias a la vez en varios núcleos. Un camarero con 10 mesas es concurrente; 10 camareros son paralelos.
Un entorno es una instancia completa donde corre tu app con su propia config y datos: local (tu máquina), staging (réplica de prueba), producción (la real). Cada entorno tiene sus propias llaves y su propia base de datos.
Una base de datos es el programa que guarda datos de forma permanente y los responde rápido. Relacional (Postgres): tablas con relaciones. No relacional: documentos, clave-valor, grafos — cada una para una forma de dato distinta.
Una variable de entorno es configuración que vive fuera del código: DATABASE_URL, JWT_SECRET. El mismo binario corre en dev y prod con distintas vars — los secretos nunca se escriben en el código.
Un secreto es un dato que no puede publicarse: contraseñas de BD, llaves de API, el secreto que firma los JWT. Viven en .env (fuera de git) o en un secret manager — nunca en el código ni en el repo.
Un dominio es el nombre que compras (midominio.com) y apuntas a tu servidor por DNS. Sin él, tus usuarios tendrían que memorizar una IP. El HTTPS serio requiere dominio.
Una imagen es la plantilla inmutable de la que nacen contenedores: la foto del disco + cómo arrancar. Se construye en capas (Dockerfile), se versiona con tags, se publica en un registry.
Un webhook es una llamada HTTP invertida: en vez de tú preguntarle a Stripe “¿llegó el pago?”, Stripe te llama a tu endpoint cuando pasa. Se verifica la firma (es público) y se responde 200 rápido.