API de preços do ouro em JavaScript: Node.js, TypeScript e Next.js
Obtenha o preço do ouro em JavaScript com uma API: fetch com async/await, tipos TypeScript, uma rota Express ou Next.js com cache e uso seguro da chave de API.
API de preços do ouro em JavaScript: uma primeira requisição
Chamar uma API de preços do ouro a partir de JavaScript exige uma única chamada fetch, mas uma integração em produção também precisa de tipos, tratamento de erros, cache e um lugar seguro para a chave de API. Este artigo constrói tudo isso com a MetalAPI, uma API REST em JSON para metais preciosos, metais industriais, energia, moedas e cripto: primeiro um script Node.js simples, depois um cliente tipado em TypeScript e, por fim, uma pequena rota Express ou Next.js que entrega o preço do ouro ao seu frontend sem expor a chave.
O Node.js 18 e as versões mais recentes já trazem fetch, então nenhuma biblioteca HTTP é necessária. Crie uma chave no painel (as chaves começam com mk_live_), coloque-a em um arquivo .env como METALAPI_KEY=mk_live_... e execute o script com node --env-file=.env gold.mjs. O Node.js 20.6 e as versões mais recentes leem o arquivo; nas versões anteriores, exporte a variável no seu 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()}`);Com os valores de exemplo da documentação, rates.USDXAU é 3997.89: o preço de uma onça troy de ouro na moeda base. O mesmo objeto também contém rates.XAU, cerca de 0.00025, que é o inverso, onças de ouro por dólar. Exibir esse valor por engano é o bug clássico de preços do ouro. Mude base para EUR e a chave passa a ser EURXAU; adicione unit=gram para obter um preço por grama. timestamp é o horário dos dados de preço em segundos Unix, e não o horário da sua requisição.
Tipos TypeScript para a resposta da API de preços do ouro
Os formatos de resposta são pequenos e estáveis, o que facilita tipá-los. Respostas bem-sucedidas têm success: true. Falhas têm success: false e um objeto error com um code numérico, um type e uma mensagem info, então uma união discriminada em success permite ao TypeScript distinguir os dois casos. Este cliente também transforma as falhas em um erro tipado que guarda o valor de Retry-After das respostas de limite de requisições:
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; // horário dos dados de preço, segundos Unix
date: string;
base: string;
unit?: string; // presente quando unit não é troy_oz
rates: Record<string, number>; // "USDXAU": preço, "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") retorna o preço em euros por grama com o seu timestamp, e metalapi<TimeframeResponse>("timeframe", { start_date, end_date, currencies: "XAU" }) retorna os fechamentos diários de até 30 dias por requisição no Free e 365 nos planos pagos. Tome decisões com base em err.code, nunca na mensagem. Repita apenas 104 (o limite por minuto, após retryAfter segundos), 500, 503 e erros de rede; essas requisições com falha não são cobradas. Os outros códigos exigem uma correção: 101 ou 102 indicam uma chave ausente ou inválida, 105 uma cota mensal esgotada e 106 um endpoint que o seu plano não inclui.
Uma rota de preço do ouro com cache no Express
Uma rota no servidor precisa de cache. Cada plano atualiza os preços em um intervalo fixo (diário no Free, a cada 30 minutos no Lite, 10 minutos no Standard e 60 segundos no Advanced e no Pro), então perguntar antes disso retorna os mesmos números e só gasta cota. Esta rota guarda em cache cada moeda e unidade por 10 minutos, faz as requisições simultâneas compartilharem uma única chamada à API e continua servindo o último preço válido se a MetalAPI não puder ser acessada:
// npm install express, depois: METALAPI_KEY=mk_live_... npx tsx server.ts
import express from "express";
import { goldPrice } from "./metalapi";
const TTL_MS = 10 * 60_000; // Plano Standard: os preços são atualizados a 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: uma chamada à API por chave
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 }); // desatualizado, tenta de novo em 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"));A lista de valores permitidos importa tanto para a sua cota quanto para a segurança. Cada combinação de moeda e unidade é uma entrada de cache separada que custa uma requisição por atualização, então seis combinações mantidas aquecidas o tempo todo consomem cerca de 26.800 requisições por mês. Limite a lista ao que as suas páginas exibem, ou busque várias moedas em uma única requisição e converta localmente, como faz o tutorial de PHP e WordPress. O cache fica na memória do processo; com várias instâncias ou funções serverless, mova-o para o Redis ou para o cache da sua plataforma.
O frontend então chama a sua rota, 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>A mesma rota no Next.js
No App Router do Next.js, um Route Handler faz o mesmo trabalho, e o cache de dados do Next.js pode substituir o cache escrito à mão. Salve o cliente como lib/metalapi.ts e mantenha a chave em .env.local como METALAPI_KEY, sem o prefixo NEXT_PUBLIC_, para que ela exista apenas no 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 }, // Plano Standard: reutiliza a resposta por 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 } armazena a resposta da API por até 10 minutos e a compartilha entre as requisições. O Next.js só armazena respostas com status HTTP 200, então um erro da MetalAPI nunca entra no cache. Se o seu app habilita Cache Components, mova o fetch para um helper marcado com 'use cache' e defina o tempo de vida com cacheLife.
Próximos passos
Crie uma conta grátis para obter uma chave. O plano Free inclui 100 requisições por mês com atualização diária e não exige cartão de crédito, o suficiente para desenvolver e testar tudo o que está acima. Quando a sua rota entrar no ar, escolha um plano pela atualidade dos preços e pelo volume: uma entrada de cache de 10 minutos consome cerca de 4.500 requisições por mês, o que cabe nas 10.000 do Standard. A página de preços compara todos os planos, e API de preços do ouro em Python aborda a mesma API para scripts, pandas e gráficos.
Perguntas frequentes
Posso chamar a API de preços do ouro diretamente do navegador?
Sim. A API responde a requisições cross-origin, então fetch funciona a partir de uma página web. Use uma chave restrita aos seus domínios e lembre-se de que os visitantes ainda podem lê-la e de que cada visualização de página custa uma requisição. Uma rota no servidor com cache é mais barata e mantém a chave privada.
Por que a minha página mostra 0,00025 em vez do preço do ouro?
Você está lendo rates.XAU, a cotação inversa. Leia rates.USDXAU, ou rates[base + "XAU"] quando a moeda base varia. Com os valores de exemplo, 1 ÷ 0,0002501319 resulta em 3.997,89.
Como obter o preço do ouro em euros ou por grama em JavaScript?
Passe base=EUR e unit=gram. O preço passa a ser rates.EURXAU, em euros por grama, e a resposta inclui unit: "gram". unit também aceita troy_oz (o padrão), kilogram, ounce, pound, metric_ton e short_ton. Para o ouro, evite ounce, que é a onça avoirdupois.
Com que frequência devo atualizar o preço do ouro?
No máximo uma vez por intervalo de atualização do seu plano: diariamente no Free, de hora em hora no Lite (a cada 30 minutos seriam cerca de 1.490 requisições por mês para uma cota de 1.000) e a cada 10 minutos no Standard. Os preços ficam parados no fim de semana, quando o ouro à vista não é negociado das 17h de sexta às 17h de domingo, horário de Nova York.
Posso obter preços do ouro em tempo real sem polling?
No plano Pro e superiores, /stream envia as atualizações de preço via Server-Sent Events, então você nunca precisa fazer polling em /latest. Para lógica de limites, como o ouro ultrapassando US$ 4.000, os alertas de preço por webhook (plano Advanced e superiores) chamam o seu servidor quando a condição é atendida.
Experimente com seus próprios dados
Crie uma conta grátis e faça sua primeira requisição em menos de dois minutos. 100 requisições por mês, sem cartão de crédito.
Continue lendo
Significado de XAU/USD: o que são XAU, XAG, XPT e XPD
O significado de XAU/USD: por que o ouro tem um código de moeda ISO 4217, o que XAG, XPT e XPD representam e como ler a direção das cotações em preços e APIs.
API de preço do ouro grátis: o que 100 requisições por mês fazem
Uma API de preço do ouro grátis, sem exageros: o que o plano Free da MetalAPI inclui, como render 100 requisições por mês com cache e lotes, e quando migrar.
API de preços do ouro em Python: consulta, cache e gráficos
API de preços do ouro em Python: cotações ao vivo com requests, tratamento de erros e novas tentativas, cache e gráficos de séries do pandas com matplotlib.