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‎%‎
جميع المقالات
الذهبJavaScriptTypeScriptدرس تطبيقي

واجهة API لأسعار الذهب في JavaScript: Node.js وTypeScript وNext.js

سعر الذهب في JavaScript عبر واجهة API لأسعار الذهب: fetch مع async/await، وأنواع TypeScript، ومسار Express أو Next.js مخزَّن مؤقتًا، واستخدام آمن لمفتاح API.

بقلم MetalAPI Team··وقت القراءة: 7 دقيقة

واجهة API لأسعار الذهب في JavaScript: الطلب الأول

يكفي استدعاء fetch واحد لطلب واجهة API لأسعار الذهب من JavaScript، لكن التكامل في بيئة الإنتاج يحتاج أيضًا إلى أنواع ومعالجة للأخطاء وذاكرة تخزين مؤقت ومكان آمن لمفتاح API. يبني هذا المقال كل ذلك باستخدام MetalAPI، وهي واجهة REST بصيغة JSON للمعادن الثمينة والمعادن الصناعية والطاقة والعملات والعملات المشفرة: أولًا سكربت Node.js بسيط، ثم عميل TypeScript محدَّد الأنواع، ثم مسار صغير في Express أو Next.js يقدّم سعر الذهب لواجهتك الأمامية دون كشف المفتاح.

