Guide/Arquitectura de conversores

¿Cómo mantienen actualizados los tipos de cambio las apps de conversión?

Un conversor se mantiene al día al guardar una observación completa, actualizarla con una cadencia controlada y conservar la hora real del dato cuando falla una consulta o el mercado está cerrado.

ERexchangerate.dev·Aug 23, 2026·8 min de lectura

Un conversor fiable no pide un número en cada render. Guarda el tipo junto con source, market_session y data_updated_at, reutiliza esa observación durante un periodo corto y solo la sustituye después de una actualización correcta. La hora de respuesta indica cuándo contestó la API; la hora de observación indica cuándo cambió el tipo.

Key points
Guarda la observación completa: rates, sources, market_session y data_updated_at.
Usa data_updated_at para calcular la antigüedad; timestamp es la hora de construcción de la respuesta.
Los tipos activos pueden cambiar dentro del día hábil, mientras que las referencias diarias siguen su propia publicación.
Durante un cierre conserva la última observación válida y su hora original.
Si falla una actualización, muestra el último dato válido con fecha o devuelve un error si no existe.

Trata la respuesta como una sola observación

Un tipo sin metadatos es ambiguo: puede ser spot intradía, una referencia diaria o un cruce derivado. Guarda el objeto completo en vez de extraer rates.EUR y descartar el contexto. La referencia de la API define los campos y la metodología explica cómo se asignan la fuente y la sesión.

json · abbreviated latest responsecopy
{
  "base": "USD",
  "source": "live",
  "market_session": "open",
  "timestamp": "2026-08-23T09:42:06Z",
  "data_updated_at": "2026-08-23T09:41:00Z",
  "rates": { "EUR": 0.86124, "GBP": 0.74281 },
  "sources": { "EUR": "live", "GBP": "live" },
  "derived_symbols": []
}

source resume la clase menos fresca en una respuesta con varias divisas. Consulta sources para cada símbolo. derived_symbols identifica los tipos calculados a través de otras divisas.

Dos horas con significados distintos
timestamp es la hora en que la API preparó la respuesta. data_updated_at es la hora de la observación subyacente. Muestra y evalúa la frescura con esta última.

Define la política antes del temporizador

No existe un intervalo universal. Un precio aproximado de tienda puede compartir caché durante varios minutos; un panel puede actualizar mientras esté visible; un informe diario puede consultar una vez después del fixing necesario. Hacer una solicitud en cada render gasta llamadas y puede devolver la misma observación.

UsoPolítica de la appQué mostrar
Precio aproximadoCaché compartida; refresco cada pocos minutosDivisa, carácter indicativo y hora
Panel operativoCaché corta; refresco mientras esté visibleTipo, fuente, sesión y hora
Informe diarioUna consulta programada tras el fixingTipo y hora almacenada
Importe de liquidaciónNo usar un tipo indicativoResultado del proveedor de pago

market_session puede ser open, weekend o interbank_closed. Los tipos negociados activamente pueden cambiar durante la semana; las series de referencia diaria avanzan según su calendario.

Crea una caché pequeña en el servidor

Ordena los símbolos para obtener una clave estable, guarda solo respuestas correctas y comparte una actualización concurrente. Las llamadas autenticadas deben permanecer en el servidor.

typescript · latest-rates.tscopy
type RateSource = "live" | "ecb_daily" | "fred_daily";

type LatestRates = {
  base: string;
  source: RateSource;
  sources: Record<string, RateSource>;
  market_session: "open" | "weekend" | "interbank_closed";
  timestamp: string;
  data_updated_at: string;
  rates: Record<string, number>;
  derived_symbols: string[];
};

type CacheEntry = {
  value: LatestRates;
  fetchedAt: number;
};

const cache = new Map<string, CacheEntry>();
const pending = new Map<string, Promise<LatestRates>>();

function cacheKey(base: string, symbols: string[]): string {
  return `${base}:${[...symbols].sort().join(",")}`;
}

async function fetchLatest(base: string, symbols: string[]): Promise<LatestRates> {
  const url = new URL(`https://api.exchangerate.dev/v1/latest/${base}`);
  url.searchParams.set("symbols", [...symbols].sort().join(","));

  const response = await fetch(url, {
    headers: process.env.EXCHANGERATE_API_KEY
      ? { Authorization: `Bearer ${process.env.EXCHANGERATE_API_KEY}` }
      : {},
    signal: AbortSignal.timeout(8000),
  });

  if (!response.ok) {
    throw new Error(`FX refresh failed: ${response.status}`);
  }

  return response.json() as Promise<LatestRates>;
}

