JavaScriptで金価格APIを使う:Node.js、TypeScript、Next.js
JavaScriptで金価格APIから金価格を取得する方法。async/awaitとfetch、TypeScriptのレスポンス型、キャッシュ付きのExpress・Next.jsルート、APIキーの安全な扱い方を解説します。
JavaScriptで金価格APIを使う:最初のリクエスト
JavaScriptから金価格APIを呼び出すだけならfetchを1回使えば済みますが、本番環境の連携には、型、エラー処理、キャッシュ、そしてAPIキーの安全な置き場所も必要です。この記事では、貴金属、産業用金属、エネルギー、通貨、暗号資産を扱うJSON REST APIのMetalAPIを使って、それらをすべて構築します。まずシンプルな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以降です。それより古いバージョンでは、代わりにシェルで環境変数をエクスポートしてください。
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で、金1トロイオンスの基準通貨建て価格です。同じオブジェクトには約0.00025のrates.XAUも含まれていますが、こちらは逆数で、1ドルあたりの金のオンス数です。これを誤って表示してしまうのが、金価格の典型的なバグです。baseをEURに変えるとキーはEURXAUになり、unit=gramを追加すると1グラムあたりの価格になります。timestampは価格データの時刻をUnix秒で表したもので、リクエストの時刻ではありません。
金価格APIレスポンスのTypeScript型
レスポンスの形は小さく安定しているため、型を付けるのは簡単です。成功したレスポンスにはsuccess: trueが含まれます。失敗したレスポンスにはsuccess: falseと、数値のcode、type、infoメッセージを持つerrorオブジェクトが含まれます。そのため、successによる判別可能なユニオン型を使えば、TypeScriptが両者を絞り込めます。このクライアントは、失敗を型付きのエラーに変換し、レート制限のレスポンスに含まれるRetry-Afterの値も保持します:
const API = "https://metalapi.com/api/v1";
export interface RateLimit {
limit: number;
remaining: number;
used: number;
reset: number; // Unix秒
reset_date: string;
plan: string;
}
export interface LatestResponse {
success: true;
timestamp: number; // 価格データの時刻(Unix秒)
date: string;
base: string;
unit?: string; // unitがtroy_oz以外のときに含まれる
rates: Record<string, number>; // "USDXAU":価格、"XAU":逆数
rate_limit: RateLimit;
}
export interface TimeframeResponse {
success: true;
start_date: string;
end_date: string;
base: string;
rates: Record<string, Record<string, number>>; // キーは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")は、1グラムあたりのユーロ建て価格をタイムスタンプとともに返します。metalapi<TimeframeResponse>("timeframe", { start_date, end_date, currencies: "XAU" })は日次の終値を返し、1リクエストあたりFreeでは最大30日、有料プランでは最大365日分を取得できます。分岐にはerr.codeを使い、メッセージは決して使わないでください。再試行するのは、104(1分あたりの上限。retryAfter秒後に再試行)、500、503、ネットワークエラーだけです。これらの失敗したリクエストは課金されません。それ以外のコードは修正が必要です。101や102はキーがないか無効、105は月間クォータの使い切り、106はプランに含まれていないエンドポイントを意味します。
サーバーサイドかブラウザか:APIキーを置くべき場所
クライアントサイドのJavaScriptに含まれるものは、すべて公開されます。Reactのバンドル、インラインスクリプト、NEXT_PUBLIC_やVITE_の環境変数に含まれるキーは、どの訪問者でもブラウザの開発者ツールで読み取れます。さらに、ページビューのたびにリクエストを1回消費します。ユーザーに金価格を表示する方法は3つあります:
- 自社のサーバールート(推奨)。 ブラウザは自社のエンドポイントを呼び出し、サーバーがキーを使ってMetalAPIを呼び出して結果をキャッシュします。トラフィックがどれだけ多くても、使用量はキャッシュの更新1回につき1リクエストにとどまります。
- ブラウザでドメイン制限付きキーを使う。 ダッシュボードでキーを自社ドメインに制限でき、
OriginやRefererが一致しないブラウザからのリクエストはエラー107で拒否され、課金されません。これにより、他のウェブサイトが訪問者のブラウザでキーを使うことは防げますが、キーをスクリプトにコピーする人は防げません。OriginもRefererもないリクエストは常に許可されるためです。この用途には、メインのキーではなく専用のキーを使ってください。詳細は認証ガイドをご覧ください。 - 埋め込みウィジェット。 価格を表示するだけなら、ウィジェットを使えばキーもコードも不要で、ウィジェットのページビューはクォータにカウントされません。
Expressでキャッシュ付きの金価格APIルートを作る
サーバールートにはキャッシュが必要です。価格はプランごとに決まった間隔(Freeでは毎日、Liteでは30分ごと、Standardでは10分ごと、AdvancedとProでは60秒ごと)で更新されるため、それより早くリクエストしても同じ数値が返され、クォータを消費するだけです。次のルートは、通貨と単位の組み合わせごとに10分間キャッシュし、同時に届いたリクエストで1回の上流呼び出しを共有し、MetalAPIに到達できない場合は最後に取得できた価格を返し続けます:
// npm install expressの後に実行:METALAPI_KEY=mk_live_... npx tsx server.ts
import express from "express";
import { goldPrice } from "./metalapi";
const TTL_MS = 10 * 60_000; // Standardプラン:価格は10分ごとに更新
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); // シングルフライト:キーごとに上流呼び出しは1回
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 }); // 古い価格を返し、1分後に再試行
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"));許可リストは、安全性だけでなくクォータの面でも重要です。通貨と単位の組み合わせはそれぞれ別のキャッシュエントリーになり、更新のたびに1リクエストを消費します。6つの組み合わせを24時間キャッシュし続けると、月に約26,800リクエストかかります。リストはページに表示するものだけに絞るか、PHP・WordPressのチュートリアルのように、1回のリクエストで複数の通貨を取得してローカルで換算してください。キャッシュはプロセスのメモリ上にあります。複数のインスタンスやサーバーレス関数で動かす場合は、Redisやプラットフォームのキャッシュに移してください。
フロントエンドは、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>Next.jsで同じルートを作る
Next.jsのApp Routerでは、Route Handlerが同じ役割を果たし、手書きのキャッシュの代わりにNext.jsのデータキャッシュを使えます。クライアントをlib/metalapi.tsとして保存し、キーは.env.localにMETALAPI_KEYとして、NEXT_PUBLIC_接頭辞を付けずに保存します。こうすれば、キーはサーバー上にしか存在しません:
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プラン:レスポンスを10分間再利用
});
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プランには月100リクエストと毎日の更新が含まれ、クレジットカードは不要です。ここで紹介した内容はすべてFreeプランで構築・テストできます。ルートを本番公開するときは、価格の鮮度とリクエスト数でプランを選んでください。10分間のキャッシュエントリー1つで月に約4,500リクエストかかり、Standardの10,000に収まります。料金ページですべてのプランを比較できます。また、Pythonで金価格APIを使うでは、同じAPIをスクリプト、pandas、グラフで使う方法を紹介しています。
よくある質問
ブラウザから金価格APIを直接呼び出せますか?
はい。APIはクロスオリジンのリクエストに応答するため、ウェブページからfetchを使えます。自社ドメインに制限したキーを使い、訪問者がそのキーを読み取れること、ページビューのたびにリクエストを消費することに留意してください。キャッシュ付きのサーバールートのほうがコストが低く、キーも非公開に保てます。
ページに金価格ではなく0.00025と表示されるのはなぜですか?
逆数のレートであるrates.XAUを読み取っています。rates.USDXAUを読み取るか、基準通貨が変わる場合はrates[base + "XAU"]を読み取ってください。例の値では、1÷0.0002501319で3997.89になります。
JavaScriptでユーロ建てや1グラムあたりの金価格を取得するには?
base=EURとunit=gramを渡します。価格はrates.EURXAUで、1グラムあたりのユーロ建てになり、レスポンスにはunit: "gram"が含まれます。unitにはtroy_oz(デフォルト)、kilogram、ounce、pound、metric_ton、short_tonも指定できます。金の場合、常衡オンスであるounceは避けてください。
金価格はどのくらいの頻度で更新すべきですか?
多くてもプランの更新間隔ごとに1回です。Freeでは毎日、Liteでは1時間ごと(30分ごとだと月に約1,490リクエストとなり、1,000のクォータを超えます)、Standardでは10分ごとです。週末は価格が動きません。スポット金は、ニューヨーク時間の金曜17:00から日曜17:00まで取引されないためです。
ポーリングせずにリアルタイムで金価格を取得できますか?
Proプラン以上では、/streamがServer-Sent Eventsで価格の更新をプッシュするため、/latestをポーリングする必要はありません。金価格が$4,000を超えたときのようなしきい値のロジックには、Webhookによる価格アラート(Advancedプラン以上)を使えば、条件を満たしたときにサーバーが呼び出されます。
関連記事
XAU/USDの意味:XAU・XAG・XPT・XPDが表すもの
XAU/USDの意味を解説します。金にISO 4217の通貨コードがある理由、XAG・XPT・XPDの意味、価格やAPIデータでの表示方向の読み方までわかります。
無料の金価格API:月100リクエストでできること
無料の金価格APIを正直に解説します。MetalAPIのFreeプランの内容、キャッシュとまとめ取得で月100リクエストを有効に使う方法、アップグレードの目安を紹介します。
Pythonで金価格APIを使う:価格の取得、キャッシュ、グラフ化
Pythonで金価格APIを使う方法を解説します。requestsでの最新価格の取得、エラー処理とリトライ、レスポンスのキャッシュ、pandasの時系列とmatplotlibによるグラフ化まで紹介します。