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 636,70−0,17 %ETH2 348,01−0,33 %
Tous les articles
OrJavaScriptTypeScriptTutoriel

API du cours de l'or en JavaScript : Node.js, TypeScript et Next.js

Obtenir le cours de l'or en JavaScript via une API : fetch avec async/await, types TypeScript, route Express ou Next.js avec cache et clé API protégée.

Par MetalAPI Team··7 min de lecture

API du cours de l'or en JavaScript : une première requête

Appeler une API du cours de l'or depuis JavaScript ne demande qu'un appel à fetch, mais une intégration en production a aussi besoin de types, d'une gestion des erreurs, d'un cache et d'un endroit sûr pour la clé API. Cet article construit tout cela avec MetalAPI, une API REST JSON pour les métaux précieux, les métaux industriels, l'énergie, les devises et les cryptos : d'abord un simple script Node.js, puis un client TypeScript typé, et enfin une petite route Express ou Next.js qui sert le cours de l'or à votre frontend sans exposer la clé.

Node.js 18 et les versions plus récentes intègrent fetch : aucune bibliothèque HTTP n'est nécessaire. Créez une clé dans le tableau de bord (les clés commencent par mk_live_), placez-la dans un fichier .env sous la forme METALAPI_KEY=mk_live_... et lancez le script avec node --env-file=.env gold.mjs. Node.js 20.6 et les versions plus récentes lisent ce fichier ; sur les versions antérieures, exportez plutôt la variable dans votre 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()}`);

Avec les valeurs d'exemple de la documentation, rates.USDXAU vaut 3997.89 : le prix d'une once troy d'or dans la devise de base. Le même objet contient aussi rates.XAU, environ 0.00025, qui en est l'inverse : des onces d'or par dollar. L'afficher par erreur est le bug classique du cours de l'or. Remplacez base par EUR et la clé devient EURXAU ; ajoutez unit=gram pour un prix au gramme. timestamp est l'heure des données de prix en secondes Unix, pas celle de votre requête.

Types TypeScript pour la réponse de l'API du cours de l'or

Les formes de réponse sont petites et stables, donc faciles à typer. Les réponses réussies ont success: true. Les échecs ont success: false et un objet error avec un code numérique, un type et un message info : une union discriminée sur success permet donc à TypeScript de distinguer les deux cas. Ce client transforme aussi les échecs en une erreur typée qui conserve la valeur Retry-After des réponses de limite de débit :

metalapi.ts
const API = "https://metalapi.com/api/v1";

export interface RateLimit {
  limit: number;
  remaining: number;
  used: number;
  reset: number; // secondes Unix
  reset_date: string;
  plan: string;
}

export interface LatestResponse {
  success: true;
  timestamp: number; // heure des données de prix, secondes Unix
  date: string;
  base: string;
  unit?: string; // présent quand unit n'est pas troy_oz
  rates: Record<string, number>; // "USDXAU" : prix, "XAU" : inverse
  rate_limit: RateLimit;
}

export interface TimeframeResponse {
  success: true;
  start_date: string;
  end_date: string;
  base: string;
  rates: Record<string, Record<string, number>>; // indexé par 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") renvoie le prix en euros par gramme avec son horodatage, et metalapi<TimeframeResponse>("timeframe", { start_date, end_date, currencies: "XAU" }) renvoie les clôtures quotidiennes, jusqu'à 30 jours par requête sur Free et 365 sur les plans payants. Basez votre logique sur err.code, jamais sur le message. Ne réessayez que 104 (la limite par minute, après retryAfter secondes), 500, 503 et les erreurs réseau ; ces requêtes en échec ne sont pas facturées. Les autres codes demandent plutôt une correction : 101 ou 102 signalent une clé absente ou invalide, 105 un quota mensuel épuisé et 106 un endpoint que votre plan n'inclut pas.

Côté serveur ou navigateur : où placer la clé API

Tout ce qui se trouve dans du JavaScript côté client est public. Une clé placée dans un bundle React, un script inline ou une variable d'environnement NEXT_PUBLIC_ ou VITE_ peut être lue par n'importe quel visiteur dans les outils de développement du navigateur, et chaque affichage de page consomme une de vos requêtes. Il existe trois façons de présenter un cours de l'or aux utilisateurs :