export async function getLatestRates(
  base: string,
  symbols: string[],
  maxAgeMs = 60_000,
): Promise<LatestRates> {
  const key = cacheKey(base, symbols);
  const existing = cache.get(key);

  if (existing && Date.now() - existing.fetchedAt < maxAgeMs) {
    return existing.value;
  }

  const inFlight = pending.get(key);
  if (inFlight) return inFlight;

  const refresh = fetchLatest(base, symbols)
    .then((value) => {
      cache.set(key, { value, fetchedAt: Date.now() });
      return value;
    })
    .finally(() => pending.delete(key));

  pending.set(key, refresh);
  return refresh;
}

En varias instancias usa un almacén compartido o la caché del framework. El contrato no cambia: valor y metadatos juntos, sustituidos solo tras una respuesta correcta.

Un error de refresco no crea un tipo nuevo

Un timeout, un 429 o un error externo no es una observación. Conserva el dato anterior con su data_updated_at y una advertencia si el producto permite último dato válido. Sin dato previo, devuelve un error. Respeta Retry-After y usa espera exponencial con variación para fallos transitorios; un 401 requiere corregir la configuración.

typescript · explicit stale fallbackcopy
type ConverterResult =
  | { status: "fresh"; data: LatestRates }
  | { status: "last_known_good"; data: LatestRates; reason: string };

async function getConverterRates(
  base: string,
  symbols: string[],
): Promise<ConverterResult> {
  const key = cacheKey(base, symbols);

  try {
    return { status: "fresh", data: await getLatestRates(base, symbols) };
  } catch (error) {
    const previous = cache.get(key);
    if (!previous) throw error;

    return {
      status: "last_known_good",
      data: previous.value,
      reason: error instanceof Error ? error.message : "Refresh failed",
    };
  }
}

Para respuestas 429, respeta Retry-After cuando exista y evita que todas las instancias reintenten a la vez. El backoff exponencial con variación sirve para fallos transitorios. Los errores de autenticación requieren corregir la configuración, no reintentos.

Un mercado cerrado es un estado válido

Durante el fin de semana, market_session: weekend puede acompañar la última observación de la semana. No sustituyas su hora por la de una nueva respuesta HTTP ni inventes cotizaciones de fin de semana.

El conversor puede alargar su caché de aplicación mientras el mercado está cerrado, pero debe actualizar después de que se reanude la semana de negociación. Basa la decisión en el estado de sesión devuelto y en los requisitos del producto, no en asumir que todas las monedas siguen un solo horario.

Convierte y redondea una vez

Con base USD, 25 USD a EUR es 25 × rates.EUR. Conserva la precisión del tipo y redondea solo el importe final según la divisa destino.

Indicativo, no ejecutable
El conversor sirve para mostrar, estimar y analizar. El importe cobrado debe venir del proveedor de pago o negociación, con su spread, comisiones y hora de ejecución.

Lista de comprobación

  • Solicita solo los símbolos necesarios.
  • Crea una clave con base y símbolos ordenados.
  • Guarda respuestas correctas completas, nunca errores HTTP.
  • Evita actualizaciones concurrentes duplicadas.
  • Muestra data_updated_at cuando importe la antigüedad.
  • Conserva sources, market_session y derived_symbols.
  • Mantén la clave en el servidor.
  • Etiqueta el último dato válido y conserva su hora original.
ER
exchangerate.dev
Arquitectura y guías de integración para desarrolladores que trabajan con datos FX.

Sigue leyendo

ReferenceCómo leer source y market_sessionLeer TutorialTipos de cambio en Next.js y TypeScriptLeer GuideTipos FX indicativos y ejecutablesLeer
Más ComparacionesFixer vs exchangerate.devOpen Exchange Rates vs exchangerate.devCurrencylayer vs exchangerate.dev
AprenderReading source and market_session in your pipelineIndicative vs executable FX rates: what a rates API actually gives youECB reference rates, explained
Tasas en VivoEUR/USDGBP/USDUSD/JPY