API de precios del oro en JavaScript: Node.js, TypeScript y Next.js
Obtén el precio del oro en JavaScript con una API: fetch con async/await, tipos de TypeScript, una ruta de Express o Next.js con caché y uso seguro de la clave.
API de precios del oro en JavaScript: una primera solicitud
Llamar a una API de precios del oro desde JavaScript requiere una sola llamada a fetch, pero una integración en producción también necesita tipos, gestión de errores, una caché y un lugar seguro para la clave API. Este artículo construye todo eso con MetalAPI, una API REST en JSON para metales preciosos, metales industriales, energía, divisas y criptomonedas: primero un script sencillo de Node.js, después un cliente tipado en TypeScript y, por último, una pequeña ruta de Express o Next.js que sirve el precio del oro a tu frontend sin exponer la clave.
Node.js 18 y versiones posteriores incluyen fetch, así que no necesitas ninguna biblioteca HTTP. Crea una clave en el panel (las claves empiezan por mk_live_), guárdala en un archivo .env como METALAPI_KEY=mk_live_... y ejecuta el script con node --env-file=.env gold.mjs. Node.js 20.6 y posteriores leen el archivo; en versiones anteriores, exporta la variable en tu shell.
const params = new URLSearchParams({ base: "USD", currencies: "XAU" });
const res = await fetch(`https://metalapi.com/api/v1/latest?${params}`, {
headers: { "X-API-Key": process.env.METALAPI_KEY },
signal: AbortSignal.timeout(10_000),
});
const data = await res.json();
if (!data.success) throw new Error(`MetalAPI ${data.error.code}: ${data.error.info}`);
const price = data.rates[`${data.base}XAU`];
console.log(`Gold: ${price} ${data.base} per troy ounce`);
console.log(`Price time: ${new Date(data.timestamp * 1000).toISOString()}`);Con los valores de ejemplo de la documentación, rates.USDXAU es 3997.89: el precio de una onza troy de oro en la divisa base. El mismo objeto contiene también rates.XAU, unos 0.00025, que es el inverso: onzas de oro por dólar. Mostrar ese valor por error es el fallo clásico de los precios del oro. Cambia base a EUR y la clave pasa a ser EURXAU; añade unit=gram para obtener un precio por gramo. timestamp es la hora de los datos de precios en segundos Unix, no la hora de tu solicitud.
Tipos de TypeScript para la respuesta de la API de precios del oro
Las estructuras de respuesta son pequeñas y estables, así que es fácil tiparlas. Las respuestas correctas tienen success: true. Los fallos tienen success: false y un objeto error con un code numérico, un type y un mensaje info, de modo que una unión discriminada por success permite a TypeScript distinguir entre ambos casos. Este cliente también convierte los fallos en un error tipado que conserva el valor de Retry-After de las respuestas por límite de solicitudes:
const API = "https://metalapi.com/api/v1";
export interface RateLimit {
limit: number;
remaining: number;
used: number;
reset: number; // segundos Unix
reset_date: string;
plan: string;
}
export interface LatestResponse {
success: true;
timestamp: number; // hora de los datos de precios, segundos Unix
date: string;
base: string;
unit?: string; // presente cuando unit no es troy_oz
rates: Record<string, number>; // "USDXAU": precio, "XAU": inverso
rate_limit: RateLimit;
}
export interface TimeframeResponse {
success: true;
start_date: string;
end_date: string;
base: string;
rates: Record<string, Record<string, number>>; // indexado por YYYY-MM-DD
rate_limit: RateLimit;
}
export interface ErrorResponse {
success: false;
error: { code: number; type: string; info: string; upgrade_url?: string };
}
export class MetalApiError extends Error {
code: number;
type: string;
retryAfter?: number;
constructor(code: number, type: string, info: string, retryAfter?: number) {
super(`MetalAPI ${code} (${type}): ${info}`);
this.name = "MetalApiError";
this.code = code;
this.type = type;
this.retryAfter = retryAfter;
}
}
export async function metalapi<T extends { success: true }>(
path: string,
params: Record<string, string> = {},
): Promise<T> {
const res = await fetch(`${API}/${path}?${new URLSearchParams(params)}`, {
headers: { "X-API-Key": process.env.METALAPI_KEY ?? "" },
signal: AbortSignal.timeout(10_000),
});
const body = (await res.json().catch(() => null)) as T | ErrorResponse | null;
if (body === null) throw new MetalApiError(res.status, "http_error", res.statusText);
if (body.success === false) {
const retryAfter = Number(res.headers.get("Retry-After")) || undefined;
throw new MetalApiError(body.error.code, body.error.type, body.error.info, retryAfter);
}
return body;
}
export async function goldPrice(base = "USD", unit = "troy_oz") {
const data = await metalapi<LatestResponse>("latest", { base, currencies: "XAU", unit });
return { price: data.rates[`${data.base}XAU`], base: data.base, unit, asOf: new Date(data.timestamp * 1000) };
}await goldPrice("EUR", "gram") devuelve el precio en euros por gramo con su marca de tiempo, y metalapi<TimeframeResponse>("timeframe", { start_date, end_date, currencies: "XAU" }) devuelve los cierres diarios de hasta 30 días por solicitud en Free y de 365 en los planes de pago. Decide según err.code, nunca según el mensaje. Reintenta solo 104 (el límite por minuto, después de retryAfter segundos), 500, 503 y los errores de red; esas solicitudes fallidas no se facturan. Los demás códigos requieren una corrección: 101 o 102 indican una clave ausente o no válida, 105 una cuota mensual agotada y 106 un endpoint que tu plan no incluye.
Una ruta de Express con caché para el precio del oro
Una ruta de servidor necesita una caché. Cada plan actualiza los precios a un intervalo fijo (a diario en Free, cada 30 minutos en Lite, cada 10 minutos en Standard y cada 60 segundos en Advanced y Pro), así que preguntar antes devuelve las mismas cifras y solo gasta cuota. Esta ruta guarda en caché cada divisa y unidad durante 10 minutos, hace que las solicitudes simultáneas compartan una única llamada a la API y sigue sirviendo el último precio válido si no se puede contactar con MetalAPI:
// npm install express y después: METALAPI_KEY=mk_live_... npx tsx server.ts
import express from "express";
import { goldPrice } from "./metalapi";
const TTL_MS = 10 * 60_000; // Plan Standard: los precios se actualizan cada 10 minutos
const CURRENCIES = new Set(["USD", "EUR", "GBP"]);
const UNITS = new Set(["troy_oz", "gram"]);
type Quote = Awaited<ReturnType<typeof goldPrice>>;
const cache = new Map<string, { quote: Quote; expires: number }>();
const inflight = new Map<string, Promise<Quote>>();
function cachedGold(base: string, unit: string): Promise<Quote> {
const key = `${base}:${unit}`;
const hit = cache.get(key);
if (hit && Date.now() < hit.expires) return Promise.resolve(hit.quote);
let pending = inflight.get(key); // single flight: una sola llamada a la API por clave
if (!pending) {
pending = goldPrice(base, unit)
.then((quote) => {
cache.set(key, { quote, expires: Date.now() + TTL_MS });
return quote;
})
.catch((err) => {
if (!hit) throw err;
cache.set(key, { quote: hit.quote, expires: Date.now() + 60_000 }); // obsoleto, reintenta en 1 min
return hit.quote;
})
.finally(() => inflight.delete(key));
inflight.set(key, pending);
}
return pending;
}
const app = express();
app.get("/api/gold", async (req, res) => {
const base = String(req.query.currency ?? "USD").toUpperCase();
const unit = String(req.query.unit ?? "troy_oz");
if (!CURRENCIES.has(base) || !UNITS.has(unit)) {
res.status(400).json({ error: "Unsupported currency or unit" });
return;
}
try {
res.set("Cache-Control", "public, max-age=60").json(await cachedGold(base, unit));
} catch (err) {
console.error(err);
res.status(503).json({ error: "Gold price temporarily unavailable" });
}
});
app.listen(3000, () => console.log("http://localhost:3000/api/gold?currency=EUR&unit=gram"));La lista de valores permitidos importa tanto para tu cuota como para la seguridad. Cada combinación de divisa y unidad es una entrada de caché independiente que cuesta una solicitud por actualización, así que seis combinaciones mantenidas al día las 24 horas consumen unas 26.800 solicitudes al mes. Limita la lista a lo que muestran tus páginas, o pide varias divisas en una sola solicitud y convierte localmente, como hace el tutorial de PHP y WordPress. La caché vive en la memoria del proceso; con varias instancias o funciones serverless, muévela a Redis o a la caché de tu plataforma.
Después, el frontend llama a tu ruta, nunca a MetalAPI:
<p>Gold: <strong id="gold">…</strong> <small id="gold-time"></small></p>
<script type="module">
const res = await fetch("/api/gold?currency=EUR&unit=gram");
if (res.ok) {
const q = await res.json();
const money = new Intl.NumberFormat(undefined, { style: "currency", currency: q.base });
document.getElementById("gold").textContent = `${money.format(q.price)} per gram`;
document.getElementById("gold-time").textContent = `as of ${new Date(q.asOf).toLocaleString()}`;
}
</script>La misma ruta en Next.js
En el App Router de Next.js, un Route Handler hace el mismo trabajo, y la caché de datos de Next.js puede sustituir a la caché escrita a mano. Guarda el cliente como lib/metalapi.ts y mantén la clave en .env.local como METALAPI_KEY, sin el prefijo NEXT_PUBLIC_, para que solo exista en el servidor:
import type { ErrorResponse, LatestResponse } from "@/lib/metalapi";
export async function GET() {
try {
const res = await fetch("https://metalapi.com/api/v1/latest?currencies=XAU,XAG", {
headers: { "X-API-Key": process.env.METALAPI_KEY ?? "" },
next: { revalidate: 600 }, // Plan Standard: reutiliza la respuesta durante 10 minutos
});
const data = (await res.json()) as LatestResponse | ErrorResponse;
if (data.success) {
return Response.json({ gold: data.rates.USDXAU, silver: data.rates.USDXAG, timestamp: data.timestamp });
}
console.error("MetalAPI error", data.error.code, data.error.info);
} catch (err) {
console.error(err);
}
return Response.json({ error: "Prices temporarily unavailable" }, { status: 503 });
}next: { revalidate: 600 } guarda la respuesta de la API durante un máximo de 10 minutos y la comparte entre solicitudes. Next.js solo guarda respuestas con estado HTTP 200, así que un error de MetalAPI nunca se queda en caché. Si tu aplicación activa Cache Components, mueve el fetch a una función auxiliar marcada con 'use cache' y define su duración con cacheLife.
Próximos pasos
Crea una cuenta gratuita para obtener una clave. El plan Free incluye 100 solicitudes al mes con actualizaciones diarias y sin tarjeta de crédito, suficiente para crear y probar todo lo anterior. Cuando tu ruta esté en producción, elige un plan según la frescura de los datos y el volumen: una entrada de caché de 10 minutos consume unas 4500 solicitudes al mes, que caben en las 10.000 de Standard. La página de precios compara todos los planes, y API de precios del oro en Python trata la misma API para scripts, pandas y gráficos.
Preguntas frecuentes
¿Puedo llamar a la API de precios del oro directamente desde el navegador?
Sí. La API responde a solicitudes de origen cruzado, así que fetch funciona desde una página web. Usa una clave restringida a tus dominios, y recuerda que los visitantes pueden seguir leyéndola y que cada visita a la página cuesta una solicitud. Una ruta de servidor con caché es más barata y mantiene la clave en privado.
¿Por qué mi página muestra 0,00025 en lugar del precio del oro?
Estás leyendo rates.XAU, la cotización inversa. Lee rates.USDXAU, o rates[base + "XAU"] cuando la divisa base varía. Con los valores de ejemplo, 1 ÷ 0,0002501319 da 3997,89.
¿Cómo obtengo el precio del oro en euros o por gramo en JavaScript?
Pasa base=EUR y unit=gram. El precio es entonces rates.EURXAU, en euros por gramo, y la respuesta incluye unit: "gram". unit también acepta troy_oz (el valor por defecto), kilogram, ounce, pound, metric_ton y short_ton. Para el oro, evita ounce, que es la onza avoirdupois.
¿Con qué frecuencia debo actualizar el precio del oro?
Como máximo una vez por intervalo de actualización de tu plan: a diario en Free, cada hora en Lite (cada 30 minutos supondría unas 1490 solicitudes al mes frente a una cuota de 1000) y cada 10 minutos en Standard. Los precios no cambian durante el fin de semana, cuando el oro al contado no cotiza desde el viernes a las 17:00 hasta el domingo a las 17:00, hora de Nueva York.
¿Puedo obtener precios del oro en tiempo real sin consultas periódicas?
En el plan Pro y superiores, /stream envía las actualizaciones de precios mediante Server-Sent Events, así que nunca tienes que consultar /latest. Para lógica de umbrales, como que el oro supere los 4000 USD, las alertas de precio por webhook (plan Advanced y superiores) llaman a tu servidor cuando se cumple la condición.
Pruébalo con tus propios datos
Crea una cuenta gratuita y haz tu primera solicitud en menos de dos minutos. 100 solicitudes al mes, sin tarjeta de crédito.
Sigue leyendo
Qué significa XAU/USD: los códigos XAU, XAG, XPT y XPD
Qué significa XAU/USD: por qué el oro tiene un código de divisa ISO 4217, qué son XAG, XPT y XPD y cómo leer el sentido de las cotizaciones y datos de API.
API gratuita de precios del oro: qué dan 100 solicitudes al mes
Una API gratuita de precios del oro explicada con honestidad: qué incluye el plan Free de MetalAPI, cómo aprovechar 100 solicitudes al mes y cuándo cambiar.
API de precios del oro en Python: consulta, caché y gráficos
Usa una API de precios del oro en Python: obtén precios en tiempo real con requests, gestiona errores y reintentos, usa caché y grafica una serie con pandas.