  • Votre propre route serveur (recommandé). Le navigateur appelle votre endpoint, et votre serveur appelle MetalAPI avec la clé et met le résultat en cache. La consommation reste d'une requête par rafraîchissement du cache, quel que soit votre trafic.
  • Une clé restreinte par domaine dans le navigateur. Dans le tableau de bord, vous pouvez restreindre une clé à vos domaines : les requêtes de navigateur dont l'Origin ou le Referer ne correspond pas sont rejetées avec l'erreur 107, sans être facturées. Cela empêche d'autres sites web d'utiliser la clé dans le navigateur de leurs visiteurs, mais pas quelqu'un qui la copie dans un script : les requêtes sans Origin ni Referer sont toujours autorisées. Utilisez une clé distincte pour cet usage, jamais votre clé principale. Le guide d'authentification donne les détails.
  • Un widget intégrable. Pour un simple affichage de prix, un widget ne demande ni clé ni code, et les affichages de pages du widget ne sont pas décomptés de votre quota.

Une route Express avec cache pour le cours de l'or

Une route serveur a besoin d'un cache. Chaque plan rafraîchit les prix à intervalle fixe (une fois par jour sur Free, toutes les 30 minutes sur Lite, toutes les 10 minutes sur Standard et toutes les 60 secondes sur Advanced et Pro) : demander plus tôt renvoie les mêmes chiffres et ne fait que consommer du quota. Cette route met en cache chaque devise et chaque unité pendant 10 minutes, fait partager un seul appel en amont aux requêtes simultanées et continue de servir le dernier prix valide si MetalAPI est injoignable :

server.ts
// npm install express, puis : METALAPI_KEY=mk_live_... npx tsx server.ts
import express from "express";
import { goldPrice } from "./metalapi";

const TTL_MS = 10 * 60_000; // Plan Standard : prix mis à jour toutes les 10 minutes
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 : un seul appel en amont par clé
  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 }); // périmé, nouvel essai dans 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 liste autorisée compte autant pour votre quota que pour la sécurité. Chaque combinaison de devise et d'unité est une entrée de cache distincte qui coûte une requête par rafraîchissement : six combinaisons maintenues à jour en permanence consomment environ 26 800 requêtes par mois. Limitez la liste à ce qu'affichent vos pages, ou récupérez plusieurs devises en une requête et convertissez localement, comme le fait le tutoriel PHP et WordPress. Le cache vit dans la mémoire du processus ; avec plusieurs instances ou des fonctions serverless, déplacez-le dans Redis ou dans le cache de votre plateforme.

Le frontend appelle ensuite votre route, jamais 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>

La même route dans Next.js

Dans l'App Router de Next.js, un Route Handler fait le même travail, et le cache de données de Next.js peut remplacer le cache écrit à la main. Enregistrez le client sous lib/metalapi.ts et gardez la clé dans .env.local sous le nom METALAPI_KEY, sans préfixe NEXT_PUBLIC_, pour qu'elle n'existe que sur le serveur :

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 }, // Plan Standard : réutilise la réponse pendant 10 minutes
    });
    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 } conserve la réponse en amont jusqu'à 10 minutes et la partage entre les requêtes. Next.js ne stocke que les réponses avec un statut HTTP 200 : une erreur de MetalAPI n'est donc jamais mise en cache. Si votre application active les Cache Components, déplacez plutôt le fetch dans un helper marqué 'use cache' et définissez sa durée de vie avec cacheLife.

Étapes suivantes

Créez un compte gratuit pour obtenir une clé. Le plan Free inclut 100 requêtes par mois avec des mises à jour quotidiennes, sans carte bancaire, ce qui suffit pour construire et tester tout ce qui précède. Quand votre route passe en production, choisissez un plan selon la fraîcheur et le volume : une entrée de cache de 10 minutes consomme environ 4 500 requêtes par mois, ce qui tient dans les 10 000 du plan Standard. La page des tarifs compare tous les plans, et API du cours de l'or en Python présente la même API pour les scripts, pandas et les graphiques.

Questions fréquentes

Puis-je appeler l'API du cours de l'or directement depuis le navigateur ?

Oui. L'API répond aux requêtes cross-origin : fetch fonctionne donc depuis une page web. Utilisez une clé restreinte à vos domaines, et n'oubliez pas que les visiteurs peuvent quand même la lire et que chaque affichage de page coûte une requête. Une route serveur avec cache revient moins cher et garde la clé privée.

Pourquoi ma page affiche-t-elle 0,00025 au lieu du cours de l'or ?

Vous lisez rates.XAU, le taux inverse. Lisez rates.USDXAU, ou rates[base + "XAU"] lorsque la devise de base varie. Avec les valeurs d'exemple, 1 ÷ 0,0002501319 redonne 3 997,89.

Comment obtenir le cours de l'or en euros ou au gramme en JavaScript ?

Passez base=EUR et unit=gram. Le prix est alors rates.EURXAU, en euros par gramme, et la réponse inclut unit: "gram". unit accepte aussi troy_oz (la valeur par défaut), kilogram, ounce, pound, metric_ton et short_ton. Pour l'or, évitez ounce, qui désigne l'once avoirdupois.

À quelle fréquence rafraîchir le cours de l'or ?

Au plus une fois par intervalle de mise à jour de votre plan : une fois par jour sur Free, toutes les heures sur Lite (un appel toutes les 30 minutes demanderait environ 1 490 requêtes par mois pour un quota de 1 000), toutes les 10 minutes sur Standard. Les prix restent figés le week-end, car l'or au comptant ne se négocie pas du vendredi 17:00 au dimanche 17:00, heure de New York.

Puis-je obtenir le cours de l'or en temps réel sans polling ?

À partir du plan Pro, /stream pousse les mises à jour de prix via Server-Sent Events : vous n'avez donc jamais à interroger /latest. Pour une logique de seuil, par exemple l'or qui franchit 4 000 $, les alertes de prix par webhook (à partir du plan Advanced) appellent votre serveur lorsque la condition est remplie.

Essayez avec vos propres données

Créez un compte gratuit et envoyez votre première requête en moins de deux minutes. 100 requêtes par mois, sans carte bancaire.

À lire aussi