MetalAPI
XAU3.997,89−1,54%XAG53,936−1,51%XPT1.554,47−0,83%XPD1.047,03−0,44%XCU6,5080+2,29%WTI103,27+3,82%BRENT111,70−1,30%EUR1,120400−0,01%GBP1,337500−0,01%JPY0,0063761+0,14%CHF1,213445−0,25%TRY0,020125−0,12%BTC80.638,10−0,17%ETH2.354,36−0,06%
Todos os artigos
OuroJavaScriptTypeScriptTutorial

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.

Por MetalAPI Team··7 min de leitura

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.

gold.mjs
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:

metalapi.ts
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.

No servidor ou no navegador: onde fica a chave de API

Tudo o que está no JavaScript do lado do cliente é público. Uma chave em um bundle React, em um script inline ou em uma variável de ambiente NEXT_PUBLIC_ ou VITE_ pode ser lida por qualquer visitante nas ferramentas de desenvolvedor do navegador, e cada visualização de página gasta uma das suas requisições. Há três formas de colocar um preço do ouro diante dos usuários:

  • A sua própria rota no servidor (recomendado). O navegador chama o seu endpoint, e o seu servidor chama a MetalAPI com a chave e guarda o resultado em cache. O consumo fica em uma requisição por atualização do cache, não importa quanto tráfego você receba.
  • Uma chave restrita por domínio no navegador. No painel, você pode restringir uma chave aos seus domínios, e as requisições de navegador cujo Origin ou Referer não corresponda são rejeitadas com o erro 107, sem cobrança. Isso impede que outros sites usem a chave nos navegadores dos visitantes deles, mas não impede que alguém a copie para um script: requisições sem Origin ou Referer são sempre permitidas. Use uma chave separada para isso, nunca a principal. O guia de autenticação traz os detalhes.
  • Um widget incorporável. Para uma simples exibição de preço, um widget não precisa de chave nem de código, e as visualizações de página do widget não contam para a sua cota.

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:

server.ts
// 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:

index.html
<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:

app/api/gold/route.ts
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