يتضمن Node.js 18 وما بعده fetch، فلا حاجة إلى مكتبة HTTP. أنشئ مفتاحًا في لوحة التحكم (تبدأ المفاتيح بـ mk_live_)، وضعه في ملف .env على الصورة METALAPI_KEY=mk_live_...، ثم شغّل السكربت بالأمر node --env-file=.env gold.mjs. يقرأ Node.js 20.6 وما بعده هذا الملف؛ أما في الإصدارات الأقدم، فصدّر المتغير في الطرفية بدلًا من ذلك.

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()}`);

مع القيم النموذجية من التوثيق، يكون rates.USDXAU هو 3997.89: سعر أونصة تروي واحدة من الذهب بعملة الأساس. ويحتوي الكائن نفسه أيضًا على rates.XAU، وقيمته نحو 0.00025، وهو المقلوب، أي عدد أونصات الذهب لكل دولار. وعرض هذه القيمة عن طريق الخطأ هو الخطأ الكلاسيكي في أسعار الذهب. غيّر base إلى EUR فيصبح المفتاح EURXAU؛ وأضف unit=gram للحصول على سعر الغرام. وtimestamp هو وقت بيانات السعر بثواني Unix، لا وقت طلبك.

أنواع TypeScript لاستجابة واجهة API لأسعار الذهب

صيغ الاستجابة صغيرة ومستقرة، ما يجعل تحديد أنواعها سهلًا. للاستجابات الناجحة success: true. أما الفاشلة فلها success: false وكائن error يتضمن code رقميًا وtype ورسالة info، لذا يتيح اتحاد مميَّز (discriminated union) على success لـ TypeScript التمييز بين الحالتين. ويحوّل هذا العميل أيضًا حالات الفشل إلى خطأ محدَّد النوع يحتفظ بقيمة Retry-After في استجابات حد المعدل:

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

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

export interface LatestResponse {
  success: true;
  timestamp: number; // time of the price data, Unix seconds
  date: string;
  base: string;
  unit?: string; // present when unit is not troy_oz
  rates: Record<string, number>; // "USDXAU": price, "XAU": inverse
  rate_limit: RateLimit;
}

export interface TimeframeResponse {
  success: true;
  start_date: string;
  end_date: string;
  base: string;
  rates: Record<string, Record<string, number>>; // keyed by 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") سعر الغرام باليورو مع طابعه الزمني، ويُرجع metalapi<TimeframeResponse>("timeframe", { start_date, end_date, currencies: "XAU" }) أسعار الإغلاق اليومية لما يصل إلى 30 يومًا لكل طلب في Free و365 في الخطط المدفوعة. ابنِ منطقك على err.code، لا على الرسالة أبدًا. ولا تُعِد المحاولة إلا مع 104 (الحد لكل دقيقة، بعد retryAfter ثانية)، و500، و503، وأخطاء الشبكة؛ فهذه الطلبات الفاشلة لا تُحتسب. أما الرموز الأخرى فتحتاج إلى إصلاح بدلًا من ذلك: 101 أو 102 يعنيان مفتاحًا مفقودًا أو غير صالح، و105 استنفاد الحصة الشهرية، و106 نقطة نهاية لا تشملها خطتك.

من جهة الخادم أم في المتصفح: أين يوضع مفتاح API

كل ما في JavaScript من جهة العميل علني. فالمفتاح الموجود في حزمة React، أو في سكربت مضمَّن، أو في متغير بيئة NEXT_PUBLIC_ أو VITE_ يمكن لكل زائر قراءته في أدوات المطوّر في المتصفح، وكل مشاهدة للصفحة تستهلك أحد طلباتك. وهناك ثلاث طرق لعرض سعر الذهب على المستخدمين:

  • مسار على خادمك (موصى به). يستدعي المتصفح نقطة النهاية الخاصة بك، ويستدعي خادمك MetalAPI بالمفتاح ويخزّن النتيجة مؤقتًا. ويبقى الاستهلاك عند طلب واحد لكل تحديث لذاكرة التخزين المؤقت، مهما بلغت حركة الزوار.
  • مفتاح مقيَّد بالنطاق في المتصفح. يمكنك في لوحة التحكم تقييد المفتاح بنطاقاتك، فتُرفض طلبات المتصفح التي لا تتطابق فيها ترويسة Origin أو Referer بالخطأ 107 دون احتسابها. يمنع ذلك المواقع الأخرى من استخدام المفتاح في متصفحات زوارها، لكنه لا يمنع من ينسخه إلى سكربت: فالطلبات التي لا تحمل Origin أو Referer مسموح بها دائمًا. استخدم لهذا الغرض مفتاحًا منفصلًا، ولا تستخدم مفتاحك الرئيسي أبدًا. ويتضمن دليل المصادقة التفاصيل.
  • ودجة قابلة للتضمين. لعرض بسيط للسعر، لا تحتاج الودجة إلى مفتاح ولا إلى شيفرة، ولا تُحتسب مشاهدات صفحات الودجة من حصتك.

مسار مخزَّن مؤقتًا لأسعار الذهب في Express

يحتاج مسار الخادم إلى ذاكرة تخزين مؤقت. فكل خطة تحدّث الأسعار على فترات ثابتة (يوميًا في Free، وكل 30 دقيقة في Lite، و10 دقائق في Standard، و60 ثانية في Advanced وPro)، لذا فإن الطلب قبل ذلك يُرجع الأرقام نفسها ولا يفعل سوى استهلاك الحصة. يخزّن هذا المسار كل عملة ووحدة مؤقتًا لمدة 10 دقائق، ويجعل الطلبات المتزامنة تتشارك استدعاءً واحدًا للواجهة، ويواصل تقديم آخر سعر صالح إذا تعذّر الوصول إلى MetalAPI:

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

const TTL_MS = 10 * 60_000; // Standard plan: prices update every 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: one upstream call per key
  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 }); // stale, retry in 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"));

قائمة السماح مهمة لحصتك بقدر أهميتها للأمان. فكل تركيبة من عملة ووحدة هي مدخل منفصل في ذاكرة التخزين المؤقت يكلّف طلبًا واحدًا لكل تحديث، لذا فإن إبقاء ست تركيبات محدَّثة على مدار الساعة يستهلك نحو 26,800 طلب شهريًا. اقصر القائمة على ما تعرضه صفحاتك، أو اجلب عدة عملات في طلب واحد وحوّلها محليًا، كما يفعل درس PHP وWordPress. وتقيم ذاكرة التخزين المؤقت في ذاكرة العملية؛ ومع عدة نسخ أو دوال بلا خادم (serverless)، انقلها إلى Redis أو إلى ذاكرة التخزين المؤقت في منصتك.

ثم تستدعي الواجهة الأمامية مسارك، ولا تستدعي 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>

المسار نفسه في Next.js

في App Router الخاص بـ Next.js، يؤدي معالج المسار (Route Handler) المهمة نفسها، ويمكن لذاكرة البيانات المؤقتة في Next.js أن تحل محل ذاكرة التخزين المؤقت المكتوبة يدويًا. احفظ العميل باسم lib/metalapi.ts واحتفظ بالمفتاح في .env.local باسم METALAPI_KEY، دون البادئة NEXT_PUBLIC_، حتى لا يوجد إلا على الخادم:

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 }, // Standard plan: reuse the response for 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 } استجابة الواجهة لمدة تصل إلى 10 دقائق ويشاركها بين الطلبات. ولا يخزّن Next.js إلا الاستجابات ذات حالة HTTP 200، لذا لا يُخزَّن خطأ MetalAPI مؤقتًا أبدًا. وإذا كان تطبيقك يفعّل Cache Components، فانقل استدعاء fetch إلى دالة مساعدة معلَّمة بـ 'use cache' واضبط مدة صلاحيتها باستخدام cacheLife بدلًا من ذلك.

الخطوات التالية

أنشئ حسابًا مجانيًا للحصول على مفتاح. تتضمن خطة Free مئة طلب شهريًا مع تحديثات يومية ودون بطاقة ائتمان، وهذا يكفي لبناء كل ما سبق واختباره. وحين يصبح مسارك متاحًا للجمهور، اختر خطة بحسب حداثة الأسعار وحجم الطلبات: فمدخل ذاكرة تخزين مؤقت مدته 10 دقائق يستهلك نحو 4,500 طلب شهريًا، وهذا يقع ضمن حد Standard البالغ 10,000. وتقارن صفحة الأسعار جميع الخطط، ويتناول مقال واجهة API لأسعار الذهب في Python الواجهة نفسها للسكربتات وpandas والرسوم البيانية.

الأسئلة الشائعة

هل يمكنني استدعاء واجهة API لأسعار الذهب مباشرةً من المتصفح؟

نعم. تجيب الواجهة على الطلبات عبر النطاقات (cross-origin)، لذا يعمل fetch من صفحة الويب. استخدم مفتاحًا مقيَّدًا بنطاقاتك، وتذكّر أن الزوار ما زالوا قادرين على قراءته وأن كل مشاهدة للصفحة تكلّف طلبًا. أما مسار الخادم مع ذاكرة تخزين مؤقت فأقل تكلفة ويُبقي المفتاح خاصًا.

لماذا تعرض صفحتي 0.00025 بدلًا من سعر الذهب؟

أنت تقرأ rates.XAU، وهو السعر المقلوب. اقرأ rates.USDXAU، أو rates[base + "XAU"] حين تتغير عملة الأساس. ومع القيم النموذجية، يعطي 1 ÷ 0.0002501319 القيمة 3997.89.

كيف أحصل على سعر الذهب باليورو أو لكل غرام في JavaScript؟

مرّر base=EUR وunit=gram. عندها يكون السعر هو rates.EURXAU باليورو لكل غرام، وتتضمن الاستجابة unit: "gram". ويقبل unit أيضًا troy_oz (الافتراضية)، وkilogram، وounce، وpound، وmetric_ton، وshort_ton. وللذهب، تجنّب ounce، فهي أونصة الأفوارديبوا.

كم مرة ينبغي تحديث سعر الذهب؟

مرة واحدة على الأكثر في كل فترة تحديث لخطتك: يوميًا في Free، وكل ساعة في Lite (فالتحديث كل 30 دقيقة يستهلك نحو 1,490 طلبًا شهريًا مقابل حصة قدرها 1,000)، وكل 10 دقائق في Standard. وتبقى الأسعار ثابتة في عطلة نهاية الأسبوع، حين لا يُتداول الذهب الفوري من الجمعة 17:00 حتى الأحد 17:00 بتوقيت نيويورك.

هل يمكنني الحصول على أسعار الذهب في الوقت الفعلي دون استطلاع؟

في خطة Pro وما فوقها، تدفع /stream تحديثات الأسعار عبر Server-Sent Events، فلا تحتاج أبدًا إلى استطلاع /latest. ولمنطق العتبات، مثل تجاوز الذهب مستوى $4,000، تستدعي تنبيهات الأسعار عبر خطافات الويب (في خطة Advanced وما فوقها) خادمك عند تحقق الشرط.

جرّبه مع بياناتك الخاصة

أنشئ حسابًا مجانيًا وأرسل طلبك الأول في أقل من دقيقتين. 100 طلب شهريًا، دون بطاقة ائتمان.

تابع القراءة